]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
sd-boot: Add support for changing console mode at runtime 20483/head
authorJan Janssen <medhefgo@web.de>
Sun, 15 Aug 2021 11:44:47 +0000 (13:44 +0200)
committerJan Janssen <medhefgo@web.de>
Wed, 15 Sep 2021 14:32:18 +0000 (16:32 +0200)
man/systemd-boot.xml
src/boot/efi/boot.c
src/boot/efi/console.c
src/boot/efi/console.h
src/boot/efi/util.c
src/boot/efi/util.h

index d3306593e413f86886421f71f9284dafb8cfcdad..8685ed50ffd59b764afef6c3c8c95e7127109b87 100644 (file)
         <listitem><para>Decrease the timeout</para></listitem>
       </varlistentry>
 
+      <varlistentry>
+        <term><keycap>r</keycap></term>
+        <listitem><para>Change screen resolution, skipping any unsupported modes.</para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><keycap>R</keycap></term>
+        <listitem><para>Reset screen resolution to firmware or configuration file default.</para></listitem>
+      </varlistentry>
+
       <varlistentry>
         <term><keycap>p</keycap></term>
         <listitem><para>Print status</para></listitem>
index df369b426b2669abc14dd1115bef3489f58a82f0..af02cfb7e91a65d2e75e8334e3dd11d52f436eec 100644 (file)
@@ -70,6 +70,7 @@ typedef struct {
         BOOLEAN auto_firmware;
         BOOLEAN force_menu;
         INT64 console_mode;
+        INT64 console_mode_efivar;
         RandomSeedMode random_seed_mode;
 } Config;
 
@@ -374,9 +375,7 @@ static VOID print_status(Config *config, CHAR16 *loaded_image_path) {
         assert(config);
         assert(loaded_image_path);
 
-        uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, COLOR_NORMAL);
-        uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
-
+        clear_screen(COLOR_NORMAL);
         console_query_mode(&x_max, &y_max);
 
         Print(L"systemd-boot version:   " GIT_VERSION "\n");
@@ -385,6 +384,7 @@ static VOID print_status(Config *config, CHAR16 *loaded_image_path) {
         Print(L"UEFI specification:     %d.%02d\n", ST->Hdr.Revision >> 16, ST->Hdr.Revision & 0xffff);
         Print(L"firmware vendor:        %s\n", ST->FirmwareVendor);
         Print(L"firmware version:       %d.%02d\n", ST->FirmwareRevision >> 16, ST->FirmwareRevision & 0xffff);
+        Print(L"console mode:           %d/%ld\n", ST->ConOut->Mode->Mode, ST->ConOut->Mode->MaxMode - 1LL);
         Print(L"console size:           %d x %d\n", x_max, y_max);
         Print(L"SecureBoot:             %s\n", yes_no(secure_boot_enabled()));
 
@@ -497,8 +497,6 @@ static VOID print_status(Config *config, CHAR16 *loaded_image_path) {
                 Print(L"\n--- press key ---\n\n");
                 console_key_read(&key, 0);
         }
-
-        uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
 }
 
 static BOOLEAN menu_run(
@@ -511,98 +509,115 @@ static BOOLEAN menu_run(
         assert(loaded_image_path);
 
         EFI_STATUS err;
-        UINTN visible_max;
+        UINTN visible_max = 0;
         UINTN idx_highlight = config->idx_default;
         UINTN idx_highlight_prev = 0;
-        UINTN idx_first;
-        UINTN idx_last;
-        BOOLEAN refresh = TRUE;
-        BOOLEAN highlight = FALSE;
-        UINTN line_width = 0;
-        UINTN entry_padding = 3;
-        CHAR16 **lines;
-        UINTN x_start;
-        UINTN y_start;
-        UINTN x_max;
-        UINTN y_max;
-        CHAR16 *status = NULL;
-        CHAR16 *clearline;
+        UINTN idx_first = 0, idx_last = 0;
+        BOOLEAN new_mode = TRUE, clear = TRUE;
+        BOOLEAN refresh = TRUE, highlight = FALSE;
+        UINTN x_start = 0, y_start = 0, y_status = 0;
+        UINTN x_max, y_max;
+        CHAR16 **lines = NULL, *status = NULL, *clearline = NULL;
         UINTN timeout_remain = config->timeout_sec;
         INT16 idx;
-        BOOLEAN exit = FALSE;
-        BOOLEAN run = TRUE;
+        BOOLEAN exit = FALSE, run = TRUE;
+        INT64 console_mode_initial = ST->ConOut->Mode->Mode, console_mode_efivar_saved = config->console_mode_efivar;
 
         graphics_mode(FALSE);
         uefi_call_wrapper(ST->ConIn->Reset, 2, ST->ConIn, FALSE);
         uefi_call_wrapper(ST->ConOut->EnableCursor, 2, ST->ConOut, FALSE);
-        uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, COLOR_NORMAL);
 
         /* draw a single character to make ClearScreen work on some firmware */
         Print(L" ");
 
-        err = console_set_mode(config->console_mode);
-        uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
-        if (EFI_ERROR(err))
+        err = console_set_mode(config->console_mode_efivar != CONSOLE_MODE_KEEP ?
+                               config->console_mode_efivar : config->console_mode);
+        if (EFI_ERROR(err)) {
+                clear_screen(COLOR_NORMAL);
                 log_error_stall(L"Error switching console mode: %r", err);
-        console_query_mode(&x_max, &y_max);
+        }
 
-        visible_max = y_max - 2;
+        while (!exit) {
+                UINT64 key;
 
-        /* Drawing entries starts at idx_first until idx_last. We want to make
-         * sure that idx_highlight is centered, but not if we are close to the
-         * beginning/end of the entry list. Otherwise we would have a half-empty
-         * screen. */
-        if (config->entry_count <= visible_max || idx_highlight <= visible_max / 2)
-                idx_first = 0;
-        else if (idx_highlight >= config->entry_count - (visible_max / 2))
-                idx_first = config->entry_count - visible_max;
-        else
-                idx_first = idx_highlight - (visible_max / 2);
-        idx_last = idx_first + visible_max - 1;
+                if (new_mode) {
+                        UINTN line_width = 0, entry_padding = 3;
 
-        /* length of the longest entry */
-        for (UINTN i = 0; i < config->entry_count; i++)
-                line_width = MAX(line_width, StrLen(config->entries[i]->title_show));
-        line_width = MIN(line_width + 2 * entry_padding, x_max);
+                        console_query_mode(&x_max, &y_max);
 
-        /* offsets to center the entries on the screen */
-        x_start = (x_max - (line_width)) / 2;
-        if (config->entry_count < visible_max)
-                y_start = ((visible_max - config->entry_count) / 2) + 1;
-        else
-                y_start = 0;
+                        /* account for padding+status */
+                        visible_max = y_max - 2;
 
-        /* menu entries title lines */
-        lines = AllocatePool(sizeof(CHAR16 *) * config->entry_count);
-        for (UINTN i = 0; i < config->entry_count; i++) {
-                UINTN j;
+                        /* Drawing entries starts at idx_first until idx_last. We want to make
+                        * sure that idx_highlight is centered, but not if we are close to the
+                        * beginning/end of the entry list. Otherwise we would have a half-empty
+                        * screen. */
+                        if (config->entry_count <= visible_max || idx_highlight <= visible_max / 2)
+                                idx_first = 0;
+                        else if (idx_highlight >= config->entry_count - (visible_max / 2))
+                                idx_first = config->entry_count - visible_max;
+                        else
+                                idx_first = idx_highlight - (visible_max / 2);
+                        idx_last = idx_first + visible_max - 1;
+
+                        /* length of the longest entry */
+                        for (UINTN i = 0; i < config->entry_count; i++)
+                                line_width = MAX(line_width, StrLen(config->entries[i]->title_show));
+                        line_width = MIN(line_width + 2 * entry_padding, x_max);
+
+                        /* offsets to center the entries on the screen */
+                        x_start = (x_max - (line_width)) / 2;
+                        if (config->entry_count < visible_max)
+                                y_start = ((visible_max - config->entry_count) / 2) + 1;
+                        else
+                                y_start = 0;
+
+                        /* Put status line after the entry list, but give it some breathing room. */
+                        y_status = MIN(y_start + MIN(visible_max, config->entry_count) + 4, y_max - 1);
+
+                        if (lines) {
+                                for (UINTN i = 0; i < config->entry_count; i++)
+                                        FreePool(lines[i]);
+                                FreePool(lines);
+                                FreePool(clearline);
+                        }
 
-                lines[i] = AllocatePool(((line_width + 1) * sizeof(CHAR16)));
-                UINTN padding = (line_width - MIN(StrLen(config->entries[i]->title_show), line_width)) / 2;
+                        /* menu entries title lines */
+                        lines = AllocatePool(sizeof(CHAR16 *) * config->entry_count);
+                        for (UINTN i = 0; i < config->entry_count; i++) {
+                                UINTN j, padding;
 
-                for (j = 0; j < padding; j++)
-                        lines[i][j] = ' ';
+                                lines[i] = AllocatePool(((line_width + 1) * sizeof(CHAR16)));
+                                padding = (line_width - MIN(StrLen(config->entries[i]->title_show), line_width)) / 2;
 
-                for (UINTN k = 0; config->entries[i]->title_show[k] != '\0' && j < line_width; j++, k++)
-                        lines[i][j] = config->entries[i]->title_show[k];
+                                for (j = 0; j < padding; j++)
+                                        lines[i][j] = ' ';
 
-                for (; j < line_width; j++)
-                        lines[i][j] = ' ';
-                lines[i][line_width] = '\0';
-        }
+                                for (UINTN k = 0; config->entries[i]->title_show[k] != '\0' && j < line_width; j++, k++)
+                                        lines[i][j] = config->entries[i]->title_show[k];
 
-        clearline = AllocatePool((x_max+1) * sizeof(CHAR16));
-        for (UINTN i = 0; i < x_max; i++)
-                clearline[i] = ' ';
-        clearline[x_max] = 0;
+                                for (; j < line_width; j++)
+                                        lines[i][j] = ' ';
+                                lines[i][line_width] = '\0';
+                        }
 
-        while (!exit) {
-                UINT64 key;
+                        clearline = AllocatePool((x_max+1) * sizeof(CHAR16));
+                        for (UINTN i = 0; i < x_max; i++)
+                                clearline[i] = ' ';
+                        clearline[x_max] = 0;
+
+                        new_mode = FALSE;
+                        clear = TRUE;
+                }
+
+                if (clear) {
+                        clear_screen(COLOR_NORMAL);
+                        clear = FALSE;
+                        refresh = TRUE;
+                }
 
                 if (refresh) {
-                        for (UINTN i = 0; i < config->entry_count; i++) {
-                                if (i < idx_first || i > idx_last)
-                                        continue;
+                        for (UINTN i = idx_first; i <= idx_last && i < config->entry_count; i++) {
                                 print_at(x_start, y_start + i - idx_first,
                                          (i == idx_highlight) ? COLOR_HIGHLIGHT : COLOR_ENTRY,
                                          lines[i]);
@@ -638,7 +653,7 @@ static BOOLEAN menu_run(
                                 x = (x_max - len) / 2;
                         else
                                 x = 0;
-                        print_at(0, y_max - 1, COLOR_NORMAL, clearline + (x_max - x));
+                        print_at(0, y_status, COLOR_NORMAL, clearline + (x_max - x));
                         uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, status);
                         uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, clearline+1 + x + len);
                 }
@@ -660,7 +675,7 @@ static BOOLEAN menu_run(
                 if (status) {
                         FreePool(status);
                         status = NULL;
-                        print_at(0, y_max - 1, COLOR_NORMAL, clearline + 1);
+                        print_at(0, y_status, COLOR_NORMAL, clearline + 1);
                 }
 
                 idx_highlight_prev = idx_highlight;
@@ -722,7 +737,7 @@ static BOOLEAN menu_run(
                 case KEYPRESS(0, 0, 'H'):
                 case KEYPRESS(0, 0, '?'):
                         /* This must stay below 80 characters! Q/v/Ctrl+l deliberately not advertised. */
-                        status = StrDuplicate(L"(d)efault (t/T)timeout (e)dit (p)rint (h)elp");
+                        status = StrDuplicate(L"(d)efault (t/T)timeout (e)dit (r/R)resolution (p)rint (h)elp");
                         break;
 
                 case KEYPRESS(0, 0, 'Q'):
@@ -802,9 +817,9 @@ static BOOLEAN menu_run(
                          * causing a scroll to happen that screws with our beautiful boot loader output.
                          * Since we cannot paint the last character of the edit line, we simply start
                          * at x-offset 1 for symmetry. */
-                        print_at(1, y_max - 1, COLOR_EDIT, clearline + 2);
-                        exit = line_edit(config->entries[idx_highlight]->options, &config->options_edit, x_max-2, y_max-1);
-                        print_at(1, y_max - 1, COLOR_NORMAL, clearline + 2);
+                        print_at(1, y_status, COLOR_EDIT, clearline + 2);
+                        exit = line_edit(config->entries[idx_highlight]->options, &config->options_edit, x_max - 2, y_status);
+                        print_at(1, y_status, COLOR_NORMAL, clearline + 2);
                         break;
 
                 case KEYPRESS(0, 0, 'v'):
@@ -816,12 +831,35 @@ static BOOLEAN menu_run(
                 case KEYPRESS(0, 0, 'p'):
                 case KEYPRESS(0, 0, 'P'):
                         print_status(config, loaded_image_path);
-                        refresh = TRUE;
+                        clear = TRUE;
                         break;
 
                 case KEYPRESS(EFI_CONTROL_PRESSED, 0, 'l'):
                 case KEYPRESS(EFI_CONTROL_PRESSED, 0, CHAR_CTRL('l')):
-                        refresh = TRUE;
+                        clear = TRUE;
+                        break;
+
+                case KEYPRESS(0, 0, 'r'):
+                        err = console_set_mode(CONSOLE_MODE_NEXT);
+                        if (EFI_ERROR(err))
+                                status = PoolPrint(L"Error changing console mode: %r", err);
+                        else {
+                                config->console_mode_efivar = ST->ConOut->Mode->Mode;
+                                status = PoolPrint(L"Console mode changed to %ld.", config->console_mode_efivar);
+                        }
+                        new_mode = TRUE;
+                        break;
+
+                case KEYPRESS(0, 0, 'R'):
+                        config->console_mode_efivar = CONSOLE_MODE_KEEP;
+                        err = console_set_mode(config->console_mode == CONSOLE_MODE_KEEP ?
+                                               console_mode_initial : config->console_mode);
+                        if (EFI_ERROR(err))
+                                status = PoolPrint(L"Error resetting console mode: %r", err);
+                        else
+                                status = PoolPrint(L"Console mode reset to %s default.",
+                                                   config->console_mode == CONSOLE_MODE_KEEP ? L"firmware" : L"configuration file");
+                        new_mode = TRUE;
                         break;
 
                 default:
@@ -849,13 +887,23 @@ static BOOLEAN menu_run(
 
         *chosen_entry = config->entries[idx_highlight];
 
+        /* The user is likely to cycle through several modes before
+         * deciding to keep one. Therefore, we update the EFI var after
+         * we left the menu to reduce nvram writes. */
+        if (console_mode_efivar_saved != config->console_mode_efivar) {
+                if (config->console_mode_efivar == CONSOLE_MODE_KEEP)
+                        efivar_set(LOADER_GUID, L"LoaderConfigConsoleMode", NULL, EFI_VARIABLE_NON_VOLATILE);
+                else
+                        efivar_set_uint_string(LOADER_GUID, L"LoaderConfigConsoleMode",
+                                               config->console_mode_efivar, EFI_VARIABLE_NON_VOLATILE);
+        }
+
         for (UINTN i = 0; i < config->entry_count; i++)
                 FreePool(lines[i]);
         FreePool(lines);
         FreePool(clearline);
 
-        uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, COLOR_NORMAL);
-        uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
+        clear_screen(COLOR_NORMAL);
         return run;
 }
 
@@ -1398,7 +1446,7 @@ static VOID config_entry_add_from_file(
 
 static VOID config_load_defaults(Config *config, EFI_FILE *root_dir) {
         _cleanup_freepool_ CHAR8 *content = NULL;
-        UINTN sec;
+        UINTN value;
         EFI_STATUS err;
 
         assert(root_dir);
@@ -1410,27 +1458,32 @@ static VOID config_load_defaults(Config *config, EFI_FILE *root_dir) {
                 .random_seed_mode = RANDOM_SEED_WITH_SYSTEM_TOKEN,
                 .idx_default_efivar = -1,
                 .console_mode = CONSOLE_MODE_KEEP,
+                .console_mode_efivar = CONSOLE_MODE_KEEP,
         };
 
         err = file_read(root_dir, L"\\loader\\loader.conf", 0, 0, &content, NULL);
         if (!EFI_ERROR(err))
                 config_defaults_load_from_file(config, content);
 
-        err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigTimeout", &sec);
+        err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigTimeout", &value);
         if (!EFI_ERROR(err)) {
-                config->timeout_sec_efivar = sec > INTN_MAX ? INTN_MAX : sec;
-                config->timeout_sec = sec;
+                config->timeout_sec_efivar = value > INTN_MAX ? INTN_MAX : value;
+                config->timeout_sec = value;
         } else
                 config->timeout_sec_efivar = -1;
 
-        err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigTimeoutOneShot", &sec);
+        err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigTimeoutOneShot", &value);
         if (!EFI_ERROR(err)) {
                 /* Unset variable now, after all it's "one shot". */
                 (void) efivar_set(LOADER_GUID, L"LoaderConfigTimeoutOneShot", NULL, EFI_VARIABLE_NON_VOLATILE);
 
-                config->timeout_sec = sec;
+                config->timeout_sec = value;
                 config->force_menu = TRUE; /* force the menu when this is set */
         }
+
+        err = efivar_get_uint_string(LOADER_GUID, L"LoaderConfigConsoleMode", &value);
+        if (!EFI_ERROR(err))
+                config->console_mode_efivar = value;
 }
 
 static VOID config_load_entries(
index 5f1b3500dbc884fa4dec8f2ee365cb51e838b051..b581c21bdab0c69d4ed45892de56b720ff6cde34 100644 (file)
@@ -198,6 +198,21 @@ EFI_STATUS console_set_mode(INT64 mode) {
                         return change_mode(CONSOLE_MODE_RANGE_MIN);
                 return EFI_SUCCESS;
 
+        case CONSOLE_MODE_NEXT:
+                if (ST->ConOut->Mode->MaxMode <= CONSOLE_MODE_RANGE_MIN)
+                        return EFI_UNSUPPORTED;
+
+                mode = MAX(CONSOLE_MODE_RANGE_MIN, ST->ConOut->Mode->Mode);
+                do {
+                        mode = (mode + 1) % ST->ConOut->Mode->MaxMode;
+                        if (!EFI_ERROR(change_mode(mode)))
+                                break;
+                        /* If this mode is broken/unsupported, try the next.
+                         * If mode is 0, we wrapped around and should stop. */
+                } while (mode > CONSOLE_MODE_RANGE_MIN);
+
+                return EFI_SUCCESS;
+
         case CONSOLE_MODE_AUTO:
                 return change_mode(get_auto_mode());
 
index ec1a96871a2c13417801958f1be3c74b0f5fdd42..90086028c04142255822e13901048f66a08b8f00 100644 (file)
@@ -21,6 +21,7 @@ enum {
 
         /* These are our own mode values that map to concrete values at runtime. */
         CONSOLE_MODE_KEEP = CONSOLE_MODE_RANGE_MAX + 1LL,
+        CONSOLE_MODE_NEXT,
         CONSOLE_MODE_AUTO,
         CONSOLE_MODE_FIRMWARE_MAX, /* 'max' in config. */
 };
index b78408e1abc209b2eb8d7928e530fcb02aec2c4f..065e1ea3966027f0353c2d90714393996a260c33 100644 (file)
@@ -522,3 +522,8 @@ VOID print_at(UINTN x, UINTN y, UINTN attr, const CHAR16 *str) {
         uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, attr);
         uefi_call_wrapper(ST->ConOut->OutputString, 2, ST->ConOut, (CHAR16*)str);
 }
+
+VOID clear_screen(UINTN attr) {
+        uefi_call_wrapper(ST->ConOut->SetAttribute, 2, ST->ConOut, attr);
+        uefi_call_wrapper(ST->ConOut->ClearScreen, 1, ST->ConOut);
+}
index 9eef51cc7e91da5f23e72343b120b04d6cb79e22..a5b6435f1b685c242fa40dc3bb1148384c2f6b8f 100644 (file)
@@ -93,3 +93,4 @@ static inline VOID *mempmem_safe(const VOID *haystack, UINTN haystack_len, const
 }
 
 VOID print_at(UINTN x, UINTN y, UINTN attr, const CHAR16 *str);
+VOID clear_screen(UINTN attr);