Backend support for S6-style readiness notification.
[oweals/dinit.git] / src / run-child-proc.cc
index 9ee2860eff676e16bb7acda176a7db3c82bb462b..fa1cff1b3b9f39e61537badb06606246def24434 100644 (file)
@@ -1,4 +1,5 @@
 #include <cstdlib>
+#include <cstring>
 
 #include <sys/types.h>
 #include <sys/stat.h>
 
 #include "service.h"
 
+// Move an fd, if necessary, to another fd. The destination fd must be available (not open).
+// if fd is specified as -1, returns -1 immediately. Returns 0 on success.
+static int move_fd(int fd, int dest)
+{
+    if (fd == -1) return -1;
+    if (fd == dest) return 0;
+
+    if (dup2(fd, dest) == -1) {
+        return -1;
+    }
+
+    close(fd);
+    return 0;
+}
+
 void service_record::run_child_proc(const char * const *args, const char *working_dir,
         const char *logfile, bool on_console, int wpipefd, int csfd, int socket_fd,
+        int notify_fd, int force_notify_fd, const char *notify_var,
         uid_t uid, gid_t gid) noexcept
 {
-    // Child process. Must not allocate memory (or otherwise risk throwing any exception)
-    // from here until exit().
+    // Child process. Must not risk throwing any uncaught exception from here until exit().
 
     // If the console already has a session leader, presumably it is us. On the other hand
     // if it has no session leader, and we don't create one, then control inputs such as
@@ -34,19 +50,31 @@ void service_record::run_child_proc(const char * const *args, const char *workin
     sigdelset(&sigwait_set, SIGTERM);
     sigdelset(&sigwait_set, SIGQUIT);
 
-    constexpr int bufsz = ((CHAR_BIT * sizeof(pid_t)) / 3 + 2) + 11;
+    constexpr int bufsz = 11 + ((CHAR_BIT * sizeof(pid_t) + 2) / 3) + 1;
     // "LISTEN_PID=" - 11 characters; the expression above gives a conservative estimate
     // on the maxiumum number of bytes required for LISTEN=nnn, including nul terminator,
     // where nnn is a pid_t in decimal (i.e. one decimal digit is worth just over 3 bits).
     char nbuf[bufsz];
 
     // "DINIT_CS_FD=" - 12 bytes. (we -1 from sizeof(int) in account of sign bit).
-    constexpr int csenvbufsz = ((CHAR_BIT * sizeof(int) - 1) / 3 + 2) + 12;
+    constexpr int csenvbufsz = 12 + ((CHAR_BIT * sizeof(int) - 1 + 2) / 3) + 1;
     char csenvbuf[csenvbufsz];
 
     int minfd = (socket_fd == -1) ? 3 : 4;
 
-    // Move wpipefd/csfd to another fd if necessary
+    // Move wpipefd/csfd/notifyfd to another fd if necessary
+
+    // first allocate the forced notification fd, if specified:
+    if (force_notify_fd != -1) {
+        if (notify_fd != force_notify_fd) {
+            if (dup2(notify_fd, force_notify_fd) == -1) {
+                goto failure_out;
+            }
+            close(notify_fd);
+            notify_fd = force_notify_fd;
+        }
+    }
+
     if (wpipefd < minfd) {
         wpipefd = fcntl(wpipefd, F_DUPFD_CLOEXEC, minfd);
         if (wpipefd == -1) goto failure_out;
@@ -57,12 +85,28 @@ void service_record::run_child_proc(const char * const *args, const char *workin
         if (csfd == -1) goto failure_out;
     }
 
+    if (notify_fd < minfd && notify_fd != force_notify_fd) {
+        notify_fd = fcntl(notify_fd, F_DUPFD, minfd);
+        if (notify_fd == -1) goto failure_out;
+    }
+
+    // Set up notify-fd variable
+    if (notify_var != nullptr && *notify_var != 0) {
+        // We need to do an allocation: the variable name length, '=', and space for the value,
+        // and nul terminator:
+        int notify_var_len = strlen(notify_var);
+        int req_sz = notify_var_len + ((CHAR_BIT * sizeof(int) - 1 + 2) / 3) + 1;
+        char * var_str = (char *) malloc(req_sz);
+        if (var_str == nullptr) goto failure_out;
+        snprintf(var_str, req_sz, "%s=%d", notify_var, notify_fd);
+        if (putenv(var_str)) goto failure_out;
+    }
+
+    // Set up Systemd-style socket activation
     if (socket_fd != -1) {
-        // If we passing a pre-opened socket, it has to be fd number 3. (Thanks, systemd).
+        // If we passing a pre-opened socket, it has to be fd number 3. (Thanks, Systemd).
         if (dup2(socket_fd, 3) == -1) goto failure_out;
-        if (socket_fd != 3) {
-            close(socket_fd);
-        }
+        if (socket_fd != 3) close(socket_fd);
 
         if (putenv(const_cast<char *>("LISTEN_FDS=1"))) goto failure_out;
         snprintf(nbuf, bufsz, "LISTEN_PID=%jd", static_cast<intmax_t>(getpid()));
@@ -82,15 +126,22 @@ void service_record::run_child_proc(const char * const *args, const char *workin
 
     if (! on_console) {
         // Re-set stdin, stdout, stderr
-        close(0); close(1); close(2);
+        for (int i = 0; i < 3; i++) {
+            if (i != force_notify_fd) close(i);
+        }
 
-        if (open("/dev/null", O_RDONLY) == 0) {
-            // stdin = 0. That's what we should have; proceed with opening
-            // stdout and stderr.
-            if (open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR) != 1) {
-                goto failure_out;
+        if (notify_fd == 0 || move_fd(open("/dev/null", O_RDONLY), 0) == 0) {
+            // stdin = 0. That's what we should have; proceed with opening stdout and stderr. We have to
+            // take care not to clobber the notify_fd.
+            if (notify_fd != 1) {
+                if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 1) != 0) {
+                    goto failure_out;
+                }
+                if (notify_fd != 2 && dup2(1, 2) != 2) {
+                    goto failure_out;
+                }
             }
-            if (dup2(1, 2) != 2) {
+            else if (move_fd(open(logfile, O_WRONLY | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR), 2) != 0) {
                 goto failure_out;
             }
         }