From 8bd34b293e27aa0aee3409c58bd8b745c53db653 Mon Sep 17 00:00:00 2001 From: Matt Spurrier Date: Sat, 30 Mar 2024 22:38:09 +0800 Subject: [PATCH 01/18] remove pull-requests from release triggers, only trigger on tags. (#25) --- .github/workflows/release.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7eda933..49dd14d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,7 +1,6 @@ name: goreleaser on: - pull_request: push: # run only against tags tags: @@ -37,4 +36,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution - # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} \ No newline at end of file + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} From 16fe298cb9d42cf4d0a19f73853c93bc987e8f33 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 30 Mar 2024 17:18:16 +0100 Subject: [PATCH 02/18] chore: update readme with new information --- README.md | 79 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 41 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index a2cea71..a03868e 100644 --- a/README.md +++ b/README.md @@ -3,45 +3,12 @@ A project to simplify setting up GPU passthrough on your Linux host for libvirt/virt-manager ![Quickpassthrough Preview GIF](https://raw.githubusercontent.com/HikariKnight/quickpassthrough/main/quickpassthrough_preview.gif) -You can use it by simply downloading the latest [release](https://github.com/HikariKnight/quickpassthrough/releases/) and run it inside a terminal/shell or by downloading and compiling it yourself with the commands below. +You can use it by simply downloading the latest [release](https://github.com/HikariKnight/quickpassthrough/releases/) and run it inside a terminal or by downloading and compiling it yourself with the commands below. -## Build with current dependencies -```bash -git clone https://github.com/HikariKnight/quickpassthrough.git -cd quickpassthrough -go mod download -CGO_ENABLED=0 go build -o quickpassthrough cmd/main.go -``` +This project is aimed at **desktops with 2 GPUs** and **headless servers**, where the only GPU is not needed. -## Build with newest dependencies (may break) -```bash -git clone https://github.com/HikariKnight/quickpassthrough.git -cd quickpassthrough -go mod download -go get -u ./cmd -CGO_ENABLED=0 go build -o quickpassthrough cmd/main.go -``` +**Note:** Quickpassthrough is not designed to be installed by a package manager! As you would usually have to only run it once, unless you change the GPU. -## Does this work on immutable systems? -Currently no, there might be support for ostree (fedora silverblue, kinoite, etc) at a later time. - -## How do I undo the changes? -There is a "backup/" folder generated on the first run that will have a copy of all your files (and their paths) from before we edited anything. -Copy the files back to your system (blank files inside .d/ folders will be used to "undo" any new config files we wrote) and rebuild your initramfs then remove the kernel arguments listed in config/kernel_args from your bootloader (if your system use kernelstub, grubby or you had to manually add them). - -## How do I just disable vfio for 1 boot? -Remove the vfio kernel arguments from your bootloader by pressing E on the boot menu. The kernel arguments added to the bootloader can be found in the config/kernel_args file.
-NOTE: You can also just remove them from your bootloader permanently and update your bootloader if you want to keep the config files on your system. - -## What this project does NOT do -* Setup or configure your Virtual Machine (that is your job) -* Optimize your Virtual Machine for Passthrough (again this is your job) -* Optimize your host machine for Passthrough or Virtualization (out of this projects scope) -* Setup and configure GPU Passthrough on systems with 1 graphic card (iGPU counts as 1 Graphic Card by itself, so iGPU with another GPU will work) -* Does not configure passthrough of 3D controllers, as it will not work (this is most gaming laptops so do not even think about it). If you try run this on a laptop with a 3D controller, the "2nd GPU" will not show up. - -NOTE: This project is aimed at desktops and headless servers. - ## Features * Show general warning to user and inform about making a backup and general expectations * Detect if user has an amd or intel CPU and provide the correct IOMMU kernel args based on that @@ -56,18 +23,54 @@ NOTE: This project is aimed at desktops and headless servers. * Make sure [vendor-reset](https://github.com/gnif/vendor-reset) module is loaded before vfio, check the repository for the list of cards that require it! * Provides you with the correct kernel arguments to add to your bootloader entry if a supported bootloader is not found -## Features now handled by [ls-iommu](https://github.com/HikariKnight/ls-iommu) +## Features handled by [ls-iommu](https://github.com/HikariKnight/ls-iommu) * Automatically handle GPUs where parts of it might be in separate IOMMU groups (ex: RX6600XT) * Fetch the ID and PCI Address of devices * Locate the vbios rom path on the system * Tell the user to enable IOMMU (VT-d/AMD-v) on their motherboard and bootloader * Get a list of devices, their IOMMU groups and various other information +## Build with current dependencies +```bash +git clone https://github.com/HikariKnight/quickpassthrough.git +cd quickpassthrough +go mod download +CGO_ENABLED=0 go build -ldflags="-X github.com/HikariKnight/quickpassthrough/internal/version.Version=$(git rev-parse --short HEAD)" -o quickpassthrough cmd/main.go +``` + +## Build with newest dependencies (may break) +```bash +git clone https://github.com/HikariKnight/quickpassthrough.git +cd quickpassthrough +go mod download +go get -u ./cmd +CGO_ENABLED=0 go build -ldflags="-X github.com/HikariKnight/quickpassthrough/internal/version.Version=$(git rev-parse --short HEAD)" -o quickpassthrough cmd/main.go +``` + +## Does this work on immutable systems? +Currently no, however [Bazzite](https://bazzite.gg) has an `ujust` command that does a very similar job. +In Bazzite you run `ujust setup-virtualization` and follow the prompts to `Enable Virtualization` and `Enable VFIO drivers`. + +## How do I undo the changes? +There is a `backup/` folder generated on the first run that will have a copy of all your files (and their paths) from before we edited anything. +Compare that folder with the `config/` folder to see which files you need to delete in addition to copying the files from `backup/` to your system before rebuilding your initramfs and updating your bootloader config. + +## How do I just disable vfio for 1 boot? +Remove the vfio kernel arguments from your bootloader by pressing E on the boot menu. The kernel arguments added to the bootloader can be found in the config/kernel_args file.
+NOTE: You can also just remove them from your bootloader permanently and update your bootloader if you want to keep the config files on your system. + +## What this project does NOT do +* Setup or configure your Virtual Machine (that is your job) +* Optimize your Virtual Machine for Passthrough (again this is your job) +* Optimize your host machine for Passthrough or Virtualization (out of this projects scope) +* Setup and configure GPU Passthrough on systems with 1 graphic card (iGPU counts as 1 Graphic Card by itself, so iGPU with another GPU will work) +* Does not configure passthrough of 3D controllers, as it will not work (this is most gaming laptops so do not even think about it). If you try run this on a laptop with a 3D controller, the "2nd GPU" will not show up. + ## Contributing This project originally started out as a bash only project, upon completing the proof of concept it became very clear that bash would become very messy with all the weird quirks and regex and inline editing of files.
-So the project moved over to golang, this lets us utilize TUI toolkits like to build a proper menu system for the project.
+So the project moved over to golang, this lets us utilize TUI toolkits to build a proper menu system for the project.
If you know golang, passthrough or qemu, you are welcome to help! Just make a pull request!
Just remember to add comments to document the work and explain it for people who are less familiar with the golang syntax or anything else you use. 😄 From 82e21005d5c4ddee153b7df79cb14c7150a62a7d Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sun, 31 Mar 2024 00:41:12 +0100 Subject: [PATCH 03/18] chore: update readme with link to lantian's documentation for laptop gpus --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a03868e..78b893e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ NOTE: You can also just remove them from your bootloader permanently and update * Optimize your Virtual Machine for Passthrough (again this is your job) * Optimize your host machine for Passthrough or Virtualization (out of this projects scope) * Setup and configure GPU Passthrough on systems with 1 graphic card (iGPU counts as 1 Graphic Card by itself, so iGPU with another GPU will work) -* Does not configure passthrough of 3D controllers, as it will not work (this is most gaming laptops so do not even think about it). If you try run this on a laptop with a 3D controller, the "2nd GPU" will not show up. +* Does not configure passthrough of 3D controllers, [as it will not work](https://lantian.pub/en/article/modify-computer/laptop-intel-nvidia-optimus-passthrough.lantian/) (this is most gaming laptops so do not even think about it). If you try run this on a laptop with a 3D controller, the "2nd GPU" will not show up. ## Contributing From a9b889e5acf57feac1330ef6d964811073d2ecca Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:30:41 +0200 Subject: [PATCH 04/18] fix: update download url for ls-iommu checksums.txt after implementing github actions the checksums file was made versioned --- internal/ls_iommu_downloader/ls_iommu_downloader.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/ls_iommu_downloader/ls_iommu_downloader.go b/internal/ls_iommu_downloader/ls_iommu_downloader.go index 8becfa5..d8114f0 100644 --- a/internal/ls_iommu_downloader/ls_iommu_downloader.go +++ b/internal/ls_iommu_downloader/ls_iommu_downloader.go @@ -124,7 +124,8 @@ func CheckLsIOMMU() { // Generate checksums.txt url checkSumsUrl := fmt.Sprintf( - "https://github.com/HikariKnight/ls-iommu/releases/download/%s/checksums.txt", + "https://github.com/HikariKnight/ls-iommu/releases/download/%s/ls-iommu_%s_checksums.txt", + result.TagName, result.TagName, ) From 3bda7cadfcaa33bc641828d143d0170da2533643 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Tue, 2 Apr 2024 11:37:07 +0200 Subject: [PATCH 05/18] chore: update name of the github job --- .github/workflows/release.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 49dd14d..9b74d58 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,4 +1,4 @@ -name: goreleaser +name: Build & Release on: push: From 47567c11fbf13b2556918825741f69d848fddfb1 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Wed, 24 Apr 2024 21:44:18 +0200 Subject: [PATCH 06/18] chore: update funding --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4e87519..9cfb5e6 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,6 +1,6 @@ # These are supported funding model platforms -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +github: HikariKnight # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with patreon username open_collective: # Replace with a single Open Collective username ko_fi: HikariKnight From 0273aabeccd627e4c412ac366e07755b780229bc Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 15 Jun 2024 01:51:57 +0200 Subject: [PATCH 07/18] chore: change quickemu folder to qemu folder --- internal/configs/config_vbios_dumper.go | 8 ++++---- internal/configs/configs.go | 4 ++-- internal/pages/06_finalize.go | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/configs/config_vbios_dumper.go b/internal/configs/config_vbios_dumper.go index e8bc92c..b075018 100644 --- a/internal/configs/config_vbios_dumper.go +++ b/internal/configs/config_vbios_dumper.go @@ -28,7 +28,7 @@ func GenerateVBIOSDumper(vbios_path string) { vbios_script_template := fmt.Sprint( "#!/bin/bash\n", "# THIS FILE IS AUTO GENERATED!\n", - "# IF YOU HAVE CHANGED GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n", + "# IF YOU HAVE CHANGED THE GPU, PLEASE RE-RUN QUICKPASSTHROUGH!\n", "mkdir -p \"%s\"\n", "echo Attempting to enable reading from rom\n", "echo 1 | sudo tee %s\n", @@ -43,13 +43,13 @@ func GenerateVBIOSDumper(vbios_path string) { vbios_script := fmt.Sprintf( vbios_script_template, - config.Path.QUICKEMU, + config.Path.QEMU, vbios_path, vbios_path, scriptdir, - config.Path.QUICKEMU, + config.Path.QEMU, scriptdir, - config.Path.QUICKEMU, + config.Path.QEMU, vbios_path, ) diff --git a/internal/configs/configs.go b/internal/configs/configs.go index fa0ab87..50d6174 100644 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -19,7 +19,7 @@ type Path struct { INITRAMFS string ETCMODULES string DEFAULT string - QUICKEMU string + QEMU string DRACUT string MKINITCPIO string } @@ -40,7 +40,7 @@ func GetConfigPaths() *Path { INITRAMFS: "config/etc/initramfs-tools", ETCMODULES: "config/etc/modules", DEFAULT: "config/etc/default", - QUICKEMU: "config/quickemu", + QEMU: "config/qemu", DRACUT: "config/etc/dracut.conf.d", MKINITCPIO: "config/etc/mkinitcpio.conf", } diff --git a/internal/pages/06_finalize.go b/internal/pages/06_finalize.go index 3aebd92..232bd26 100644 --- a/internal/pages/06_finalize.go +++ b/internal/pages/06_finalize.go @@ -61,7 +61,7 @@ func finalize(config *configs.Config) { "located inside the \"config\" folder\n", "\n", "* The \"kernel_args\" file contains kernel arguments that your bootloader needs\n", - //"* The \"quickemu\" folder contains files that might be\n useable for quickemu in the future\n", + "* The \"qemu\" folder contains files that might be\n neccessary for passing through the GPU\n", "* The files inside the \"etc\" folder must be copied to your system.\n", " NOTE: Verify that these files are correctly formated/edited!\n", "* Once all files have been copied, you need to update your bootloader and rebuild\n", From 1002f4d64cf6c4d9c9c474572caee1803a4570f0 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 15 Jun 2024 02:17:53 +0200 Subject: [PATCH 08/18] chore: add missing on comment for command.Run --- pkg/command/command.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/command/command.go b/pkg/command/command.go index 9c4a0e4..e7c6fca 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -11,6 +11,7 @@ import ( "github.com/HikariKnight/ls-iommu/pkg/errorcheck" ) +// Run a command and return STDOUT func Run(binary string, args ...string) ([]string, error) { var stdout, stderr bytes.Buffer From 4d0086df417cd0c937e9f09badfca6ad5eb1c145 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 15 Jun 2024 02:53:38 +0200 Subject: [PATCH 09/18] feat: Generate dummy rom for amd RX7000 series cards By recommendation from gnif on discord. Co-authored-by: Geoffrey McRae --- internal/pages/03_vbios_extract.go | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/internal/pages/03_vbios_extract.go b/internal/pages/03_vbios_extract.go index fb79bc2..598b584 100644 --- a/internal/pages/03_vbios_extract.go +++ b/internal/pages/03_vbios_extract.go @@ -30,17 +30,28 @@ func genVBIOS_dumper(config *configs.Config) { vbios_path := lsiommu.GetIOMMU("-g", "-i", config.Gpu_Group, "--rom")[0] configs.GenerateVBIOSDumper(vbios_path) + // Make the qemu config folder + os.Mkdir(fmt.Sprintf("%s/%s", scriptdir, config.Path.QEMU), os.ModePerm) + + // Generate a dummy rom (1MB rom of zeroes) for use with AMD RX 7000 series cards by recommendation from Gnif + // Source: https://forum.level1techs.com/t/the-state-of-amd-rx-7000-series-vfio-passthrough-april-2024/210242 + command.Run("dd", "if=/dev/zero", fmt.Sprintf("of=%s/%s/dummy.rom", scriptdir, config.Path.QEMU), "bs=1M", "count=1") + // Write a title title := color.New(color.BgHiBlue, color.White, color.Bold) - title.Println("Generated \"dump VBIOS\" script") + title.Println("VBIOS roms for Passthrough") - // Tell users about the VBIOS dumper script + // Tell users about the VBIOS dumper script and dummy rom for RX 7000 series cards fmt.Print( - "For some GPUs, you will need to dump the VBIOS and pass the\n", + "If you have an RX 7000 series (and possibly newer AMD cards) GPUs, please use the dummy.rom file\n", + fmt.Sprintf("%s/%s/dummy.rom\n", scriptdir, config.Path.QEMU), + "Or disable ROM BAR for the card in qemu/libvirt\n", + "\n", + "For some other GPUs, you will need to instead dump the VBIOS (and possibly patch it) and pass the\n", "rom to the VM along with the card in order to get a functional passthrough.\n", "In many cases you can find your vbios at https://www.techpowerup.com/vgabios/\n", "\n", - "You can also attempt to dump your own vbios using the script in\n", + "You can also attempt to dump your own vbios from TTY using the script in\n", fmt.Sprintf("%s/utils/dump_vbios.sh\n", scriptdir), "\n", ) From 6c48a35180237fbf8da3dc03a9eb355c9e11b60e Mon Sep 17 00:00:00 2001 From: kayos Date: Sat, 27 Jul 2024 08:38:39 -0700 Subject: [PATCH 10/18] Feat: conditional permissions behavior (#28) * Fix: don't need sudo if we're root + other aesthetics * Heavy refactoring, see PR #28 * Fix: avoid silent fatalities demo: https://tcp.ac/i/JMSUc.gif * Fix: Inverse check on `IsRoot` * D.R.Y: check for permissions error in `common.ErrorCheck` Reduce cognitive complexity. * Fix: Issue with copying * Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646535918 * Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646606680 and https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646594105 * Revert "Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646606680 and https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646594105" This reverts commit ce1521300982a86e799676ad1a630ea95de3b8c2. * Resolve https://github.com/HikariKnight/quickpassthrough/pull/28#discussion_r1646730751 --- internal/common/errors.go | 50 +++++ internal/configs/config_bootloaders.go | 107 +++++----- internal/configs/config_dracut.go | 6 +- internal/configs/config_initramfstools.go | 6 +- internal/configs/config_mkinitcpio.go | 6 +- internal/configs/config_modprobe.go | 4 +- internal/configs/config_vbios_dumper.go | 6 +- internal/configs/config_vfio_video.go | 4 +- internal/configs/configs.go | 150 ++++++++++--- internal/configs/configs_test.go | 33 +++ .../ls_iommu_downloader.go | 27 +-- internal/pages/02_select_gpu.go | 15 +- internal/pages/05_select_usbctrl.go | 5 +- internal/pages/06_finalize.go | 200 ++++++++++-------- internal/pages/06_finalize_test.go | 23 ++ internal/ui_main.go | 11 +- pkg/command/command.go | 91 ++++++-- pkg/command/command_test.go | 61 ++++++ pkg/fileio/fileio.go | 45 ++-- pkg/menu/manual.go | 5 +- pkg/untar/untar.go | 4 +- 21 files changed, 604 insertions(+), 255 deletions(-) create mode 100644 internal/common/errors.go create mode 100644 internal/configs/configs_test.go create mode 100644 internal/pages/06_finalize_test.go create mode 100644 pkg/command/command_test.go diff --git a/internal/common/errors.go b/internal/common/errors.go new file mode 100644 index 0000000..0c83407 --- /dev/null +++ b/internal/common/errors.go @@ -0,0 +1,50 @@ +package common + +import ( + "errors" + "os" + "time" + + "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/gookit/color" +) + +const PermissionNotice = ` +Permissions error occured during file operations. + +Hint: + + If you initially ran QuickPassthrough as root or using sudo, + but are now running it as a normal user, this is expected behavior. + + Try running QuickPassthrough as root or using sudo if so. + + If this does not work, double check your filesystem's permissions, + and be sure to check the debug log for more information. +` + +// ErrorCheck serves as a wrapper for HikariKnight/ls-iommu/pkg/common.ErrorCheck that allows for visibile error messages +func ErrorCheck(err error, msg ...string) { + _, _ = os.Stdout.WriteString("\033[H\033[2J") // clear the screen + if err == nil { + return + } + if errors.Is(err, os.ErrPermission) { + color.Printf(PermissionNotice) + } + oneMsg := "" + if len(msg) < 1 { + oneMsg = "" + } else { + for _, v := range msg { + oneMsg += v + "\n" + } + } + color.Printf("\nFATAL: %s\n%s\nAborting", err.Error(), oneMsg) + for i := 0; i < 10; i++ { + time.Sleep(1 * time.Second) + print(".") + } + print("\n") + errorcheck.ErrorCheck(err, msg...) +} diff --git a/internal/configs/config_bootloaders.go b/internal/configs/config_bootloaders.go index 2f8607d..281cc4d 100644 --- a/internal/configs/config_bootloaders.go +++ b/internal/configs/config_bootloaders.go @@ -1,16 +1,19 @@ package configs import ( + "errors" "fmt" "os" + "os/exec" "regexp" "strings" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/klauspost/cpuid/v2" + + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/internal/logger" "github.com/HikariKnight/quickpassthrough/pkg/command" "github.com/HikariKnight/quickpassthrough/pkg/fileio" - "github.com/klauspost/cpuid/v2" ) // This function just adds what bootloader the system has to our config.bootloader value @@ -70,39 +73,32 @@ func Set_Cmdline(gpu_IDs []string) { fileio.AppendContent(fmt.Sprintf(" vfio_pci.ids=%s", strings.Join(gpu_IDs, ",")), config.Path.CMDLINE) } -// Configures systemd-boot using kernelstub -func Set_KernelStub() string { +// Set_KernelStub configures systemd-boot using kernelstub. +func Set_KernelStub(isRoot bool) { // Get the config config := GetConfig() // Get the kernel args kernel_args := fileio.ReadFile(config.Path.CMDLINE) - // Write to logger - logger.Printf("Running command:\nsudo kernelstub -a \"%s\"\n", kernel_args) - - // Run the command - _, err := command.Run("sudo", "kernelstub", "-a", kernel_args) - errorcheck.ErrorCheck(err, "Error, kernelstub command returned exit code 1") - - // Return what we did - return fmt.Sprintf("Executed: sudo kernelstub -a \"%s\"", kernel_args) + // Run and log, check for errors + common.ErrorCheck( + command.ExecAndLogSudo(isRoot, true, "kernelstub", "-a", kernel_args), + "Error, kernelstub command returned exit code 1", + ) } -// Configures grub2 and/or systemd-boot using grubby -func Set_Grubby() string { +// Set_Grubby configures grub2 and/or systemd-boot using grubby +func Set_Grubby(isRoot bool) string { // Get the config config := GetConfig() // Get the kernel args kernel_args := fileio.ReadFile(config.Path.CMDLINE) - // Write to logger - logger.Printf("Running command:\nsudo grubby --update-kernel=ALL --args=\"%s\"\n", kernel_args) - - // Run the command - _, err := command.Run("sudo", "grubby", "--update-kernel=ALL", fmt.Sprintf("--args=%s", kernel_args)) - errorcheck.ErrorCheck(err, "Error, grubby command returned exit code 1") + // Run and log, check for errors + err := command.ExecAndLogSudo(isRoot, true, "grubby", "--update-kernel=ALL", fmt.Sprintf("--args=%s", kernel_args)) + common.ErrorCheck(err, "Error, grubby command returned exit code 1") // Return what we did return fmt.Sprintf("Executed: sudo grubby --update-kernel=ALL --args=\"%s\"", kernel_args) @@ -116,8 +112,8 @@ func Configure_Grub2() { conffile := fmt.Sprintf("%s/grub", config.Path.DEFAULT) // Make sure we start from scratch by deleting any old file - if fileio.FileExist(conffile) { - os.Remove(conffile) + if exists, _ := fileio.FileExist(conffile); exists { + _ = os.Remove(conffile) } // Make a regex to get the system path instead of the config path @@ -201,8 +197,8 @@ func clean_Grub2_Args(old_kernel_args []string) []string { return clean_kernel_args } -// This function copies our config to /etc/default/grub and updates grub -func Set_Grub2() ([]string, error) { +// Set_Grub2 copies our config to /etc/default/grub and updates grub +func Set_Grub2(isRoot bool) error { // Get the config config := GetConfig() @@ -213,38 +209,45 @@ func Set_Grub2() ([]string, error) { sysfile_re := regexp.MustCompile(`^config`) sysfile := sysfile_re.ReplaceAllString(conffile, "") - // Write to logger - logger.Printf("Executing command:\nsudo cp -v \"%s\" %s\n", conffile, sysfile) + // [CopyToSystem] will log the operation + // logger.Printf("Executing command:\nsudo cp -v \"%s\" %s\n", conffile, sysfile) - // Make our output slice - var output []string - - // Copy files to system - output = append(output, CopyToSystem(conffile, sysfile)) + // Copy files to system, logging and error checking is done in the function + CopyToSystem(isRoot, conffile, sysfile) // Set a variable for the mkconfig command - mkconfig := "grub-mkconfig" + var mkconfig string + var grubPath = "/boot/grub/grub.cfg" + var lpErr error + // Check for grub-mkconfig - _, err := command.Run("which", "grub-mkconfig") - if err == nil { - // Set binary as grub-mkconfig - mkconfig = "grub-mkconfig" - } else { - mkconfig = "grub2-mkconfig" + mkconfig, lpErr = exec.LookPath("grub-mkconfig") + switch { + case errors.Is(lpErr, exec.ErrNotFound) || mkconfig == "": + // Check for grub2-mkconfig + mkconfig, lpErr = exec.LookPath("grub2-mkconfig") + if lpErr == nil && mkconfig != "" { + grubPath = "/boot/grub2/grub.cfg" + break // skip below, we found grub2-mkconfig + } + if lpErr == nil { + // we know mkconfig is empty despite no error; + // so set an error for [common.ErrorCheck]. + lpErr = errors.New("neither grub-mkconfig or grub2-mkconfig found") + } + common.ErrorCheck(lpErr, lpErr.Error()+"\n") + return lpErr // note: unreachable as [common.ErrorCheck] calls fatal + default: } - // Update grub.cfg - if fileio.FileExist("/boot/grub/grub.cfg") { - output = append(output, fmt.Sprintf("Executed: sudo %s -o /boot/grub/grub.cfg\nSee debug.log for more detailed output", mkconfig)) - _, mklog, err := command.RunErr("sudo", mkconfig, "-o", "/boot/grub/grub.cfg") - logger.Printf(strings.Join(mklog, "\n")) - errorcheck.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg") - } else { - output = append(output, fmt.Sprintf("Executed: sudo %s -o /boot/grub/grub.cfg\nSee debug.log for more detailed output", mkconfig)) - _, mklog, err := command.RunErr("sudo", mkconfig, "-o", "/boot/grub2/grub.cfg") - logger.Printf(strings.Join(mklog, "\n")) - errorcheck.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg") - } + _, mklog, err := command.RunErrSudo(isRoot, mkconfig, "-o", grubPath) - return output, err + // tabulate the output, [command.RunErrSudo] logged the execution. + logger.Printf("\t" + strings.Join(mklog, "\n\t")) + common.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg") + + // always returns nil as [common.ErrorCheck] calls fatal + // keeping the ret signature, as we should consider passing down errors + // but that's a massive rabbit hole to go down for this codebase as a whole + return err } diff --git a/internal/configs/config_dracut.go b/internal/configs/config_dracut.go index f31a2ff..70f837e 100644 --- a/internal/configs/config_dracut.go +++ b/internal/configs/config_dracut.go @@ -9,7 +9,7 @@ import ( "github.com/HikariKnight/quickpassthrough/pkg/fileio" ) -// This function writes a dracut configuration file for /etc/dracut.conf.d/ +// Set_Dracut writes a dracut configuration file for `/etc/dracut.conf.d/`. func Set_Dracut() { config := GetConfig() @@ -17,8 +17,8 @@ func Set_Dracut() { dracutConf := fmt.Sprintf("%s/vfio.conf", config.Path.DRACUT) // If the file already exists then delete it - if fileio.FileExist(dracutConf) { - os.Remove(dracutConf) + if exists, _ := fileio.FileExist(dracutConf); exists { + _ = os.Remove(dracutConf) } // Write to logger diff --git a/internal/configs/config_initramfstools.go b/internal/configs/config_initramfstools.go index f198959..d3a7bd5 100644 --- a/internal/configs/config_initramfstools.go +++ b/internal/configs/config_initramfstools.go @@ -7,7 +7,7 @@ import ( "regexp" "strings" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/pkg/fileio" ) @@ -15,7 +15,7 @@ import ( func initramfs_readHeader(lines int, fileName string) string { // Open the file f, err := os.Open(fileName) - errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening %s", fileName)) + common.ErrorCheck(err, fmt.Sprintf("Error opening %s", fileName)) defer f.Close() header_re := regexp.MustCompile(`^#`) @@ -50,7 +50,7 @@ func initramfs_addModules(conffile string) { // Open the system file for reading sysfile, err := os.Open(syspath) - errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening file for reading %s", syspath)) + common.ErrorCheck(err, fmt.Sprintf("Error opening file for reading %s", syspath)) defer sysfile.Close() // Check if user has vendor-reset installed/enabled and make sure that is first diff --git a/internal/configs/config_mkinitcpio.go b/internal/configs/config_mkinitcpio.go index 219c16b..6c95f8f 100644 --- a/internal/configs/config_mkinitcpio.go +++ b/internal/configs/config_mkinitcpio.go @@ -10,14 +10,14 @@ import ( "github.com/HikariKnight/quickpassthrough/pkg/fileio" ) -// This function copies the content of /etc/mkinitcpio.conf to the config folder and does an inline replace/insert on the MODULES=() line +// Set_Mkinitcpio copies the content of /etc/mkinitcpio.conf to the config folder and does an inline replace/insert on the MODULES=() line func Set_Mkinitcpio() { // Get the config struct config := GetConfig() // Make sure we start from scratch by deleting any old file - if fileio.FileExist(config.Path.MKINITCPIO) { - os.Remove(config.Path.MKINITCPIO) + if exists, _ := fileio.FileExist(config.Path.MKINITCPIO); exists { + _ = os.Remove(config.Path.MKINITCPIO) } // Make a regex to get the system path instead of the config path diff --git a/internal/configs/config_modprobe.go b/internal/configs/config_modprobe.go index 63a5dbc..715a267 100644 --- a/internal/configs/config_modprobe.go +++ b/internal/configs/config_modprobe.go @@ -30,9 +30,9 @@ func Set_Modprobe(gpu_IDs []string) { conffile := fmt.Sprintf("%s/vfio.conf", config.Path.MODPROBE) // If the file exists - if fileio.FileExist(conffile) { + if exists, _ := fileio.FileExist(conffile); exists { // Delete the old file - os.Remove(conffile) + _ = os.Remove(conffile) } content := fmt.Sprint( diff --git a/internal/configs/config_vbios_dumper.go b/internal/configs/config_vbios_dumper.go index b075018..775cf7a 100644 --- a/internal/configs/config_vbios_dumper.go +++ b/internal/configs/config_vbios_dumper.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/internal/logger" ) @@ -55,12 +55,12 @@ func GenerateVBIOSDumper(vbios_path string) { // Make the script file scriptfile, err := os.Create("utils/dump_vbios.sh") - errorcheck.ErrorCheck(err, "Cannot create file \"utils/dump_vbios.sh\"") + common.ErrorCheck(err, "Cannot create file \"utils/dump_vbios.sh\"") defer scriptfile.Close() // Make the script executable scriptfile.Chmod(0775) - errorcheck.ErrorCheck(err, "Could not change permissions of \"utils/dump_vbios.sh\"") + common.ErrorCheck(err, "Could not change permissions of \"utils/dump_vbios.sh\"") // Write to logger logger.Printf("Writing utils/dump_vbios.sh\n") diff --git a/internal/configs/config_vfio_video.go b/internal/configs/config_vfio_video.go index 63230b1..ae7dbeb 100644 --- a/internal/configs/config_vfio_video.go +++ b/internal/configs/config_vfio_video.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/internal/logger" "github.com/HikariKnight/quickpassthrough/pkg/fileio" ) @@ -26,7 +26,7 @@ func DisableVFIOVideo(i int) { if strings.Contains(kernel_args, "vfio_pci.disable_vga") { // Remove the old file err := os.Remove(config.Path.CMDLINE) - errorcheck.ErrorCheck(err, fmt.Sprintf("Could not rewrite %s", config.Path.CMDLINE)) + common.ErrorCheck(err, fmt.Sprintf("Could not rewrite %s", config.Path.CMDLINE)) // Enable or disable the VGA based on our given value if i == 0 { diff --git a/internal/configs/configs.go b/internal/configs/configs.go index 50d6174..74ecb60 100644 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -1,16 +1,20 @@ package configs import ( + "errors" "fmt" "os" + "path/filepath" "regexp" + "strings" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/klauspost/cpuid/v2" + + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/internal/logger" "github.com/HikariKnight/quickpassthrough/pkg/command" "github.com/HikariKnight/quickpassthrough/pkg/fileio" "github.com/HikariKnight/quickpassthrough/pkg/uname" - "github.com/klauspost/cpuid/v2" ) type Path struct { @@ -30,9 +34,10 @@ type Config struct { Path *Path Gpu_Group string Gpu_IDs []string + IsRoot bool } -// Gets the path to all the config files +// GetConfigPaths retrieves the path to all the config files. func GetConfigPaths() *Path { Paths := &Path{ CMDLINE: "config/kernel_args", @@ -48,7 +53,7 @@ func GetConfigPaths() *Path { return Paths } -// Gets all the configs and returns the struct +// GetConfig retrieves all the configs and returns the struct. func GetConfig() *Config { config := &Config{ Bootloader: "unknown", @@ -64,7 +69,7 @@ func GetConfig() *Config { return config } -// Constructs the empty config files and folders based on what exists on the system +// InitConfigs constructs the empty config files and folders based on what exists on the system func InitConfigs() { config := GetConfig() @@ -77,10 +82,17 @@ func InitConfigs() { } // Remove old config - os.RemoveAll("config") + if err := os.RemoveAll("config"); err != nil && !errors.Is(err, os.ErrNotExist) { + + // won't be called if the error is ErrNotExist + common.ErrorCheck(err, "\nError removing old config") + } // Make the config folder - os.Mkdir("config", os.ModePerm) + if err := os.Mkdir("config", os.ModePerm); err != nil && !errors.Is(err, os.ErrExist) { + // won't be called if the error is ErrExist + common.ErrorCheck(err, "\nError making config folder") + } // Make a regex to get the system path instead of the config path syspath_re := regexp.MustCompile(`^config`) @@ -90,8 +102,16 @@ func InitConfigs() { // Get the system path syspath := syspath_re.ReplaceAllString(confpath, "") + exists, err := fileio.FileExist(syspath) + + // If we received an error that is not ErrNotExist + if err != nil { + common.ErrorCheck(err, "\nError checking for directory: "+syspath) + continue // note: unreachable due to ErrorCheck calling fatal + } + // If the path exists - if fileio.FileExist(syspath) { + if exists { // Write to log logger.Printf( "%s found on the system\n"+ @@ -104,8 +124,10 @@ func InitConfigs() { makeBackupDir(syspath) // Create the directories for our configs - err := os.MkdirAll(confpath, os.ModePerm) - errorcheck.ErrorCheck(err) + if err = os.MkdirAll(confpath, os.ModePerm); err != nil && !errors.Is(err, os.ErrExist) { + common.ErrorCheck(err, "\nError making directory: "+confpath) + return // note: unreachable due to ErrorCheck calling fatal + } } } @@ -128,7 +150,15 @@ func InitConfigs() { sysfile := syspath_re.ReplaceAllString(conffile, "") // If the file exists - if fileio.FileExist(sysfile) { + exists, err := fileio.FileExist(sysfile) + + // If we received an error that is not ErrNotExist + if err != nil { + common.ErrorCheck(err, "\nError checking for file: "+sysfile) + continue // note: unreachable due to ErrorCheck calling fatal + } + + if exists { // Write to log logger.Printf( "%s found on the system\n"+ @@ -139,16 +169,22 @@ func InitConfigs() { // Create the directories for our configs file, err := os.Create(conffile) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) // Close the file so we can edit it - file.Close() + _ = file.Close() // Backup the sysfile if we do not have a backup backupFile(sysfile) } + exists, err = fileio.FileExist(conffile) + if err != nil { + common.ErrorCheck(err, "\nError checking for file: "+conffile) + continue // note: unreachable + } + // If we now have a config that exists - if fileio.FileExist(conffile) { + if exists { switch conffile { case config.Path.ETCMODULES: // Write to logger @@ -204,14 +240,28 @@ func backupFile(source string) { // Make a destination path dest := fmt.Sprintf("backup%s", source) + configExists, configFileError := fileio.FileExist(fmt.Sprintf("config%s", source)) + sysExists, sysFileError := fileio.FileExist(source) + destExists, destFileError := fileio.FileExist(dest) + + // If we received an error that is not ErrNotExist on any of the files + for _, err := range []error{configFileError, sysFileError, destFileError} { + if err != nil { + common.ErrorCheck(configFileError, "\nError checking for file: "+source) + return // note: unreachable + } + } + + switch { // If the file exists in the config but not on the system it is a file we make - if fileio.FileExist(fmt.Sprintf("config%s", source)) && !fileio.FileExist(source) { + case configExists && !sysExists: // Create the blank file so that a copy of the backup folder to /etc file, err := os.Create(dest) - errorcheck.ErrorCheck(err, "Error creating file %s\n", dest) - file.Close() - } else if !fileio.FileExist(dest) { + common.ErrorCheck(err, "Error creating file %s\n", dest) + _ = file.Close() + // If a backup of the file does not exist + case sysExists && !destExists: // Write to the logger logger.Printf("No first time backup of %s detected.\nCreating a backup at %s\n", source, dest) @@ -223,29 +273,67 @@ func backupFile(source string) { func makeBackupDir(dest string) { // If a backup directory does not exist - if !fileio.FileExist("backup/") { + exists, err := fileio.FileExist("backup/") + if err != nil { + // If we received an error that is not ErrNotExist + common.ErrorCheck(err, "Error checking for backup/ folder") + return // note: unreachable + } + + if !exists { // Write to the logger logger.Printf("Backup directory does not exist!\nCreating backup directory for first run backup") } // Make the empty directories - err := os.MkdirAll(fmt.Sprintf("backup/%s", dest), os.ModePerm) - errorcheck.ErrorCheck(err, "Error making backup/ folder") + if err = os.MkdirAll(fmt.Sprintf("backup/%s", dest), os.ModePerm); errors.Is(err, os.ErrExist) { + // ignore if the directory already exists + err = nil + } + // will return without incident if there's no error + common.ErrorCheck(err, "Error making backup/ folder") } -// Copy a file to the system, make sure you have run command.Elevate() recently -func CopyToSystem(conffile, sysfile string) string { +// CopyToSystem copies a file to the system. +func CopyToSystem(isRoot bool, conffile, sysfile string) { // Since we should be elevated with our sudo token we will copy with cp // (using built in functions will not work as we are running as the normal user) - output, _ := command.Run("sudo", "cp", "-v", conffile, sysfile) - // Clean the output - clean_re := regexp.MustCompile(`\n`) - clean_output := clean_re.ReplaceAllString(output[0], "") + // ExecAndLogSudo will write to the logger, so just print here + fmt.Printf("Copying: %s to %s\n", conffile, sysfile) - // Write output to logger - logger.Printf("%s\n", clean_output) + if isRoot { + logger.Printf("Copying %s to %s\n", conffile, sysfile) + fmt.Printf("Copying %s to %s\n", conffile, sysfile) + fDat, err := os.ReadFile(conffile) + common.ErrorCheck(err, fmt.Sprintf("Failed to read %s", conffile)) + err = os.WriteFile(sysfile, fDat, 0644) + common.ErrorCheck(err, fmt.Sprintf("Failed to write %s", sysfile)) + logger.Printf("Copied %s to %s\n", conffile, sysfile) + return + } - // Return the output - return fmt.Sprintf("Copying: %s", clean_output) + if !filepath.IsAbs(conffile) { + conffile, _ = filepath.Abs(conffile) + } + + conffile = strings.ReplaceAll(conffile, " ", "\\ ") + cmd := fmt.Sprintf("cp -v %s %s", conffile, sysfile) + + err := command.ExecAndLogSudo(isRoot, false, cmd) + + errMsg := "" + if err != nil { + errMsg = err.Error() + } + + // [command.ExecAndLogSudo] will log the command's output + common.ErrorCheck(err, fmt.Sprintf("Failed to copy %s to %s:\n%s", conffile, sysfile, errMsg)) + + // --------------------------------------------------------------------------------- + // note that if we failed the error check, the following will not appear in the log! + // this is because the [common.ErrorCheck] function will call [log.Fatalf] and exit + // --------------------------------------------------------------------------------- + + logger.Printf("Copied %s to %s\n", conffile, sysfile) } diff --git a/internal/configs/configs_test.go b/internal/configs/configs_test.go new file mode 100644 index 0000000..8cc6290 --- /dev/null +++ b/internal/configs/configs_test.go @@ -0,0 +1,33 @@ +package configs + +import ( + "os" + "path/filepath" + "testing" +) + +func TestCopyToSystem(t *testing.T) { + if err := os.Mkdir("testdir", 0755); err != nil { + t.Fatal(err) + } + tFilePath := filepath.Join("testdir", "testfile") + if err := os.WriteFile(tFilePath, []byte("test"), 0644); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.RemoveAll("testdir"); err != nil { + t.Fatal(err) + } + }) + isRoot := os.Getuid() == 0 + switch isRoot { + case true: + t.Run("TestCopyToSystem_AsRoot", func(t *testing.T) { + CopyToSystem(true, tFilePath, "/etc/testfile") + }) + default: + t.Run("TestCopyToSystem_AsUser", func(t *testing.T) { + CopyToSystem(false, tFilePath, "/etc/testfile") + }) + } +} diff --git a/internal/ls_iommu_downloader/ls_iommu_downloader.go b/internal/ls_iommu_downloader/ls_iommu_downloader.go index d8114f0..0c26cfd 100644 --- a/internal/ls_iommu_downloader/ls_iommu_downloader.go +++ b/internal/ls_iommu_downloader/ls_iommu_downloader.go @@ -12,10 +12,11 @@ import ( "strings" "time" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/cavaliergopher/grab/v3" + + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/pkg/fileio" "github.com/HikariKnight/quickpassthrough/pkg/untar" - "github.com/cavaliergopher/grab/v3" ) // Generated from github API response using https://mholt.github.io/json-to-go/ @@ -95,14 +96,14 @@ type Response struct { func CheckLsIOMMU() { // Check the API for releases resp, err := http.Get("https://api.github.com/repos/hikariknight/ls-iommu/releases/latest") - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) // Close the response when function ends defer resp.Body.Close() // Get the response body body, err := io.ReadAll(resp.Body) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) var result Response if err := json.Unmarshal(body, &result); err != nil { @@ -111,9 +112,9 @@ func CheckLsIOMMU() { // Make the directory for ls-iommu if it does not exist path := "utils" - if !fileio.FileExist(path) { + if exists, _ := fileio.FileExist(path); !exists { err := os.Mkdir(path, os.ModePerm) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) } // Generate the download url @@ -133,30 +134,30 @@ func CheckLsIOMMU() { // Get the checksum data checksums, err := http.Get(checkSumsUrl) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) defer checksums.Body.Close() checksums_txt, err := io.ReadAll(checksums.Body) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) // Check if the tar.gz exists - if !fileio.FileExist(fileName) { + if exists, _ := fileio.FileExist(fileName); !exists { downloadNewVersion(path, fileName, downloadUrl) if checkSum(string(checksums_txt), fileName) { err = untar.Untar(fmt.Sprintf("%s/", path), fileName) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) } } else { if !checkSum(string(checksums_txt), fileName) { downloadNewVersion(path, fileName, downloadUrl) err = untar.Untar(fmt.Sprintf("%s/", path), fileName) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) } } } func checkSum(checksums string, fileName string) bool { r, err := os.Open(fileName) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) defer r.Close() hasher := sha256.New() @@ -182,7 +183,7 @@ func downloadNewVersion(path, fileName, downloadUrl string) { // check for errors if err := download.Err(); err != nil { fmt.Fprintf(os.Stderr, "Download failed: %v\n", err) - if !fileio.FileExist("utils/ls-iommu") { + if exists, _ := fileio.FileExist("utils/ls-iommu"); !exists { log.Fatal("If the above error is 404, then we could not communicate with the GitHub API\n Please manually download and extract ls-iommu to: utils/\nYou can download it from: https://github.com/HikariKnight/ls-iommu/releases") } else { fmt.Println("Existing ls-iommu binary detected in \"utils/\", will use that instead as the GitHub API did not respond.") diff --git a/internal/pages/02_select_gpu.go b/internal/pages/02_select_gpu.go index 1b270c5..a035f65 100644 --- a/internal/pages/02_select_gpu.go +++ b/internal/pages/02_select_gpu.go @@ -4,13 +4,14 @@ import ( "fmt" "os" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/gookit/color" + + "github.com/HikariKnight/quickpassthrough/internal/common" "github.com/HikariKnight/quickpassthrough/internal/configs" - lsiommu "github.com/HikariKnight/quickpassthrough/internal/lsiommu" + "github.com/HikariKnight/quickpassthrough/internal/lsiommu" "github.com/HikariKnight/quickpassthrough/pkg/command" "github.com/HikariKnight/quickpassthrough/pkg/fileio" "github.com/HikariKnight/quickpassthrough/pkg/menu" - "github.com/gookit/color" ) func SelectGPU(config *configs.Config) { @@ -94,10 +95,10 @@ func viewGPU(config *configs.Config, ext ...int) { config.Gpu_IDs = lsiommu.GetIOMMU("-g", mode, "-i", config.Gpu_Group, "--id") // If the kernel_args file already exists - if fileio.FileExist(config.Path.CMDLINE) { + if exists, _ := fileio.FileExist(config.Path.CMDLINE); exists { // Delete it as we will have to make a new one anyway err := os.Remove(config.Path.CMDLINE) - errorcheck.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE)) + common.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE)) } // Write initial kernel_arg file @@ -114,10 +115,10 @@ func viewGPU(config *configs.Config, ext ...int) { ) // If the kernel_args file already exists - if fileio.FileExist(config.Path.CMDLINE) { + if exists, _ := fileio.FileExist(config.Path.CMDLINE); exists { // Delete it as we will have to make a new one anyway err := os.Remove(config.Path.CMDLINE) - errorcheck.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE)) + common.ErrorCheck(err, fmt.Sprintf("Could not remove %s", config.Path.CMDLINE)) } // Write initial kernel_arg file diff --git a/internal/pages/05_select_usbctrl.go b/internal/pages/05_select_usbctrl.go index 3264f7a..2df9583 100644 --- a/internal/pages/05_select_usbctrl.go +++ b/internal/pages/05_select_usbctrl.go @@ -4,11 +4,12 @@ import ( "fmt" "os" + "github.com/gookit/color" + "github.com/HikariKnight/quickpassthrough/internal/configs" - lsiommu "github.com/HikariKnight/quickpassthrough/internal/lsiommu" + "github.com/HikariKnight/quickpassthrough/internal/lsiommu" "github.com/HikariKnight/quickpassthrough/pkg/command" "github.com/HikariKnight/quickpassthrough/pkg/menu" - "github.com/gookit/color" ) func selectUSB(config *configs.Config) { diff --git a/internal/pages/06_finalize.go b/internal/pages/06_finalize.go index 232bd26..a1ab656 100644 --- a/internal/pages/06_finalize.go +++ b/internal/pages/06_finalize.go @@ -6,34 +6,34 @@ import ( "log" "os" "os/user" - "strings" "syscall" + "github.com/gookit/color" + "golang.org/x/term" + "github.com/HikariKnight/quickpassthrough/internal/configs" "github.com/HikariKnight/quickpassthrough/internal/logger" "github.com/HikariKnight/quickpassthrough/pkg/command" "github.com/HikariKnight/quickpassthrough/pkg/fileio" "github.com/HikariKnight/quickpassthrough/pkg/menu" "github.com/HikariKnight/quickpassthrough/pkg/uname" - "github.com/gookit/color" - "golang.org/x/term" ) func prepModules(config *configs.Config) { // If we have files for modprobe - if fileio.FileExist(config.Path.MODPROBE) { + if exists, _ := fileio.FileExist(config.Path.MODPROBE); exists { // Configure modprobe configs.Set_Modprobe(config.Gpu_IDs) } // If we have a folder for dracut - if fileio.FileExist(config.Path.DRACUT) { + if exists, _ := fileio.FileExist(config.Path.DRACUT); exists { // Configure dracut configs.Set_Dracut() } // If we have a mkinitcpio.conf file - if fileio.FileExist(config.Path.MKINITCPIO) { + if exists, _ := fileio.FileExist(config.Path.MKINITCPIO); exists { configs.Set_Mkinitcpio() } @@ -48,6 +48,43 @@ func prepModules(config *configs.Config) { finalize(config) } +func finalizeNotice(isRoot bool) { + color.Print(` +The configuration files have been generated and are located inside the "config" folder + + * The "kernel_args" file contains kernel arguments that your bootloader needs + * The "qemu" folder contains files that may be needed for passthrough + * The files inside the "etc" folder must be copied to your system. + + Verify that these files are correctly formated/edited! + +Once all files have been copied, the following steps must be taken: + + * bootloader configuration must be updated + * initramfs must be rebuilt + +`) + switch isRoot { + case true: + color.Print("This program can do this for you, if desired.\n") + default: + color.Print(`This program can do this for you, however your sudo password is required. +To avoid this: + + * press CTRL+C and perform the steps mentioned above manually. + OR + * run ` + os.Args[0] + ` as root. + +`) + } + + color.Print(` +If you want to go back and change something, choose Back. + +NOTE: A backup of the original files from the first run can be found in the backup folder +`) +} + func finalize(config *configs.Config) { // Clear the screen command.Clear() @@ -56,60 +93,43 @@ func finalize(config *configs.Config) { title := color.New(color.BgHiBlue, color.White, color.Bold) title.Println("Finalizing configuration") - color.Print( - "The configuration files have been generated and are\n", - "located inside the \"config\" folder\n", - "\n", - "* The \"kernel_args\" file contains kernel arguments that your bootloader needs\n", - "* The \"qemu\" folder contains files that might be\n neccessary for passing through the GPU\n", - "* The files inside the \"etc\" folder must be copied to your system.\n", - " NOTE: Verify that these files are correctly formated/edited!\n", - "* Once all files have been copied, you need to update your bootloader and rebuild\n", - " your initramfs using the tools to do so by your system.\n", - "\n", - "This program can do this for you, however the program will have to\n", - "type your password to sudo using STDIN, to avoid using STDIN press CTRL+C\n", - "and copy the files, update your bootloader and rebuild your initramfs manually.\n", - "If you want to go back and change something, choose Back\n", - "\nNOTE: A backup of the original files from the first run can be found in the backup folder\n", - ) + config.IsRoot = os.Getuid() == 0 - // Make a choice of going next or back - choice := menu.Next("Press Next to continue with sudo using STDIN, ESC to exit or Back to go back.") + finalizeNotice(config.IsRoot) - // Parse the choice - switch choice { + // Make a choice of going next or back and parse the choice + switch menu.Next("Press Next to continue with sudo using STDIN, ESC to exit or Back to go back.") { case "next": installPassthrough(config) - case "back": // Go back disableVideo(config) } - } func installPassthrough(config *configs.Config) { // Get the user data - user, err := user.Current() + currentUser, err := user.Current() if err != nil { log.Fatalf(err.Error()) } - // Provide a password prompt - fmt.Printf("[sudo] password for %s: ", user.Username) - bytep, err := term.ReadPassword(int(syscall.Stdin)) - if err != nil { - os.Exit(1) - } - fmt.Print("\n") + if !config.IsRoot { + // Provide a password prompt + fmt.Printf("[sudo] password for %s: ", currentUser.Username) + bytep, err := term.ReadPassword(syscall.Stdin) + if err != nil { + os.Exit(1) + } + fmt.Print("\n") - // Elevate with sudo - command.Elevate( - base64.StdEncoding.EncodeToString( - bytep, - ), - ) + // Elevate with sudo + command.Elevate( + base64.StdEncoding.EncodeToString( + bytep, + ), + ) + } // Make an output string var output string @@ -120,22 +140,24 @@ func installPassthrough(config *configs.Config) { logger.Printf("Configuring systemd-boot using kernelstub\n") // Configure kernelstub - output = configs.Set_KernelStub() - fmt.Printf("%s\n", output) + // callee logs the output and checks for errors + configs.Set_KernelStub(config.IsRoot) } else if config.Bootloader == "grubby" { // Write to logger logger.Printf("Configuring bootloader using grubby\n") // Configure kernelstub - output = configs.Set_Grubby() + output = configs.Set_Grubby(config.IsRoot) fmt.Printf("%s\n", output) } else if config.Bootloader == "grub2" { // Write to logger logger.Printf("Applying grub2 changes\n") - grub_output, _ := configs.Set_Grub2() - fmt.Printf("%s\n", strings.Join(grub_output, "\n")) + _ = configs.Set_Grub2(config.IsRoot) // note: we set config.IsRoot earlier + + // we'll print the output in the [configs.Set_Grub2] method + // fmt.Printf("%s\n", strings.Join(grub_output, "\n")) } else { kernel_args := fileio.ReadFile(config.Path.CMDLINE) @@ -145,68 +167,62 @@ func installPassthrough(config *configs.Config) { // A lot of linux systems support modprobe along with their own module system // So copy the modprobe files if we have them modprobeFile := fmt.Sprintf("%s/vfio.conf", config.Path.MODPROBE) - if fileio.FileExist(modprobeFile) { - // Copy initramfs-tools module to system - output = configs.CopyToSystem(modprobeFile, "/etc/modprobe.d/vfio.conf") - fmt.Printf("%s\n", output) + + // lets hope by now we've already handled any permissions issues... + // TODO: verify that we actually can drop the errors on [fileio.FileExist] call below + + if exists, _ := fileio.FileExist(modprobeFile); exists { + // Copy initramfs-tools module to system, note that CopyToSystem will log the command and output + // as well as check for errors + configs.CopyToSystem(config.IsRoot, modprobeFile, "/etc/modprobe.d/vfio.conf") } // Copy the config files for the system we have initramfsFile := fmt.Sprintf("%s/modules", config.Path.INITRAMFS) dracutFile := fmt.Sprintf("%s/vfio.conf", config.Path.DRACUT) - if fileio.FileExist(initramfsFile) { + + initramFsExists, initramFsErr := fileio.FileExist(initramfsFile) + dracutExists, dracutErr := fileio.FileExist(dracutFile) + mkinitcpioExists, mkinitcpioErr := fileio.FileExist(config.Path.MKINITCPIO) + + for _, err = range []error{initramFsErr, dracutErr, mkinitcpioErr} { + if err == nil { + continue + } + // we know this error isn't ErrNotExist, so we should throw it and exit + log.Fatalf("Failed to stat file: %s", err) + } + + switch { + case initramFsExists: // Copy initramfs-tools module to system - output = configs.CopyToSystem(initramfsFile, "/etc/initramfs-tools/modules") - fmt.Printf("%s\n", output) + configs.CopyToSystem(config.IsRoot, initramfsFile, "/etc/initramfs-tools/modules") // Copy the modules file to /etc/modules - output = configs.CopyToSystem(config.Path.ETCMODULES, "/etc/modules") - fmt.Printf("%s\n", output) + configs.CopyToSystem(config.IsRoot, config.Path.ETCMODULES, "/etc/modules") - // Write to logger - logger.Printf("Executing: sudo update-initramfs -u\n") + if err = command.ExecAndLogSudo(config.IsRoot, true, "update-initramfs", "-u"); err != nil { + log.Fatalf("Failed to update initramfs: %s", err) + } - // Update initramfs - fmt.Println("Executed: sudo update-initramfs -u\nSee debug.log for detailed output") - cmd_out, cmd_err, _ := command.RunErr("sudo", "update-initramfs", "-u") - - cmd_out = append(cmd_out, cmd_err...) - - // Write to logger - logger.Printf(strings.Join(cmd_out, "\n")) - } else if fileio.FileExist(dracutFile) { + case dracutExists: // Copy dracut config to /etc/dracut.conf.d/vfio - output = configs.CopyToSystem(dracutFile, "/etc/dracut.conf.d/vfio") - fmt.Printf("%s\n", output) + configs.CopyToSystem(config.IsRoot, dracutFile, "/etc/dracut.conf.d/vfio") // Get systeminfo sysinfo := uname.New() - // Write to logger - logger.Printf("Executing: sudo dracut -f -v --kver %s\n", sysinfo.Release) + if err = command.ExecAndLogSudo(config.IsRoot, true, "dracut", "-f", "-v", "--kver", sysinfo.Release); err != nil { + log.Fatalf("Failed to update initramfs: %s", err) + } - // Update initramfs - fmt.Printf("Executed: sudo dracut -f -v --kver %s\nSee debug.log for detailed output", sysinfo.Release) - _, cmd_err, _ := command.RunErr("sudo", "dracut", "-f", "-v", "--kver", sysinfo.Release) - - // Write to logger - logger.Printf(strings.Join(cmd_err, "\n")) - } else if fileio.FileExist(config.Path.MKINITCPIO) { + case mkinitcpioExists: // Copy dracut config to /etc/dracut.conf.d/vfio - output = configs.CopyToSystem(config.Path.MKINITCPIO, "/etc/mkinitcpio.conf") - fmt.Printf("%s\n", output) + configs.CopyToSystem(config.IsRoot, config.Path.MKINITCPIO, "/etc/mkinitcpio.conf") - // Write to logger - logger.Printf("Executing: sudo mkinitcpio -P") - - // Update initramfs - fmt.Println("Executed: sudo mkinitcpio -P\nSee debug.log for detailed output") - cmd_out, cmd_err, _ := command.RunErr("sudo", "mkinitcpio", "-P") - - cmd_out = append(cmd_out, cmd_err...) - - // Write to logger - logger.Printf(strings.Join(cmd_out, "\n")) + if err = command.ExecAndLogSudo(config.IsRoot, true, "mkinitcpio", "-P"); err != nil { + log.Fatalf("Failed to update initramfs: %s", err) + } } // Make sure prompt end up on next line diff --git a/internal/pages/06_finalize_test.go b/internal/pages/06_finalize_test.go new file mode 100644 index 0000000..df9a7d0 --- /dev/null +++ b/internal/pages/06_finalize_test.go @@ -0,0 +1,23 @@ +package pages + +import ( + "strings" + "testing" +) + +func TestFinalizeNotice(t *testing.T) { + msg := "\n%s\nprinting the finalize notice for manual review, this test should always pass.\n%s\n\n" + divider := strings.Repeat("-", len(msg)-12) + t.Logf(msg, divider, divider) + t.Log("\n\nWith isRoot == true:\n\n") + + finalizeNotice(true) + + println("\n\n") + + t.Log("\n\nWith isRoot == false:\n\n") + + finalizeNotice(false) + + println("\n\n") +} diff --git a/internal/ui_main.go b/internal/ui_main.go index 9b0db17..5bf650d 100644 --- a/internal/ui_main.go +++ b/internal/ui_main.go @@ -6,17 +6,18 @@ package internal import ( "os" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" - "github.com/HikariKnight/quickpassthrough/internal/pages" tea "github.com/charmbracelet/bubbletea" + + "github.com/HikariKnight/quickpassthrough/internal/common" + "github.com/HikariKnight/quickpassthrough/internal/pages" ) // This is where we build everything func Tui() { // Log all errors to a new logfile (super useful feature of BubbleTea!) - os.Remove("debug.log") - logfile, err := tea.LogToFile("debug.log", "") - errorcheck.ErrorCheck(err, "Error creating log file") + _ = os.Rename("quickpassthrough_debug.log", "quickpassthrough_debug_old.log") + logfile, err := tea.LogToFile("quickpassthrough_debug.log", "") + common.ErrorCheck(err, "Error creating log file") defer logfile.Close() // New WIP Tui diff --git a/pkg/command/command.go b/pkg/command/command.go index e7c6fca..e6ba915 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -3,12 +3,15 @@ package command import ( "bytes" "encoding/base64" + "fmt" "io" "os" "os/exec" + "strings" "time" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/common" + "github.com/HikariKnight/quickpassthrough/internal/logger" ) // Run a command and return STDOUT @@ -27,14 +30,14 @@ func Run(binary string, args ...string) ([]string, error) { output, _ := io.ReadAll(&stdout) // Get the output - outputs := []string{} + outputs := make([]string, 0, 1) outputs = append(outputs, string(output)) // Return our list of items return outputs, err } -// This function is just like command.Run() but also returns STDERR +// RunErr is just like command.Run() but also returns STDERR func RunErr(binary string, args ...string) ([]string, []string, error) { var stdout, stderr bytes.Buffer @@ -59,8 +62,18 @@ func RunErr(binary string, args ...string) ([]string, []string, error) { return outputs, outerrs, err } -// This functions runs the command "sudo -Sk -- echo", this forces sudo -// to re-authenticate and lets us enter the password to STDIN +func RunErrSudo(isRoot bool, binary string, args ...string) ([]string, []string, error) { + if !isRoot && binary != "sudo" { + args = append([]string{binary}, args...) + binary = "sudo" + } + logger.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " ")) + fmt.Printf("Executing (elevated): %s %s\n", binary, strings.Join(args, " ")) + return RunErr(binary, args...) +} + +// Elevate elevates this functions runs the command "sudo -Sk -- echo", +// this forces sudo to re-authenticate and lets us enter the password to STDIN // giving us the ability to run sudo commands func Elevate(password string) { // Do a simple sudo command to just authenticate with sudo @@ -71,29 +84,83 @@ func Elevate(password string) { // Open STDIN stdin, err := cmd.StdinPipe() - errorcheck.ErrorCheck(err, "\nFailed to get sudo STDIN") + common.ErrorCheck(err, "\nFailed to get sudo STDIN") // Start the authentication - cmd.Start() + err = cmd.Start() + common.ErrorCheck(err, "\nFailed to start sudo command") // Get the passed password pw, _ := base64.StdEncoding.DecodeString(password) _, err = stdin.Write([]byte(string(pw) + "\n")) - errorcheck.ErrorCheck(err, "\nFailed at typing to STDIN") + common.ErrorCheck(err, "\nFailed at typing to STDIN") // Clear the password pw = nil password = "" - stdin.Close() + _ = stdin.Close() // Wait for the sudo prompt (If the correct password was given, it will not stay behind) err = cmd.Wait() - errorcheck.ErrorCheck(err, "\nError, password given was wrong") + common.ErrorCheck(err, "\nError, password given was wrong") } -// Function to just clear the terminal +// Clear clears the terminal. func Clear() { c := exec.Command("clear") c.Stdout = os.Stdout - c.Run() + _ = c.Run() +} + +// ExecAndLogSudo executes an elevated command and logs the output. +// +// * if we're root, the command is executed directly +// * if we're not root, the command is prefixed with "sudo" +// +// - noisy determines if we should print the command to the user +// noisy isn't set to true by our copy caller, as it logs differently, +// but other callers set it. +func ExecAndLogSudo(isRoot, noisy bool, exe string, args ...string) error { + if !isRoot && exe != "sudo" { + og := exe + exe = "sudo" + newArgs := make([]string, 0) + newArgs = append(newArgs, og) + newArgs = append(newArgs, args...) + args = newArgs + } + + // Write to logger + logger.Printf("Executing (elevated): %s %s\n", exe, strings.Join(args, " ")) + + if noisy { + // Print to the user + fmt.Printf("Executing (elevated): %s %s\nSee debug.log for detailed output\n", exe, strings.Join(args, " ")) + } + + wd, err := os.Getwd() + if err != nil { + return err + } + + r := exec.Command(exe, args...) + r.Dir = wd + + cmdCombinedOut, err := r.CombinedOutput() + outStr := string(cmdCombinedOut) + + // Write to logger, tabulate output + // tabulation denotes it's hierarchy as a child of the command + outStr = strings.ReplaceAll(outStr, "\n", "\n\t") + logger.Printf("\t" + string(cmdCombinedOut) + "\n") + if noisy { + // Print to the user + fmt.Printf("%s\n", outStr) + } + + if err != nil { + err = fmt.Errorf("failed to execute %s: %w\n%s", exe, err, outStr) + } + + return err } diff --git a/pkg/command/command_test.go b/pkg/command/command_test.go new file mode 100644 index 0000000..f6fa119 --- /dev/null +++ b/pkg/command/command_test.go @@ -0,0 +1,61 @@ +package command + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +const fakeSudo = `#!/bin/sh +"$@" -qptest` + +const fakeUtil = `#!/bin/sh +echo "$@" +if [ "$4" = "-qptest" ]; then exit 0; else exit 1; fi` + +func setupExecTestEnv(t *testing.T) (string, string) { + t.Helper() + + tmpDir := t.TempDir() + fakeSudoPath := filepath.Join(tmpDir, "sudo") + fakeUtilPath := filepath.Join(tmpDir, "util") + + if err := os.WriteFile(fakeSudoPath, []byte(fakeSudo), 0755); err != nil { + t.Fatalf("failed to write fake sudo stub: %s", err.Error()) + } + if err := os.WriteFile(fakeUtilPath, []byte(fakeUtil), 0755); err != nil { + t.Fatalf("failed to write fake util stub: %s", err.Error()) + } + t.Setenv("PATH", tmpDir+":"+os.Getenv("PATH")) + + return fakeSudoPath, fakeUtilPath +} + +func TestExecAndLogSudo(t *testing.T) { + _, fakeUtilPath := setupExecTestEnv(t) + + args := []string{"i am a string with spaces", "i came to ruin parsers and chew bubble gum", "and I'm all out of bubblegum."} + + t.Run("is_not_root", func(t *testing.T) { + if err := ExecAndLogSudo(false, false, "util", args...); err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + }) + + t.Run("is_root", func(t *testing.T) { + newFakeUtil := strings.Replace(fakeUtil, "exit 1", "exit 0", 1) + newFakeUtil = strings.Replace(newFakeUtil, "exit 0", "exit 1", 1) + if err := os.WriteFile(fakeUtilPath, []byte(newFakeUtil), 0755); err != nil { + t.Fatalf("failed to overwrite fake util with modified stub: %s", err.Error()) + } + if err := ExecAndLogSudo(false, false, "util", args...); err == nil { + t.Errorf("expected error when using modified util with sudo, got nil") + } + + if err := ExecAndLogSudo(true, true, "util", args...); err != nil { + t.Errorf("unexpected error: %s", err.Error()) + } + }) + +} diff --git a/pkg/fileio/fileio.go b/pkg/fileio/fileio.go index 38c5a7a..b727ede 100644 --- a/pkg/fileio/fileio.go +++ b/pkg/fileio/fileio.go @@ -7,29 +7,31 @@ import ( "io" "os" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/common" ) /* * This just implements repetetive tasks I have to do with files */ -// Creates a file and appends the content to the file (ending newline must be supplied with content string) +// AppendContent creates a file and appends the content to the file. +// (ending newline must be supplied with content string) func AppendContent(content string, fileName string) { // Open the file f, err := os.OpenFile(fileName, os.O_CREATE|os.O_APPEND|os.O_WRONLY, os.ModePerm) - errorcheck.ErrorCheck(err, fmt.Sprintf("Error opening \"%s\" for writing", fileName)) + + common.ErrorCheck(err, fmt.Sprintf("Error opening \"%s\" for writing", fileName)) defer f.Close() // Write the content _, err = f.WriteString(content) - errorcheck.ErrorCheck(err, fmt.Sprintf("Error writing to %s", fileName)) + common.ErrorCheck(err, fmt.Sprintf("Error writing to %s", fileName)) } -// Reads the file and returns a stringlist with each line +// ReadLines reads the file and returns a stringlist with each line. func ReadLines(fileName string) []string { content, err := os.Open(fileName) - errorcheck.ErrorCheck(err, fmt.Sprintf("Error reading file %s", fileName)) + common.ErrorCheck(err, fmt.Sprintf("Error reading file %s", fileName)) defer content.Close() // Make a list of lines @@ -46,54 +48,55 @@ func ReadLines(fileName string) []string { } -// Reads a file and returns all the content as a string +// ReadFile reads a file and returns all the content as a string. func ReadFile(fileName string) string { // Read the whole file content, err := os.ReadFile(fileName) - errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to ReadFile on %s", fileName)) + common.ErrorCheck(err, fmt.Sprintf("Failed to ReadFile on %s", fileName)) // Return all the lines as one string return string(content) } -// Checks if a file exists and returns a bool -func FileExist(fileName string) bool { +// FileExist checks if a file exists and returns a bool and any error that isn't os.ErrNotExist. +func FileExist(fileName string) (bool, error) { var exist bool // Check if the file exists - if _, err := os.Stat(fileName); !errors.Is(err, os.ErrNotExist) { - // Set the value to true + _, err := os.Stat(fileName) + switch { + case err == nil: exist = true - } else { - // Set the value to false + case errors.Is(err, os.ErrNotExist): + // Set the value to true exist = false + err = nil } - // Return if the file exists - return exist + return exist, err } -// Copies a FILE from source to dest +// FileCopy copies a FILE from source to dest. func FileCopy(sourceFile, destFile string) { // Get the file info filestat, err := os.Stat(sourceFile) - errorcheck.ErrorCheck(err, "Error getting fileinfo of: %s", sourceFile) + common.ErrorCheck(err, "Error getting fileinfo of: %s", sourceFile) // If the file is a regular file if filestat.Mode().IsRegular() { // Open the source file for reading source, err := os.Open(sourceFile) - errorcheck.ErrorCheck(err, "Error opening %s for copying", sourceFile) + common.ErrorCheck(err, "Error opening %s for copying", sourceFile) defer source.Close() // Create the destination file dest, err := os.Create(destFile) - errorcheck.ErrorCheck(err, "Error creating %s", destFile) + common.ErrorCheck(err, "Error creating %s", destFile) defer dest.Close() // Copy the contents of source to dest using io _, err = io.Copy(dest, source) - errorcheck.ErrorCheck(err, "Failed to copy \"%s\" to \"%s\"", sourceFile, destFile) + common.ErrorCheck(err, "Failed to copy \"%s\" to \"%s\"", sourceFile, destFile) } } diff --git a/pkg/menu/manual.go b/pkg/menu/manual.go index 5e0b06c..5db9da7 100644 --- a/pkg/menu/manual.go +++ b/pkg/menu/manual.go @@ -4,8 +4,9 @@ import ( "fmt" "strings" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" "github.com/gookit/color" + + "github.com/HikariKnight/quickpassthrough/internal/common" ) func ManualInput(msg string, format string) []string { @@ -18,7 +19,7 @@ func ManualInput(msg string, format string) []string { // Get the user input var input string _, err := fmt.Scan(&input) - errorcheck.ErrorCheck(err) + common.ErrorCheck(err) input_list := strings.Split(input, ",") diff --git a/pkg/untar/untar.go b/pkg/untar/untar.go index 96ab454..f94b399 100644 --- a/pkg/untar/untar.go +++ b/pkg/untar/untar.go @@ -8,7 +8,7 @@ import ( "os" "path/filepath" - "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/HikariKnight/quickpassthrough/internal/common" ) // Slightly modified from source: https://medium.com/@skdomino/taring-untaring-files-in-go-6b07cf56bc07 @@ -17,7 +17,7 @@ import ( // creating the file structure at 'dst' along the way, and writing any files func Untar(dst string, fileName string) error { r, err := os.Open(fileName) - errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName)) + common.ErrorCheck(err, fmt.Sprintf("Failed to open: %s", fileName)) defer r.Close() gzr, err := gzip.NewReader(r) From 380c875213e1a1b18cf2297723b1eb0ee89565ce Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 27 Jul 2024 17:49:46 +0200 Subject: [PATCH 11/18] fix: CopyToSystem now correctly uses ExecAndLogSudo --- internal/configs/configs.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/configs/configs.go b/internal/configs/configs.go index 74ecb60..4b1da82 100644 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -6,7 +6,6 @@ import ( "os" "path/filepath" "regexp" - "strings" "github.com/klauspost/cpuid/v2" @@ -317,10 +316,7 @@ func CopyToSystem(isRoot bool, conffile, sysfile string) { conffile, _ = filepath.Abs(conffile) } - conffile = strings.ReplaceAll(conffile, " ", "\\ ") - cmd := fmt.Sprintf("cp -v %s %s", conffile, sysfile) - - err := command.ExecAndLogSudo(isRoot, false, cmd) + err := command.ExecAndLogSudo(isRoot, false, "cp", "-v", conffile, sysfile) errMsg := "" if err != nil { From 3f934351fc919e90342f7978efe195307cd1e06f Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 10 Aug 2024 08:57:54 +0200 Subject: [PATCH 12/18] fix: correctly detect kernel 6.10 and newer as a newer kernel --- internal/configs/configs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/configs/configs.go b/internal/configs/configs.go index 4b1da82..c9b851b 100644 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -221,7 +221,7 @@ func vfio_modules() []string { // If we are on a kernel older than 6.2 sysinfo := uname.New() - kernel_re := regexp.MustCompile(`^(6\.1|6\.0|[1-5]\.)`) + kernel_re := regexp.MustCompile(`^(6\.1|6\.0|[1-5]\.\d{1,2})\.`) if kernel_re.MatchString(sysinfo.Kernel) { // Write to the debug log logger.Printf("Linux kernel version %s detected!\nIncluding vfio_virqfd module\n", sysinfo.Kernel) From d1e44d94d593f29ab42c29c00b6683a9f8952181 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 10 Aug 2024 09:02:49 +0200 Subject: [PATCH 13/18] chore: add quickpassthrough_debug.log to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f0eea18..1313c71 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,5 @@ bin/ dist/ main quickpassthrough -debug.log \ No newline at end of file +debug.log +quickpassthrough_debug.log From 05afa8c543c76b18169a89e71c96cbe0f1c2c892 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sat, 10 Aug 2024 09:11:03 +0200 Subject: [PATCH 14/18] chore: cleanup the readme --- README.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 78b893e..46bfbaf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A project to simplify setting up GPU passthrough on your Linux host for libvirt/ You can use it by simply downloading the latest [release](https://github.com/HikariKnight/quickpassthrough/releases/) and run it inside a terminal or by downloading and compiling it yourself with the commands below. -This project is aimed at **desktops with 2 GPUs** and **headless servers**, where the only GPU is not needed. +This project is aimed at **systems with 2 GPUs** and **headless servers**, where the only GPU is not needed. **Note:** Quickpassthrough is not designed to be installed by a package manager! As you would usually have to only run it once, unless you change the GPU. @@ -47,16 +47,18 @@ go get -u ./cmd CGO_ENABLED=0 go build -ldflags="-X github.com/HikariKnight/quickpassthrough/internal/version.Version=$(git rev-parse --short HEAD)" -o quickpassthrough cmd/main.go ``` -## Does this work on immutable systems? -Currently no, however [Bazzite](https://bazzite.gg) has an `ujust` command that does a very similar job. -In Bazzite you run `ujust setup-virtualization` and follow the prompts to `Enable Virtualization` and `Enable VFIO drivers`. +## Does this work on atomic or immutable systems? +Currently no, however [Bazzite](https://bazzite.gg), [Bluefin](https://projectbluefin.io) and [Aurora](https://getaurora.dev) has an `ujust` command that does a very similar job. + +In Bazzite you run `ujust setup-virtualization` and follow the prompts to `Enable Virtualization` and `Enable VFIO drivers`.
+In Bluefin and Aurora you run `ujust setup-vfio` and follow the prompts. ## How do I undo the changes? There is a `backup/` folder generated on the first run that will have a copy of all your files (and their paths) from before we edited anything. Compare that folder with the `config/` folder to see which files you need to delete in addition to copying the files from `backup/` to your system before rebuilding your initramfs and updating your bootloader config. ## How do I just disable vfio for 1 boot? -Remove the vfio kernel arguments from your bootloader by pressing E on the boot menu. The kernel arguments added to the bootloader can be found in the config/kernel_args file.
+Remove the vfio kernel arguments from your bootloader by pressing E on the boot menu. The kernel arguments added to the bootloader can be found in the `config/kernel_args` file.
NOTE: You can also just remove them from your bootloader permanently and update your bootloader if you want to keep the config files on your system. ## What this project does NOT do From 4f51fd5c5f0094b45d3d05589c62b7bd3bb46358 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sun, 8 Sep 2024 02:08:09 +0200 Subject: [PATCH 15/18] chore: remove outdated info from readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 46bfbaf..e75085d 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,6 @@ NOTE: You can also just remove them from your bootloader permanently and update * Optimize your Virtual Machine for Passthrough (again this is your job) * Optimize your host machine for Passthrough or Virtualization (out of this projects scope) * Setup and configure GPU Passthrough on systems with 1 graphic card (iGPU counts as 1 Graphic Card by itself, so iGPU with another GPU will work) -* Does not configure passthrough of 3D controllers, [as it will not work](https://lantian.pub/en/article/modify-computer/laptop-intel-nvidia-optimus-passthrough.lantian/) (this is most gaming laptops so do not even think about it). If you try run this on a laptop with a 3D controller, the "2nd GPU" will not show up. ## Contributing From 276cfd953aad7a4f8f231e8fc17031195997d35b Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Sun, 29 Dec 2024 05:34:03 +0100 Subject: [PATCH 16/18] fix: correct vfio_pci to vfio-pci in rd.driver.pre for dracut --- internal/configs/config_dracut.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/configs/config_dracut.go b/internal/configs/config_dracut.go index 70f837e..1aa0f23 100644 --- a/internal/configs/config_dracut.go +++ b/internal/configs/config_dracut.go @@ -31,9 +31,9 @@ func Set_Dracut() { kernel_args := fileio.ReadFile(config.Path.CMDLINE) // If the kernel argument is not already in the file - if !strings.Contains(kernel_args, "rd.driver.pre=vfio_pci") { + if !strings.Contains(kernel_args, "rd.driver.pre=vfio-pci") { // Add to our kernel arguments file that vfio_pci should load early (dracut does this using kernel arguments) - fileio.AppendContent(" rd.driver.pre=vfio_pci", config.Path.CMDLINE) + fileio.AppendContent(" rd.driver.pre=vfio-pci", config.Path.CMDLINE) } // Make a backup of dracutConf if there is one there From 0efcf4ee19446bff0d3a5358244d5763ffc84980 Mon Sep 17 00:00:00 2001 From: HikariKnight <2557889+HikariKnight@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:15:45 +0100 Subject: [PATCH 17/18] fix(dracut): force load vfio modules earlier this should make them load before any gpu drivers --- internal/configs/config_dracut.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/configs/config_dracut.go b/internal/configs/config_dracut.go index 1aa0f23..bc0b503 100644 --- a/internal/configs/config_dracut.go +++ b/internal/configs/config_dracut.go @@ -22,10 +22,10 @@ func Set_Dracut() { } // Write to logger - logger.Printf("Writing to %s:\nadd_drivers+=\" %s \"\n", dracutConf, strings.Join(vfio_modules(), " ")) + logger.Printf("Writing to %s:\nforce_drivers+=\" %s \"\n", dracutConf, strings.Join(vfio_modules(), " ")) // Write the dracut config file - fileio.AppendContent(fmt.Sprintf("add_drivers+=\" %s \"\n", strings.Join(vfio_modules(), " ")), dracutConf) + fileio.AppendContent(fmt.Sprintf("force_drivers+=\" %s \"\n", strings.Join(vfio_modules(), " ")), dracutConf) // Get the current kernel arguments we have generated kernel_args := fileio.ReadFile(config.Path.CMDLINE) From c1f11ce1c3a656667c91913c7c97a279d958adbf Mon Sep 17 00:00:00 2001 From: jedrw Date: Sun, 29 Dec 2024 18:54:20 +0000 Subject: [PATCH 18/18] fix: avoid potential index out of range error if ls-iommu cannot find a vbios path Signed-off-by: HikariKnight <2557889+HikariKnight@users.noreply.github.com> --- internal/pages/03_vbios_extract.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/pages/03_vbios_extract.go b/internal/pages/03_vbios_extract.go index 598b584..44dcc04 100644 --- a/internal/pages/03_vbios_extract.go +++ b/internal/pages/03_vbios_extract.go @@ -26,9 +26,11 @@ func genVBIOS_dumper(config *configs.Config) { scriptdir, _ = os.Getwd() } - // Get the vbios path and generate the vbios dumping script - vbios_path := lsiommu.GetIOMMU("-g", "-i", config.Gpu_Group, "--rom")[0] - configs.GenerateVBIOSDumper(vbios_path) + // Search for a vbios path and generate the vbios dumping script if found + vbios_paths := lsiommu.GetIOMMU("-g", "-i", config.Gpu_Group, "--rom") + if len(vbios_paths) != 0 { + configs.GenerateVBIOSDumper(vbios_paths[0]) + } // Make the qemu config folder os.Mkdir(fmt.Sprintf("%s/%s", scriptdir, config.Path.QEMU), os.ModePerm) @@ -51,7 +53,7 @@ func genVBIOS_dumper(config *configs.Config) { "rom to the VM along with the card in order to get a functional passthrough.\n", "In many cases you can find your vbios at https://www.techpowerup.com/vgabios/\n", "\n", - "You can also attempt to dump your own vbios from TTY using the script in\n", + "If we found a romfile for your GPU you can also attempt to dump your own vbios from TTY using the script in\n", fmt.Sprintf("%s/utils/dump_vbios.sh\n", scriptdir), "\n", )