/
githubmirror
/
fwupd
Обзор
Документация
Войти
/
githubmirror
/
fwupd
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/fu-console.c
915 строк
22 KB
Richard Hughes
Refactor the command line tools to share a common core
10 авг 2026, 17:02
10 авг 2026, 17:02
54ca24a
Код
Авторство
О чём код?
/* * Copyright 2017 Richard Hughes <richard@hughsie.com> * * SPDX-License-Identifier: LGPL-2.1-or-later */ #define G_LOG_DOMAIN "FuProgressBar" #include "config.h" #include <glib/gi18n.h> #include <stdio.h> #ifdef HAVE_READLINE #include <readline/history.h> #include <readline/readline.h> #endif #include "fu-console.h" #ifdef _WIN32 #include <wchar.h> #include <windows.h> #endif struct _FuConsole { GObject parent_instance; GMainContext *main_ctx; FwupdStatus status; gboolean spinner_count_up; /* width in visible chars */ guint spinner_idx; /* width in visible chars */ guint length_percentage; /* width in visible chars */ guint length_status; /* width in visible chars */ gdouble percentage; GSource *timer_source; gint64 last_animated; /* monotonic */ GTimer *time_elapsed; gdouble last_estimate; gboolean interactive; gboolean contents_to_clear; }; G_DEFINE_TYPE(FuConsole, fu_console, G_TYPE_OBJECT) gboolean fu_console_setup(FuConsole *self, GError **error) { #ifdef _WIN32 HANDLE out; DWORD mode = 0; /* enable VT sequences */ out = GetStdHandle(STD_OUTPUT_HANDLE); if (out == INVALID_HANDLE_VALUE) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get stdout [%u]", (guint)GetLastError()); return FALSE; } if (!GetConsoleMode(out, &mode)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to get mode [%u]", (guint)GetLastError()); return FALSE; } mode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING; if (!SetConsoleMode(out, mode)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set mode [%u]", (guint)GetLastError()); return FALSE; } if (!SetConsoleOutputCP(CP_UTF8)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set output UTF-8 [%u]", (guint)GetLastError()); return FALSE; } if (!SetConsoleCP(CP_UTF8)) { g_set_error(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "failed to set UTF-8 [%u]", (guint)GetLastError()); return FALSE; } #else if (isatty(fileno(stdin)) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a TTY"); return FALSE; } if (isatty(fileno(stdout)) == 0) { g_set_error_literal(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED, "not a TTY"); return FALSE; } #endif /* success */ return TRUE; } static void fu_console_erase_line(FuConsole *self) { if (!self->interactive) return; g_print("\033[G"); } static void fu_console_reset_line(FuConsole *self) { if (self->contents_to_clear) { fu_console_erase_line(self); /* re-enable cursor */ g_print("\033[?25h\n"); self->contents_to_clear = FALSE; } } void fu_console_print_kv(FuConsole *self, const gchar *title, const gchar *msg) { gsize title_len; g_auto(GStrv) lines = NULL; if (msg == NULL) return; fu_console_reset_line(self); g_print("%s:", title); /* pad */ title_len = fu_strwidth(title) + 1; lines = g_strsplit(msg, "\n", -1); for (guint j = 0; lines[j] != NULL; j++) { for (gsize i = title_len; i < 25; i++) g_print(" "); g_print("%s\n", lines[j]); title_len = 0; } } #ifndef HAVE_READLINE static gchar * readline(const gchar *prompt) /* nocheck:name */ { char buffer[64] = {0}; if (prompt != NULL) { g_print("%s", prompt); /* ensure the prompt reaches the terminal before we block on stdin */ fflush(stdout); } if (!fgets(buffer, sizeof(buffer), stdin)) return NULL; g_strdelimit(buffer, "\n", '\0'); return g_strndup(buffer, sizeof(buffer)); } #endif guint fu_console_input_uint(FuConsole *self, guint maxnum, const gchar *format, ...) { gint retval; guint answer = 0; va_list args; g_autofree gchar *tmp = NULL; g_autofree gchar *prompt = NULL; g_autofree gchar *retry_prompt = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); /* clear active progress spinner so the question is not mangled */ fu_console_reset_line(self); /* Pass prompt to readline() so it can redraw it when the terminal * is resized (SIGWINCH) - otherwise readline only knows about the empty * prompt it was given and the question text disappears on resize */ prompt = g_strdup_printf("%s [0-%u]: ", tmp, maxnum); do { g_autofree gchar *buffer = readline(retry_prompt != NULL ? retry_prompt : prompt); if (buffer == NULL) break; /* get a number */ retval = sscanf(buffer, "%u", &answer); /* positive */ if (retval == 1 && answer <= maxnum) break; if (retry_prompt == NULL) { g_autoptr(GString) str = g_string_new(NULL); g_string_append_printf( str, /* TRANSLATORS: the user isn't reading the question */ _("Please enter a number from 0 to %u:"), maxnum); g_string_append(str, " "); retry_prompt = g_string_free(g_steal_pointer(&str), FALSE); } } while (TRUE); return answer; } gboolean fu_console_input_bool(FuConsole *self, gboolean def, const gchar *format, ...) { va_list args; g_autofree gchar *tmp = NULL; g_autofree gchar *prompt = NULL; g_autofree gchar *retry_prompt = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); /* clear active progress spinner so the question is not mangled */ fu_console_reset_line(self); /* Pass prompt to readline() so it can redraw it when the terminal * is resized (SIGWINCH) - otherwise readline only knows about the empty * prompt it was given and the question text disappears on resize */ prompt = g_strdup_printf("%s [%s]: ", tmp, def ? "Y|n" : "y|N"); do { g_autofree gchar *buffer = readline(retry_prompt != NULL ? retry_prompt : prompt); if (buffer == NULL || !strlen(buffer)) return def; buffer[0] = g_ascii_toupper(buffer[0]); if (g_strcmp0(buffer, "Y") == 0) return TRUE; if (g_strcmp0(buffer, "N") == 0) return FALSE; if (retry_prompt == NULL) { g_autoptr(GString) str = g_string_new(NULL); g_string_append_printf(str, /* TRANSLATORS: the user isn't reading the question -- %1 is 'Y' and %2 is 'N' */ _("Please enter either %s or %s:"), "Y", "N"); g_string_append(str, " "); retry_prompt = g_string_free(g_steal_pointer(&str), FALSE); } } while (TRUE); return FALSE; } gchar * fu_console_input_string(FuConsole *self, const gchar *format, ...) { va_list args; g_autofree gchar *tmp = NULL; g_autofree gchar *prompt = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); /* clear active progress spinner so the question is not mangled */ fu_console_reset_line(self); prompt = g_strdup_printf("%s: ", tmp); return readline(prompt); } static GPtrArray * fu_console_strsplit_words(const gchar *text, guint line_len) { g_auto(GStrv) tokens = NULL; g_autoptr(GPtrArray) lines = g_ptr_array_new_with_free_func(g_free); g_autoptr(GString) curline = g_string_new(NULL); /* sanity check */ if (text == NULL || text[0] == '\0') return NULL; if (line_len == 0) return NULL; /* tokenize the string */ tokens = g_strsplit(text, " ", -1); for (guint i = 0; tokens[i] != NULL; i++) { /* current line plus new token is okay */ if (curline->len + fu_strwidth(tokens[i]) < line_len) { g_string_append_printf(curline, "%s ", tokens[i]); continue; } /* too long, so remove space, add newline and dump */ if (curline->len > 0) g_string_truncate(curline, curline->len - 1); g_ptr_array_add(lines, g_strdup(curline->str)); g_string_truncate(curline, 0); g_string_append_printf(curline, "%s ", tokens[i]); } /* any incomplete line? */ if (curline->len > 0) { g_string_truncate(curline, curline->len - 1); g_ptr_array_add(lines, g_strdup(curline->str)); } return g_steal_pointer(&lines); } static void fu_console_box_line(const gchar *start, const gchar *text, const gchar *end, const gchar *padding, guint width) { guint offset = 0; if (start != NULL) { offset += fu_strwidth(start); g_print("%s", start); } if (text != NULL) { offset += fu_strwidth(text); g_print("%s", text); } if (end != NULL) offset += fu_strwidth(end); for (guint i = offset; i < width; i++) g_print("%s", padding); if (end != NULL) g_print("%s\n", end); } void fu_console_line(FuConsole *self, guint width) { g_autoptr(GString) str = g_string_new_len(NULL, width); for (guint i = 0; i < width; i++) g_string_append(str, "─"); fu_console_print_literal(self, str->str); } void fu_console_box(FuConsole *self, const gchar *title, const gchar *body, guint width) { /* nothing to do */ if (title == NULL && body == NULL) return; /* header */ fu_console_reset_line(self); fu_console_box_line("╔", NULL, "╗", "═", width); /* optional title */ if (title != NULL) { g_autoptr(GPtrArray) lines = fu_console_strsplit_words(title, width - 4); for (guint j = 0; j < lines->len; j++) { const gchar *line = g_ptr_array_index(lines, j); fu_console_box_line("║ ", line, " ║", " ", width); } } /* join */ if (title != NULL && body != NULL) fu_console_box_line("╠", NULL, "╣", "═", width); /* optional body */ if (body != NULL) { gboolean has_nonempty = FALSE; g_auto(GStrv) split = g_strsplit(body, "\n", -1); for (guint i = 0; split[i] != NULL; i++) { g_autoptr(GPtrArray) lines = fu_console_strsplit_words(split[i], width - 4); if (lines == NULL) { if (has_nonempty) { fu_console_box_line("║ ", NULL, " ║", " ", width); has_nonempty = FALSE; } continue; } for (guint j = 0; j < lines->len; j++) { const gchar *line = g_ptr_array_index(lines, j); fu_console_box_line("║ ", line, " ║", " ", width); } has_nonempty = TRUE; } } /* footer */ fu_console_box_line("╚", NULL, "╝", "═", width); } static const gchar * fu_console_status_to_string(FwupdStatus status) { switch (status) { case FWUPD_STATUS_IDLE: /* TRANSLATORS: daemon is inactive */ return _("Idle…"); break; case FWUPD_STATUS_DECOMPRESSING: /* TRANSLATORS: decompressing the firmware file */ return _("Decompressing…"); break; case FWUPD_STATUS_LOADING: /* TRANSLATORS: parsing the firmware information */ return _("Loading…"); break; case FWUPD_STATUS_DEVICE_RESTART: /* TRANSLATORS: restarting the device to pick up new F/W */ return _("Restarting device…"); break; case FWUPD_STATUS_DEVICE_READ: /* TRANSLATORS: reading from the flash chips */ return _("Reading…"); break; case FWUPD_STATUS_DEVICE_WRITE: /* TRANSLATORS: writing to the flash chips */ return _("Writing…"); break; case FWUPD_STATUS_DEVICE_ERASE: /* TRANSLATORS: erasing contents of the flash chips */ return _("Erasing…"); break; case FWUPD_STATUS_DEVICE_VERIFY: /* TRANSLATORS: verifying we wrote the firmware correctly */ return _("Verifying…"); break; case FWUPD_STATUS_SCHEDULING: /* TRANSLATORS: scheduling an update to be done on the next boot */ return _("Scheduling…"); break; case FWUPD_STATUS_DOWNLOADING: /* TRANSLATORS: downloading from a remote server */ return _("Downloading…"); break; case FWUPD_STATUS_WAITING_FOR_AUTH: /* TRANSLATORS: waiting for user to authenticate */ return _("Authenticating…"); break; case FWUPD_STATUS_DEVICE_BUSY: case FWUPD_STATUS_WAITING_FOR_USER: /* TRANSLATORS: waiting for device to do something */ return _("Waiting…"); break; default: break; } /* TRANSLATORS: current daemon status is unknown */ return _("Unknown"); } static gboolean _fu_status_is_predictable(FwupdStatus status) { if (status == FWUPD_STATUS_DEVICE_ERASE) return TRUE; if (status == FWUPD_STATUS_DEVICE_VERIFY) return TRUE; if (status == FWUPD_STATUS_DEVICE_READ) return TRUE; if (status == FWUPD_STATUS_DEVICE_WRITE) return TRUE; if (status == FWUPD_STATUS_DOWNLOADING) return TRUE; return FALSE; } static gboolean fu_console_estimate_ready(FuConsole *self, gdouble percentage) { gdouble old; gdouble elapsed; /* now invalid */ if (!fwupd_percentage_is_valid(percentage)) { g_timer_start(self->time_elapsed); self->last_estimate = 0; return FALSE; } /* allow-list things that make sense... */ if (!_fu_status_is_predictable(self->status)) return FALSE; old = self->last_estimate; elapsed = g_timer_elapsed(self->time_elapsed, NULL); self->last_estimate = elapsed / percentage * (100.0 - percentage); /* estimate is ready if we have decreased */ return old > self->last_estimate; } static gchar * fu_console_time_remaining_str(FuConsole *self) { /* less than 5 seconds remaining */ if (self->last_estimate < 5) return NULL; /* less than 60 seconds remaining */ if (self->last_estimate < 60) { /* TRANSLATORS: time remaining for completing firmware flash */ return g_strdup(_("Less than one minute remaining")); } return g_strdup_printf( /* TRANSLATORS: more than a minute */ ngettext("%.0f minute remaining", "%.0f minutes remaining", self->last_estimate / 60), self->last_estimate / 60); } static const gchar * fu_console_get_progress_part(gdouble frac) { static const gchar *data[] = {"⣀", "⣄", "⣤", "⣦", "⣶", "⣷"}; if (frac <= 0) return " "; if (frac >= 1.0) return "⣿"; return data[(guint)(frac * G_N_ELEMENTS(data))]; } static void fu_console_refresh(FuConsole *self) { const gchar *title; guint i; g_autoptr(GString) str = g_string_new(NULL); /* sanity check */ if (self->status == FWUPD_STATUS_IDLE || self->status == FWUPD_STATUS_UNKNOWN) return; /* erase previous line */ fu_console_erase_line(self); /* add status */ title = fu_console_status_to_string(self->status); g_string_append(str, title); for (i = fu_strwidth(str->str); i < self->length_status; i++) g_string_append_c(str, ' '); /* add console */ g_string_append(str, "▕"); if (self->percentage >= 100.0) { for (i = 0; i < self->length_percentage - 1; i++) g_string_append(str, "⣿"); } else if (fwupd_percentage_is_valid(self->percentage)) { gdouble midpoint = (self->length_percentage - 1) * self->percentage / 100.0; guint midpoint_floor = (guint)midpoint; for (i = 0; i < midpoint_floor; i++) g_string_append(str, "⣿"); g_string_append(str, fu_console_get_progress_part(midpoint - midpoint_floor)); for (i = midpoint_floor + 1; i < self->length_percentage - 1; i++) g_string_append_c(str, ' '); } else { const gchar *chars[] = { "⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷", }; for (i = 0; i < self->spinner_idx; i++) g_string_append_c(str, ' '); g_string_append(str, chars[i / 2 % G_N_ELEMENTS(chars)]); for (i = i + 1; i < self->length_percentage - 1; i++) g_string_append_c(str, ' '); } g_string_append(str, "▏"); /* once we have good data show an estimate of time remaining */ if (fu_console_estimate_ready(self, self->percentage)) { g_autofree gchar *remaining = fu_console_time_remaining_str(self); if (remaining != NULL) g_string_append_printf(str, " %s…", remaining); } /* dump to screen, disabling the cursor */ g_print("%s\033[?25l", str->str); self->contents_to_clear = TRUE; } gchar * fu_console_color_format(const gchar *text, FuConsoleColor fg_color) { if (g_getenv("NO_COLOR") != NULL) return g_strdup(text); return g_strdup_printf("\033[%um\033[1m%s\033[0m", fg_color, text); } /** * fu_console_print_full: * @self: a #FuConsole * @flags; a #FuConsolePrintFlags, e.g. %FU_CONSOLE_PRINT_FLAG_STDERR * @text: string * * Clears the console, and prints the text. **/ void fu_console_print_full(FuConsole *self, FuConsolePrintFlags flags, const gchar *format, ...) { va_list args; g_autoptr(GString) str = g_string_new(NULL); va_start(args, format); g_string_append_vprintf(str, format, args); va_end(args); if (flags & FU_CONSOLE_PRINT_FLAG_LIST_ITEM) g_string_prepend(str, " • "); if (flags & FU_CONSOLE_PRINT_FLAG_WARNING) { /* TRANSLATORS: this is a prefix on the console */ g_autofree gchar *fmt = fu_console_color_format(_("WARNING"), FU_CONSOLE_COLOR_RED); g_string_prepend(str, ": "); g_string_prepend(str, fmt); flags |= FU_CONSOLE_PRINT_FLAG_STDERR; } if (flags & FU_CONSOLE_PRINT_FLAG_NEWLINE) g_string_append(str, "\n"); fu_console_reset_line(self); if (flags & FU_CONSOLE_PRINT_FLAG_STDERR) { g_printerr("%s", str->str); } else { g_print("%s", str->str); } } void fu_console_print_literal(FuConsole *self, const gchar *text) { fu_console_reset_line(self); g_print("%s\n", text); } /** * fu_console_print: * @self: a #FuConsole * @text: string * * Clears the console, prints the text and prints a newline. **/ void fu_console_print(FuConsole *self, const gchar *format, ...) { va_list args; g_autofree gchar *tmp = NULL; va_start(args, format); tmp = g_strdup_vprintf(format, args); va_end(args); fu_console_print_literal(self, tmp); } /** * fu_console_set_progress_title: * @self: A #FuConsole * @title: A string * * Sets console title **/ void fu_console_set_progress_title(FuConsole *self, const gchar *title) { if (!self->interactive) return; fu_console_erase_line(self); g_print("%s\n", title); fu_console_refresh(self); } /** * fu_console_set_main_context: * @self: A #FuConsole * @main_ctx: (nullable): main context * * Sets console main context to use for animations. **/ void fu_console_set_main_context(FuConsole *self, GMainContext *main_ctx) { self->main_ctx = g_main_context_ref(main_ctx); } static void fu_console_spin_inc(FuConsole *self) { /* reset */ self->last_animated = g_get_monotonic_time(); /* up to down */ if (self->spinner_count_up) { if (++self->spinner_idx > self->length_percentage - 3) self->spinner_count_up = FALSE; } else { if (--self->spinner_idx == 0) self->spinner_count_up = TRUE; } } static gboolean fu_console_spin_cb(gpointer user_data) { FuConsole *self = FU_CONSOLE(user_data); /* move the spinner index up to down */ fu_console_spin_inc(self); /* update the terminal */ fu_console_refresh(self); return G_SOURCE_CONTINUE; } static void fu_console_spin_end(FuConsole *self) { if (self->timer_source != NULL) { g_source_destroy(self->timer_source); self->timer_source = NULL; /* reset when the spinner has been stopped */ g_timer_start(self->time_elapsed); } /* go back to the start when we next go into unknown percentage mode */ self->spinner_idx = 0; self->spinner_count_up = TRUE; } static void fu_console_spin_start(FuConsole *self) { if (self->timer_source != NULL) return; self->timer_source = g_timeout_source_new(40); g_source_set_callback(self->timer_source, fu_console_spin_cb, self, NULL); g_source_attach(self->timer_source, self->main_ctx); } /** * fu_console_set_progress: * @self: A #FuConsole * @status: A #FwupdStatus * @percentage: value * * Refreshes the progress bar with the new percentage and status. **/ void fu_console_set_progress(FuConsole *self, FwupdStatus status, gdouble percentage) { g_return_if_fail(FU_IS_CONSOLE(self)); /* not useful */ if (status == FWUPD_STATUS_UNKNOWN) return; /* cache */ self->status = status; self->percentage = percentage; /* dumb */ if (!self->interactive) { if (fwupd_percentage_is_valid(percentage)) { g_printerr("%s: %.1f%%\n", fu_console_status_to_string(status), percentage); } else { g_printerr("%s\n", fu_console_status_to_string(status)); } return; } /* if the main loop isn't spinning and we've not had a chance to * execute the callback just do the refresh now manually */ if (percentage <= 0.0 && status != FWUPD_STATUS_IDLE && self->status != FWUPD_STATUS_UNKNOWN) { if ((g_get_monotonic_time() - self->last_animated) / 1000 > 40) { fu_console_spin_inc(self); fu_console_refresh(self); } } /* enable or disable the spinner timeout */ if (fwupd_percentage_is_valid(percentage)) { fu_console_spin_end(self); } else { fu_console_spin_start(self); } /* update the terminal */ fu_console_refresh(self); } /** * fu_console_set_interactive: * @self: A #FuConsole * @interactive: #gboolean * * Marks the console as interactive or not **/ void fu_console_set_interactive(FuConsole *self, gboolean interactive) { g_return_if_fail(FU_IS_CONSOLE(self)); self->interactive = interactive; } gboolean fu_console_get_interactive(FuConsole *self) { g_return_val_if_fail(FU_IS_CONSOLE(self), FALSE); return self->interactive; } /** * fu_console_set_status_length: * @self: A #FuConsole * @len: unsigned integer * * Sets the width of the progressbar status, which must be greater that 3. **/ void fu_console_set_status_length(FuConsole *self, guint len) { g_return_if_fail(FU_IS_CONSOLE(self)); g_return_if_fail(len > 3); self->length_status = len; } /** * fu_console_set_percentage_length: * @self: A #FuConsole * @len: unsigned integer * * Sets the width of the progressbar percentage, which must be greater that 3. **/ void fu_console_set_percentage_length(FuConsole *self, guint len) { g_return_if_fail(FU_IS_CONSOLE(self)); g_return_if_fail(len > 3); self->length_percentage = len; } void fu_console_beep(FuConsole *self, guint count) { for (guint i = 0; i < count; i++) { if (i > 0) g_usleep(250000); g_print("\007"); } } static void fu_console_init(FuConsole *self) { self->length_percentage = 40; self->length_status = 25; self->spinner_count_up = TRUE; self->time_elapsed = g_timer_new(); self->interactive = TRUE; self->percentage = -1; } static void fu_console_finalize(GObject *obj) { FuConsole *self = FU_CONSOLE(obj); fu_console_reset_line(self); if (self->timer_source != NULL) { g_source_destroy(self->timer_source); g_source_unref(self->timer_source); } if (self->main_ctx != NULL) g_main_context_unref(self->main_ctx); g_timer_destroy(self->time_elapsed); G_OBJECT_CLASS(fu_console_parent_class)->finalize(obj); } static void fu_console_class_init(FuConsoleClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS(klass); object_class->finalize = fu_console_finalize; } /** * fu_console_new: * * Creates a new #FuConsole **/ FuConsole * fu_console_new(void) { FuConsole *self; self = g_object_new(FU_TYPE_CONSOLE, NULL); return FU_CONSOLE(self); }