mirror of
https://github.com/Dasharo/systemd.git
synced 2026-03-06 15:02:31 -08:00
importctl: draw a pretty progress bar while downloading
Everybody loves pretty terminal progress bar.
This commit is contained in:
@@ -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"○",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
34
src/test/test-progress-bar.c
Normal file
34
src/test/test-progress-bar.c
Normal file
@@ -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);
|
||||
Reference in New Issue
Block a user