github.com/andrewsun2898/u-root@v6.0.1-0.20200616011413-4b2895c1b815+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 "errors" 11 "fmt" 12 "io" 13 "log" 14 "os" 15 "os/signal" 16 "strconv" 17 "syscall" 18 "time" 19 20 "github.com/u-root/u-root/pkg/boot" 21 "github.com/u-root/u-root/pkg/sh" 22 "golang.org/x/crypto/ssh/terminal" 23 "golang.org/x/sys/unix" 24 ) 25 26 const ( 27 initialTimeout = 10 * time.Second 28 subsequentTimeout = 60 * time.Second 29 ) 30 31 // Entry is a menu entry. 32 type Entry interface { 33 // Label is the string displayed to the user in the menu. 34 Label() string 35 36 // Do is called when the entry is chosen. 37 Do() error 38 39 // IsDefault indicates that this action should be run by default if the 40 // user didn't make an entry choice. 41 IsDefault() bool 42 } 43 44 // Choose presents the user a menu on input to choose an entry from and returns that entry. 45 func Choose(input *os.File, entries ...Entry) Entry { 46 fmt.Println("") 47 for i, e := range entries { 48 fmt.Printf("%02d. %s\n\n", i+1, e.Label()) 49 } 50 fmt.Println("\r") 51 52 oldState, err := terminal.MakeRaw(int(input.Fd())) 53 if err != nil { 54 log.Printf("BUG: Please report: We cannot actually let you choose from menu (MakeRaw failed): %v", err) 55 return nil 56 } 57 defer terminal.Restore(int(input.Fd()), oldState) 58 59 // TODO(chrisko): reduce this timeout a la GRUB. 3 seconds, and hitting 60 // any button resets the timeout. We could save 7 seconds here. 61 t := time.NewTimer(initialTimeout) 62 63 boot := make(chan Entry, 1) 64 65 go func() { 66 // Read exactly one line. 67 term := terminal.NewTerminal(input, "Choose a menu option (hit enter to boot the default - 01 is the default option) > ") 68 69 term.AutoCompleteCallback = func(line string, pos int, key rune) (string, int, bool) { 70 // We ain't gonna autocomplete, but we'll reset the countdown timer when you press a key. 71 t.Reset(subsequentTimeout) 72 return "", 0, false 73 } 74 75 for { 76 choice, err := term.ReadLine() 77 if err != nil { 78 if err != io.EOF { 79 fmt.Printf("BUG: Please report: Terminal read error: %v.\r\n", err) 80 } 81 boot <- nil 82 return 83 } 84 85 if choice == "" { 86 // nil will result in the default order. 87 boot <- nil 88 return 89 } 90 num, err := strconv.Atoi(choice) 91 if err != nil { 92 fmt.Printf("%s is not a valid entry number: %v.\r\n", choice, err) 93 continue 94 } 95 if num-1 < 0 || num > len(entries) { 96 fmt.Printf("%s is not a valid entry number.\r\n", choice) 97 continue 98 } 99 boot <- entries[num-1] 100 return 101 } 102 }() 103 104 select { 105 case entry := <-boot: 106 if entry != nil { 107 fmt.Printf("Chosen option %s.\r\n\r\n", entry.Label()) 108 } 109 return entry 110 111 case <-t.C: 112 return nil 113 } 114 } 115 116 // errStopTestOnly makes ShowMenuAndBoot return if Entry.Do returns it. This 117 // exists because we expect all menu entries to take over execution context if 118 // they succeed (e.g. exec a shell, reboot, exec a kernel). Success for Do() is 119 // only if it never returns. 120 // 121 // We can't test that it won't return, so we use this placeholder value instead 122 // to indicate "it worked". 123 var errStopTestOnly = errors.New("makes ShowMenuAndBoot return only in tests") 124 125 // ShowMenuAndBoot lets the user choose one of entries and boots it. 126 func ShowMenuAndBoot(input *os.File, entries ...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.Do(); err != nil { 143 log.Printf("Failed to do %s: %v", entry.Label(), err) 144 } 145 } 146 147 fmt.Println("") 148 149 // We only get one shot at actually booting, so boot the first kernel 150 // that can be loaded correctly. 151 for _, e := range entries { 152 // Only perform actions that are default actions. I.e. don't 153 // drop to shell. 154 if e.IsDefault() { 155 fmt.Printf("Attempting to boot %s.\n\n", e) 156 if err := e.Do(); err == errStopTestOnly { 157 return 158 } else if err != nil { 159 log.Printf("Failed to boot %s: %v", e.Label(), err) 160 } 161 } 162 } 163 } 164 165 // OSImages returns menu entries for the given OSImages. 166 func OSImages(dryRun bool, imgs ...boot.OSImage) []Entry { 167 var menu []Entry 168 for _, img := range imgs { 169 menu = append(menu, &OSImageAction{ 170 OSImage: img, 171 DryRun: dryRun, 172 }) 173 } 174 return menu 175 } 176 177 // OSImageAction is a menu.Entry that boots an OSImage. 178 type OSImageAction struct { 179 boot.OSImage 180 DryRun bool 181 } 182 183 // Do implements Entry.Do by booting the image. 184 func (oia OSImageAction) Do() error { 185 if err := oia.OSImage.Load(oia.DryRun); err != nil { 186 log.Printf("Could not load image %s: %v", oia.OSImage, err) 187 } 188 if oia.DryRun { 189 // err should only be nil in a dry run. 190 log.Printf("Loaded kernel %s.", oia.OSImage) 191 os.Exit(0) 192 } 193 if err := boot.Execute(); err != nil { 194 return err 195 } 196 return nil 197 } 198 199 // IsDefault returns true -- this action should be performed in order by 200 // default if the user did not choose a boot entry. 201 func (OSImageAction) IsDefault() bool { return true } 202 203 // StartShell is a menu.Entry that starts a LinuxBoot shell. 204 type StartShell struct{} 205 206 // Label is the label to show to the user. 207 func (StartShell) Label() string { 208 return "Enter a LinuxBoot shell" 209 } 210 211 // Do implements Entry.Do by running /bin/defaultsh. 212 func (StartShell) Do() error { 213 // Reset signal handler for SIGINT to enable user interrupts again 214 signal.Reset(syscall.SIGINT) 215 return sh.RunWithLogs("/bin/defaultsh") 216 } 217 218 // IsDefault indicates that this should not be run as a default action. 219 func (StartShell) IsDefault() bool { return false } 220 221 // Reboot is a menu.Entry that reboots the machine. 222 type Reboot struct{} 223 224 // Label is the label to show to the user. 225 func (Reboot) Label() string { 226 return "Reboot" 227 } 228 229 // Do reboots the machine using sys_reboot. 230 func (Reboot) Do() error { 231 unix.Sync() 232 return unix.Reboot(unix.LINUX_REBOOT_CMD_RESTART) 233 } 234 235 // IsDefault indicates that this should not be run as a default action. 236 func (Reboot) IsDefault() bool { return false }