BUG/MEDIUM: startup: report status if daemonized process fails
authorValentine Krasnobaeva <vkrasnobaeva@haproxy.com>
Mon, 9 Dec 2024 17:51:34 +0000 (18:51 +0100)
committerWilly Tarreau <w@1wt.eu>
Tue, 10 Dec 2024 10:09:53 +0000 (11:09 +0100)
Due to master-worker rework, daemonization fork happens now before parsing
and applying the configuration. This makes impossible to report correctly all
warnings and alerts to shell's stdout. Daemonzied process fails, while being
already in background, exit code reported by shell via '$?' equals to 0, as
it's the exit code of his parent.

To fix this, let's create a pipe between parent and daemonized child. The
child will send into this pipe a "READY" message, when it finishes his
initialization. The parent will wait on the "read" end of the pipe until
receiving something. If read() fails, parent obtains the status of the
exited child with waitpid(). So, the parent can correctly report the error to
the stdout and he can exit with child's exitcode.

This fix should be backported only in 3.1.

(cherry picked from commit 663d75e7a00a9c50185cca322cbc64de934b6631)
Signed-off-by: Willy Tarreau <w@1wt.eu>

include/haproxy/global.h
src/haproxy.c

index 3c09968..a5730bc 100644 (file)
@@ -53,6 +53,7 @@ extern char **init_env;
 extern char *progname;
 extern char **old_argv;
 extern const char *old_unixsocket;
+extern int daemon_fd[2];
 
 struct proxy;
 struct server;
index 1d51c97..1581a9e 100644 (file)
@@ -155,6 +155,7 @@ static struct list cfg_cfgfiles = LIST_HEAD_INIT(cfg_cfgfiles);
 int  pid;                      /* current process id */
 char **init_env;               /* to keep current process env variables backup */
 int  pidfd = -1;               /* FD to keep PID */
+int daemon_fd[2] = {-1, -1};   /* pipe to communicate with parent process */
 
 static unsigned long stopping_tgroup_mask; /* Thread groups acknowledging stopping */
 
@@ -1756,6 +1757,17 @@ static void generate_random_cluster_secret()
 static void apply_daemon_mode()
 {
        int ret;
+       int wstatus = 0;
+       int exitcode = 0;
+       pid_t child_pid;
+       char buf[2];
+
+       if (pipe(daemon_fd) < 0) {
+               ha_alert("[%s.main()] Cannot create pipe for getting the status of "
+                        "child process: %s.\n", progname, strerror(errno));
+
+               exit(EXIT_FAILURE);
+       }
 
        ret = fork();
        switch(ret) {
@@ -1767,12 +1779,44 @@ static void apply_daemon_mode()
                /* in child, change the process group ID, in the master-worker
                 * mode, this will be the master process
                 */
+               close(daemon_fd[0]);
+               daemon_fd[0] = -1;
                setsid();
 
                break;
        default:
-               /* in parent, which leaves to daemonize */
-               exit(0);
+               /* in parent */
+               close(daemon_fd[1]);
+               daemon_fd[1] = -1;
+               /* In standalone + daemon modes: parent (launcher process) tries
+                * to read the child's (daemonized process) "READY" message. Child
+                * writes this message, when he has finished initialization. If
+                * child failed to start, we get his status.
+                * In master-worker mode: daemonized process is the master. He
+                * sends his READY message to launcher, only when
+                * he has received the READY message from the worker, see
+                * _send_status().
+                */
+               if (read(daemon_fd[0], buf, 1) == 0) {
+                       child_pid = waitpid(ret, &wstatus, 0);
+                       if (child_pid < 0) {
+                               ha_alert("[%s.main()] waitpid() failed: %s\n",
+                                        progname, strerror(errno));
+                               exit(EXIT_FAILURE);
+                       }
+                       if (WIFEXITED(wstatus))
+                               wstatus = WEXITSTATUS(wstatus);
+                       else if (WIFSIGNALED(wstatus))
+                               wstatus = 128 + WTERMSIG(wstatus);
+                       else
+                               wstatus = 255;
+
+                       ha_alert("Process %d exited with code %d (%s)\n",
+                                child_pid, wstatus, (wstatus >= 128) ? strsignal(wstatus - 128) : "Exit");
+                       if (wstatus != 0 && wstatus != 143)
+                               exitcode = wstatus;
+               }
+               exit(exitcode);
        }
 }
 
@@ -3248,6 +3292,7 @@ int main(int argc, char **argv)
        struct cfgfile *cfg, *cfg_tmp;
        struct ring *tmp_startup_logs = NULL;
        struct mworker_proc *proc;
+       char *msg = "READY\n";
 
        /* Catch broken toolchains */
        if (sizeof(long) != sizeof(void *) || (intovf + 0x7FFFFFFF >= intovf)) {
@@ -3749,6 +3794,19 @@ int main(int argc, char **argv)
                startup_logs_free(startup_logs);
                startup_logs = tmp_startup_logs;
        }
+
+       /* worker is already sent its READY message to master. This applies only
+        * for daemon standalone mode. Master in daemon mode will "forward" the READY
+        * message received from the worker to the launching process, see _send_status().
+        */
+       if ((global.mode & MODE_DAEMON) && !(global.mode & MODE_MWORKER)) {
+               if (write(daemon_fd[1], msg, strlen(msg)) < 0) {
+                       ha_alert("[%s.main()] Failed to write into pipe with parent process: %s\n", progname, strerror(errno));
+                       exit(1);
+               }
+               close(daemon_fd[1]);
+               daemon_fd[1] = -1;
+       }
        /* can't unset MODE_STARTING earlier, otherwise worker's last alerts
         * should be not written in startup logs.
         */