Blame SOURCES/0002-exec-polling-fixes.patch

ef398b
From 592820b7a4300cfdc4f85ecd9548f7c2321689fc Mon Sep 17 00:00:00 2001
ef398b
From: Tomas Bzatek <tbzatek@redhat.com>
ef398b
Date: Wed, 16 Sep 2020 17:45:07 +0200
ef398b
Subject: [PATCH 1/5] exec: Fix polling for stdout and stderr
ef398b
ef398b
The condition of having both the stdout and the stderr fds ready
ef398b
may never be satisfied in the case of a full stdout buffer waiting
ef398b
to be read with no output on stderr yet while both fds being still
ef398b
open. In such case the old code got stuck in an endless loop and
ef398b
the spawned process being stuck on writing to stdout/stderr. Let's
ef398b
read data from any fd once available and only react on EOF/HUP.
ef398b
ef398b
This change also makes use of POSIX poll() instead of g_poll()
ef398b
as it's more clear what the return values mean - Glib docs are
ef398b
vague in this regard and one can only guess.
ef398b
---
ef398b
 src/utils/exec.c | 32 ++++++++++++++++++--------------
ef398b
 1 file changed, 18 insertions(+), 14 deletions(-)
ef398b
ef398b
diff --git a/src/utils/exec.c b/src/utils/exec.c
ef398b
index 37bd960..ebbcaf2 100644
ef398b
--- a/src/utils/exec.c
ef398b
+++ b/src/utils/exec.c
ef398b
@@ -22,6 +22,7 @@
ef398b
 #include "extra_arg.h"
ef398b
 #include <syslog.h>
ef398b
 #include <stdlib.h>
ef398b
+#include <poll.h>
ef398b
 #include <errno.h>
ef398b
 #include <sys/types.h>
ef398b
 #include <sys/wait.h>
ef398b
@@ -293,7 +294,7 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
     gint poll_status = 0;
ef398b
     guint i = 0;
ef398b
     guint8 completion = 0;
ef398b
-    GPollFD fds[2] = {ZERO_INIT, ZERO_INIT};
ef398b
+    struct pollfd fds[2] = { ZERO_INIT, ZERO_INIT };
ef398b
     gboolean out_done = FALSE;
ef398b
     gboolean err_done = FALSE;
ef398b
     GString *stdout_data = g_string_new (NULL);
ef398b
@@ -360,13 +361,16 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
 
ef398b
     fds[0].fd = out_fd;
ef398b
     fds[1].fd = err_fd;
ef398b
-    fds[0].events = G_IO_IN | G_IO_HUP | G_IO_ERR;
ef398b
-    fds[1].events = G_IO_IN | G_IO_HUP | G_IO_ERR;
ef398b
+    fds[0].events = POLLIN | POLLHUP | POLLERR;
ef398b
+    fds[1].events = POLLIN | POLLHUP | POLLERR;
ef398b
     while (!out_done || !err_done) {
ef398b
-        poll_status = g_poll (fds, 2, -1);
ef398b
+        poll_status = poll (fds, 2, -1);
ef398b
+        g_warn_if_fail (poll_status != 0);  /* no timeout specified, zero should never be returned */
ef398b
         if (poll_status < 0) {
ef398b
+            if (errno == EAGAIN || errno == EINTR)
ef398b
+                continue;
ef398b
             g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
-                         "Failed to poll output FDs.");
ef398b
+                         "Failed to poll output FDs: %m");
ef398b
             bd_utils_report_finished (progress_id, (*error)->message);
ef398b
             g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
             g_io_channel_unref (out_pipe);
ef398b
@@ -375,12 +379,9 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
             g_string_free (stdout_data, TRUE);
ef398b
             g_string_free (stderr_data, TRUE);
ef398b
             return FALSE;
ef398b
-        } else if (poll_status != 2)
ef398b
-            /* both revents fields were not filled yet */
ef398b
-            continue;
ef398b
-        if (!(fds[0].revents & G_IO_IN))
ef398b
-            out_done = TRUE;
ef398b
-        while (!out_done) {
ef398b
+        }
ef398b
+
ef398b
+        while (!out_done && (fds[0].revents & POLLIN)) {
ef398b
             io_status = g_io_channel_read_line (out_pipe, &line, NULL, NULL, error);
ef398b
             if (io_status == G_IO_STATUS_NORMAL) {
ef398b
                 if (prog_extract && prog_extract (line, &completion))
ef398b
@@ -401,9 +402,10 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
                 return FALSE;
ef398b
             }
ef398b
         }
ef398b
-        if (!(fds[1].revents & G_IO_IN))
ef398b
-            err_done = TRUE;
ef398b
-        while (!err_done) {
ef398b
+        if (fds[0].revents & POLLHUP || fds[0].revents & POLLERR || fds[0].revents & POLLNVAL)
ef398b
+            out_done = TRUE;
ef398b
+
ef398b
+        while (!err_done && (fds[1].revents & POLLIN)) {
ef398b
             io_status = g_io_channel_read_line (err_pipe, &line, NULL, NULL, error);
ef398b
             if (io_status == G_IO_STATUS_NORMAL) {
ef398b
                 g_string_append (stderr_data, line);
ef398b
@@ -421,6 +423,8 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
                 return FALSE;
ef398b
             }
ef398b
         }
ef398b
+        if (fds[1].revents & POLLHUP || fds[1].revents & POLLERR || fds[1].revents & POLLNVAL)
ef398b
+            err_done = TRUE;
ef398b
     }
ef398b
 
ef398b
     child_ret = waitpid (pid, &status, 0);
ef398b
-- 
ef398b
2.26.2
ef398b
ef398b
ef398b
From 3025deda9ab670a454bfe373166e49f2acd1c151 Mon Sep 17 00:00:00 2001
ef398b
From: Tomas Bzatek <tbzatek@redhat.com>
ef398b
Date: Fri, 25 Sep 2020 18:19:46 +0200
ef398b
Subject: [PATCH 2/5] exec: Use non-blocking read and process the buffer
ef398b
 manually
ef398b
ef398b
Waiting for data or a newline character on one fd may create a deadlock
ef398b
while the other fd is being filled with data, exhausting the pipe buffer.
ef398b
Setting both stdout and stderr fds non-blocking allows us to get indication
ef398b
of an empty pipe, continuing with the read cycle over remaining watched fds.
ef398b
ef398b
This also gets rid of GIOChannel as no extended functionality like GSource
ef398b
notifications were used, degrading GIOChannel in a simple GObject wrapper
ef398b
over an fd with just a convenience read_line method that we had to get rid of
ef398b
anyway. Let's use standard POSIX calls and split the read buffer manually
ef398b
by matching the newline character. NULL bytes should be handled gracefully
ef398b
however the stack higher up is not ready for that anyway.
ef398b
---
ef398b
 src/utils/exec.c | 277 +++++++++++++++++++++++++++--------------------
ef398b
 1 file changed, 159 insertions(+), 118 deletions(-)
ef398b
ef398b
diff --git a/src/utils/exec.c b/src/utils/exec.c
ef398b
index ebbcaf2..317fb55 100644
ef398b
--- a/src/utils/exec.c
ef398b
+++ b/src/utils/exec.c
ef398b
@@ -23,6 +23,7 @@
ef398b
 #include <syslog.h>
ef398b
 #include <stdlib.h>
ef398b
 #include <poll.h>
ef398b
+#include <fcntl.h>
ef398b
 #include <errno.h>
ef398b
 #include <sys/types.h>
ef398b
 #include <sys/wait.h>
ef398b
@@ -272,6 +273,87 @@ gboolean bd_utils_exec_and_report_status_error (const gchar **argv, const BDExtr
ef398b
     return TRUE;
ef398b
 }
ef398b
 
ef398b
+/* buffer size in bytes used to read from stdout and stderr */
ef398b
+#define _EXEC_BUF_SIZE 64*1024
ef398b
+
ef398b
+/* similar to g_strstr_len() yet treats 'null' byte as @needle. */
ef398b
+static gchar *bd_strchr_len_null (const gchar *haystack, gssize haystack_len, const gchar needle) {
ef398b
+    gchar *ret;
ef398b
+    gchar *ret_null;
ef398b
+
ef398b
+    ret = memchr (haystack, needle, haystack_len);
ef398b
+    ret_null = memchr (haystack, 0, haystack_len);
ef398b
+    if (ret && ret_null)
ef398b
+        return MIN (ret, ret_null);
ef398b
+    else
ef398b
+        return MAX (ret, ret_null);
ef398b
+}
ef398b
+
ef398b
+static gboolean
ef398b
+_process_fd_event (gint fd, struct pollfd *poll_fd, GString *read_buffer, GString *filtered_buffer, gsize *read_buffer_pos, gboolean *done,
ef398b
+                   guint64 progress_id, guint8 *progress, BDUtilsProgExtract prog_extract, GError **error) {
ef398b
+    gchar buf[_EXEC_BUF_SIZE] = { 0 };
ef398b
+    ssize_t num_read;
ef398b
+    gchar *line;
ef398b
+    gchar *newline_pos;
ef398b
+    int errno_saved;
ef398b
+    gboolean eof = FALSE;
ef398b
+
ef398b
+    if (! *done && (poll_fd->revents & POLLIN)) {
ef398b
+        /* read until we get EOF (0) or error (-1), expecting EAGAIN */
ef398b
+        while ((num_read = read (fd, buf, _EXEC_BUF_SIZE)) > 0)
ef398b
+            g_string_append_len (read_buffer, buf, num_read);
ef398b
+        errno_saved = errno;
ef398b
+
ef398b
+        /* process the fresh data by lines */
ef398b
+        if (read_buffer->len > *read_buffer_pos) {
ef398b
+            gchar *buf_ptr;
ef398b
+            gsize buf_len;
ef398b
+
ef398b
+            while ((buf_ptr = read_buffer->str + *read_buffer_pos,
ef398b
+                    buf_len = read_buffer->len - *read_buffer_pos,
ef398b
+                    newline_pos = bd_strchr_len_null (buf_ptr, buf_len, '\n'))) {
ef398b
+                line = g_strndup (buf_ptr, newline_pos - buf_ptr + 1);
ef398b
+                if (prog_extract && prog_extract (line, progress))
ef398b
+                    bd_utils_report_progress (progress_id, *progress, NULL);
ef398b
+                else
ef398b
+                    g_string_append (filtered_buffer, line);
ef398b
+                g_free (line);
ef398b
+                *read_buffer_pos = newline_pos - read_buffer->str + 1;
ef398b
+            }
ef398b
+        }
ef398b
+
ef398b
+        /* read error */
ef398b
+        if (num_read < 0 && errno_saved != EAGAIN && errno_saved != EINTR) {
ef398b
+            g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
+                         "Error reading from pipe: %s", g_strerror (errno_saved));
ef398b
+            return FALSE;
ef398b
+        }
ef398b
+
ef398b
+        /* EOF */
ef398b
+        if (num_read == 0)
ef398b
+            eof = TRUE;
ef398b
+    }
ef398b
+
ef398b
+    if (poll_fd->revents & POLLHUP || poll_fd->revents & POLLERR || poll_fd->revents & POLLNVAL)
ef398b
+        eof = TRUE;
ef398b
+
ef398b
+    if (eof) {
ef398b
+        *done = TRUE;
ef398b
+        /* process the remaining buffer */
ef398b
+        line = read_buffer->str + *read_buffer_pos;
ef398b
+        /* GString guarantees the buffer is always NULL-terminated. */
ef398b
+        if (strlen (line) > 0) {
ef398b
+            if (prog_extract && prog_extract (line, progress))
ef398b
+                bd_utils_report_progress (progress_id, *progress, NULL);
ef398b
+            else
ef398b
+                g_string_append (filtered_buffer, line);
ef398b
+        }
ef398b
+    }
ef398b
+
ef398b
+    return TRUE;
ef398b
+}
ef398b
+
ef398b
 static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExtraArg **extra, BDUtilsProgExtract prog_extract, gint *proc_status, gchar **stdout, gchar **stderr, GError **error) {
ef398b
     const gchar **args = NULL;
ef398b
     guint args_len = 0;
ef398b
@@ -283,24 +365,26 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
     gchar *msg = NULL;
ef398b
     GPid pid = 0;
ef398b
     gint out_fd = 0;
ef398b
-    GIOChannel *out_pipe = NULL;
ef398b
     gint err_fd = 0;
ef398b
-    GIOChannel *err_pipe = NULL;
ef398b
-    gchar *line = NULL;
ef398b
     gint child_ret = -1;
ef398b
     gint status = 0;
ef398b
     gboolean ret = FALSE;
ef398b
-    GIOStatus io_status = G_IO_STATUS_NORMAL;
ef398b
     gint poll_status = 0;
ef398b
     guint i = 0;
ef398b
     guint8 completion = 0;
ef398b
     struct pollfd fds[2] = { ZERO_INIT, ZERO_INIT };
ef398b
+    int flags;
ef398b
     gboolean out_done = FALSE;
ef398b
     gboolean err_done = FALSE;
ef398b
-    GString *stdout_data = g_string_new (NULL);
ef398b
-    GString *stderr_data = g_string_new (NULL);
ef398b
+    GString *stdout_data;
ef398b
+    GString *stdout_buffer;
ef398b
+    GString *stderr_data;
ef398b
+    GString *stderr_buffer;
ef398b
+    gsize stdout_buffer_pos = 0;
ef398b
+    gsize stderr_buffer_pos = 0;
ef398b
     gchar **old_env = NULL;
ef398b
     gchar **new_env = NULL;
ef398b
+    gboolean success = TRUE;
ef398b
 
ef398b
     /* TODO: share this code between functions */
ef398b
     if (extra) {
ef398b
@@ -336,15 +420,13 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
                                     G_SPAWN_DEFAULT|G_SPAWN_SEARCH_PATH|G_SPAWN_DO_NOT_REAP_CHILD,
ef398b
                                     NULL, NULL, &pid, NULL, &out_fd, &err_fd, error);
ef398b
 
ef398b
+    g_strfreev (new_env);
ef398b
+
ef398b
     if (!ret) {
ef398b
         /* error is already populated */
ef398b
-        g_string_free (stdout_data, TRUE);
ef398b
-        g_string_free (stderr_data, TRUE);
ef398b
-        g_strfreev (new_env);
ef398b
         g_free (args);
ef398b
         return FALSE;
ef398b
     }
ef398b
-    g_strfreev (new_env);
ef398b
 
ef398b
     args_str = g_strjoinv (" ", args ? (gchar **) args : (gchar **) argv);
ef398b
     msg = g_strdup_printf ("Started '%s'", args_str);
ef398b
@@ -353,18 +435,25 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
     g_free (args);
ef398b
     g_free (msg);
ef398b
 
ef398b
-    out_pipe = g_io_channel_unix_new (out_fd);
ef398b
-    err_pipe = g_io_channel_unix_new (err_fd);
ef398b
+    /* set both fds for non-blocking read */
ef398b
+    flags = fcntl (out_fd, F_GETFL, 0);
ef398b
+    if (fcntl (out_fd, F_SETFL, flags | O_NONBLOCK))
ef398b
+        g_warning ("_utils_exec_and_report_progress: Failed to set out_fd non-blocking: %m");
ef398b
+    flags = fcntl (err_fd, F_GETFL, 0);
ef398b
+    if (fcntl (err_fd, F_SETFL, flags | O_NONBLOCK))
ef398b
+        g_warning ("_utils_exec_and_report_progress: Failed to set err_fd non-blocking: %m");
ef398b
 
ef398b
-    g_io_channel_set_encoding (out_pipe, NULL, NULL);
ef398b
-    g_io_channel_set_encoding (err_pipe, NULL, NULL);
ef398b
+    stdout_data = g_string_new (NULL);
ef398b
+    stdout_buffer = g_string_new (NULL);
ef398b
+    stderr_data = g_string_new (NULL);
ef398b
+    stderr_buffer = g_string_new (NULL);
ef398b
 
ef398b
     fds[0].fd = out_fd;
ef398b
     fds[1].fd = err_fd;
ef398b
     fds[0].events = POLLIN | POLLHUP | POLLERR;
ef398b
     fds[1].events = POLLIN | POLLHUP | POLLERR;
ef398b
-    while (!out_done || !err_done) {
ef398b
-        poll_status = poll (fds, 2, -1);
ef398b
+    while (! (out_done && err_done)) {
ef398b
+        poll_status = poll (fds, 2, -1 /* timeout */);
ef398b
         g_warn_if_fail (poll_status != 0);  /* no timeout specified, zero should never be returned */
ef398b
         if (poll_status < 0) {
ef398b
             if (errno == EAGAIN || errno == EINTR)
ef398b
@@ -372,140 +461,90 @@ static gboolean _utils_exec_and_report_progress (const gchar **argv, const BDExt
ef398b
             g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
                          "Failed to poll output FDs: %m");
ef398b
             bd_utils_report_finished (progress_id, (*error)->message);
ef398b
-            g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (out_pipe);
ef398b
-            g_io_channel_shutdown (err_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (err_pipe);
ef398b
-            g_string_free (stdout_data, TRUE);
ef398b
-            g_string_free (stderr_data, TRUE);
ef398b
-            return FALSE;
ef398b
+            success = FALSE;
ef398b
+            break;
ef398b
         }
ef398b
 
ef398b
-        while (!out_done && (fds[0].revents & POLLIN)) {
ef398b
-            io_status = g_io_channel_read_line (out_pipe, &line, NULL, NULL, error);
ef398b
-            if (io_status == G_IO_STATUS_NORMAL) {
ef398b
-                if (prog_extract && prog_extract (line, &completion))
ef398b
-                    bd_utils_report_progress (progress_id, completion, NULL);
ef398b
-                else
ef398b
-                    g_string_append (stdout_data, line);
ef398b
-                g_free (line);
ef398b
-            } else if (io_status == G_IO_STATUS_EOF) {
ef398b
-                out_done = TRUE;
ef398b
-            } else if (error && (*error)) {
ef398b
+        if (!out_done) {
ef398b
+            if (! _process_fd_event (out_fd, &fds[0], stdout_buffer, stdout_data, &stdout_buffer_pos, &out_done, progress_id, &completion, prog_extract, error)) {
ef398b
                 bd_utils_report_finished (progress_id, (*error)->message);
ef398b
-                g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
-                g_io_channel_unref (out_pipe);
ef398b
-                g_io_channel_shutdown (err_pipe, FALSE, NULL);
ef398b
-                g_io_channel_unref (err_pipe);
ef398b
-                g_string_free (stdout_data, TRUE);
ef398b
-                g_string_free (stderr_data, TRUE);
ef398b
-                return FALSE;
ef398b
+                success = FALSE;
ef398b
+                break;
ef398b
             }
ef398b
         }
ef398b
-        if (fds[0].revents & POLLHUP || fds[0].revents & POLLERR || fds[0].revents & POLLNVAL)
ef398b
-            out_done = TRUE;
ef398b
 
ef398b
-        while (!err_done && (fds[1].revents & POLLIN)) {
ef398b
-            io_status = g_io_channel_read_line (err_pipe, &line, NULL, NULL, error);
ef398b
-            if (io_status == G_IO_STATUS_NORMAL) {
ef398b
-                g_string_append (stderr_data, line);
ef398b
-                g_free (line);
ef398b
-            } else if (io_status == G_IO_STATUS_EOF) {
ef398b
-                err_done = TRUE;
ef398b
-            } else if (error && (*error)) {
ef398b
+        if (!err_done) {
ef398b
+            if (! _process_fd_event (err_fd, &fds[1], stderr_buffer, stderr_data, &stderr_buffer_pos, &err_done, progress_id, &completion, prog_extract, error)) {
ef398b
                 bd_utils_report_finished (progress_id, (*error)->message);
ef398b
-                g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
-                g_io_channel_unref (out_pipe);
ef398b
-                g_io_channel_shutdown (err_pipe, FALSE, NULL);
ef398b
-                g_io_channel_unref (err_pipe);
ef398b
-                g_string_free (stdout_data, TRUE);
ef398b
-                g_string_free (stderr_data, TRUE);
ef398b
-                return FALSE;
ef398b
+                success = FALSE;
ef398b
+                break;
ef398b
             }
ef398b
         }
ef398b
-        if (fds[1].revents & POLLHUP || fds[1].revents & POLLERR || fds[1].revents & POLLNVAL)
ef398b
-            err_done = TRUE;
ef398b
     }
ef398b
 
ef398b
+    g_string_free (stdout_buffer, TRUE);
ef398b
+    g_string_free (stderr_buffer, TRUE);
ef398b
+    close (out_fd);
ef398b
+    close (err_fd);
ef398b
+
ef398b
     child_ret = waitpid (pid, &status, 0);
ef398b
-    *proc_status = WEXITSTATUS(status);
ef398b
-    if (child_ret > 0) {
ef398b
-        if (*proc_status != 0) {
ef398b
-            if (stderr_data->str && (g_strcmp0 ("", stderr_data->str) != 0))
ef398b
-                msg = stderr_data->str;
ef398b
-            else
ef398b
-                msg = stdout_data->str;
ef398b
-            g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
-                         "Process reported exit code %d: %s", *proc_status, msg);
ef398b
-            bd_utils_report_finished (progress_id, (*error)->message);
ef398b
-            g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (out_pipe);
ef398b
-            g_io_channel_shutdown (err_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (err_pipe);
ef398b
-            g_string_free (stdout_data, TRUE);
ef398b
-            g_string_free (stderr_data, TRUE);
ef398b
-            return FALSE;
ef398b
-        }
ef398b
-        if (WIFSIGNALED(status)) {
ef398b
-            g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
-                         "Process killed with a signal");
ef398b
-            bd_utils_report_finished (progress_id, (*error)->message);
ef398b
-            g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (out_pipe);
ef398b
-            g_io_channel_shutdown (err_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (err_pipe);
ef398b
-            g_string_free (stdout_data, TRUE);
ef398b
-            g_string_free (stderr_data, TRUE);
ef398b
-            return FALSE;
ef398b
-        }
ef398b
-    } else if (child_ret == -1) {
ef398b
-        if (errno != ECHILD) {
ef398b
-            errno = 0;
ef398b
-            g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
-                         "Failed to wait for the process");
ef398b
-            bd_utils_report_finished (progress_id, (*error)->message);
ef398b
-            g_io_channel_shutdown (out_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (out_pipe);
ef398b
-            g_io_channel_shutdown (err_pipe, FALSE, NULL);
ef398b
-            g_io_channel_unref (err_pipe);
ef398b
-            g_string_free (stdout_data, TRUE);
ef398b
-            g_string_free (stderr_data, TRUE);
ef398b
-            return FALSE;
ef398b
+    *proc_status = WEXITSTATUS (status);
ef398b
+    if (success) {
ef398b
+        if (child_ret > 0) {
ef398b
+            if (*proc_status != 0) {
ef398b
+                msg = stderr_data->len > 0 ? stderr_data->str : stdout_data->str;
ef398b
+                g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
+                             "Process reported exit code %d: %s", *proc_status, msg);
ef398b
+                bd_utils_report_finished (progress_id, (*error)->message);
ef398b
+                success = FALSE;
ef398b
+            } else if (WIFSIGNALED (status)) {
ef398b
+                g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
+                             "Process killed with a signal");
ef398b
+                bd_utils_report_finished (progress_id, (*error)->message);
ef398b
+                success = FALSE;
ef398b
+            }
ef398b
+        } else if (child_ret == -1) {
ef398b
+            if (errno != ECHILD) {
ef398b
+                errno = 0;
ef398b
+                g_set_error (error, BD_UTILS_EXEC_ERROR, BD_UTILS_EXEC_ERROR_FAILED,
ef398b
+                             "Failed to wait for the process");
ef398b
+                bd_utils_report_finished (progress_id, (*error)->message);
ef398b
+                success = FALSE;
ef398b
+            } else {
ef398b
+                /* no such process (the child exited before we tried to wait for it) */
ef398b
+                errno = 0;
ef398b
+            }
ef398b
         }
ef398b
-        /* no such process (the child exited before we tried to wait for it) */
ef398b
-        errno = 0;
ef398b
+        if (success)
ef398b
+            bd_utils_report_finished (progress_id, "Completed");
ef398b
     }
ef398b
-
ef398b
-    bd_utils_report_finished (progress_id, "Completed");
ef398b
     log_out (task_id, stdout_data->str, stderr_data->str);
ef398b
     log_done (task_id, *proc_status);
ef398b
 
ef398b
-    /* we don't care about the status here */
ef398b
-    g_io_channel_shutdown (out_pipe, FALSE, error);
ef398b
-    g_io_channel_unref (out_pipe);
ef398b
-    g_io_channel_shutdown (err_pipe, FALSE, error);
ef398b
-    g_io_channel_unref (err_pipe);
ef398b
-
ef398b
-    if (stdout)
ef398b
+    if (success && stdout)
ef398b
         *stdout = g_string_free (stdout_data, FALSE);
ef398b
     else
ef398b
         g_string_free (stdout_data, TRUE);
ef398b
-    if (stderr)
ef398b
+    if (success && stderr)
ef398b
         *stderr = g_string_free (stderr_data, FALSE);
ef398b
     else
ef398b
         g_string_free (stderr_data, TRUE);
ef398b
 
ef398b
-    return TRUE;
ef398b
+    return success;
ef398b
 }
ef398b
 
ef398b
 /**
ef398b
  * bd_utils_exec_and_report_progress:
ef398b
  * @argv: (array zero-terminated=1): the argv array for the call
ef398b
  * @extra: (allow-none) (array zero-terminated=1): extra arguments
ef398b
- * @prog_extract: (scope notified): function for extracting progress information
ef398b
+ * @prog_extract: (scope notified) (nullable): function for extracting progress information
ef398b
  * @proc_status: (out): place to store the process exit status
ef398b
  * @error: (out): place to store error (if any)
ef398b
  *
ef398b
+ * Note that any NULL bytes read from standard output and standard error
ef398b
+ * output are treated as separators similar to newlines and @prog_extract
ef398b
+ * will be called with the respective chunk.
ef398b
+ *
ef398b
  * Returns: whether the @argv was successfully executed (no error and exit code 0) or not
ef398b
  */
ef398b
 gboolean bd_utils_exec_and_report_progress (const gchar **argv, const BDExtraArg **extra, BDUtilsProgExtract prog_extract, gint *proc_status, GError **error) {
ef398b
@@ -519,6 +558,9 @@ gboolean bd_utils_exec_and_report_progress (const gchar **argv, const BDExtraArg
ef398b
  * @output: (out): variable to store output to
ef398b
  * @error: (out): place to store error (if any)
ef398b
  *
ef398b
+ * Note that any NULL bytes read from standard output and standard error
ef398b
+ * output will be discarded.
ef398b
+ *
ef398b
  * Returns: whether the @argv was successfully executed capturing the output or not
ef398b
  */
ef398b
 gboolean bd_utils_exec_and_capture_output (const gchar **argv, const BDExtraArg **extra, gchar **output, GError **error) {
ef398b
@@ -549,7 +591,6 @@ gboolean bd_utils_exec_and_capture_output (const gchar **argv, const BDExtraArg
ef398b
         g_free (stderr);
ef398b
         return TRUE;
ef398b
     }
ef398b
-
ef398b
 }
ef398b
 
ef398b
 /**
ef398b
-- 
ef398b
2.26.2
ef398b
ef398b
ef398b
From f5eb61c91ffc6b0d1fd709076a9579105655ff17 Mon Sep 17 00:00:00 2001
ef398b
From: Tomas Bzatek <tbzatek@redhat.com>
ef398b
Date: Fri, 25 Sep 2020 18:27:02 +0200
ef398b
Subject: [PATCH 3/5] exec: Clarify the BDUtilsProgExtract callback
ef398b
 documentation
ef398b
ef398b
---
ef398b
 src/utils/exec.h | 24 ++++++++++++++++++++++--
ef398b
 1 file changed, 22 insertions(+), 2 deletions(-)
ef398b
ef398b
diff --git a/src/utils/exec.h b/src/utils/exec.h
ef398b
index ad169e4..0e262a2 100644
ef398b
--- a/src/utils/exec.h
ef398b
+++ b/src/utils/exec.h
ef398b
@@ -31,10 +31,30 @@ typedef void (*BDUtilsProgFunc) (guint64 task_id, BDUtilsProgStatus status, guin
ef398b
 
ef398b
 /**
ef398b
  * BDUtilsProgExtract:
ef398b
- * @line: line from extract progress from
ef398b
+ * @line: line to extract progress from
ef398b
  * @completion: (out): percentage of completion
ef398b
  *
ef398b
- * Returns: whether the line was a progress reporting line or not
ef398b
+ * Callback function used to process a line captured from spawned command's standard
ef398b
+ * output and standard error output. Typically used to extract completion percentage
ef398b
+ * of a long-running job.
ef398b
+ *
ef398b
+ * Note that both outputs are read simultaneously with no guarantees of message order
ef398b
+ * this function is called with.
ef398b
+ *
ef398b
+ * The value the @completion points to may contain value previously returned from
ef398b
+ * this callback or zero when called for the first time. This is useful for extractors
ef398b
+ * where only some kind of a tick mark is printed out as a progress and previous value
ef398b
+ * is needed to compute an incremented value. It's important to keep in mind that this
ef398b
+ * function is only called over lines, i.e. progress reporting printing out tick marks
ef398b
+ * (e.g. dots) without a newline character might not work properly.
ef398b
+ *
ef398b
+ * The @line string usually contains trailing newline character, which may be absent
ef398b
+ * however in case the spawned command exits without printing one. It's guaranteed
ef398b
+ * this function is called over remaining buffer no matter what the trailing
ef398b
+ * character is.
ef398b
+ *
ef398b
+ * Returns: whether the line was a progress reporting line and should be excluded
ef398b
+ *          from the collected standard output string or not.
ef398b
  */
ef398b
 typedef gboolean (*BDUtilsProgExtract) (const gchar *line, guint8 *completion);
ef398b
 
ef398b
-- 
ef398b
2.26.2
ef398b
ef398b
ef398b
From 8a7f0de5f63099a3e8bcaca005c4de04df959113 Mon Sep 17 00:00:00 2001
ef398b
From: Tomas Bzatek <tbzatek@redhat.com>
ef398b
Date: Fri, 25 Sep 2020 18:27:41 +0200
ef398b
Subject: [PATCH 4/5] tests: Add bufferbloat exec tests
ef398b
ef398b
This should test pipe buffer exhaustion as well as potential pipe
ef398b
read starvation while filling the other fd.
ef398b
---
ef398b
 tests/utils_test.py | 105 +++++++++++++++++++++++++++++++++++++++++++-
ef398b
 1 file changed, 104 insertions(+), 1 deletion(-)
ef398b
ef398b
diff --git a/tests/utils_test.py b/tests/utils_test.py
ef398b
index 2bec5ed..ed13611 100644
ef398b
--- a/tests/utils_test.py
ef398b
+++ b/tests/utils_test.py
ef398b
@@ -1,8 +1,9 @@
ef398b
 import unittest
ef398b
 import re
ef398b
 import os
ef398b
+import six
ef398b
 import overrides_hack
ef398b
-from utils import fake_utils, create_sparse_tempfile, create_lio_device, delete_lio_device, run_command, TestTags, tag_test
ef398b
+from utils import fake_utils, create_sparse_tempfile, create_lio_device, delete_lio_device, run_command, TestTags, tag_test, read_file
ef398b
 
ef398b
 from gi.repository import BlockDev, GLib
ef398b
 
ef398b
@@ -65,6 +66,9 @@ class UtilsExecLoggingTest(UtilsTestCase):
ef398b
         succ = BlockDev.utils_exec_and_report_error(["true"])
ef398b
         self.assertTrue(succ)
ef398b
 
ef398b
+        with six.assertRaisesRegex(self, GLib.GError, r"Process reported exit code 1"):
ef398b
+            succ = BlockDev.utils_exec_and_report_error(["/bin/false"])
ef398b
+
ef398b
         succ, out = BlockDev.utils_exec_and_capture_output(["echo", "hi"])
ef398b
         self.assertTrue(succ)
ef398b
         self.assertEqual(out, "hi\n")
ef398b
@@ -178,6 +182,105 @@ class UtilsExecLoggingTest(UtilsTestCase):
ef398b
         self.assertTrue(succ)
ef398b
         self.assertIn("LC_ALL=C", out)
ef398b
 
ef398b
+    @tag_test(TestTags.NOSTORAGE, TestTags.CORE)
ef398b
+    def test_exec_buffer_bloat(self):
ef398b
+        """Verify that very large output from a command is handled properly"""
ef398b
+
ef398b
+        # easy 64kB of data
ef398b
+        cnt = 65536
ef398b
+        succ, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "for i in {1..%d}; do echo -n .; done" % cnt])
ef398b
+        self.assertTrue(succ)
ef398b
+        self.assertEquals(len(out), cnt)
ef398b
+
ef398b
+        succ, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "for i in {1..%d}; do echo -n .; echo -n \# >&2; done" % cnt])
ef398b
+        self.assertTrue(succ)
ef398b
+        self.assertEquals(len(out), cnt)
ef398b
+
ef398b
+        # now exceed the system pipe buffer size
ef398b
+        # pipe(7): The maximum size (in bytes) of individual pipes that can be set by users without the CAP_SYS_RESOURCE capability.
ef398b
+        cnt = int(read_file("/proc/sys/fs/pipe-max-size")) + 100
ef398b
+        self.assertGreater(cnt, 0)
ef398b
+
ef398b
+        succ, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "for i in {1..%d}; do echo -n .; done" % cnt])
ef398b
+        self.assertTrue(succ)
ef398b
+        self.assertEquals(len(out), cnt)
ef398b
+
ef398b
+        succ, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "for i in {1..%d}; do echo -n .; echo -n \# >&2; done" % cnt])
ef398b
+        self.assertTrue(succ)
ef398b
+        self.assertEquals(len(out), cnt)
ef398b
+
ef398b
+        # make use of some newlines
ef398b
+        succ, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "for i in {1..%d}; do echo -n .; if [ $(($i%%500)) -eq 0 ]; then echo ''; fi; done" % cnt])
ef398b
+        self.assertTrue(succ)
ef398b
+        self.assertGreater(len(out), cnt)
ef398b
+
ef398b
+        succ, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "for i in {1..%d}; do echo -n .; echo -n \# >&2; if [ $(($i%%500)) -eq 0 ]; then echo ''; echo '' >&2; fi; done" % cnt])
ef398b
+        self.assertTrue(succ)
ef398b
+        self.assertGreater(len(out), cnt)
ef398b
+
ef398b
+
ef398b
+    EXEC_PROGRESS_MSG = "Aloha, I'm the progress line you should match."
ef398b
+
ef398b
+    def my_exec_progress_func_concat(self, line):
ef398b
+        """Expect an concatenated string"""
ef398b
+        s = ""
ef398b
+        for i in range(10):
ef398b
+            s += self.EXEC_PROGRESS_MSG
ef398b
+        self.assertEqual(line, s)
ef398b
+        self.num_matches += 1
ef398b
+        return 0
ef398b
+
ef398b
+    def my_exec_progress_func(self, line):
ef398b
+        self.assertTrue(re.match(r".*%s.*" % self.EXEC_PROGRESS_MSG, line))
ef398b
+        self.num_matches += 1
ef398b
+        return 0
ef398b
+
ef398b
+    def test_exec_buffer_bloat_progress(self):
ef398b
+        """Verify that progress reporting can handle large output"""
ef398b
+
ef398b
+        # no newlines, should match just a single occurrence on EOF
ef398b
+        cnt = 10
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..10}; do echo -n \"%s\"; done" % self.EXEC_PROGRESS_MSG], None, self.my_exec_progress_func_concat)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, 1)
ef398b
+
ef398b
+        # ten matches
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\"; done" % (cnt, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt)
ef398b
+
ef398b
+        # 100k matches
ef398b
+        cnt = 100000
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\"; done" % (cnt, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt)
ef398b
+
ef398b
+        # 100k matches on stderr
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt)
ef398b
+
ef398b
+        # 100k matches on stderr and stdout
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\"; echo \"%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt * 2)
ef398b
+
ef398b
+        # stdout and stderr output, non-zero return from the command and very long exception message
ef398b
+        self.num_matches = 0
ef398b
+        with six.assertRaisesRegex(self, GLib.GError, r"Process reported exit code 66"):
ef398b
+            status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\"; echo \"%s\" >&2; done; exit 66" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertEqual(self.num_matches, cnt * 2)
ef398b
+
ef398b
+        # no progress reporting callback given, tests slightly different code path
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\"; echo \"%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, None)
ef398b
+        self.assertTrue(status)
ef398b
+
ef398b
+
ef398b
 class UtilsDevUtilsTestCase(UtilsTestCase):
ef398b
     @tag_test(TestTags.NOSTORAGE, TestTags.CORE)
ef398b
     def test_resolve_device(self):
ef398b
-- 
ef398b
2.26.2
ef398b
ef398b
ef398b
From 7a3fd3d32dd325fb5188bcba74966e414e33c343 Mon Sep 17 00:00:00 2001
ef398b
From: Tomas Bzatek <tbzatek@redhat.com>
ef398b
Date: Wed, 30 Sep 2020 14:52:27 +0200
ef398b
Subject: [PATCH 5/5] tests: Add null-byte exec tests
ef398b
ef398b
Some commands may print out NULL bytes in the middle of their output,
ef398b
make sure everything works correctly.
ef398b
---
ef398b
 tests/utils_test.py | 48 +++++++++++++++++++++++++++++++++++++++++++++
ef398b
 1 file changed, 48 insertions(+)
ef398b
ef398b
diff --git a/tests/utils_test.py b/tests/utils_test.py
ef398b
index ed13611..1235be3 100644
ef398b
--- a/tests/utils_test.py
ef398b
+++ b/tests/utils_test.py
ef398b
@@ -280,6 +280,54 @@ class UtilsExecLoggingTest(UtilsTestCase):
ef398b
         status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo \"%s\"; echo \"%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, None)
ef398b
         self.assertTrue(status)
ef398b
 
ef398b
+    def test_exec_null_bytes(self):
ef398b
+        """Verify that null bytes in the output are processed properly"""
ef398b
+
ef398b
+        TEST_DATA = ["First line", "Second line", "Third line"]
ef398b
+
ef398b
+        status, out = BlockDev.utils_exec_and_capture_output(["bash", "-c", "echo -e \"%s\\0%s\\n%s\"" % (TEST_DATA[0], TEST_DATA[1], TEST_DATA[2])])
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertTrue(TEST_DATA[0] in out)
ef398b
+        self.assertTrue(TEST_DATA[1] in out)
ef398b
+        self.assertTrue(TEST_DATA[2] in out)
ef398b
+        self.assertFalse("kuku!" in out)
ef398b
+
ef398b
+        # ten matches
ef398b
+        cnt = 10
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo -e \"%s\\0%s\"; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt * 2)
ef398b
+
ef398b
+        # 100k matches
ef398b
+        cnt = 100000
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo -e \"%s\\0%s\"; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt * 2)
ef398b
+
ef398b
+        # 100k matches on stderr
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo -e \"%s\\0%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt * 2)
ef398b
+
ef398b
+        # 100k matches on stderr and stdout
ef398b
+        self.num_matches = 0
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo -e \"%s\\0%s\"; echo -e \"%s\\0%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertTrue(status)
ef398b
+        self.assertEqual(self.num_matches, cnt * 4)
ef398b
+
ef398b
+        # stdout and stderr output, non-zero return from the command and very long exception message
ef398b
+        self.num_matches = 0
ef398b
+        with six.assertRaisesRegex(self, GLib.GError, r"Process reported exit code 66"):
ef398b
+            status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo -e \"%s\\0%s\"; echo -e \"%s\\0%s\" >&2; done; exit 66" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, self.my_exec_progress_func)
ef398b
+        self.assertEqual(self.num_matches, cnt * 4)
ef398b
+
ef398b
+        # no progress reporting callback given, tests slightly different code path
ef398b
+        status = BlockDev.utils_exec_and_report_progress(["bash", "-c", "for i in {1..%d}; do echo -e \"%s\\0%s\"; echo -e \"%s\\0%s\" >&2; done" % (cnt, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG, self.EXEC_PROGRESS_MSG)], None, None)
ef398b
+        self.assertTrue(status)
ef398b
+
ef398b
 
ef398b
 class UtilsDevUtilsTestCase(UtilsTestCase):
ef398b
     @tag_test(TestTags.NOSTORAGE, TestTags.CORE)
ef398b
-- 
ef398b
2.26.2
ef398b