github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/boot/menu/menu.go (about) 1 // Copyright 2020-2021 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package menu displays a Terminal UI based text menu to choose boot options 6 // from. 7 package menu 8 9 import ( 10 "fmt" 11 "io" 12 "log" 13 "os" 14 "os/signal" 15 "strconv" 16 "strings" 17 "syscall" 18 "time" 19 20 "github.com/mvdan/u-root-coreutils/pkg/boot" 21 "github.com/mvdan/u-root-coreutils/pkg/sh" 22 "golang.org/x/sys/unix" 23 ) 24 25 var ( 26 initialTimeout = 10 * time.Second 27 subsequentTimeout = 60 * time.Second 28 ) 29 30 // Entry is a menu entry. 31 type Entry interface { 32 // Label is the string displayed to the user in the menu. It must be a 33 // single line to fit in the menu. 34 Label() string 35 36 // Edit the kernel command line if possible. Must be called prior to 37 // Load. 38 Edit(func(cmdline string) string) 39 40 // Load is called when the entry is chosen, but does not transfer 41 // execution to another process or kernel. 42 Load() error 43 44 // Exec transfers execution to another process or kernel. 45 // 46 // Exec either returns an error or does not return at all. 47 Exec() error 48 49 // IsDefault indicates that this action should be run by default if the 50 // user didn't make an entry choice. 51 IsDefault() bool 52 } 53 54 // ExtendedLabel calls Entry.String(), but falls back to Entry.Label(). Shortly 55 // before kexec, "Attempting to boot %s" is printed. This allows for multiple 56 // lines of information which would not otherwise fit in the menu. 57 func ExtendedLabel(e Entry) string { 58 if s, ok := e.(fmt.Stringer); ok { 59 return s.String() 60 } 61 return e.Label() 62 } 63 64 func parseBootNum(choice string, entries []Entry) (int, error) { 65 num, err := strconv.Atoi(strings.TrimSpace(choice)) 66 if err != nil || num < 1 || num > len(entries) { 67 return -1, fmt.Errorf("%q is not a valid entry number", choice) 68 } 69 return num, nil 70 } 71 72 // SetInitialTimeout sets the initial timeout of the menu to the provided duration 73 func SetInitialTimeout(timeout time.Duration) { 74 initialTimeout = timeout 75 } 76 77 // Choose presents the user a menu on input to choose an entry from and returns that entry. 78 // Note: This call can block if MenuTerminal or the underlying os.File does 79 // 80 // not support SetTimeout/SetDeadline. 81 func Choose(term MenuTerminal, allowEdit bool, entries ...Entry) Entry { 82 fmt.Println("") 83 for i, e := range entries { 84 fmt.Printf("%02d. %s\r\n\r\n", i+1, e.Label()) 85 } 86 fmt.Println("\r") 87 88 err := term.SetTimeout(initialTimeout) 89 if err != nil { 90 fmt.Printf("BUG: terminal does not support timeouts: %v\n", err) 91 } 92 93 // Reset the countdown timer when you press a key. 94 term.SetEntryCallback(func() { 95 _ = term.SetTimeout(subsequentTimeout) 96 }) 97 98 for { 99 if allowEdit { 100 term.SetPrompt("Enter an option ('01' is the default, 'e' to edit kernel cmdline):\r\n > ") 101 } else { 102 term.SetPrompt("Enter an option ('01' is the default):\r\n > ") 103 } 104 105 choice, err := term.ReadLine() 106 if err != nil { 107 if text := err.Error(); !strings.Contains(text, os.ErrDeadlineExceeded.Error()) && err != io.EOF { 108 fmt.Printf("BUG: Please report: Terminal read error: %v.\n", err) 109 } 110 return nil 111 } 112 113 if allowEdit && choice == "e" { 114 // Edit command line. 115 term.SetPrompt("Select a boot option to edit:\r\n > ") 116 choice, err := term.ReadLine() 117 if err != nil { 118 fmt.Fprintln(term, err) 119 fmt.Fprintln(term, "Returning to main menu...") 120 continue 121 } 122 num, err := parseBootNum(choice, entries) 123 if err != nil { 124 fmt.Fprintln(term, err) 125 fmt.Fprintln(term, "Returning to main menu...") 126 continue 127 } 128 entries[num-1].Edit(func(cmdline string) string { 129 fmt.Fprintf(term, "The current quoted cmdline for option %d is:\r\n > %q\r\n", num, cmdline) 130 fmt.Fprintln(term, ` * Note the cmdline is c-style quoted. Ex: \n => newline, \\ => \`) 131 term.SetPrompt("Enter an option:\r\n * (a)ppend, (o)verwrite, (r)eturn to main menu\r\n > ") 132 choice, err := term.ReadLine() 133 if err != nil { 134 fmt.Fprintln(term, err) 135 return cmdline 136 } 137 switch choice { 138 case "a": 139 term.SetPrompt("Enter unquoted cmdline to append:\r\n > ") 140 appendCmdline, err := term.ReadLine() 141 if err != nil { 142 fmt.Fprintln(term, err) 143 return cmdline 144 } 145 if appendCmdline != "" { 146 cmdline += " " + appendCmdline 147 } 148 case "o": 149 term.SetPrompt("Enter new unquoted cmdline:\r\n > ") 150 newCmdline, err := term.ReadLine() 151 if err != nil { 152 fmt.Fprintln(term, err) 153 return cmdline 154 } 155 cmdline = newCmdline 156 case "r": 157 default: 158 fmt.Fprintf(term, "Unrecognized choice %q", choice) 159 } 160 fmt.Fprintf(term, "The new quoted cmdline for option %d is:\r\n > %q\r\n", num, cmdline) 161 return cmdline 162 }) 163 fmt.Fprintln(term, "Returning to main menu...") 164 continue 165 } 166 if choice == "" { 167 // nil will result in the default order. 168 return nil 169 } 170 num, err := parseBootNum(choice, entries) 171 if err != nil { 172 fmt.Fprintln(term, err) 173 continue 174 } 175 return entries[num-1] 176 } 177 } 178 179 // ShowMenuAndLoad calls showMenuAndLoadFromFile using the default tty. 180 // Use TTY because os.stdin does not support deadlines well. 181 func ShowMenuAndLoad(allowEdit bool, entries ...Entry) Entry { 182 f, err := os.OpenFile("/dev/tty", os.O_RDWR, 0) 183 if err != nil { 184 log.Printf("Failed to open /dev/tty: %s\n", err) 185 return nil 186 } 187 defer f.Close() 188 189 return showMenuAndLoadFromFile(f, allowEdit, entries...) 190 } 191 192 // showMenuAndLoadFromFile lets the user choose one of entries and loads it. 193 // If no entry is chosen by the user, an entry whose IsDefault() is true will be 194 // returned. 195 // 196 // The user is left to call Entry.Exec when this function returns. 197 func showMenuAndLoadFromFile(file *os.File, allowEdit bool, entries ...Entry) Entry { 198 // Clear the screen (ANSI terminal escape code for screen clear). 199 fmt.Printf("\033[1;1H\033[2J\n\n") 200 fmt.Printf("Welcome to LinuxBoot's Menu\n\n") 201 fmt.Printf("Enter a number to boot a kernel:\n") 202 203 for { 204 t := NewTerminal(file) 205 // Allow the user to choose. 206 entry := Choose(t, allowEdit, entries...) 207 if err := t.Close(); err != nil { 208 log.Printf("Failed to close terminal made from file %s "+ 209 "(desc %d): %v", file.Name(), file.Fd(), err) 210 } 211 212 if entry == nil { 213 // This only returns something if the user explicitly 214 // entered something. 215 // 216 // If nothing was entered, fall back to default. 217 break 218 } 219 if err := entry.Load(); err != nil { 220 log.Printf("Failed to load %s: %v", entry.Label(), err) 221 continue 222 } 223 224 // Entry was successfully loaded. Leave it to the caller to 225 // exec, so the caller can clean up the OS before rebooting or 226 // kexecing (e.g. unmount file systems). 227 return entry 228 } 229 230 fmt.Println("") 231 232 // We only get one shot at actually booting, so boot the first kernel 233 // that can be loaded correctly. 234 for _, e := range entries { 235 // Only perform actions that are default actions. I.e. don't 236 // drop to shell. 237 if e.IsDefault() { 238 fmt.Printf("Attempting to boot %s.\n\n", ExtendedLabel(e)) 239 240 if err := e.Load(); err != nil { 241 log.Printf("Failed to load %s: %v", e.Label(), err) 242 continue 243 } 244 245 // Entry was successfully loaded. Leave it to the 246 // caller to exec, so the caller can clean up the OS 247 // before rebooting or kexecing (e.g. unmount file 248 // systems). 249 return e 250 } 251 } 252 return nil 253 } 254 255 // OSImages returns menu entries for the given OSImages. 256 func OSImages(verbose bool, imgs ...boot.OSImage) []Entry { 257 var menu []Entry 258 for _, img := range imgs { 259 menu = append(menu, &OSImageAction{ 260 OSImage: img, 261 Verbose: verbose, 262 }) 263 } 264 return menu 265 } 266 267 // OSImageAction is a menu.Entry that boots an OSImage. 268 type OSImageAction struct { 269 boot.OSImage 270 Verbose bool 271 } 272 273 // Load implements Entry.Load by loading the OS image into memory. 274 func (oia OSImageAction) Load() error { 275 if err := oia.OSImage.Load(oia.Verbose); err != nil { 276 return fmt.Errorf("could not load image %s: %v", oia.OSImage, err) 277 } 278 return nil 279 } 280 281 // Exec executes the loaded image. 282 func (oia OSImageAction) Exec() error { 283 return boot.Execute() 284 } 285 286 // IsDefault returns true -- this action should be performed in order by 287 // default if the user did not choose a boot entry. 288 func (OSImageAction) IsDefault() bool { return true } 289 290 // StartShell is a menu.Entry that starts a LinuxBoot shell. 291 type StartShell struct{} 292 293 // Label is the label to show to the user. 294 func (StartShell) Label() string { 295 return "Enter a LinuxBoot shell" 296 } 297 298 // Edit does nothing. 299 func (StartShell) Edit(func(cmdline string) string) { 300 } 301 302 // Load does nothing. 303 func (StartShell) Load() error { 304 return nil 305 } 306 307 // Exec implements Entry.Exec by running /bin/defaultsh. 308 func (StartShell) Exec() error { 309 // Reset signal handler for SIGINT to enable user interrupts again 310 signal.Reset(syscall.SIGINT) 311 return sh.RunWithLogs("/bin/defaultsh") 312 } 313 314 // IsDefault indicates that this should not be run as a default action. 315 func (StartShell) IsDefault() bool { return false } 316 317 // Reboot is a menu.Entry that reboots the machine. 318 type Reboot struct{} 319 320 // Label is the label to show to the user. 321 func (Reboot) Label() string { 322 return "Reboot" 323 } 324 325 // Edit does nothing. 326 func (Reboot) Edit(func(cmdline string) string) { 327 } 328 329 // Load does nothing. 330 func (Reboot) Load() error { 331 return nil 332 } 333 334 // Exec reboots the machine using sys_reboot. 335 func (Reboot) Exec() error { 336 unix.Sync() 337 return unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART) 338 } 339 340 // IsDefault indicates that this should not be run as a default action. 341 func (Reboot) IsDefault() bool { return false }