gitee.com/mirrors_u-root/u-root@v7.0.0+incompatible/pkg/boot/menu/menu.go (about) 1 // Copyright 2020 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 "syscall" 17 "time" 18 19 "github.com/u-root/u-root/pkg/boot" 20 "github.com/u-root/u-root/pkg/sh" 21 "golang.org/x/crypto/ssh/terminal" 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. 33 Label() string 34 35 // Load is called when the entry is chosen, but does not transfer 36 // execution to another process or kernel. 37 Load() error 38 39 // Exec transfers execution to another process or kernel. 40 // 41 // Exec either returns an error or does not return at all. 42 Exec() error 43 44 // IsDefault indicates that this action should be run by default if the 45 // user didn't make an entry choice. 46 IsDefault() bool 47 } 48 49 // Choose presents the user a menu on input to choose an entry from and returns that entry. 50 func Choose(input *os.File, entries ...Entry) Entry { 51 fmt.Println("") 52 for i, e := range entries { 53 fmt.Printf("%02d. %s\n\n", i+1, e.Label()) 54 } 55 fmt.Println("\r") 56 57 oldState, err := terminal.MakeRaw(int(input.Fd())) 58 if err != nil { 59 log.Printf("BUG: Please report: We cannot actually let you choose from menu (MakeRaw failed): %v", err) 60 return nil 61 } 62 defer terminal.Restore(int(input.Fd()), oldState) 63 64 // TODO(chrisko): reduce this timeout a la GRUB. 3 seconds, and hitting 65 // any button resets the timeout. We could save 7 seconds here. 66 t := time.NewTimer(initialTimeout) 67 68 boot := make(chan Entry, 1) 69 70 go func() { 71 // Read exactly one line. 72 term := terminal.NewTerminal(input, "Choose a menu option (hit enter to boot the default - 01 is the default option) > ") 73 74 term.AutoCompleteCallback = func(line string, pos int, key rune) (string, int, bool) { 75 // We ain't gonna autocomplete, but we'll reset the countdown timer when you press a key. 76 t.Reset(subsequentTimeout) 77 return "", 0, false 78 } 79 80 for { 81 choice, err := term.ReadLine() 82 if err != nil { 83 if err != io.EOF { 84 fmt.Printf("BUG: Please report: Terminal read error: %v.\r\n", err) 85 } 86 boot <- nil 87 return 88 } 89 90 if choice == "" { 91 // nil will result in the default order. 92 boot <- nil 93 return 94 } 95 num, err := strconv.Atoi(choice) 96 if err != nil { 97 fmt.Printf("%s is not a valid entry number: %v.\r\n", choice, err) 98 continue 99 } 100 if num-1 < 0 || num > len(entries) { 101 fmt.Printf("%s is not a valid entry number.\r\n", choice) 102 continue 103 } 104 boot <- entries[num-1] 105 return 106 } 107 }() 108 109 select { 110 case entry := <-boot: 111 if entry != nil { 112 fmt.Printf("Chosen option %s.\r\n\r\n", entry.Label()) 113 } 114 return entry 115 116 case <-t.C: 117 return nil 118 } 119 } 120 121 // ShowMenuAndLoad lets the user choose one of entries and loads it. If no 122 // entry is chosen by the user, an entry whose IsDefault() is true will be 123 // returned. 124 // 125 // The user is left to call Entry.Exec when this function returns. 126 func ShowMenuAndLoad(input *os.File, entries ...Entry) Entry { 127 // Clear the screen (ANSI terminal escape code for screen clear). 128 fmt.Printf("\033[1;1H\033[2J\n\n") 129 fmt.Printf("Welcome to NERF's Boot Menu\n\n") 130 fmt.Printf("Enter a number to boot a kernel:\n") 131 132 for { 133 // Allow the user to choose. 134 entry := Choose(input, entries...) 135 if entry == nil { 136 // This only returns something if the user explicitly 137 // entered something. 138 // 139 // If nothing was entered, fall back to default. 140 break 141 } 142 if err := entry.Load(); err != nil { 143 log.Printf("Failed to load %s: %v", entry.Label(), err) 144 continue 145 } 146 147 // Entry was successfully loaded. Leave it to the caller to 148 // exec, so the caller can clean up the OS before rebooting or 149 // kexecing (e.g. unmount file systems). 150 return entry 151 } 152 153 fmt.Println("") 154 155 // We only get one shot at actually booting, so boot the first kernel 156 // that can be loaded correctly. 157 for _, e := range entries { 158 // Only perform actions that are default actions. I.e. don't 159 // drop to shell. 160 if e.IsDefault() { 161 fmt.Printf("Attempting to boot %s.\n\n", e) 162 163 if err := e.Load(); err != nil { 164 log.Printf("Failed to load %s: %v", e.Label(), err) 165 continue 166 } 167 168 // Entry was successfully loaded. Leave it to the 169 // caller to exec, so the caller can clean up the OS 170 // before rebooting or kexecing (e.g. unmount file 171 // systems). 172 return e 173 } 174 } 175 return nil 176 } 177 178 // OSImages returns menu entries for the given OSImages. 179 func OSImages(verbose bool, imgs ...boot.OSImage) []Entry { 180 var menu []Entry 181 for _, img := range imgs { 182 menu = append(menu, &OSImageAction{ 183 OSImage: img, 184 Verbose: verbose, 185 }) 186 } 187 return menu 188 } 189 190 // OSImageAction is a menu.Entry that boots an OSImage. 191 type OSImageAction struct { 192 boot.OSImage 193 Verbose bool 194 } 195 196 // Load implements Entry.Load by loading the OS image into memory. 197 func (oia OSImageAction) Load() error { 198 if err := oia.OSImage.Load(oia.Verbose); err != nil { 199 return fmt.Errorf("could not load image %s: %v", oia.OSImage, err) 200 } 201 return nil 202 } 203 204 // Exec executes the loaded image. 205 func (oia OSImageAction) Exec() error { 206 return boot.Execute() 207 } 208 209 // IsDefault returns true -- this action should be performed in order by 210 // default if the user did not choose a boot entry. 211 func (OSImageAction) IsDefault() bool { return true } 212 213 // StartShell is a menu.Entry that starts a LinuxBoot shell. 214 type StartShell struct{} 215 216 // Label is the label to show to the user. 217 func (StartShell) Label() string { 218 return "Enter a LinuxBoot shell" 219 } 220 221 // Load does nothing. 222 func (StartShell) Load() error { 223 return nil 224 } 225 226 // Exec implements Entry.Exec by running /bin/defaultsh. 227 func (StartShell) Exec() error { 228 // Reset signal handler for SIGINT to enable user interrupts again 229 signal.Reset(syscall.SIGINT) 230 return sh.RunWithLogs("/bin/defaultsh") 231 } 232 233 // IsDefault indicates that this should not be run as a default action. 234 func (StartShell) IsDefault() bool { return false } 235 236 // Reboot is a menu.Entry that reboots the machine. 237 type Reboot struct{} 238 239 // Label is the label to show to the user. 240 func (Reboot) Label() string { 241 return "Reboot" 242 } 243 244 // Load does nothing. 245 func (Reboot) Load() error { 246 return nil 247 } 248 249 // Exec reboots the machine using sys_reboot. 250 func (Reboot) Exec() error { 251 unix.Sync() 252 return unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART) 253 } 254 255 // IsDefault indicates that this should not be run as a default action. 256 func (Reboot) IsDefault() bool { return false }