/*
* Process structure:
* client (C wrapper) connects to server
+ * (including reading ack byte)
* if fails or garbage
* === acquire lock ===
* makes new listening socket
*
* setup (pre-exec) fd0: null,
* fd[12: fd2-from-outer-caller
- * env fds: orig-fd[01], listener,
- * env fds: call(server-end)(fake)
+ * env fds: listener, call(server-end)(fake)
+ * env fds: orig-fd[01]
* close fd: lockfile
*
* setup (script) runs initialisation parts of the script
* at prefork establishment point:
- * forks to logger(1)
- * forks for server, now becomes like monitor below
- * exits
+ * setup (pm) [1] opens syslog
+ * forks for server
+ * [2] exits
*
- * server (script) fd0: null, fd[12]: syslog
- * other fds: orig-fd[01], listener,
- * other fds: call(server-end)(fake)
- *
- * right away, forks one fake-accepted monitor:
- * f-a monitor [fd0: null, fd[12]: syslgo]
- * other fds: call(server-end)(fake)
- * runs as monitor, below
- *
- * [server (script)] fd0: null, fd[12]: syslog
- * other fds: listener
- * closes fds: orig-fd[01], call(server-end)fake)
+ # server (pm) [1] [fd0: null],
+ * [fd[12: fd2-from-outer-caller]
+ * right away, forks one fa-monitor
+ * [2] closes outer caller fds and call(fake)
+ * [server (pm)] fd[012]: null
+ * other fds: listener, syslog
* runs in loop accepting and forking,
- * reaping and limiting children
+ * reaping and limiting children (incl fa-monitor)
* reports failures of monitors to syslog
+ *
+ * f-a monitor forks executor
+ * closes fd: listener
+ * [fd[12: fd2-from-outer-caller]
+ * [other fds: call(server-end)(fake), syslog]
+ * runs as monitor, below
+ *
*
* [client (C wrapper)] if client connect succeeds:
* now fd: call(client-end)
*
* [server (script)] accepts, forks monitor
*
- * monitor [fd0: null, fd[12]: syslgo]
- * other fds: call(server-end)
+ * monitor [1] [fd[012]: null]
+ * other fds: syslog, call(server-end)
+ * sends ack byte
* receives args, env, fds
* forks executor
*
* executor sorts out fds:
* fd0, fd1, fd2: from-outer-caller
* close fds: call(server-end)
- * implicitly closed fds: syslog
+ * retained fds: syslog
*
* sets cmdline, env
* runs main part of script
* exits normally
*
- * [monitor] [fd0: null, fd[12]: syslgo]
- * [other fds: call(server-end)]
+ * [monitor] [fd[012]: null]
+ * [other fds: call(server-end), syslog]
* reaps executor
* reports status via socket
*
struct sockaddr_un sun;
+#define ACK_BYTE '\n'
+
+static struct sockaddr_unix socket_sun;
+
+static void propagate_exit_status(int status, const char *what) {
+ int r;
+
+ if (WIFEXITED(status)) {
+ _exit(status);
+ }
+
+ if (WIFSIGNALED(status)) {
+ int sig = WTERMSIG(status);
+ char *signame = strsignal(sig);
+ if (signame == 0) signame = "unknown signal";
+
+ if (! WCOREDUMP(status) &&
+ (sig == SIGINT ||
+ sig == SIGHUP ||
+ sig == SIGPIPE ||
+ sig == SIGKILL)) {
+ struct sigaction sa;
+ FILLZERO(sa);
+ sa.sa_handler = SIG_DFL;
+ r = sigaction(sig, &sa, 0);
+ if (r) diee("failed to reset signal handler while propagating %s",
+ signame);
+
+ sigset_t sset;
+ sigemptyset(&sset);
+ sigaddset(&sset, sig);
+ r = sigprocmask(SA_UNBLOCK, sset, 0);
+ if (r) diee("failed to reset signal block while propagating %s",
+ signame);
+
+ raise(sig);
+ die("unexpectedly kept running after raising (to propagate) %s",
+ signame);
+ }
+
+ die("setup failed due to signal %d %s%s", sig, signame,
+ WCOREDUMP(status) ? " (core dumped)" : "");
+ }
+
+ die("setup failed with weird wait status %d 0x%x", status, status);
+}
+
+static void die_data_overflow __attribute((noreturn)) {
+ die("cannot handle data with length >2^32");
+}
+
+static void prepare_data(size_t *len, char **buf,
+ const void *data, size_t dl) {
+ if (len) {
+ if (dl >= SIZE_MAX - *len)
+ die_data_overlow();
+ *len += dl;
+ }
+ if (buf) {
+ memcpy(*buf, data, dl);
+ *buf += dl;
+ }
+}
+
+static void prepare_length(size_t *len, char **buf, size_t dl) {
+ if (dl > UINT32_MAX) die_data_overflow();
+ uint32_t dl = htonl(dl);
+ prepare_data(len, buf, &dl, sizeof(dl));
+}
+
+static void prepare_string(size_t *len, char **buf, const char *string) {
+ size_t sl = strlen(s);
+ prepare_data(len, buf, s, sl+1);
+}
+
+static void prepare_message(size_t *len, char **buf,
+ const char *const *argv) {
+ const char *s;
+
+ const char *const *p = environ;
+ while ((s = *p++)) {
+ if (strchr(s, '='))
+ prepare_string(len, buf, s);
+ }
+
+ prepare_string(len, buf, "");
+
+ p = argv;
+ while ((s = *p++))
+ prepare_string(len, buf, s);
+}
+
+static void send_fd(int via_fd, int payload_fd) {
+ union {
+ struct cmsghdr align;
+ char buf[CMSG_SPACE(sizeof(payload_fd))];
+ } msg;
+ struct msghdr msg;
+ FILLZERO(msg);
+
+ struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
+ cmsg->cmsg_level = SOL_SOCKET;
+ cmsg->cmsg_type = SCM_RIGHTS;
+ cmsg->cmsg_len = CMSG_LEN(sizeof(payload_fd));
+ *(int*)CMSG_DATA(cmsg) = payload_fd;
+
+ char dummy_byte = 0;
+
+ struct iovec iov;
+ FIULLZERO(iov);
+ iov.iov_base = &dummy_byte;
+ iov.iov_len = 1;
+
+ msg.msg_name = 0;
+ msg.msg_iov = &iov;
+ msg.msg_iovlen = 1;
+ msg.msg_control = msg.buf;
+ msg.msg_controllen = sizeof(msg.buf);
+
+ for (;;) {
+ ssize_t r = sendmsg(via_fd, &msg, 0);
+ if (r == -1) {
+ if (errno == EINTR) continue;
+ diee("send fd");
+ }
+ assert!(r == 1);
+ break;
+ }
+}
+
+static void send_request(int call_fd, const char *const *argv) {
+ // Sending these first makes it easier for the script to
+ // use buffered IO for the message.
+ send_fd(call_fd, 0);
+ send_fd(call_fd, 1);
+ send_fd(call_fd, 2);
+
+ size_t len = 4;
+ prepare_message(&len, 0, argv);
+ char *m = malloc(len);
+ if (!m) diee("failed to allocate for message");
+ char *p = m;
+ prepare_length(0, &p, len - 4);
+ prepare_message(0, &p, argv);
+ assert(p == m + len);
+
+ p = m;
+ while (len) {
+ ssize_t r = write(call_fd, p, len);
+ if (r==-1) {
+ if (errno == EINTR) continue;
+ diee("write request");
+ }
+ assert(r <= len);
+ assert(r > 0);
+ len -= r;
+ p += r;
+ }
+}
+
+// Returns: call(client-end) fd, or -1 to mean "is garbage"
+// find_socket_path must have been called
+static int connect_existing(void) {
+ int r;
+ int fd = -1;
+
+ bool isgarbage = check_garbage();
+ if (isgarbage) goto x_garbage;
+
+ fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd==-1) diee("socket() for client");
+
+ salen_t salen = sizeof(sun);
+ r = connect(client, (const struct sockaddr*)&socket_sun, salen);
+ if (r==-1) {
+ if (errno==ECONNREFUSED || errno==ENOENT) goto x_garbgae;
+ diee("connect() %s", socket_path);
+ }
+
+ for (;;) {
+ char ack;
+ sr = read(fd, &ack, 1);
+ if (sr == -1) {
+ if (errno==ECONNRESET) goto x_garbage;
+ if (errno==EINTR) continue;
+ diee("read() ack byte");
+ }
+ if (sr == 0) { goto x_garbage; }
+ if (ack != '\n') die("got ack byte 0x%02x, not '\n'", ack);
+ break;
+ }
+
+ // We're committed now, send the request (or bail out)
+ send_request(call, argv);
+
+ return fd;
+
+ x_garbage:
+ if (fd >= 0) close(fd);
+ return -1;
+}
+
+static void become_setup(int sfd, int fake_pair[2])
+ __attribute__((noreturn))
+{
+ close(fake_pair[0]);
+ int call_fd = fake_pair[1];
+
+ int fd0_save = dup(0); if (fd0_save < 0) diee("dup stdin");
+ int fd1_save = dup(1); if (fd1_save < 0) diee("dup stdin");
+
+ int null_0 = open("/dev/null", O_RDONLY); if (null_0 < 0) diee("open null");
+ if (dup2(null_0, 0)) diee("dup2 /dev/null onto stdin");
+ if (dup2(2, 1) != 1) die("dup2 stderr onto stdout");
+
+ putenv(asprintf("PREFORK_INTERP=%d,%d,%d,%d,%s",
+ sfd, call_fd, fd0_save, fd1_save, socket_path));
+ execv(
+}
+
+static int connect_or_spawn(void) {
+ int fd = connect_existing();
+ if (fd >= 0) return fd;
+
+ int lockfd = acquire_lock();
+ fd = connect_existing();
+ if (fd >= 0) { close(lockfd); return fd; }
+
+ // We must start a fresh one, and we hold the lock
+
+ r = unlink(socketpath);
+ if (r<0) diee("failed to remove stale socket %s", socketpath);
+
+ int fake_pair[2];
+ r = socketpair(AF_UNIX, SOCK_STREAM, 0, fake_pair);
+ if (r<0) diee("socketpair() for fake initial connection");
+
+ int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (sfd<0) diee("socket() for new listener");
+
+ salen_t salen = sizeof(sun);
+ r= bind(sfd, (const struct sockaddr*)&socket_sun, saledn);
+ if (r<0) diee("bind() on new listener");
+
+ // We never want callers to get ECONNREFUSED!.
+ // There is a race here: from my RTFM they may get ECONNREFUSED
+ // if they tr between our bind() and listen(). But if they do, they'll
+ // acquire the lock (serialising with us) and retry, and then it will work.
+ r = listen(sfd, INT_MAX);
+ if (r<0) diee("listen() for new listener");
+
+ pid_t setup_pid = fork();
+ if (setup_pid == (pid_t)-1) diee("fork for spawn setup");
+ if (!setup_pid) become_setup(sfd, fake_pair);
+ close(fake_pair[1]);
+ close(sfd);
+
+ int status;
+ pid_t got = waitpid(setup_pid, &status, 0);
+ if (got == (pid_t)-1) diee("waitpid setup [%ld]", (long)setup_pid);
+ if (got != setup_pid) diee("waitpid setup [%ld] gave [%ld]!",
+ (long)setup_pid, (long)got);
+ if (status != 0) propagate_exit_status(status);
+
+ close(lockfd);
+ return fake_pair[0];
+}
+
int main(int argc, const char *const *argv) {
script = process_opts(argc, argv);
assert(strlen(socket_path) <= sizeof(sun.sun_path));
strncpy(sun.sun_path, socket_path, sizeof(sun.sun_path));
- bool isgarbage = check_garbage();
-
- int client_fd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (client_fd==-1) diee("socket() for client");
-
- salen_t salen = sizeof(sun);
- r = connect(client, (const struct sockaddr*)sun, salen);
- if (r==-1) {
- if (errno==ECONNREFUSED || errno==ENOENT) {
-
+ int call_fd = connect_or_spawn();
}