/* menu.c - General supporting functionality for menus. */ /* * GRUB -- GRand Unified Bootloader * Copyright (C) 2003,2004,2005,2006,2007,2008,2009,2010 Free Software Foundation, Inc. * * GRUB is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GRUB is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GRUB. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include /* Time to delay after displaying an error message about a default/fallback entry failing to boot. */ #define DEFAULT_ENTRY_ERROR_DELAY_MS 2500 grub_err_t (*grub_gfxmenu_try_hook) (int entry, grub_menu_t menu, int nested) = NULL; /* Wait until the user pushes any key so that the user can see what happened. */ void grub_wait_after_message (void) { grub_uint64_t endtime; grub_xputs ("\n"); grub_printf_ (N_("Press any key to continue...")); grub_refresh (); endtime = grub_get_time_ms () + 10000; while (grub_get_time_ms () < endtime) if (grub_checkkey () >= 0) { grub_getkey (); break; } grub_xputs ("\n"); } /* Get a menu entry by its index in the entry list. */ grub_menu_entry_t grub_menu_get_entry (grub_menu_t menu, int no) { grub_menu_entry_t e; for (e = menu->entry_list; e && no > 0; e = e->next, no--) ; return e; } /* Return the current timeout. If the variable "timeout" is not set or invalid, return -1. */ int grub_menu_get_timeout (void) { char *val; int timeout; val = grub_env_get ("timeout"); if (! val) return -1; grub_error_push (); timeout = (int) grub_strtoul (val, 0, 0); /* If the value is invalid, unset the variable. */ if (grub_errno != GRUB_ERR_NONE) { grub_env_unset ("timeout"); grub_errno = GRUB_ERR_NONE; timeout = -1; } grub_error_pop (); return timeout; } /* Set current timeout in the variable "timeout". */ void grub_menu_set_timeout (int timeout) { /* Ignore TIMEOUT if it is zero, because it will be unset really soon. */ if (timeout > 0) { char buf[16]; grub_snprintf (buf, sizeof (buf), "%d", timeout); grub_env_set ("timeout", buf); } } /* Get the first entry number from the value of the environment variable NAME, which is a space-separated list of non-negative integers. The entry number which is returned is stripped from the value of NAME. If no entry number can be found, -1 is returned. */ static int get_and_remove_first_entry_number (const char *name) { char *val; char *tail; int entry; val = grub_env_get (name); if (! val) return -1; grub_error_push (); entry = (int) grub_strtoul (val, &tail, 0); if (grub_errno == GRUB_ERR_NONE) { /* Skip whitespace to find the next digit. */ while (*tail && grub_isspace (*tail)) tail++; grub_env_set (name, tail); } else { grub_env_unset (name); grub_errno = GRUB_ERR_NONE; entry = -1; } grub_error_pop (); return entry; } /* Run a menu entry. */ static void grub_menu_execute_entry(grub_menu_entry_t entry, int auto_boot) { grub_err_t err = GRUB_ERR_NONE; int errs_before; grub_menu_t menu; char *optr, *buf, *oldchosen = NULL, *olddefault = NULL; const char *ptr, *chosen, *def; grub_size_t sz = 0; if (entry->restricted) err = grub_auth_check_authentication (entry->users); if (err) { grub_print_error (); grub_errno = GRUB_ERR_NONE; return; } errs_before = grub_err_printed_errors; chosen = grub_env_get ("chosen"); def = grub_env_get ("default"); if (entry->submenu) { grub_env_context_open (); menu = grub_zalloc (sizeof (*menu)); if (! menu) return; grub_env_set_menu (menu); if (auto_boot) grub_env_set ("timeout", "0"); } for (ptr = entry->title; *ptr; ptr++) sz += (*ptr == '>') ? 2 : 1; if (chosen) { oldchosen = grub_strdup (chosen); if (!oldchosen) grub_print_error (); } if (def) { olddefault = grub_strdup (def); if (!olddefault) grub_print_error (); } sz++; if (chosen) sz += grub_strlen (chosen); sz++; buf = grub_malloc (sz); if (!buf) grub_print_error (); else { optr = buf; if (chosen) { optr = grub_stpcpy (optr, chosen); *optr++ = '>'; } for (ptr = entry->title; *ptr; ptr++) { if (*ptr == '>') *optr++ = '>'; *optr++ = *ptr; } *optr = 0; grub_env_set ("chosen", buf); grub_env_export ("chosen"); grub_free (buf); } for (ptr = def; *ptr; ptr++) { if (ptr[0] == '>' && ptr[1] == '>') { ptr++; continue; } if (ptr[0] == '>') break; } if (ptr[0] && ptr[1]) grub_env_set ("default", ptr + 1); else grub_env_unset ("default"); grub_script_execute_sourcecode (entry->sourcecode, entry->argc, entry->args); if (errs_before != grub_err_printed_errors) grub_wait_after_message (); if (grub_errno == GRUB_ERR_NONE && grub_loader_is_loaded ()) /* Implicit execution of boot, only if something is loaded. */ grub_command_execute ("boot", 0, 0); if (entry->submenu) { if (menu && menu->size) { grub_show_menu (menu, 1, auto_boot); grub_normal_free_menu (menu); } grub_env_context_close (); } if (oldchosen) grub_env_set ("chosen", oldchosen); else grub_env_unset ("chosen"); if (olddefault) grub_env_set ("default", olddefault); else grub_env_unset ("default"); grub_env_unset ("timeout"); } /* Execute ENTRY from the menu MENU, falling back to entries specified in the environment variable "fallback" if it fails. CALLBACK is a pointer to a struct of function pointers which are used to allow the caller provide feedback to the user. */ static void grub_menu_execute_with_fallback (grub_menu_t menu, grub_menu_entry_t entry, int autobooted, grub_menu_execute_callback_t callback, void *callback_data) { int fallback_entry; callback->notify_booting (entry, callback_data); grub_menu_execute_entry (entry, 1); /* Deal with fallback entries. */ while ((fallback_entry = get_and_remove_first_entry_number ("fallback")) >= 0) { grub_print_error (); grub_errno = GRUB_ERR_NONE; entry = grub_menu_get_entry (menu, fallback_entry); callback->notify_fallback (entry, callback_data); grub_menu_execute_entry (entry, 1); /* If the function call to execute the entry returns at all, then this is taken to indicate a boot failure. For menu entries that do something other than actually boot an operating system, this could assume incorrectly that something failed. */ } if (!autobooted) callback->notify_failure (callback_data); } static struct grub_menu_viewer *viewers; static void menu_set_chosen_entry (int entry) { struct grub_menu_viewer *cur; for (cur = viewers; cur; cur = cur->next) cur->set_chosen_entry (entry, cur->data); } static void menu_print_timeout (int timeout) { struct grub_menu_viewer *cur; for (cur = viewers; cur; cur = cur->next) cur->print_timeout (timeout, cur->data); } static void menu_fini (void) { struct grub_menu_viewer *cur, *next; for (cur = viewers; cur; cur = next) { next = cur->next; cur->fini (cur->data); grub_free (cur); } viewers = NULL; } static void menu_init (int entry, grub_menu_t menu, int nested) { struct grub_term_output *term; FOR_ACTIVE_TERM_OUTPUTS(term) { grub_err_t err; if (grub_gfxmenu_try_hook && grub_strcmp (term->name, "gfxterm") == 0) { err = grub_gfxmenu_try_hook (entry, menu, nested); if(!err) continue; grub_errno = GRUB_ERR_NONE; } err = grub_menu_try_text (term, entry, menu, nested); if(!err) continue; grub_print_error (); grub_errno = GRUB_ERR_NONE; } } static void clear_timeout (void) { struct grub_menu_viewer *cur; for (cur = viewers; cur; cur = cur->next) cur->clear_timeout (cur->data); } void grub_menu_register_viewer (struct grub_menu_viewer *viewer) { viewer->next = viewers; viewers = viewer; } static int menuentry_eq (const char *title, const char *spec) { const char *ptr1, *ptr2; ptr1 = title; ptr2 = spec; while (1) { if (*ptr2 == '>' && ptr2[1] != '>' && *ptr1 == 0) return 1; if (*ptr2 == '>' && ptr2[1] != '>') return 0; if (*ptr2 == '>') ptr2++; if (*ptr1 != *ptr2) return 0; if (*ptr1 == 0) return 1; ptr1++; ptr2++; } } /* Get the entry number from the variable NAME. */ static int get_entry_number (grub_menu_t menu, const char *name) { const char *val; int entry; val = grub_env_get (name); if (! val) return -1; grub_error_push (); entry = (int) grub_strtoul (val, 0, 0); if (grub_errno == GRUB_ERR_BAD_NUMBER) { /* See if the variable matches the title of a menu entry. */ grub_menu_entry_t e = menu->entry_list; int i; grub_errno = GRUB_ERR_NONE; for (i = 0; e; i++) { if (menuentry_eq (e->title, val)) { entry = i; break; } e = e->next; } if (! e) entry = -1; } if (grub_errno != GRUB_ERR_NONE) { grub_errno = GRUB_ERR_NONE; entry = -1; } grub_error_pop (); return entry; } #define GRUB_MENU_PAGE_SIZE 10 /* Show the menu and handle menu entry selection. Returns the menu entry index that should be executed or -1 if no entry should be executed (e.g., Esc pressed to exit a sub-menu or switching menu viewers). If the return value is not -1, then *AUTO_BOOT is nonzero iff the menu entry to be executed is a result of an automatic default selection because of the timeout. */ static int run_menu (grub_menu_t menu, int nested, int *auto_boot) { grub_uint64_t saved_time; int default_entry, current_entry; int timeout; default_entry = get_entry_number (menu, "default"); /* If DEFAULT_ENTRY is not within the menu entries, fall back to the first entry. */ if (default_entry < 0 || default_entry >= menu->size) default_entry = 0; /* If timeout is 0, drawing is pointless (and ugly). */ if (grub_menu_get_timeout () == 0) { *auto_boot = 1; return default_entry; } current_entry = default_entry; /* Initialize the time. */ saved_time = grub_get_time_ms (); refresh: menu_init (current_entry, menu, nested); timeout = grub_menu_get_timeout (); if (timeout > 0) menu_print_timeout (timeout); else clear_timeout (); while (1) { int c; timeout = grub_menu_get_timeout (); if (grub_normal_exit_level) return -1; if (timeout > 0) { grub_uint64_t current_time; current_time = grub_get_time_ms (); if (current_time - saved_time >= 1000) { timeout--; grub_menu_set_timeout (timeout); saved_time = current_time; menu_print_timeout (timeout); } } if (timeout == 0) { grub_env_unset ("timeout"); *auto_boot = 1; menu_fini (); return default_entry; } if (grub_checkkey () >= 0 || timeout < 0) { c = grub_getkey (); if (timeout >= 0) { grub_env_unset ("timeout"); grub_env_unset ("fallback"); clear_timeout (); } switch (c) { case GRUB_TERM_KEY_HOME: case GRUB_TERM_CTRL | 'a': current_entry = 0; menu_set_chosen_entry (current_entry); break; case GRUB_TERM_KEY_END: case GRUB_TERM_CTRL | 'e': current_entry = menu->size - 1; menu_set_chosen_entry (current_entry); break; case GRUB_TERM_KEY_UP: case GRUB_TERM_CTRL | 'p': case '^': if (current_entry > 0) current_entry--; menu_set_chosen_entry (current_entry); break; case GRUB_TERM_CTRL | 'n': case GRUB_TERM_KEY_DOWN: case 'v': if (current_entry < menu->size - 1) current_entry++; menu_set_chosen_entry (current_entry); break; case GRUB_TERM_CTRL | 'g': case GRUB_TERM_KEY_PPAGE: if (current_entry < GRUB_MENU_PAGE_SIZE) current_entry = 0; else current_entry -= GRUB_MENU_PAGE_SIZE; menu_set_chosen_entry (current_entry); break; case GRUB_TERM_CTRL | 'c': case GRUB_TERM_KEY_NPAGE: if (current_entry + GRUB_MENU_PAGE_SIZE < menu->size) current_entry += GRUB_MENU_PAGE_SIZE; else current_entry = menu->size - 1; menu_set_chosen_entry (current_entry); break; case '\n': case '\r': case GRUB_TERM_KEY_RIGHT: case GRUB_TERM_CTRL | 'f': menu_fini (); *auto_boot = 0; return current_entry; case '\e': if (nested) { menu_fini (); return -1; } break; case 'c': menu_fini (); grub_cmdline_run (1); goto refresh; case 'e': menu_fini (); { grub_menu_entry_t e = grub_menu_get_entry (menu, current_entry); if (e) grub_menu_entry_run (e); } goto refresh; default: { grub_menu_entry_t entry; int i; for (i = 0, entry = menu->entry_list; i < menu->size; i++, entry = entry->next) if (entry->hotkey == c) { menu_fini (); *auto_boot = 0; return i; } } break; } } } /* Never reach here. */ return -1; } /* Callback invoked immediately before a menu entry is executed. */ static void notify_booting (grub_menu_entry_t entry, void *userdata __attribute__((unused))) { grub_printf (" "); grub_printf_ (N_("Booting \'%s\'"), entry->title); grub_printf ("\n\n"); } /* Callback invoked when a default menu entry executed because of a timeout has failed and an attempt will be made to execute the next fallback entry, ENTRY. */ static void notify_fallback (grub_menu_entry_t entry, void *userdata __attribute__((unused))) { grub_printf ("\n "); grub_printf_ (N_("Falling back to \'%s\'"), entry->title); grub_printf ("\n\n"); grub_millisleep (DEFAULT_ENTRY_ERROR_DELAY_MS); } /* Callback invoked when a menu entry has failed and there is no remaining fallback entry to attempt. */ static void notify_execution_failure (void *userdata __attribute__((unused))) { if (grub_errno != GRUB_ERR_NONE) { grub_print_error (); grub_errno = GRUB_ERR_NONE; } grub_printf ("\n "); grub_printf_ (N_("Failed to boot both default and fallback entries.\n")); grub_wait_after_message (); } /* Callbacks used by the text menu to provide user feedback when menu entries are executed. */ static struct grub_menu_execute_callback execution_callback = { .notify_booting = notify_booting, .notify_fallback = notify_fallback, .notify_failure = notify_execution_failure }; static grub_err_t show_menu (grub_menu_t menu, int nested, int autobooted) { while (1) { int boot_entry; grub_menu_entry_t e; int auto_boot; boot_entry = run_menu (menu, nested, &auto_boot); if (boot_entry < 0) break; e = grub_menu_get_entry (menu, boot_entry); if (! e) continue; /* Menu is empty. */ grub_cls (); if (auto_boot) grub_menu_execute_with_fallback (menu, e, autobooted, &execution_callback, 0); else grub_menu_execute_entry (e, 0); if (autobooted) break; } return GRUB_ERR_NONE; } grub_err_t grub_show_menu (grub_menu_t menu, int nested, int autoboot) { grub_err_t err1, err2; while (1) { err1 = show_menu (menu, nested, autoboot); autoboot = 0; grub_print_error (); if (grub_normal_exit_level) break; err2 = grub_auth_check_authentication (NULL); if (err2) { grub_print_error (); grub_errno = GRUB_ERR_NONE; continue; } break; } return err1; }