1package main23import (4 "bufio"5 "fmt"6 "io"7 "io/fs"8 "io/ioutil"9 "os"10 "path/filepath"11 "sort"12 "strconv"13 "strings"1415 dbus "github.com/godbus/dbus/v5"16 "golang.org/x/sys/unix"17)1819const (20 BOOT_ENTRIES_DIR = "/boot/loader/entries"21 SD_LOGIND_KEXEC_REBOOT = uint64(2)22)2324// kernelInfo contains information for a single Grub kernel entry.25type kernelInfo struct {26 title string27 version string28 linux string29 initrd string30 options string31}3233func run() error {34 files, err := getGrubEntryFiles(BOOT_ENTRIES_DIR)35 if err != nil {36 return err37 }3839 kernels, err := parseGrubEntryFiles(files)40 if err != nil {41 return err42 }4344 // FIXME: Sorting is lexicographic rather than numeric.45 sort.Slice(kernels, func(i, j int) bool {46 return kernels[i].version > kernels[j].version47 })4849 n, err := userSelectKernel(os.Stdin, os.Stdout, kernels)50 if err != nil {51 return err52 }5354 // If EOF was reached. Treat this differently from the user hitting just55 // the enter key. Prevents accidentally rebooting if stdin is closed (e.g.56 // Ctrl-D or some sort of breakage).57 if n == -1 {58 return fmt.Errorf("operation aborted")59 }6061 err = kexecFileLoad(kernels[n])62 if err != nil {63 return err64 }6566 err = reboot()67 if err != nil {68 return err69 }7071 return nil72}7374func getGrubEntryFiles(dir string) ([]string, error) {75 files := []string{}76 err := filepath.WalkDir(77 dir,78 func(path string, d fs.DirEntry, err error) error {79 if err != nil {80 return err81 }8283 if strings.HasSuffix(path, ".conf") && d.Type().IsRegular() {84 files = append(files, path)85 }8687 return nil88 })89 if err != nil {90 return nil, err91 }9293 return files, nil94}9596func parseGrubEntry(contents string) (*kernelInfo, error) {97 lines := strings.Split(string(contents), "\n")98 infoHash := make(map[string]string, 0)99 for _, line := range lines {100 line = strings.TrimSpace(line)101 if line == "" {102 continue103 }104105 k, v, found := strings.Cut(line, " ")106 if !found {107 return nil, fmt.Errorf("file in wrong format")108 }109110 infoHash[k] = v111 }112113 info := &kernelInfo{}114115 if val, ok := infoHash["title"]; ok {116 info.title = val117 } else {118 return nil, fmt.Errorf("File did not contain title")119 }120121 if val, ok := infoHash["version"]; ok {122 info.version = val123 } else {124 return nil, fmt.Errorf("File did not contain version")125 }126127 if val, ok := infoHash["linux"]; ok {128 info.linux = val129 } else {130 return nil, fmt.Errorf("File did not contain linux")131 }132133 if val, ok := infoHash["initrd"]; ok {134 info.initrd = val135 } else {136 return nil, fmt.Errorf("File did not contain initrd")137 }138139 if val, ok := infoHash["options"]; ok {140 info.options = val141 } else {142 return nil, fmt.Errorf("File did not contain options")143 }144145 return info, nil146}147148func parseGrubEntryFiles(files []string) ([]kernelInfo, error) {149 infos := make([]kernelInfo, len(files))150 for i, file := range files {151 contents, err := ioutil.ReadFile(file)152 if err != nil {153 return nil, err154 }155156 entry, err := parseGrubEntry(string(contents))157 if err != nil {158 return nil, err159 }160161 infos[i] = *entry162 }163164 return infos, nil165}166167func userSelectKernel(168 in io.Reader,169 out io.Writer,170 kernels []kernelInfo,171) (int, error) {172 for i, info := range kernels {173 fmt.Fprintf(out, "%d\t%s\n", i, info.version)174 }175 prompt := fmt.Sprintf("Select a kernel (0-%d)[0] ", len(kernels)-1)176177 s := bufio.NewScanner(in)178 fmt.Fprint(out, prompt)179 for s.Scan() {180 line := s.Text()181 if len(line) == 0 {182 return 0, nil183 }184185 num, err := strconv.Atoi(line)186 if err != nil || num < 0 || num >= len(kernels) {187 fmt.Fprintln(out, "Invalid input")188 fmt.Fprint(out, prompt)189 continue190 }191192 return num, nil193 }194195 if s.Err() != nil {196 return -1, s.Err()197 }198199 return -1, nil200}201202func kexecFileLoad(kernel kernelInfo) error {203 // Prepare to call kexec. kexec_file_load has the following signature204 // (stolen from the manpage):205 //206 // long kexec_file_load(int kernel_fd, int initrd_fd,207 // unsigned long cmdline_len, const char *cmdline,208 // unsigned long flags);209 //210 // This means we need to open the kernel and initrd files first, but the211 // rest of the parameters we can pass in directly.212213 // Multiple initrd and kernel files are not supported by kexec, so take214 // whatever comes first. A side effect of this is that this tool doesn't215 // support Grub variables, but that should not be a problem in most cases.216 kernelFirstPart, _, _ := strings.Cut(kernel.linux, " ")217 kernelFile, err := os.Open("/boot/" + kernelFirstPart)218 if err != nil {219 return err220 }221222 // Same as above.223 initrdFirstPart, _, _ := strings.Cut(kernel.initrd, " ")224 initrdFile, err := os.Open("/boot/" + initrdFirstPart)225 if err != nil {226 return err227 }228229 return unix.KexecFileLoad(230 int(kernelFile.Fd()), int(initrdFile.Fd()), kernel.options, 0)231}232233// reboot tells logind to reboot the system and kexec.234//235// One can also use the reboot syscall with the LINUX_REBOOT_CMD_KEXEC flag,236// but this performs the kexec immediately. Using logind allows us to go237// through most of the shutdown process before calling kexec (notably, flushing238// buffers, syncing, and stopping services).239//240// In the success case, it's unlikely that this function will return.241func reboot() error {242 bus, err := dbus.SystemBus()243 if err != nil {244 return err245 }246 defer bus.Close()247248 obj := bus.Object("org.freedesktop.login1", "/org/freedesktop/login1")249250 call := obj.Call(251 "org.freedesktop.login1.Manager.RebootWithFlags",252 0,253 SD_LOGIND_KEXEC_REBOOT,254 )255 if call.Err != nil {256 return call.Err257 }258259 return nil260}261262func main() {263 err := run()264 if err != nil {265 fmt.Printf("Command failed with: %s\n", err)266 os.Exit(1)267 }268}