├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.go ├── cmd └── bootnext │ ├── .gitignore │ ├── generate.go │ └── main.go ├── go.mod ├── go.sum └── internal ├── constants └── constants.go ├── elevate ├── elevate_linux.go └── elevate_windows.go ├── process ├── process.go ├── process_linux.go └── process_windows.go ├── reboot ├── reboot_linux.go └── reboot_windows.go └── uefi ├── entry.go ├── uefi_linux.go └── uefi_windows.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | 8 | # Checkout the source code 9 | - name: Checkout 10 | uses: actions/checkout@v3 11 | 12 | # Install Go 1.20 13 | - name: Setup Go 14 | uses: actions/setup-go@v3 15 | with: 16 | go-version: "1.20" 17 | 18 | # Build binaries for both Linux and Windows 19 | - name: Build for all platforms 20 | run: go mod download && go run build.go -release 21 | 22 | # If this is a tagged release then upload the release binaries 23 | - name: Upload release binaries 24 | uses: softprops/action-gh-release@v1 25 | if: startsWith(github.ref, 'refs/tags/') 26 | with: 27 | files: ./bin/bootnext-* 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .build/ 2 | bin/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TensorWorks Pty Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bootnext: sets the UEFI `BootNext` variable and reboots 2 | 3 | `bootnext` is a simple command-line tool for Linux and Windows that sets the UEFI [BootNext](https://uefi.org/specs/UEFI/2.10/03_Boot_Manager.html#globally-defined-variables) NVRAM variable and triggers a reboot into the target operating system. This facilitates quickly switching to another OS without modifying the default boot order. **Note that this tool only supports operating systems installed in UEFI mode rather than legacy BIOS mode, so be sure to read the [*Troubleshooting*](#troubleshooting) section for details on how to check which mode your OS is running under.** 4 | 5 | Booting into another OS just requires specifying a regular expression that will be matched against the human-readable description of the target UEFI boot entry: 6 | 7 | ```bash 8 | # Selects the Windows Boot Manager and boots into it 9 | bootnext windows 10 | 11 | # Selects the GRUB bootloader installed by Ubuntu Linux and boots into it 12 | bootnext ubuntu 13 | 14 | # Selects the first available bootable USB device and boots into it 15 | # (Note that the regular expression is case insensitive, so `bootnext usb` works too) 16 | bootnext USB 17 | ``` 18 | 19 | 20 | ## Contents 21 | 22 | - [Background and intended use](#background-and-intended-use) 23 | - [Installation](#installation) 24 | - [Prerequisites](#prerequisites) 25 | - [System-wide installation](#system-wide-installation) 26 | - [Portable installation](#portable-installation) 27 | - [Usage](#usage) 28 | - [Listing boot entries](#listing-boot-entries) 29 | - [Booting into a target OS](#booting-into-a-target-os) 30 | - [Performing a dry run](#performing-a-dry-run) 31 | - [Automatic privilege elevation](#automatic-privilege-elevation) 32 | - [Setting the `BootNext` variable without rebooting](#setting-the-bootnext-variable-without-rebooting) 33 | - [Troubleshooting](#troubleshooting) 34 | - [Booting into Linux just loads its bootloader (e.g. GRUB) and boots the default menu entry](#booting-into-linux-just-loads-its-bootloader-eg-grub-and-boots-the-default-menu-entry) 35 | - [Determining whether an operating system is running under UEFI mode or legacy BIOS mode](#determining-whether-an-operating-system-is-running-under-uefi-mode-or-legacy-bios-mode) 36 | - [Running `bootnext` prints the error `unsupported system configuration: the operating system has not been booted in UEFI mode`](#running-bootnext-prints-the-error-unsupported-system-configuration-the-operating-system-has-not-been-booted-in-uefi-mode) 37 | - [Building from source](#building-from-source) 38 | - [Legal](#legal) 39 | 40 | 41 | ## Background and intended use 42 | 43 | This tool is primarily designed for scenarios where a user is remotely accessing a bare metal machine that boots multiple operating systems, and they need to switch between OSes. Unless the remote machine supports [IPMI](https://en.wikipedia.org/wiki/Intelligent_Platform_Management_Interface) or is being accessed via a mechanism that supports BIOS/UEFI interaction such as a [KVM over IP switch](https://pikvm.org/), a lack of physical access typically precludes the ability to interact with any boot menus. The simplest fallback option is to manipulate the machine's UEFI NVRAM variables through OS-specific software mechanisms, and this is precisely what `bootnext` does: 44 | 45 | - Under Linux, the [efibootmgr](https://github.com/rhboot/efibootmgr) command is used to manipulate UEFI variables 46 | 47 | - Under Windows, the [bcdedit](https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/bcdedit) command is used to manipulate UEFI variables 48 | 49 | In addition to its intended use in remote access scenarios, `bootnext` can also simply act as a convenient cross-platform tool for quickly booting into a given target OS without needing to interact with the BIOS/UEFI. It is particularly handy when booting into installers and live environments stored on bootable USB devices, and can even be stored on the USB device itself when using multiboot systems such as [Ventoy](https://www.ventoy.net/). **However, take note of the limitations discussed in the [*Listing boot entries*](#listing-boot-entries) section, since USB devices will need to be present at system startup in order to be detected.** 50 | 51 | When accessing boot entries that are already present in the UEFI NVRAM, running `bootnext` is typically faster and easier than rebooting and accessing the UEFI boot menu. Its ability to automatically select the first boot entry whose description matches a user-provided regular expression also makes it slightly faster than directly running `efibootmgr` or `bcdedit`. 52 | 53 | When accessing boot entries that are not already present in the UEFI NVRAM and require a reboot in order to be detected (e.g USB devices that were plugged in after system startup), rebooting and running `bootnext` to trigger a second reboot will always be slower than just rebooting once and accessing the UEFI boot menu directly. That being said, using `bootnext` might still be preferable if you are unsure of the correct key to press to enter the boot menu at startup, or if you are using a machine with a buggy UEFI implementation that refuses to display a boot menu. (Note that if you do happen to have a machine with a buggy UEFI implementation, there are [other workarounds available](https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface#Enter_firmware_setup_without_function_keys) that you can always use to access the UEFI setup or boot into another OS, so `bootnext` simply represents a convenience on these machines rather than a necessity.) 54 | 55 | 56 | ## Installation 57 | 58 | ### Prerequisites 59 | 60 | Although the `bootnext` executable itself is fully self-contained, it does require that the appropriate OS-specific UEFI variable manipulation tool is available at runtime: 61 | 62 | - Under Linux, the [efibootmgr](https://github.com/rhboot/efibootmgr) command needs to be installed. It is available in the system package repositories of most distributions. For example: 63 | 64 | - Arch / Manjaro: `sudo pacman -S efibootmgr` 65 | - CentOS / Fedora / RHEL: `sudo dnf install efibootmgr` 66 | - Debian / Ubuntu: `sudo apt-get install efibootmgr` 67 | - openSUSE: `sudo zypper install efibootmgr` 68 | 69 | - Under Windows, the `bcdedit` command ships with the operating system by default, so nothing needs to be installed. 70 | 71 | ### System-wide installation 72 | 73 | Pre-compiled binaries can be downloaded from the [releases page](https://github.com/TensorWorks/bootnext/releases), and just need to be placed in a directory that is included in the system's `PATH` environment variable. **Note that you will need to download the appropriate binary for your operating system and CPU architecture,** and each binary is named with the suffix `-OS-ARCH`, where `OS` is either `linux` or `windows` and `ARCH` is the name of the CPU architecture. The architecture names follow the naming conventions [used by the Go programming language](https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63#goarch-values), and the following architectures are supported: 74 | 75 | - `386`: 32-bit x86 76 | - `amd64`: 64-bit x86, also known as x86_64 77 | - `arm64`: 64-bit ARM 78 | 79 | You can use the following commands to download and install the latest release binary under Linux: 80 | 81 | ```bash 82 | # Note: replace the `amd64` suffix with the appropriate value under other CPU architectures 83 | URL="https://github.com/TensorWorks/bootnext/releases/download/v0.0.2/bootnext-linux-amd64" 84 | sudo curl -fSL "$URL" -o /usr/local/bin/bootnext 85 | sudo chmod +x /usr/local/bin/bootnext 86 | ``` 87 | 88 | Under Windows, run the following command from an elevated command prompt or PowerShell window: 89 | 90 | ```powershell 91 | # Note: replace the `amd64` suffix with the appropriate value under other CPU architectures 92 | curl.exe -fSL "https://github.com/TensorWorks/bootnext/releases/download/v0.0.2/bootnext-windows-amd64.exe" -o "C:\Windows\System32\bootnext.exe" 93 | ``` 94 | 95 | ### Portable installation 96 | 97 | If you are using `bootnext` to boot from a multiboot USB device managed by [Ventoy](https://www.ventoy.net/) then it can be handy to store the binaries on the USB device itself. As mentioned in the [*Prerequisites*](#prerequisites) section, the binaries are fully portable and self-contained, so they can simply be downloaded from the [releases page](https://github.com/TensorWorks/bootnext/releases) and copied to the Ventoy filesystem partition on the USB device. Note that if you are running `bootnext` from a USB device on a Linux system then you will still need to ensure the `efibootmgr` command is installed on the system, just as you would when running a system-wide installation of `bootnext`. 98 | 99 | With the binaries copied to the Ventoy partition, booting to the USB device is as simple as running `bootnext usb` from a terminal or command prompt in the directory containing the binaries, replacing `bootnext` with the appropriate binary for the system (e.g. `./bootnext-linux-amd64` under Linux or `.\bootnext-windows-amd64.exe` under Windows). As discussed in the [*Automatic privilege elevation*](#automatic-privilege-elevation) section, you will be prompted for elevated privileges (i.e. a `sudo` password request under Linux or a User Account Control dialog under Windows) if you are not running `bootnext` as a user with administrative privileges. 100 | 101 | For an even simpler workflow on Windows machines, you can create a batch file and place it in the same directory as the `bootnext` binaries: 102 | 103 | ```bat 104 | @rem Note: replace the `amd64` suffix with the appropriate value under other CPU architectures 105 | %~dp0.\bootnext-windows-amd64.exe usb 106 | ``` 107 | 108 | With this batch file in place, booting to the USB device is as simple as double-clicking the `.bat` file in Windows Explorer, and accepting the UAC prompt that is displayed. 109 | 110 | 111 | ## Usage 112 | 113 | ### Listing boot entries 114 | 115 | Before making any changes to the UEFI configuration, you can list the available UEFI boot entries by specifying the `--list` flag: 116 | 117 | ```bash 118 | bootnext --list 119 | ``` 120 | 121 | Querying the available boot entries can be useful in scenarios where you are unsure of exactly how a given boot entry is labelled, and can help to inform the pattern that you specify when instructing `bootnext` to select a target boot entry. 122 | 123 | There are a couple of important things to note regarding the UEFI boot entries that are listed: 124 | 125 | - Boot entries are detected by the system UEFI/BIOS at startup, and the list that the currently running operating system sees will reflect the entries that were present when the system first booted. As a result, you will only see entries for USB devices if those devices were plugged in when the machine was powered on, and you will continue to see entries for USB devices that were present at startup even if you unplug the devices and the entries are invalid. 126 | 127 | - Unlike `efibootmgr` under Linux, `bcdedit` under Windows does not seem to report boot entries for network booting options (e.g. PXE booting) on some machines. As a result, more UEFI boot entries may be listed under Linux than under Windows for the same machine. 128 | 129 | ### Booting into a target OS 130 | 131 | When booting into a target OS, `bootnext` requires a single argument to specify which UEFI boot entry should be selected. This argument represents a case-insensitive regular expression (using the [syntax supported by Go](https://pkg.go.dev/regexp/syntax), which is consistent with other languages such as Perl or Python), but specifying a simple string will behave like a plain string match so long as no characters are included that have a special meaning in the regular expression syntax. 132 | 133 | Each UEFI boot entry will be checked against the supplied pattern, and the first entry that matches will be selected as the target. **Note that only a subset of the boot entry's human-readable description needs to match the regular expression pattern, rather than the entire string.** 134 | 135 | Consider this example list of boot entries: 136 | 137 | ```bash 138 | $ bootnext --list 139 | 140 | Detected the following UEFI boot entries: 141 | - ID: "0000", Description: "ubuntu" 142 | - ID: "0002", Description: "Linux" 143 | - ID: "0003", Description: "Windows Boot Manager" 144 | - ID: "0005", Description: "UEFI: HTTP IPv4 Intel(R) I211 Gigabit Network Connection" 145 | - ID: "0006", Description: "UEFI: PXE IPv4 Intel(R) I211 Gigabit Network Connection" 146 | - ID: "0007", Description: "UEFI: HTTP IPv6 Intel(R) I211 Gigabit Network Connection" 147 | - ID: "0008", Description: "UEFI: PXE IPv6 Intel(R) I211 Gigabit Network Connection" 148 | - ID: "0009", Description: "UEFI: USB, Partition 1" 149 | ``` 150 | 151 | The following regular expressions demonstrate the matching behaviour used by `bootnext`: 152 | 153 | - The pattern "`ubuntu`" will match `ubuntu` and that entry will be booted. 154 | 155 | - The pattern "`windows`" will match `Windows Boot Manager` and that entry will be booted. 156 | 157 | - The pattern "`usb`" will match `UEFI: USB, Partition 1` and that entry will be booted. 158 | 159 | - The pattern "`network`" will match all of the network booting entries, so the first one (in this case, `UEFI: HTTP IPv4 Intel(R) I211 Gigabit Network Connection`) will be booted. 160 | 161 | - The pattern "`uefi`" will match all of the network booting and USB boot entries, so the first one (in this case, `UEFI: HTTP IPv4 Intel(R) I211 Gigabit Network Connection`) will be booted. 162 | 163 | - The pattern "`u`" will match both `ubuntu` and all of the network booting and USB boot entries, so the first one (in this case, `ubuntu`) will be booted. 164 | 165 | ### Performing a dry run 166 | 167 | If you would like to test a regular expression to determine which boot entry will be matched, without actually modifying the `BootNext` UEFI NVRAM variable or rebooting, you can specify the `--dry-run` flag: 168 | 169 | ```bash 170 | # Tests which boot entry will be matched by the pattern "uefi" without modifying the system 171 | bootnext uefi --dry-run 172 | ``` 173 | 174 | ### Automatic privilege elevation 175 | 176 | Writing to the system's UEFI NVRAM variables requires administrative privileges under both Linux and Windows, and reading the NVRAM variables also requires administrative privileges under Windows. To , `bootnext` will detect whether it is running with the required privileges for a given command, and automatically request elevated privileges when they are not present: 177 | 178 | - Under Linux, `bootnext` will re-run itself using `sudo`, which will prompt the user for their password. This behaviour will be triggered when running a command that writes to the UEFI NVRAM variables and the user is not root. It will not be triggered when running as the root user or when the `--help`, `--list` or `--dry-run` flags are specified. 179 | 180 | - Under Windows, `bootnext` will re-run itself as an elevated child process, which will trigger a [User Account Control (UAC)](https://learn.microsoft.com/en-us/windows/security/application-security/application-control/user-account-control/) dialog prompting the user to allow this. This behaviour will be triggered when running a command that reads or writes the UEFI NVRAM variables and the parent process is not already running with elevated privileges. It will not be triggered when the parent process is already running with elevated privileges or when the `--help` flag is specified. 181 | 182 | **It is important to note that when a Windows command-line application is run as an elevated child process, it will open a new console window rather than inheriting the console window from its non-elevated parent process.** To prevent the new console window from disappearing after `bootnext` finishes running, the privilege elevation logic will append the `--pause` flag when running the child process, which instructs `bootnext` to pause for the user to press a key before it exits. 183 | 184 | If you would like `bootnext` to disable this automatic privilege elevation logic and instead exit with an error when it encounters a permissions error due to insufficient privileges, then you can specify the `--no-elevate` flag: 185 | 186 | ```bash 187 | # Attempts to boot from a USB device, and fails if running with insufficient privileges 188 | bootnext usb --no-elevate 189 | ``` 190 | 191 | This flag may be useful if you are invoking `bootnext` from an automated script that will run unattended, and an interactive prompt for privilege elevation would potentially hang indefinitely. In such a scenario, the script itself should be run with administrative privileges, and running it with insufficient privileges represents a genuine error that should be detected and reported. 192 | 193 | ### Setting the `BootNext` variable without rebooting 194 | 195 | If you would like to modify the `BootNext` UEFI NVRAM variable without triggering an immediate system reboot, you can specify the `--no-reboot` flag: 196 | 197 | ```bash 198 | # Sets the BootNext variable to boot from a USB device, and exits without rebooting 199 | bootnext usb --no-reboot 200 | ``` 201 | 202 | The NVRAM variable will be set to the desired value, and will take effect the next time the machine is restarted. 203 | 204 | 205 | ## Troubleshooting 206 | 207 | ### Booting into Linux just loads its bootloader (e.g. GRUB) and boots the default menu entry 208 | 209 | This is the expected behaviour for Linux distros that use a boot manager like GRUB as their UEFI bootloader. You will need to set your Linux distro as the default menu entry to ensure the bootloader boots into your distro when targeting the bootloader from `bootnext`. 210 | 211 | ### Determining whether an operating system is running under UEFI mode or legacy BIOS mode 212 | 213 | The method of determining whether the system is booted in UEFI mode varies based on the operating system: 214 | 215 | - Under Windows, there are a number of options available for checking the firmware type, including both command line tools and GUI tools. [This article](https://www.tenforums.com/tutorials/85195-check-if-windows-10-using-uefi-legacy-bios.html) lists various options that apply to Windows 10, Windows 11, Windows Server 2019 and Windows Server 2022. 216 | 217 | - Under Linux, the simplest option is to check whether the directory `/sys/firmware/efi` exists. If it does exist then the system was booted in UEFI mode, and if it does not exist then the system was booted in either legacy BIOS mode or with non-UEFI firmware. 218 | 219 | ### Running `bootnext` prints the error `unsupported system configuration: the operating system has not been booted in UEFI mode` 220 | 221 | This indicates that the operating system under which you are running `bootnext` was not itself booted in UEFI mode, and you can confirm this by following the instructions from the section above to check which mode it was booted in. This typically means the OS was installed in legacy BIOS mode, or that you're attempting to run `bootnext` on a device that does not ship with UEFI firmware, such as a single-board computer (e.g. a [Raspberry Pi](https://www.raspberrypi.org/)) or a [Google Chromebook](https://www.google.com/intl/en_au/chromebook/). 222 | 223 | **Since `bootnext` specifically targets UEFI systems, it cannot run in these environments.** For machines that ship with UEFI firmware (e.g. most modern computers with x86 CPUs), the only way to run `bootnext` is to either reinstall the operating system in UEFI mode or attempt to convert the existing OS installation to UEFI: 224 | 225 | - Existing Windows installations can be converted to UEFI using Microsoft's [MBR2GPT](https://learn.microsoft.com/en-us/windows/deployment/mbr-to-gpt) tool, which ships with Windows 10 version 1703 and newer. The tool facilitates in-place conversion without the need to reinstall anything, and is typically the easiest option for Windows. Correct use of MBR2GPT is outside the scope of this README. 226 | 227 | - Existing Linux installations can be converted to UEFI by manually modifying filesystem partitions and installing bootloader files to the EFI System Partition (ESP). This may be more difficult than performing a full reinstall if you are unfamiliar with disk partitioning and with manual configuration of your distribution's preferred bootloader (e.g. GRUB). The details of this process will vary based on your chosen Linux distribution and are outside the scope of this README. 228 | 229 | For devices that do not ship with UEFI firmware, it may be possible to flash custom firmware. Note however that this does not guarantee that a device will be able to run `bootnext`, and could also impact the functionality of your device: 230 | 231 | - The [Pi Firmware Task Force](https://github.com/pftf) GitHub organisation provides UEFI firmware images for [Raspberry Pi 3](https://github.com/pftf/RPi3) and [Raspberry Pi 4](https://github.com/pftf/RPi4) single-board computers. **However, since [the Raspberry Pi does not have NVRAM](https://github.com/tianocore/edk2-platforms/tree/master/Platform/RaspberryPi/RPi4#nvram), tools such as `efibootmgr` (and by extension `bootnext`) will not function correctly.** Note that use of the UEFI firmware images may also impact the ability to access certain hardware features such as GPIO. 232 | 233 | - The [MrChromebox](https://mrchromebox.tech/) website provides a script that can install UEFI firmware on some Google Chromebooks with x86 CPUs. Note that the custom UEFI firmware cannot boot ChromeOS, so a device flashed with this firmware will only be able to boot other operating systems such as Linux and Windows. 234 | 235 | 236 | ## Building from source 237 | 238 | Building `bootnext` from source requires [Go](https://go.dev/) 1.18 or newer (1.20 or newer is recommended). To build the binaries for your host platform, run the following commands from the root of the source tree: 239 | 240 | ```bash 241 | # Downloads the packages that bootnext depends upon 242 | go mod download 243 | 244 | # Builds the bootnext binary for the host OS and CPU architecture 245 | go run build.go 246 | ``` 247 | 248 | To build binaries for all supported platforms, specify the `-release` flag: 249 | 250 | ```bash 251 | # Builds bootnext binaries for all supported OSes and CPU architectures 252 | go run build.go -release 253 | ``` 254 | 255 | 256 | ## Legal 257 | 258 | Copyright © 2023 TensorWorks Pty Ltd. Licensed under the MIT License, see the file [LICENSE](./LICENSE) for details. 259 | -------------------------------------------------------------------------------- /build.go: -------------------------------------------------------------------------------- 1 | //go:build never 2 | 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "os" 8 | 9 | module "github.com/tensorworks/go-build-helpers/pkg/module" 10 | validation "github.com/tensorworks/go-build-helpers/pkg/validation" 11 | ) 12 | 13 | // Alias validation.ExitIfError() as check() 14 | var check = validation.ExitIfError 15 | 16 | func main() { 17 | 18 | // Parse our command-line flags 19 | doClean := flag.Bool("clean", false, "cleans build outputs") 20 | doRelease := flag.Bool("release", false, "builds executables for all target platforms") 21 | flag.Parse() 22 | 23 | // Disable CGO 24 | os.Setenv("CGO_ENABLED", "0") 25 | 26 | // Create a build helper for the Go module in the current working directory 27 | mod, err := module.ModuleInCwd() 28 | check(err) 29 | 30 | // Determine if we're cleaning the build outputs 31 | if *doClean == true { 32 | check(mod.CleanAll()) 33 | os.Exit(0) 34 | } 35 | 36 | // Install the `go-winres` tool that we use for embedding manifest data in Windows builds 37 | check(mod.InstallGoTools([]string{ 38 | "github.com/tc-hib/go-winres@v0.3.1", 39 | })) 40 | 41 | // Run `go generate` to invoke `go-winres` 42 | check(mod.Generate()) 43 | 44 | // Determine if we're building our executables for just the host platform or for the full matrix of release platforms 45 | if *doRelease == false { 46 | check(mod.BuildBinariesForHost(module.DefaultBinDir, module.BuildOptions{Scheme: module.Undecorated})) 47 | } else { 48 | check(mod.BuildBinariesForMatrix( 49 | module.DefaultBinDir, 50 | 51 | module.BuildOptions{ 52 | AdditionalFlags: []string{"-ldflags", "-s -w"}, 53 | Scheme: module.SuffixedFilenames, 54 | }, 55 | 56 | module.BuildMatrix{ 57 | Platforms: []string{"linux", "windows"}, 58 | Architectures: []string{"386", "amd64", "arm64"}, 59 | Ignore: []string{}, 60 | }, 61 | )) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /cmd/bootnext/.gitignore: -------------------------------------------------------------------------------- 1 | *.syso 2 | -------------------------------------------------------------------------------- /cmd/bootnext/generate.go: -------------------------------------------------------------------------------- 1 | //go:generate go-winres simply --arch 386,amd64,arm64 --product-name "BootNext" --file-description "BootNext" 2 | 3 | package main 4 | -------------------------------------------------------------------------------- /cmd/bootnext/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "regexp" 8 | "runtime" 9 | "strings" 10 | 11 | "github.com/spf13/cobra" 12 | "github.com/tensorworks/bootnext/internal/constants" 13 | "github.com/tensorworks/bootnext/internal/elevate" 14 | "github.com/tensorworks/bootnext/internal/process" 15 | "github.com/tensorworks/bootnext/internal/reboot" 16 | "github.com/tensorworks/bootnext/internal/uefi" 17 | ) 18 | 19 | func run(pattern string, dryRun bool, listOnly bool, noElevate bool, noReboot bool) error { 20 | 21 | // Verify that the operating system has been booted in UEFI mode 22 | enabled, err := uefi.IsUEFIEnabled() 23 | if err != nil { 24 | return fmt.Errorf("failed to query system UEFI status: %v", err) 25 | } else if !enabled { 26 | return fmt.Errorf("unsupported system configuration: the operating system has not been booted in UEFI mode") 27 | } 28 | 29 | // Verify that all of the system tools we require for interacting with UEFI NVRAM variables are available 30 | requiredTools := uefi.RequiredTools() 31 | for _, tool := range requiredTools { 32 | if _, err := exec.LookPath(tool); err != nil { 33 | return fmt.Errorf("a required application was not found in the system PATH: %v", tool) 34 | } 35 | } 36 | 37 | // Determine whether we require elevated privileges 38 | // (We need them for writing to NVRAM variables under Linux, and for both reading and writing under Windows) 39 | requireElevation := (!dryRun && !listOnly) || runtime.GOOS == "windows" 40 | 41 | // Determine whether the process is running with insufficient privileges 42 | if requireElevation && !elevate.IsElevated() { 43 | 44 | // Determine whether we should automatically request elevated privileges 45 | if !noElevate { 46 | 47 | // Re-run the process with elevated privileges and propagate the exit code 48 | exitCode, err := elevate.RunElevated() 49 | if err != nil { 50 | return fmt.Errorf("failed to re-launch the process with elevated privileges: %v", err) 51 | } else { 52 | os.Exit(exitCode) 53 | } 54 | 55 | } else { 56 | fmt.Print("Warning: running without elevated privileges, access to UEFI NVRAM variables may be denied.\n\n") 57 | } 58 | } 59 | 60 | // Retrieve the list of UEFI boot entries 61 | entries, err := uefi.ListBootEntries() 62 | if err != nil { 63 | return fmt.Errorf("failed to list UEFI boot entries: %v", err) 64 | } 65 | 66 | // Print the list of boot entries 67 | fmt.Println("Detected the following UEFI boot entries:") 68 | for _, entry := range entries { 69 | fmt.Print("- ID: \"", entry.ID, "\", Description: \"", entry.Description, "\"\n") 70 | } 71 | 72 | // If we are just listing the boot entries then stop here 73 | if listOnly { 74 | return nil 75 | } 76 | 77 | // Compile the regular expression pattern supplied by the user, enabling case-insensitive matching 78 | regex, err := regexp.Compile(fmt.Sprintf("(?i)%s", pattern)) 79 | if err != nil { 80 | return fmt.Errorf("failed to compile regular expression \"%s\": %v", pattern, err) 81 | } 82 | 83 | // Identify the first boot entry that matches the pattern 84 | fmt.Printf("\nMatching boot entries against regular expression \"%s\"\n", pattern) 85 | for _, entry := range entries { 86 | if regex.MatchString(entry.Description) { 87 | 88 | // Print the matching boot entry 89 | fmt.Printf("Found matching boot entry: \"%s\"\n", entry.Description) 90 | 91 | // Don't modify the BootNext variable or reboot if we are performing a dry run 92 | if !dryRun { 93 | 94 | // Set the value of the BootNext variable to the entry's identifier 95 | fmt.Println("Setting the BootNext variable...") 96 | if err := uefi.SetBootNext(entry); err != nil { 97 | return fmt.Errorf("failed to set BootNext variable value: %v", err) 98 | } 99 | 100 | // Determine whether we are triggering a reboot 101 | if !noReboot { 102 | fmt.Println("Rebooting now...") 103 | if err := reboot.Reboot(); err != nil { 104 | return fmt.Errorf("failed to reboot: %v", err) 105 | } 106 | } 107 | } 108 | 109 | return nil 110 | } 111 | } 112 | 113 | // If we reach this point then none of the boot entries matched the pattern 114 | return fmt.Errorf("could not find any UEFI boot entries matching the pattern \"%s\"", pattern) 115 | } 116 | 117 | func main() { 118 | 119 | // Define our Cobra command 120 | command := &cobra.Command{ 121 | 122 | Long: strings.Join([]string{ 123 | fmt.Sprintf("bootnext v%s", constants.VERSION), 124 | "Copyright (c) 2023, TensorWorks Pty Ltd", 125 | "", 126 | "Sets the UEFI \"BootNext\" variable and triggers a reboot into the target operating system.", 127 | "This facilitates quickly switching to another OS without modifying the default boot order.", 128 | }, "\n"), 129 | 130 | Use: "bootnext pattern", 131 | 132 | SilenceUsage: true, 133 | 134 | Example: strings.Join([]string{ 135 | " bootnext windows Selects the Windows Boot Manager and boots into it", 136 | " bootnext ubuntu Selects the GRUB bootloader installed by Ubuntu Linux and boots into it", 137 | " bootnext USB Selects the first available bootable USB device and boots into it", 138 | }, "\n"), 139 | } 140 | 141 | // Inject the usage information for our command's positional arguments 142 | patternUsage := strings.Join([]string{ 143 | " pattern A regular expression that will be used to select the target boot entry", 144 | " (case insensitive)", 145 | }, "\n") 146 | template := command.UsageTemplate() 147 | template = strings.Replace(template, "\nFlags:\n", fmt.Sprintf("\nPositional Arguments:\n%s\n\nFlags:\n", patternUsage), 1) 148 | command.SetUsageTemplate(template) 149 | 150 | // Define the command-line flags for our command 151 | dryRun := command.Flags().Bool("dry-run", false, "Describe the actions that would be performed but do not make any changes to the system") 152 | listOnly := command.Flags().Bool("list", false, "Print the list of UEFI boot entries but do not set the BootNext variable") 153 | noElevate := command.Flags().Bool("no-elevate", false, "Do not automatically prompt for elevated privileges when required") 154 | noReboot := command.Flags().Bool("no-reboot", false, "Do not automatically reboot after setting the BootNext variable") 155 | pause := command.Flags().Bool("pause", false, "Pause for input when the application is finished running") 156 | 157 | // Wire up the validation logic for our command-line flags and positional arguments 158 | command.RunE = func(cmd *cobra.Command, args []string) error { 159 | 160 | // If no flags or arguments were specified then print the usage message 161 | if len(os.Args) < 2 { 162 | cmd.Help() 163 | return nil 164 | } 165 | 166 | // Verify that a pattern was provided if `--list` was not specified 167 | pattern := "" 168 | if len(args) > 0 { 169 | pattern = args[0] 170 | } else if !*listOnly { 171 | return fmt.Errorf("a pattern must be specified for selecting the target UEFI boot entry") 172 | } 173 | 174 | // Process the provided input values and propagate any errors 175 | return run(pattern, *dryRun, *listOnly, *noElevate, *noReboot) 176 | } 177 | 178 | // Execute the command 179 | err := command.Execute() 180 | if err != nil { 181 | process.ExitWithPause(1, *pause) 182 | } 183 | 184 | process.ExitWithPause(0, *pause) 185 | } 186 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tensorworks/bootnext 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/spf13/cobra v1.7.0 7 | github.com/tensorworks/go-build-helpers v0.0.5 8 | golang.org/x/sys v0.8.0 9 | ) 10 | 11 | require ( 12 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 13 | github.com/spf13/pflag v1.0.5 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 3 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 4 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 5 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= 6 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= 7 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 8 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 9 | github.com/tensorworks/go-build-helpers v0.0.5 h1:6XcKJ0fJ+x6c+cnBOcJpZhJtNoaR+oEZ2gJLjB9Jrng= 10 | github.com/tensorworks/go-build-helpers v0.0.5/go.mod h1:t7C4BkFt5RsSAPOII2D0SgahcdoizZvhejrAjd/Biyc= 11 | golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= 12 | golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 15 | -------------------------------------------------------------------------------- /internal/constants/constants.go: -------------------------------------------------------------------------------- 1 | package constants 2 | 3 | // The version number for bootnext 4 | const VERSION = "0.0.2" 5 | -------------------------------------------------------------------------------- /internal/elevate/elevate_linux.go: -------------------------------------------------------------------------------- 1 | package elevate 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/tensorworks/bootnext/internal/process" 7 | "golang.org/x/sys/unix" 8 | ) 9 | 10 | // Determines whether the current process is running with elevated privileges 11 | func IsElevated() bool { 12 | return unix.Geteuid() == 0 13 | } 14 | 15 | // Re-launches the current process with elevated privileges 16 | func RunElevated() (int, error) { 17 | 18 | // Retrieve the path to the executable for the current process 19 | executable, err := os.Executable() 20 | if err != nil { 21 | return -1, err 22 | } 23 | 24 | // Attempt to re-run the executable using `sudo`, ensuring it inherits the standard streams from the parent 25 | command := append([]string{"sudo", executable}, os.Args[1:]...) 26 | return process.RunWithInheritedHandles(command) 27 | } 28 | -------------------------------------------------------------------------------- /internal/elevate/elevate_windows.go: -------------------------------------------------------------------------------- 1 | package elevate 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "syscall" 8 | "unsafe" 9 | 10 | "golang.org/x/sys/windows" 11 | ) 12 | 13 | var ( 14 | kernel32 = windows.NewLazyDLL("kernel32.dll") 15 | shell32 = windows.NewLazyDLL("shell32.dll") 16 | attachConsole = kernel32.NewProc("AttachConsole") 17 | freeConsole = kernel32.NewProc("FreeConsole") 18 | shellExecuteExW = shell32.NewProc("ShellExecuteExW") 19 | ) 20 | 21 | // Constants from: 22 | const ( 23 | 24 | // Bitmask flags 25 | _SEE_MASK_NOCLOSEPROCESS uint32 = 0x00000040 26 | _SEE_MASK_NO_CONSOLE uint32 = 0x00008000 27 | 28 | // Error codes 29 | _SE_ERR_FNF uint32 = 2 30 | _SE_ERR_PNF uint32 = 3 31 | _SE_ERR_ACCESSDENIED uint32 = 5 32 | _SE_ERR_OOM uint32 = 8 33 | _SE_ERR_SHARE uint32 = 26 34 | _SE_ERR_ASSOCINCOMPLETE uint32 = 27 35 | _SE_ERR_DDETIMEOUT uint32 = 28 36 | _SE_ERR_DDEFAIL uint32 = 29 37 | _SE_ERR_DDEBUSY uint32 = 30 38 | _SE_ERR_NOASSOC uint32 = 31 39 | _SE_ERR_DLLNOTFOUND uint32 = 32 40 | ) 41 | 42 | // Constant from: 43 | const ATTACH_PARENT_PROCESS uint32 = 0x0ffffffff 44 | 45 | // SHELLEXECUTEINFOW structure, from: 46 | type _SHELLEXECUTEINFOW struct { 47 | cbSize uint32 48 | fMask uint32 49 | hwnd uintptr 50 | lpVerb uintptr 51 | lpFile uintptr 52 | lpParameters uintptr 53 | lpDirectory uintptr 54 | nShow int32 55 | hInstApp uintptr 56 | lpIDList uintptr 57 | lpClass uintptr 58 | hkeyClass uintptr 59 | dwHotKey uint32 60 | union_hIcon_hMonitor uintptr 61 | hProcess windows.Handle 62 | } 63 | 64 | // Detect when we are an elevated child process launched by `RunElevated()` and attach to the console of our 65 | // non-elevated parent process 66 | // 67 | // Note: this workaround is necessary because the elevated executable will always open in a new console window, 68 | // which will then close almost immediately, before the user has a chance to read any output. 69 | // 70 | // Although methods do exist for creating elevated processes that inherit the standard handles from a non-elevated 71 | // parent process (), these workflows are excessively complex 72 | // and require the use of an additional executable that would need to be bundled with bootnext. 73 | // 74 | // Instead, we programmatically detect when we're the elevated child process and attach to the parent process console. 75 | // This workaround is inspired by the implementation of the `sudo` command in Luke Sampson's "psutils" project: 76 | // 77 | func init() { 78 | 79 | // Don't bother checking our parent process details if we're running as a non-elevated process 80 | if !IsElevated() { 81 | return 82 | } 83 | 84 | // Retrieve the PID for the current process 85 | currentPID := windows.GetCurrentProcessId() 86 | 87 | // Retrieve the path to the executable for the current process 88 | executable, err := os.Executable() 89 | if err != nil { 90 | return 91 | } 92 | 93 | // Create a snapshot of the processes currently running on the system 94 | snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPPROCESS, 0) 95 | if err != nil { 96 | return 97 | } 98 | 99 | // Ensure the snapshot handle is closed when this function completes 100 | defer windows.CloseHandle(snapshot) 101 | 102 | // Create a `PROCESSENTRY32` struct and populate its size field 103 | var processEntry windows.ProcessEntry32 104 | processEntry.Size = uint32(unsafe.Sizeof(processEntry)) 105 | 106 | // Iterate over the processes in the snapshot until we find the details for the current process 107 | err = windows.Process32First(snapshot, &processEntry) 108 | for err == nil && processEntry.ProcessID != currentPID { 109 | err = windows.Process32Next(snapshot, &processEntry) 110 | } 111 | 112 | // Verify that we found the details for the current process 113 | if err != nil { 114 | return 115 | } 116 | 117 | // Retrieve the PID of our parent process and attempt to open a process handle 118 | // Attempt to open a handle to our parent process using its PID 119 | parentProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, true, processEntry.ParentProcessID) 120 | if err != nil { 121 | return 122 | } 123 | 124 | // Ensure the parent process handle is closed when this function completes 125 | defer windows.CloseHandle(parentProcess) 126 | 127 | // Retrieve the module for the parent process's executable 128 | var parentModule windows.Handle 129 | var bytesNeeded uint32 130 | if err := windows.EnumProcessModules(parentProcess, &parentModule, uint32(unsafe.Sizeof(parentModule)), &bytesNeeded); err != nil { 131 | return 132 | } 133 | 134 | // Retrieve the path to the executable for the parent process 135 | const bufSize = 4096 136 | var buffer [bufSize]uint16 137 | if err := windows.GetModuleFileNameEx(parentProcess, parentModule, &buffer[0], bufSize); err != nil { 138 | return 139 | } 140 | 141 | // If the executable paths match for the current process and the parent process then we're an elevated child process created by `RunElevated()` 142 | if windows.UTF16ToString(buffer[:]) == executable { 143 | 144 | // Detach from the console that was created for the elevated child process 145 | freeConsole.Call() 146 | 147 | // Attach to the console of the non-elevated parent process 148 | attachConsole.Call(uintptr(ATTACH_PARENT_PROCESS)) 149 | 150 | // Update the standard handles in the `syscall` package 151 | // (See: ) 152 | syscall.Stdin, _ = syscall.GetStdHandle(syscall.STD_INPUT_HANDLE) 153 | syscall.Stdout, _ = syscall.GetStdHandle(syscall.STD_OUTPUT_HANDLE) 154 | syscall.Stderr, _ = syscall.GetStdHandle(syscall.STD_ERROR_HANDLE) 155 | 156 | // Update the corresponding file objects in the `os` package 157 | // (See: ) 158 | os.Stdin = os.NewFile(uintptr(syscall.Stdin), "/dev/stdin") 159 | os.Stdout = os.NewFile(uintptr(syscall.Stdout), "/dev/stdout") 160 | os.Stderr = os.NewFile(uintptr(syscall.Stderr), "/dev/stderr") 161 | } 162 | } 163 | 164 | // Determines whether the current process is running with elevated privileges 165 | func IsElevated() bool { 166 | return windows.GetCurrentProcessToken().IsElevated() 167 | } 168 | 169 | // Re-launches the current process with elevated privileges 170 | func RunElevated() (int, error) { 171 | 172 | // Retrieve the path to the executable for the current process 173 | executable, err := os.Executable() 174 | if err != nil { 175 | return -1, err 176 | } 177 | 178 | // Escape each of the arguments that were passed to the current process 179 | escapedArgs := []string{} 180 | for _, arg := range os.Args[1:] { 181 | 182 | // Iterate over each character in the argument 183 | var builder strings.Builder 184 | for index, char := range arg { 185 | 186 | // If the last character in the argument is a backslash then ignore it 187 | // (This ensures our trailing double quote isn't inadvertently escaped) 188 | if char == '\\' && index == len(arg)-1 { 189 | continue 190 | } 191 | 192 | // Prepend backslashes to any double quotes or existing backslashes 193 | if char == '"' || char == '\\' { 194 | builder.WriteRune('\\') 195 | } 196 | 197 | // Add the character to our escaped string 198 | builder.WriteRune(char) 199 | } 200 | 201 | // Wrap the escaped argument in double quotes, regardless of whether it contains any spaces 202 | escapedArgs = append(escapedArgs, fmt.Sprintf("\"%s\"", builder.String())) 203 | } 204 | 205 | // Convert each of the text parameters to UTF-16 strings 206 | verb, err := windows.UTF16PtrFromString("runas") 207 | if err != nil { 208 | return -1, err 209 | } 210 | file, err := windows.UTF16PtrFromString(executable) 211 | if err != nil { 212 | return -1, err 213 | } 214 | args, err := windows.UTF16PtrFromString(strings.Join(escapedArgs, " ")) 215 | if err != nil { 216 | return -1, err 217 | } 218 | 219 | // Prepare our input struct for `ShellExecuteExW()` 220 | execInfo := &_SHELLEXECUTEINFOW{ 221 | cbSize: uint32(unsafe.Sizeof(_SHELLEXECUTEINFOW{})), 222 | fMask: _SEE_MASK_NOCLOSEPROCESS, 223 | hwnd: 0, 224 | lpVerb: uintptr(unsafe.Pointer(verb)), 225 | lpFile: uintptr(unsafe.Pointer(file)), 226 | lpParameters: uintptr(unsafe.Pointer(args)), 227 | lpDirectory: 0, 228 | nShow: windows.SW_HIDE, 229 | hInstApp: 0, // Will be set by `ShellExecuteExW()` 230 | lpIDList: 0, 231 | lpClass: 0, 232 | hkeyClass: 0, 233 | dwHotKey: 0, 234 | union_hIcon_hMonitor: 0, 235 | hProcess: 0, // Will be set by `ShellExecuteExW()` 236 | } 237 | 238 | // Attempt to launch the process with elevated privileges and verify that the child process started successfully 239 | result, _, lastError := shellExecuteExW.Call(uintptr(unsafe.Pointer(execInfo))) 240 | if uint32(result) == 0 { 241 | errorMessages := map[uint32]string{ 242 | _SE_ERR_FNF: "File not found.", 243 | _SE_ERR_PNF: "Path not found.", 244 | _SE_ERR_ACCESSDENIED: "Access denied.", 245 | _SE_ERR_OOM: "Out of memory.", 246 | _SE_ERR_SHARE: "Cannot share an open file.", 247 | _SE_ERR_ASSOCINCOMPLETE: "File association information not complete.", 248 | _SE_ERR_DDETIMEOUT: "DDE operation timed out.", 249 | _SE_ERR_DDEFAIL: "DDE operation failed.", 250 | _SE_ERR_DDEBUSY: "DDE operation is busy.", 251 | _SE_ERR_NOASSOC: "File association not available.", 252 | _SE_ERR_DLLNOTFOUND: "Dynamic-link library not found.", 253 | } 254 | if message, found := errorMessages[uint32(execInfo.hInstApp)]; found { 255 | return -1, fmt.Errorf(message) 256 | } else { 257 | return -1, lastError 258 | } 259 | } 260 | 261 | // Ensure we close the handle to the child process when we're done 262 | defer windows.CloseHandle(execInfo.hProcess) 263 | 264 | // Wait for the child process to complete 265 | if _, err := windows.WaitForSingleObject(execInfo.hProcess, windows.INFINITE); err != nil { 266 | return -1, err 267 | } 268 | 269 | // Retrieve the exit code from the child process 270 | var exitCode uint32 271 | if err := windows.GetExitCodeProcess(execInfo.hProcess, &exitCode); err != nil { 272 | return -1, err 273 | } 274 | 275 | return int(exitCode), nil 276 | } 277 | -------------------------------------------------------------------------------- /internal/process/process.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | // Executes the specified command and captures its output, providing pretty error messages for non-zero exit codes 11 | func CaptureOutput(command []string) (string, error) { 12 | 13 | // Run the command and retrieve the combined stdout and stderr 14 | cmd := exec.Command(command[0], command[1:]...) 15 | output, err := cmd.CombinedOutput() 16 | 17 | // If an error occurred, determine whether it was a non-zero exit code or a failure to run the command 18 | if err != nil { 19 | var exitError *exec.ExitError 20 | if errors.As(err, &exitError) { 21 | return "", fmt.Errorf( 22 | "command %v failed with exit code %v and output:\n%s", 23 | command, 24 | exitError.ProcessState.ExitCode(), 25 | string(output), 26 | ) 27 | } else { 28 | return "", fmt.Errorf("failed to run command %v: %v", command, err) 29 | } 30 | } 31 | 32 | // Treat the output as a UTF-8 string 33 | return string(output), nil 34 | } 35 | 36 | // Executes the specified command, ensuring it inherits the standard streams from the current process 37 | func RunWithInheritedHandles(command []string) (int, error) { 38 | 39 | // Retrieve the path to the executable 40 | executable, err := exec.LookPath(command[0]) 41 | if err != nil { 42 | return -1, err 43 | } 44 | 45 | // Attempt to run the executable, ensuring it inherits the standard streams from the current process 46 | process, err := os.StartProcess(executable, command, &os.ProcAttr{ 47 | Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, 48 | }) 49 | 50 | // Verify that the child process started successfully 51 | if err != nil { 52 | return -1, err 53 | } 54 | 55 | // Wait for the child process to complete 56 | status, err := process.Wait() 57 | if err != nil { 58 | return -1, err 59 | } 60 | 61 | // Return the exit code from the child process 62 | return status.ExitCode(), nil 63 | } 64 | 65 | // Exit the current process with the specified exit code, optionally pausing beforehand 66 | func ExitWithPause(exitCode int, pause bool) { 67 | 68 | // Pause if requested 69 | if pause { 70 | PauseForInput() 71 | } 72 | 73 | // Exit with the specified exit code 74 | os.Exit(exitCode) 75 | } 76 | -------------------------------------------------------------------------------- /internal/process/process_linux.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | // Pauses for user input, using Bash's built-in `read` command 4 | func PauseForInput() { 5 | RunWithInheritedHandles([]string{"bash", "-c", `read -n 1 -rsp "Press any key to continue..."; echo ""`}) 6 | } 7 | -------------------------------------------------------------------------------- /internal/process/process_windows.go: -------------------------------------------------------------------------------- 1 | package process 2 | 3 | // Pauses for user input, using the command prompt's built-in `pause` command 4 | func PauseForInput() { 5 | RunWithInheritedHandles([]string{"cmd.exe", "/C", "pause"}) 6 | } 7 | -------------------------------------------------------------------------------- /internal/reboot/reboot_linux.go: -------------------------------------------------------------------------------- 1 | package reboot 2 | 3 | import "github.com/tensorworks/bootnext/internal/process" 4 | 5 | // Attempts to reboot the system 6 | func Reboot() error { 7 | _, err := process.CaptureOutput([]string{"reboot", "now"}) 8 | return err 9 | } 10 | -------------------------------------------------------------------------------- /internal/reboot/reboot_windows.go: -------------------------------------------------------------------------------- 1 | package reboot 2 | 3 | import "github.com/tensorworks/bootnext/internal/process" 4 | 5 | // Attempts to reboot the system 6 | func Reboot() error { 7 | _, err := process.CaptureOutput([]string{"shutdown", "/r", "/t", "0"}) 8 | return err 9 | } 10 | -------------------------------------------------------------------------------- /internal/uefi/entry.go: -------------------------------------------------------------------------------- 1 | package uefi 2 | 3 | // Represents an individual UEFI boot entry 4 | type BootEntry struct { 5 | 6 | // The system-specific identifier for the boot entry 7 | // (Under Linux this is a hexadecimal number, under Windows it is a GUID) 8 | ID string 9 | 10 | // The human-readable description for the boot entry 11 | Description string 12 | } 13 | -------------------------------------------------------------------------------- /internal/uefi/uefi_linux.go: -------------------------------------------------------------------------------- 1 | package uefi 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/tensorworks/bootnext/internal/process" 10 | ) 11 | 12 | // Determines whether the operating system has been booted in UEFI mode 13 | func IsUEFIEnabled() (bool, error) { 14 | 15 | // Determine whether `/sys/firmware/efi` exists 16 | _, err := os.Stat("/sys/firmware/efi") 17 | notExist := errors.Is(err, os.ErrNotExist) 18 | 19 | // If the query failed then propagate the error 20 | if err != nil && !notExist { 21 | return false, err 22 | } else { 23 | return !notExist, nil 24 | } 25 | } 26 | 27 | // Returns the list of system tools that we require in order to interact with UEFI NVRAM variables 28 | func RequiredTools() []string { 29 | return []string{"efibootmgr"} 30 | } 31 | 32 | // Lists the UEFI boot entries for the host machine 33 | func ListBootEntries() ([]BootEntry, error) { 34 | 35 | // Run `efibootmgr` with no flags to print the list of boot entries 36 | output, err := process.CaptureOutput([]string{"efibootmgr"}) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // Compile our regular expression for parsing the output 42 | regex, err := regexp.Compile(`Boot([0-9]+)\*?\s+(.+)`) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | // Parse the list of entries 48 | entries := []BootEntry{} 49 | lines := strings.Split(output, "\n") 50 | for _, line := range lines { 51 | if groups := regex.FindStringSubmatch(line); groups != nil { 52 | entries = append(entries, BootEntry{ 53 | ID: groups[1], 54 | Description: groups[2], 55 | }) 56 | } 57 | } 58 | 59 | return entries, nil 60 | } 61 | 62 | // Sets the value of the BootNext UEFI NVRAM variable 63 | func SetBootNext(entry BootEntry) error { 64 | _, err := process.CaptureOutput([]string{"efibootmgr", "--bootnext", entry.ID}) 65 | return err 66 | } 67 | -------------------------------------------------------------------------------- /internal/uefi/uefi_windows.go: -------------------------------------------------------------------------------- 1 | package uefi 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "github.com/tensorworks/bootnext/internal/process" 8 | ) 9 | 10 | // Determines whether the operating system has been booted in UEFI mode 11 | func IsUEFIEnabled() (bool, error) { 12 | 13 | // Use PowerShell to query the system firmware type 14 | output, err := process.CaptureOutput([]string{ 15 | "powershell.exe", 16 | "-ExecutionPolicy", "Bypass", 17 | "-Command", "Write-Host $env:firmware_type", 18 | }) 19 | if err != nil { 20 | return false, err 21 | } 22 | 23 | // Determine whether the reported firmware type is legacy BIOS or UEFI 24 | return strings.TrimSpace(strings.ToUpper(output)) == "UEFI", nil 25 | } 26 | 27 | // Returns the list of system tools that we require in order to interact with UEFI NVRAM variables 28 | func RequiredTools() []string { 29 | return []string{"bcdedit"} 30 | } 31 | 32 | // Lists the UEFI boot entries for the host machine 33 | func ListBootEntries() ([]BootEntry, error) { 34 | 35 | // Run `bcdedit` to print the list of boot entries 36 | output, err := process.CaptureOutput([]string{"bcdedit", "/enum", "firmware"}) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | // Compile our regular expressions for parsing the output 42 | separatorRegex, err := regexp.Compile(`^-+$`) 43 | if err != nil { 44 | return nil, err 45 | } 46 | identifierRegex, err := regexp.Compile(`identifier +(.+)`) 47 | if err != nil { 48 | return nil, err 49 | } 50 | descriptionRegex, err := regexp.Compile(`description +(.+)`) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | // Examine each line of the output and parse the boot entries 56 | entries := []BootEntry{} 57 | lines := strings.Split(strings.ReplaceAll(output, "\r\n", "\n"), "\n") 58 | for _, line := range lines { 59 | 60 | // Determine whether the line is a separator that marks the start of a new boot entry 61 | if separatorRegex.FindStringIndex(line) != nil { 62 | entries = append(entries, BootEntry{}) 63 | 64 | } else if len(entries) > 0 { 65 | 66 | // Determine whether the line provides the GUID or the description for the boot entry 67 | if match := identifierRegex.FindStringSubmatch(line); match != nil { 68 | entries[len(entries)-1].ID = match[1] 69 | 70 | } else if match := descriptionRegex.FindStringSubmatch(line); match != nil { 71 | entries[len(entries)-1].Description = match[1] 72 | 73 | } 74 | } 75 | } 76 | 77 | // Filter out any malformed boot entries 78 | filtered := []BootEntry{} 79 | for _, entry := range entries { 80 | if entry.ID != "" && entry.Description != "" { 81 | filtered = append(filtered, entry) 82 | } 83 | } 84 | 85 | return filtered, nil 86 | } 87 | 88 | // Sets the value of the BootNext UEFI NVRAM variable 89 | func SetBootNext(entry BootEntry) error { 90 | _, err := process.CaptureOutput([]string{"bcdedit", "/set", "{fwbootmgr}", "bootsequence", entry.ID}) 91 | return err 92 | } 93 | --------------------------------------------------------------------------------