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

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