From 3337efcb8f400b8bb8b1d831f886dd2ee4b46413 Mon Sep 17 00:00:00 2001 From: "kayos@tcp.direct" Date: Sun, 16 Jun 2024 23:57:19 -0700 Subject: [PATCH] Heavy refactoring, see PR #28 --- internal/common/errors.go | 5 + internal/configs/config_bootloaders.go | 104 ++++++------ internal/configs/config_dracut.go | 6 +- internal/configs/config_mkinitcpio.go | 6 +- internal/configs/config_modprobe.go | 4 +- internal/configs/configs.go | 148 +++++++++++++++--- .../ls_iommu_downloader.go | 9 +- internal/pages/02_select_gpu.go | 4 +- internal/pages/06_finalize.go | 96 ++++++------ pkg/command/command.go | 64 +++++++- pkg/fileio/fileio.go | 33 ++-- 11 files changed, 323 insertions(+), 156 deletions(-) create mode 100644 internal/common/errors.go diff --git a/internal/common/errors.go b/internal/common/errors.go new file mode 100644 index 0000000..fd67e39 --- /dev/null +++ b/internal/common/errors.go @@ -0,0 +1,5 @@ +package common + +const PermissionNotice = "\nPermissions error occured during file operation.\n" + + "This could mean you initially ran QuickPassthrough as root or with sudo, but are now running it as a normal user. " + + "Please try running QuickPassthrough as root/with sudo again." diff --git a/internal/configs/config_bootloaders.go b/internal/configs/config_bootloaders.go index 2f8607d..796a9f8 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/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,38 +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 + errorcheck.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)) + // Run and log, check for errors + err := command.ExecAndLogSudo(isRoot, true, "grubby --update-kernel=ALL "+fmt.Sprintf("--args=%s", kernel_args)) errorcheck.ErrorCheck(err, "Error, grubby command returned exit code 1") // Return what we did @@ -116,8 +113,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 +198,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 +210,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 [errorcheck.ErrorCheck]. + lpErr = errors.New("neither grub-mkconfig or grub2-mkconfig found") + } + errorcheck.ErrorCheck(lpErr, lpErr.Error()+"\n") + return lpErr // note: unreachable as [errorcheck.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")) + errorcheck.ErrorCheck(err, "Failed to update /boot/grub/grub.cfg") + + // always returns nil as [errorcheck.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_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/configs.go b/internal/configs/configs.go index 68438ef..cad7aee 100644 --- a/internal/configs/configs.go +++ b/internal/configs/configs.go @@ -1,6 +1,7 @@ package configs import ( + "errors" "fmt" "os" "regexp" @@ -8,6 +9,7 @@ import ( "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" @@ -34,7 +36,7 @@ type Config struct { 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", @@ -50,7 +52,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", @@ -66,7 +68,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() @@ -79,10 +81,24 @@ func InitConfigs() { } // Remove old config - os.RemoveAll("config") + if err := os.RemoveAll("config"); err != nil && !errors.Is(err, os.ErrNotExist) { + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + // won't be called if the error is ErrNotExist + errorcheck.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) { + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + // won't be called if the error is ErrExist + errorcheck.ErrorCheck(err, "\nError making config folder") + } // Make a regex to get the system path instead of the config path syspath_re := regexp.MustCompile(`^config`) @@ -92,8 +108,20 @@ 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 { + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + errorcheck.ErrorCheck(err, "\nError checking for directory: "+syspath) + continue // note: also unreachable + } + // If the path exists - if fileio.FileExist(syspath) { + if exists { // Write to log logger.Printf( "%s found on the system\n"+ @@ -106,8 +134,13 @@ 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) { + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + errorcheck.ErrorCheck(err, "\nError making directory: "+confpath) + } } } @@ -130,7 +163,19 @@ 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 { + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + errorcheck.ErrorCheck(err, "\nError checking for file: "+sysfile) + continue // note: also unreachable + } + + if exists { // Write to log logger.Printf( "%s found on the system\n"+ @@ -143,14 +188,24 @@ func InitConfigs() { file, err := os.Create(conffile) errorcheck.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 { + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + errorcheck.ErrorCheck(err, "\nError checking for file: "+conffile) + continue // note: also unreachable + } + // If we now have a config that exists - if fileio.FileExist(conffile) { + if exists { switch conffile { case config.Path.ETCMODULES: // Write to logger @@ -206,14 +261,32 @@ 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 { + if errors.Is(configFileError, os.ErrPermission) { + errorcheck.ErrorCheck(configFileError, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + errorcheck.ErrorCheck(configFileError, "\nError checking for file: "+source) + return // note: also 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) { + _ = 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) @@ -225,29 +298,52 @@ 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 + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } + errorcheck.ErrorCheck(err, "Error checking for backup/ folder") + return // note: also 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) + if err = os.MkdirAll(fmt.Sprintf("backup/%s", dest), os.ModePerm); errors.Is(err, os.ErrExist) { + err = nil + } + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return // note: unreachable due to ErrorCheck calling fatal + } errorcheck.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) + // [command.ExecAndLogSudo] will log the command's output + errorcheck.ErrorCheck(command.ExecAndLogSudo(isRoot, false, + fmt.Sprintf("cp -v \"%s\" %s", conffile, sysfile), + ), // if error, log and exit + fmt.Sprintf("Failed to copy %s to %s", conffile, sysfile), + ) - // Return the output - return fmt.Sprintf("Copying: %s", clean_output) + // --------------------------------------------------------------------------------- + // note that if we failed the error check, the following will not appear in the log! + // this is because the [errorcheck.ErrorCheck] function will call [log.Fatalf] and exit + // --------------------------------------------------------------------------------- + + logger.Printf("Copied %s to %s\n", conffile, sysfile) } diff --git a/internal/ls_iommu_downloader/ls_iommu_downloader.go b/internal/ls_iommu_downloader/ls_iommu_downloader.go index d8114f0..dea6174 100644 --- a/internal/ls_iommu_downloader/ls_iommu_downloader.go +++ b/internal/ls_iommu_downloader/ls_iommu_downloader.go @@ -13,9 +13,10 @@ import ( "time" "github.com/HikariKnight/ls-iommu/pkg/errorcheck" + "github.com/cavaliergopher/grab/v3" + "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/ @@ -111,7 +112,7 @@ 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) } @@ -139,7 +140,7 @@ func CheckLsIOMMU() { errorcheck.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) @@ -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 42893f9..b2c3eea 100644 --- a/internal/pages/02_select_gpu.go +++ b/internal/pages/02_select_gpu.go @@ -95,7 +95,7 @@ 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)) @@ -115,7 +115,7 @@ 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)) diff --git a/internal/pages/06_finalize.go b/internal/pages/06_finalize.go index 879761f..35260ec 100644 --- a/internal/pages/06_finalize.go +++ b/internal/pages/06_finalize.go @@ -5,9 +5,7 @@ import ( "fmt" "log" "os" - "os/exec" "os/user" - "strings" "syscall" "github.com/gookit/color" @@ -23,19 +21,19 @@ import ( 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() } @@ -95,11 +93,9 @@ func finalize(config *configs.Config) { title := color.New(color.BgHiBlue, color.White, color.Bold) title.Println("Finalizing configuration") - isRoot := os.Getuid() == 0 + config.IsRoot = os.Getuid() == 0 - config.IsRoot = isRoot - - finalizeNotice(isRoot) + finalizeNotice(config.IsRoot) // 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.") { @@ -144,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) @@ -169,62 +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) - } - execAndLogSudo := func(cmd string) { - if !config.IsRoot && !strings.HasPrefix(cmd, "sudo") { - cmd = fmt.Sprintf("sudo %s", cmd) - } - // Write to logger - logger.Printf("Executing: %s\n", cmd) + // 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 - // Update initramfs - fmt.Printf("Executing: %s\nSee debug.log for detailed output\n", cmd) - cs := strings.Fields(cmd) - r := exec.Command(cs[0], cs[1:]...) - - cmd_out, _ := r.CombinedOutput() - - // Write to logger - logger.Printf(string(cmd_out) + "\n") + 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) + + 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 fileio.FileExist(initramfsFile): + 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") - execAndLogSudo("update-initramfs -u") + if err = command.ExecAndLogSudo(config.IsRoot, true, "update-initramfs -u"); err != nil { + log.Fatalf("Failed to update initramfs: %s", err) + } - case 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() - execAndLogSudo(fmt.Sprintf("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) + } - case 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) - - execAndLogSudo("mkinitcpio -P") + configs.CopyToSystem(config.IsRoot, config.Path.MKINITCPIO, "/etc/mkinitcpio.conf") + 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/pkg/command/command.go b/pkg/command/command.go index 52224cf..caa655d 100644 --- a/pkg/command/command.go +++ b/pkg/command/command.go @@ -3,12 +3,16 @@ 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/logger" ) // Run a command and return STDOUT @@ -27,7 +31,7 @@ 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 @@ -59,6 +63,16 @@ func RunErr(binary string, args ...string) ([]string, []string, error) { return outputs, outerrs, err } +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 @@ -74,7 +88,8 @@ func Elevate(password string) { errorcheck.ErrorCheck(err, "\nFailed to get sudo STDIN") // Start the authentication - cmd.Start() + err = cmd.Start() + errorcheck.ErrorCheck(err, "\nFailed to start sudo command") // Get the passed password pw, _ := base64.StdEncoding.DecodeString(password) @@ -84,16 +99,55 @@ func Elevate(password string) { 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") } -// 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, cmd string) error { + if isRoot && !strings.HasPrefix(cmd, "sudo") { + cmd = fmt.Sprintf("sudo %s", cmd) + } + + // Write to logger + logger.Printf("Executing (elevated): %s\n", cmd) + + if noisy { + // Print to the user + fmt.Printf("Executing (elevated): %s\nSee debug.log for detailed output\n", cmd) + } + + cs := strings.Fields(cmd) + r := exec.Command(cs[0], cs[1:]...) + + 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) + } + + return err } diff --git a/pkg/fileio/fileio.go b/pkg/fileio/fileio.go index 38c5a7a..bb7d33b 100644 --- a/pkg/fileio/fileio.go +++ b/pkg/fileio/fileio.go @@ -8,16 +8,20 @@ import ( "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)) defer f.Close() @@ -26,7 +30,7 @@ func AppendContent(content string, fileName string) { errorcheck.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)) @@ -46,10 +50,14 @@ 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) + if errors.Is(err, os.ErrPermission) { + errorcheck.ErrorCheck(err, common.PermissionNotice) + return "" // note: unreachable due to ErrorCheck calling fatal + } errorcheck.ErrorCheck(err, fmt.Sprintf("Failed to ReadFile on %s", fileName)) // Return all the lines as one string @@ -57,24 +65,25 @@ func ReadFile(fileName string) string { } -// 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)