chiark / gitweb /
cgi-fcgi-interp: prefork: Make name variable
[chiark-utils.git] / prefork-interp.c
index 0921835255c9e8a4898e6a5763985a56946bd6d6..07e9fb0dd94eb4ee32cbac996b92fc27aff938fc 100644 (file)
@@ -6,6 +6,7 @@
 /*
  * 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);
 
@@ -87,14 +358,5 @@ int main(int argc, const char *const *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();
 }