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

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