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 }