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 }