]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
importctl: draw a pretty progress bar while downloading
authorLennart Poettering <lennart@poettering.net>
Mon, 26 Feb 2024 14:46:50 +0000 (15:46 +0100)
committerLennart Poettering <lennart@poettering.net>
Fri, 1 Mar 2024 21:25:42 +0000 (22:25 +0100)
Everybody loves pretty terminal progress bar.

src/basic/glyph-util.c
src/basic/glyph-util.h
src/import/importctl.c
src/import/importd.c
src/shared/pretty-print.c
src/shared/pretty-print.h
src/test/meson.build
src/test/test-locale-util.c
src/test/test-progress-bar.c [new file with mode: 0644]

index b6b0f40ca6f378c62761f1f22f652f836c2f63e8..d37be3234af17493e38755d9bef9802a304c77cf 100644 (file)
@@ -41,6 +41,8 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) {
                         [SPECIAL_GLYPH_TREE_SPACE]              = "  ",
                         [SPECIAL_GLYPH_TREE_TOP]                = ",-",
                         [SPECIAL_GLYPH_VERTICAL_DOTTED]         = ":",
+                        [SPECIAL_GLYPH_HORIZONTAL_DOTTED]       = "-",
+                        [SPECIAL_GLYPH_HORIZONTAL_FAT]          = "=",
                         [SPECIAL_GLYPH_TRIANGULAR_BULLET]       = ">",
                         [SPECIAL_GLYPH_BLACK_CIRCLE]            = "*",
                         [SPECIAL_GLYPH_WHITE_CIRCLE]            = "*",
@@ -91,6 +93,8 @@ const char *special_glyph_full(SpecialGlyph code, bool force_utf) {
 
                         /* Single glyphs in both cases */
                         [SPECIAL_GLYPH_VERTICAL_DOTTED]         = u8"┆",
+                        [SPECIAL_GLYPH_HORIZONTAL_DOTTED]       = u8"┄",
+                        [SPECIAL_GLYPH_HORIZONTAL_FAT]          = u8"━",
                         [SPECIAL_GLYPH_TRIANGULAR_BULLET]       = u8"‣",
                         [SPECIAL_GLYPH_BLACK_CIRCLE]            = u8"●",
                         [SPECIAL_GLYPH_WHITE_CIRCLE]            = u8"○",
index 2f70b187fcd7062eacf1f329604c84e6381c18f6..db8dbbff262933f18a27d61dbfdbc3f41d9cd2b7 100644 (file)
@@ -13,6 +13,8 @@ typedef enum SpecialGlyph {
         SPECIAL_GLYPH_TREE_SPACE,
         SPECIAL_GLYPH_TREE_TOP,
         SPECIAL_GLYPH_VERTICAL_DOTTED,
+        SPECIAL_GLYPH_HORIZONTAL_DOTTED,
+        SPECIAL_GLYPH_HORIZONTAL_FAT,
         SPECIAL_GLYPH_TRIANGULAR_BULLET,
         SPECIAL_GLYPH_BLACK_CIRCLE,
         SPECIAL_GLYPH_WHITE_CIRCLE,
index 688e583d07e7f9dc8a343c51445a2800f5ac3ab1..7ada0e51df0be65adf74122e895b525b7ebbba47 100644 (file)
@@ -45,6 +45,8 @@ static const char* arg_format = NULL;
 static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF;
 static ImageClass arg_image_class = _IMAGE_CLASS_INVALID;
 
+#define PROGRESS_PREFIX "Total: "
+
 static int settle_image_class(void) {
 
         if (arg_image_class < 0) {
@@ -68,13 +70,21 @@ static int settle_image_class(void) {
         return 0;
 }
 
+typedef struct Context {
+        const char *object_path;
+        double progress;
+} Context;
+
 static int match_log_message(sd_bus_message *m, void *userdata, sd_bus_error *error) {
-        const char **our_path = userdata, *line;
+        Context *c = ASSERT_PTR(userdata);
+        const char *line;
         unsigned priority;
         int r;
 
         assert(m);
-        assert(our_path);
+
+        if (!streq_ptr(c->object_path, sd_bus_message_get_path(m)))
+                return 0;
 
         r = sd_bus_message_read(m, "us", &priority, &line);
         if (r < 0) {
@@ -82,23 +92,51 @@ static int match_log_message(sd_bus_message *m, void *userdata, sd_bus_error *er
                 return 0;
         }
 
-        if (!streq_ptr(*our_path, sd_bus_message_get_path(m)))
-                return 0;
-
         if (arg_quiet && LOG_PRI(priority) >= LOG_INFO)
                 return 0;
 
+        if (!arg_quiet)
+                clear_progress_bar(PROGRESS_PREFIX);
+
         log_full(priority, "%s", line);
+
+        if (!arg_quiet)
+                draw_progress_bar(PROGRESS_PREFIX, c->progress * 100);
+
+        return 0;
+}
+
+static int match_progress_update(sd_bus_message *m, void *userdata, sd_bus_error *error) {
+        Context *c = ASSERT_PTR(userdata);
+        int r;
+
+        assert(m);
+
+        if (!streq_ptr(c->object_path, sd_bus_message_get_path(m)))
+                return 0;
+
+        r = sd_bus_message_read(m, "d", &c->progress);
+        if (r < 0) {
+                bus_log_parse_error(r);
+                return 0;
+        }
+
+        if (!arg_quiet)
+                draw_progress_bar(PROGRESS_PREFIX, c->progress * 100);
+
         return 0;
 }
 
 static int match_transfer_removed(sd_bus_message *m, void *userdata, sd_bus_error *error) {
-        const char **our_path = userdata, *path, *result;
+        Context *c = ASSERT_PTR(userdata);
+        const char *path, *result;
         uint32_t id;
         int r;
 
         assert(m);
-        assert(our_path);
+
+        if (!arg_quiet)
+                clear_progress_bar(PROGRESS_PREFIX);
 
         r = sd_bus_message_read(m, "uos", &id, &path, &result);
         if (r < 0) {
@@ -106,7 +144,7 @@ static int match_transfer_removed(sd_bus_message *m, void *userdata, sd_bus_erro
                 return 0;
         }
 
-        if (!streq_ptr(*our_path, path))
+        if (!streq_ptr(c->object_path, path))
                 return 0;
 
         sd_event_exit(sd_bus_get_event(sd_bus_message_get_bus(m)), !streq_ptr(result, "done"));
@@ -117,6 +155,9 @@ static int transfer_signal_handler(sd_event_source *s, const struct signalfd_sig
         assert(s);
         assert(si);
 
+        if (!arg_quiet)
+                clear_progress_bar(PROGRESS_PREFIX);
+
         if (!arg_quiet)
                 log_info("Continuing download in the background. Use \"%s cancel-transfer %" PRIu32 "\" to abort transfer.",
                          program_invocation_short_name,
@@ -127,11 +168,11 @@ static int transfer_signal_handler(sd_event_source *s, const struct signalfd_sig
 }
 
 static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
-        _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot_job_removed = NULL, *slot_log_message = NULL;
+        _cleanup_(sd_bus_slot_unrefp) sd_bus_slot *slot_job_removed = NULL, *slot_log_message = NULL, *slot_progress_update = NULL;
         _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
         _cleanup_(sd_bus_message_unrefp) sd_bus_message *reply = NULL;
         _cleanup_(sd_event_unrefp) sd_event* event = NULL;
-        const char *path = NULL;
+        Context c = {};
         uint32_t id;
         int r;
 
@@ -153,7 +194,9 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
                         &slot_job_removed,
                         bus_import_mgr,
                         "TransferRemoved",
-                        match_transfer_removed, NULL, &path);
+                        match_transfer_removed,
+                        /* add_callback= */ NULL,
+                        &c);
         if (r < 0)
                 return log_error_errno(r, "Failed to request match: %m");
 
@@ -161,10 +204,25 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
                         bus,
                         &slot_log_message,
                         "org.freedesktop.import1",
-                        NULL,
+                        /* object_path= */ NULL,
                         "org.freedesktop.import1.Transfer",
                         "LogMessage",
-                        match_log_message, NULL, &path);
+                        match_log_message,
+                        /* add_callback= */ NULL,
+                        &c);
+        if (r < 0)
+                return log_error_errno(r, "Failed to request match: %m");
+
+        r = sd_bus_match_signal_async(
+                        bus,
+                        &slot_progress_update,
+                        "org.freedesktop.import1",
+                        /* object_path= */ NULL,
+                        "org.freedesktop.import1.Transfer",
+                        "ProgressUpdate",
+                        match_progress_update,
+                        /* add_callback= */ NULL,
+                        &c);
         if (r < 0)
                 return log_error_errno(r, "Failed to request match: %m");
 
@@ -172,12 +230,15 @@ static int transfer_image_common(sd_bus *bus, sd_bus_message *m) {
         if (r < 0)
                 return log_error_errno(r, "Failed to transfer image: %s", bus_error_message(&error, r));
 
-        r = sd_bus_message_read(reply, "uo", &id, &path);
+        r = sd_bus_message_read(reply, "uo", &id, &c.object_path);
         if (r < 0)
                 return bus_log_parse_error(r);
 
-        if (!arg_quiet)
+        if (!arg_quiet) {
+                clear_progress_bar(PROGRESS_PREFIX);
                 log_info("Enqueued transfer job %u. Press C-c to continue download in background.", id);
+                draw_progress_bar(PROGRESS_PREFIX, c.progress);
+        }
 
         (void) sd_event_add_signal(event, NULL, SIGINT|SD_EVENT_SIGNAL_PROCMASK, transfer_signal_handler, UINT32_TO_PTR(id));
         (void) sd_event_add_signal(event, NULL, SIGTERM|SD_EVENT_SIGNAL_PROCMASK, transfer_signal_handler, UINT32_TO_PTR(id));
index 5aeb0b18955e7bec1f63da45fdc0452220769d0d..493e1991b2761ec6f1b61102290553bf64975b8d 100644 (file)
@@ -83,6 +83,7 @@ struct Transfer {
 
         unsigned n_canceled;
         unsigned progress_percent;
+        unsigned progress_percent_sent;
 
         int stdin_fd;
         int stdout_fd;
@@ -166,7 +167,8 @@ static int transfer_new(Manager *m, Transfer **ret) {
                 .stdin_fd = -EBADF,
                 .stdout_fd = -EBADF,
                 .verify = _IMPORT_VERIFY_INVALID,
-                .progress_percent= UINT_MAX,
+                .progress_percent = UINT_MAX,
+                .progress_percent_sent = UINT_MAX,
         };
 
         id = m->current_transfer_id + 1;
@@ -217,7 +219,28 @@ static void transfer_send_log_line(Transfer *t, const char *line) {
                         line);
         if (r < 0)
                 log_warning_errno(r, "Cannot emit log message signal, ignoring: %m");
- }
+}
+
+static void transfer_send_progress_update(Transfer *t) {
+        int r;
+
+        assert(t);
+
+        if (t->progress_percent_sent == t->progress_percent)
+                return;
+
+        r = sd_bus_emit_signal(
+                        t->manager->bus,
+                        t->object_path,
+                        "org.freedesktop.import1.Transfer",
+                        "ProgressUpdate",
+                        "d",
+                        transfer_percent_as_double(t));
+        if (r < 0)
+                log_warning_errno(r, "Cannot emit progress update signal, ignoring: %m");
+
+        t->progress_percent_sent = t->progress_percent;
+}
 
 static void transfer_send_logs(Transfer *t, bool flush) {
         assert(t);
@@ -635,6 +658,8 @@ static int manager_on_notify(sd_event_source *s, int fd, uint32_t revents, void
         t->progress_percent = (unsigned) r;
 
         log_debug("Got percentage from client: %u%%", t->progress_percent);
+
+        transfer_send_progress_update(t);
         return 0;
 }
 
@@ -1369,6 +1394,10 @@ static const sd_bus_vtable transfer_vtable[] = {
                                  SD_BUS_PARAM(priority)
                                  SD_BUS_PARAM(line),
                                  0),
+        SD_BUS_SIGNAL_WITH_NAMES("ProgressUpdate",
+                                 "d",
+                                 SD_BUS_PARAM(progress),
+                                 0),
 
         SD_BUS_VTABLE_END,
 };
index a4e5809446c4c7b59548384b271e342c60ed0c5d..543a9ebdcb9712ed503b20c671c39efe48826cf6 100644 (file)
@@ -462,3 +462,75 @@ int terminal_tint_color(double hue, char **ret) {
 
         return 0;
 }
+
+void draw_progress_bar(const char *prefix, double percentage) {
+
+        fputs("\r", stderr);
+        if (prefix)
+                fputs(prefix, stderr);
+
+        if (!terminal_is_dumb()) {
+                size_t cols = columns();
+                size_t prefix_length = strlen_ptr(prefix);
+                size_t length = cols > prefix_length + 6 ? cols - prefix_length - 6 : 0;
+
+                fputs(ansi_highlight_green(), stderr);
+
+                if (length > 5 && percentage >= 0.0 && percentage <= 100.0) {
+                        size_t p = (size_t) (length * percentage / 100.0);
+                        bool separator_done = false;
+
+                        for (size_t i = 0; i < length; i++) {
+
+                                if (i <= p) {
+                                        if (get_color_mode() == COLOR_24BIT) {
+                                                uint8_t r8, g8, b8;
+                                                double z = i == 0 ? 0 : (((double) i / p) * 100);
+                                                hsv_to_rgb(145 /* green */, z, 33 + z*2/3, &r8, &g8, &b8);
+                                                fprintf(stderr, "\x1B[38;2;%u;%u;%um", r8, g8, b8);
+                                        }
+
+                                        fputs(special_glyph(SPECIAL_GLYPH_HORIZONTAL_FAT), stderr);
+                                } else if (i+1 < length && !separator_done) {
+                                        fputs(ansi_normal(), stderr);
+                                        fputc(' ', stderr);
+                                        separator_done = true;
+                                        fputs(ansi_grey(), stderr);
+                                } else
+                                        fputs(special_glyph(SPECIAL_GLYPH_HORIZONTAL_DOTTED), stderr);
+                        }
+
+                        fputs(ansi_normal(), stderr);
+                        fputc(' ', stderr);
+                }
+        }
+
+        fprintf(stderr,
+                "%s%3.0f%%%s",
+                ansi_highlight(),
+                percentage,
+                ansi_normal());
+
+        if (!terminal_is_dumb())
+                fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
+
+        fputc('\r', stderr);
+        fflush(stderr);
+}
+
+void clear_progress_bar(const char *prefix) {
+
+        fputc('\r', stderr);
+
+        if (terminal_is_dumb()) {
+                size_t l = strlen_ptr(prefix);
+                for (size_t i = 0; i < l; i ++)
+                        fputc(' ', stderr);
+
+                fputs("    ", stderr);
+        } else
+                fputs(ANSI_ERASE_TO_END_OF_LINE, stderr);
+
+        fputc('\r', stderr);
+        fflush(stderr);
+}
index 6069318be7778e17f5de6b398ebee9fe823c1cd2..940d34775bcb732c027918a009279cde9a255fc9 100644 (file)
@@ -49,3 +49,6 @@ static inline const char *green_check_mark_internal(char buffer[static GREEN_CHE
 #define COLOR_MARK_BOOL(b) ((b) ? GREEN_CHECK_MARK() : RED_CROSS_MARK())
 
 int terminal_tint_color(double hue, char **ret);
+
+void draw_progress_bar(const char *prefix, double percentage);
+void clear_progress_bar(const char *prefix);
index c628eaa7dbe9f3dfa46b59e627f0e8b0f38424a5..a0f38824808a96cdaac3c3922944d88217783b37 100644 (file)
@@ -380,6 +380,10 @@ executables += [
                 'sources' : files('test-process-util.c'),
                 'dependencies' : threads,
         },
+        test_template + {
+                'sources' : files('test-progress-bar.c'),
+                'type' : 'manual',
+        },
         test_template + {
                 'sources' : files('test-qrcode-util.c'),
                 'dependencies' : libdl,
index 67d9c7e65cdfd786eac8267c284958cd9864f6d4..ab2d1f5746cf22fa20c272f1a08f7dac6ef45fd6 100644 (file)
@@ -92,6 +92,8 @@ TEST(dump_special_glyphs) {
         dump_glyph(SPECIAL_GLYPH_TREE_SPACE);
         dump_glyph(SPECIAL_GLYPH_TREE_TOP);
         dump_glyph(SPECIAL_GLYPH_VERTICAL_DOTTED);
+        dump_glyph(SPECIAL_GLYPH_HORIZONTAL_DOTTED);
+        dump_glyph(SPECIAL_GLYPH_HORIZONTAL_FAT);
         dump_glyph(SPECIAL_GLYPH_TRIANGULAR_BULLET);
         dump_glyph(SPECIAL_GLYPH_BLACK_CIRCLE);
         dump_glyph(SPECIAL_GLYPH_WHITE_CIRCLE);
diff --git a/src/test/test-progress-bar.c b/src/test/test-progress-bar.c
new file mode 100644 (file)
index 0000000..b47adf0
--- /dev/null
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "pretty-print.h"
+#include "random-util.h"
+#include "tests.h"
+
+#define PROGRESS_PREFIX "test: "
+
+TEST(progress_bar) {
+
+        draw_progress_bar(PROGRESS_PREFIX, 0);
+
+        bool paused = false;
+
+        for (double d = 0; d <= 100; d += 0.5) {
+                usleep_safe(random_u64_range(20 * USEC_PER_MSEC));
+                draw_progress_bar(PROGRESS_PREFIX, d);
+
+                if (!paused && d >= 50) {
+                        clear_progress_bar(PROGRESS_PREFIX);
+                        fputs("Sleeping for 1s...", stdout);
+                        fflush(stdout);
+                        usleep_safe(USEC_PER_SEC);
+                        paused = true;
+                }
+        }
+
+        draw_progress_bar(PROGRESS_PREFIX, 100);
+        usleep_safe(300 * MSEC_PER_SEC);
+        clear_progress_bar(PROGRESS_PREFIX);
+        fputs("Done.\n", stdout);
+}
+
+DEFINE_TEST_MAIN(LOG_INFO);