diff --git a/src/go/rdctl/cmd/start.go b/src/go/rdctl/cmd/start.go index e09d04c7291..5b2bfff8a91 100644 --- a/src/go/rdctl/cmd/start.go +++ b/src/go/rdctl/cmd/start.go @@ -20,7 +20,6 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "runtime" "strings" @@ -83,23 +82,10 @@ func doStartCommand(cmd *cobra.Command) error { if err != nil { return err } - if applicationPath == "" { - pathLookupFuncs := map[string]func(rdctlPath string) string{ - "windows": utils.GetWindowsRDPath, - "linux": getLinuxRDPath, - "darwin": utils.GetDarwinRDPath, - } - getPathFunc, ok := pathLookupFuncs[runtime.GOOS] - if !ok { - return fmt.Errorf("don't know how to find the path to Rancher Desktop on OS %s", runtime.GOOS) - } - rdctlPath, err := os.Executable() + if !cmd.Flags().Changed("path") { + applicationPath, err = utils.GetRDPath() if err != nil { - rdctlPath = "" - } - applicationPath = getPathFunc(rdctlPath) - if applicationPath == "" { - return fmt.Errorf("could not locate main Rancher Desktop executable; please retry with the --path option") + return fmt.Errorf("failed to locate main Rancher Desktop executable: %w\nplease retry with the --path option", err) } } return launchApp(applicationPath, commandLineArgs) @@ -128,13 +114,3 @@ func launchApp(applicationPath string, commandLineArgs []string) error { cmd.Stderr = os.Stderr return cmd.Start() } -func getLinuxRDPath(rdctlPath string) string { - if rdctlPath != "" { - normalParentPath := utils.MoveToParent(rdctlPath, 5) - candidatePath := utils.CheckExistence(filepath.Join(normalParentPath, "rancher-desktop"), 0o111) - if candidatePath != "" { - return candidatePath - } - } - return utils.CheckExistence("/opt/rancher-desktop/rancher-desktop", 0o111) -} diff --git a/src/go/rdctl/pkg/autostart/autostart_darwin.go b/src/go/rdctl/pkg/autostart/autostart_darwin.go index 33fbb020c8f..e686382a5f8 100644 --- a/src/go/rdctl/pkg/autostart/autostart_darwin.go +++ b/src/go/rdctl/pkg/autostart/autostart_darwin.go @@ -84,14 +84,9 @@ func EnsureAutostart(autostartDesired bool) error { } func getDesiredLaunchAgentFileContents() ([]byte, error) { - // get path to main Rancher Desktop executable - rdctlPath, err := os.Executable() + rancherDesktopPath, err := utils.GetRDPath() if err != nil { - return []byte{}, fmt.Errorf("failed to get path to rdctl: %w", err) - } - rancherDesktopPath := utils.GetDarwinRDPath(rdctlPath) - if rancherDesktopPath == "" { - return []byte{}, errors.New("failed to get path to main Rancher Desktop executable") + return []byte{}, fmt.Errorf("failed to get path to main Rancher Desktop executable: %w", err) } // get desired contents of LaunchAgent file diff --git a/src/go/rdctl/pkg/autostart/autostart_windows.go b/src/go/rdctl/pkg/autostart/autostart_windows.go index 6c1e2a4afa6..259b0f91aea 100644 --- a/src/go/rdctl/pkg/autostart/autostart_windows.go +++ b/src/go/rdctl/pkg/autostart/autostart_windows.go @@ -3,7 +3,6 @@ package autostart import ( "errors" "fmt" - "os" "github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/utils" "golang.org/x/sys/windows/registry" @@ -26,13 +25,9 @@ func EnsureAutostart(autostartDesired bool) error { defer autostartKey.Close() if autostartDesired { - rdctlPath, err := os.Executable() + rancherDesktopPath, err := utils.GetRDPath() if err != nil { - return fmt.Errorf("failed to get path to rdctl: %w", err) - } - rancherDesktopPath := utils.GetWindowsRDPath(rdctlPath) - if rancherDesktopPath == "" { - return errors.New("failed to get path to Rancher Desktop.exe") + return fmt.Errorf("failed to get path to Rancher Desktop.exe: %w", err) } err = autostartKey.SetStringValue(nameValue, rancherDesktopPath) if err != nil { diff --git a/src/go/rdctl/pkg/utils/utils.go b/src/go/rdctl/pkg/utils/utils.go index 0e9cb01b263..40a9979c9f6 100644 --- a/src/go/rdctl/pkg/utils/utils.go +++ b/src/go/rdctl/pkg/utils/utils.go @@ -1,91 +1,14 @@ package utils import ( - "io/fs" - "os" "path/filepath" - - "github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories" ) -// Get the parent (or grandparent, or great-grandparent...) directory of fullPath. -// numberTimes is the number of steps to ascend in the directory hierarchy. -func MoveToParent(fullPath string, numberTimes int) string { +// Get the steps-th parent directory of fullPath. +func getParentDir(fullPath string, steps int) string { fullPath = filepath.Clean(fullPath) - for ; numberTimes > 0; numberTimes-- { + for ; steps > 0; steps-- { fullPath = filepath.Dir(fullPath) } return fullPath } - -/** - * Verify the path exists. For Linux pass in mode bits to guarantee the file is executable (for at least one - * category of user). Note that on macOS the candidate is a directory, so never pass in mode bits. - * And mode bits don't make sense on Windows. - */ -func CheckExistence(candidatePath string, modeBits fs.FileMode) string { - stat, err := os.Stat(candidatePath) - if err != nil { - return "" - } - if modeBits != 0 && (!stat.Mode().IsRegular() || stat.Mode().Perm()&modeBits == 0) { - // The modeBits check is only for executability -- we only care if at least one of the three - // `x` mode bits is on. So this check isn't used for a general permission-mode-bit check. - return "" - } - return candidatePath -} - -// Returns the absolute path to the Rancher Desktop executable. -// Returns an empty string if the executable was not found. -func GetWindowsRDPath(rdctlPath string) string { - if rdctlPath != "" { - normalParentPath := MoveToParent(rdctlPath, 5) - candidatePath := CheckExistence(filepath.Join(normalParentPath, "Rancher Desktop.exe"), 0) - if candidatePath != "" { - return candidatePath - } - } - homedir, err := os.UserHomeDir() - if err != nil { - homedir = "" - } - dataPaths := []string{} - // %LOCALAPPDATA% - dir, err := directories.GetLocalAppDataDirectory() - if err == nil { - dataPaths = append(dataPaths, dir) - } - // %APPDATA% - dir, err = directories.GetRoamingAppDataDirectory() - if err == nil { - dataPaths = append(dataPaths, dir) - } - // Add these two paths if the above two fail to find where the program was installed - dataPaths = append( - dataPaths, - filepath.Join(homedir, "AppData", "Local"), - filepath.Join(homedir, "AppData", "Roaming"), - ) - for _, dataDir := range dataPaths { - candidatePath := CheckExistence(filepath.Join(dataDir, "Programs", "Rancher Desktop", "Rancher Desktop.exe"), 0) - if candidatePath != "" { - return candidatePath - } - } - return "" -} - -func GetDarwinRDPath(rdctlPath string) string { - if rdctlPath != "" { - // we're at .../Applications/R D.app (could have a different name)/Contents/Resources/resources/darwin/bin - // and want to move to the "R D.app" part - RDAppParentPath := MoveToParent(rdctlPath, 6) - if CheckExistence(filepath.Join(RDAppParentPath, "Contents", "MacOS", "Rancher Desktop"), 0o111) != "" { - return RDAppParentPath - } - } - // This fallback is mostly for running `npm run dev` and using the installed app because there is no app - // that rdctl would launch directly in dev mode. - return CheckExistence(filepath.Join("/Applications", "Rancher Desktop.app"), 0) -} diff --git a/src/go/rdctl/pkg/utils/utils_darwin.go b/src/go/rdctl/pkg/utils/utils_darwin.go new file mode 100644 index 00000000000..e1784e38b0a --- /dev/null +++ b/src/go/rdctl/pkg/utils/utils_darwin.go @@ -0,0 +1,46 @@ +package utils + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +// Returns the absolute path to the Rancher Desktop executable. +// Returns an empty string if the executable was not found. +func GetRDPath() (string, error) { + rdctlSymlinkPath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("failed to get path to rdctl: %w", err) + } + rdctlPath, err := filepath.EvalSymlinks(rdctlSymlinkPath) + if err != nil { + return "", fmt.Errorf("failed to resolve %q: %w", rdctlSymlinkPath, err) + } + + // we're at .../Applications/R D.app (could have a different name)/Contents/Resources/resources/darwin/bin/rdctl + // and want to move to the "R D.app" part + RDAppParentPath := getParentDir(rdctlPath, 6) + executablePath := filepath.Join(RDAppParentPath, "Contents", "MacOS", "Rancher Desktop") + usable, err := checkUsableApplication(executablePath, true) + if err != nil { + return "", fmt.Errorf("failed to check usability of %q: %w", executablePath, err) + } + if usable { + return RDAppParentPath, nil + } + + // This fallback is mostly for running `npm run dev` and using the installed app because there is no app + // that rdctl would launch directly in dev mode. + candidatePath := filepath.Join("/Applications", "Rancher Desktop.app") + usable, err = checkUsableApplication(candidatePath, false) + if err != nil { + return "", fmt.Errorf("failed to check usability of %q: %w", candidatePath, err) + } + if usable { + return RDAppParentPath, nil + } + + return "", errors.New("search locations exhausted") +} diff --git a/src/go/rdctl/pkg/utils/utils_linux.go b/src/go/rdctl/pkg/utils/utils_linux.go new file mode 100644 index 00000000000..d2b3b215ee5 --- /dev/null +++ b/src/go/rdctl/pkg/utils/utils_linux.go @@ -0,0 +1,38 @@ +package utils + +import ( + "errors" + "fmt" + "os" + "path/filepath" +) + +// Returns the absolute path to the Rancher Desktop executable, +// or an error if it was unable to find Rancher Desktop. +func GetRDPath() (string, error) { + rdctlSymlinkPath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("failed to get path to rdctl: %w", err) + } + rdctlPath, err := filepath.EvalSymlinks(rdctlSymlinkPath) + if err != nil { + return "", fmt.Errorf("failed to resolve %q: %w", rdctlSymlinkPath, err) + } + // rdctl should be at /resources/resources/linux/bin/rdctl. + // rancher-desktop should be 5 directories up from that, at /rancher-desktop. + normalParentPath := getParentDir(rdctlPath, 5) + candidatePaths := []string{ + filepath.Join(normalParentPath, "rancher-desktop"), + "/opt/rancher-desktop/rancher-desktop", + } + for _, candidatePath := range candidatePaths { + usable, err := checkUsableApplication(candidatePath, true) + if err != nil { + return "", fmt.Errorf("failed to check usability of %q: %w", candidatePath, err) + } + if usable { + return candidatePath, nil + } + } + return "", errors.New("search locations exhausted") +} diff --git a/src/go/rdctl/pkg/utils/utils_unix.go b/src/go/rdctl/pkg/utils/utils_unix.go new file mode 100644 index 00000000000..47627d83eea --- /dev/null +++ b/src/go/rdctl/pkg/utils/utils_unix.go @@ -0,0 +1,39 @@ +//go:build linux || darwin + +package utils + +import ( + "errors" + "fmt" + "golang.org/x/sys/unix" + "io/fs" + "os" +) + +// Verify that the candidatePath is usable as a Rancher Desktop "executable". This means: +// - check that candidatePath exists +// - if checkExecutability is true, check that candidatePath is a regular file, +// and that it is executable +// +// Note that candidatePath may not always be a file; in macOS, it may be a +// .app directory. +func checkUsableApplication(candidatePath string, checkExecutability bool) (bool, error) { + statResult, err := os.Stat(candidatePath) + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + if err != nil { + return false, fmt.Errorf("failed to get info on %q: %w", candidatePath, err) + } + + if !checkExecutability { + return true, nil + } + + if !statResult.Mode().IsRegular() { + return false, nil + } + + err = unix.Access(candidatePath, unix.X_OK) + return err == nil, nil +} diff --git a/src/go/rdctl/pkg/utils/utils_windows.go b/src/go/rdctl/pkg/utils/utils_windows.go new file mode 100644 index 00000000000..b5dc2defd39 --- /dev/null +++ b/src/go/rdctl/pkg/utils/utils_windows.go @@ -0,0 +1,68 @@ +package utils + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/rancher-sandbox/rancher-desktop/src/go/rdctl/pkg/directories" +) + +// Returns the absolute path to the Rancher Desktop executable. +// Returns an empty string if the executable was not found. +func GetRDPath() (string, error) { + rdctlSymlinkPath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("failed to get path to rdctl: %w", err) + } + rdctlPath, err := filepath.EvalSymlinks(rdctlSymlinkPath) + if err != nil { + return "", fmt.Errorf("failed to resolve %q: %w", rdctlSymlinkPath, err) + } + // rdctl should be at /resources/resources/win32/bin/rdctl.exe. + // rancher-desktop should be 5 directories up from that, at /Rancher Desktop.exe. + normalParentPath := getParentDir(rdctlPath, 5) + candidatePath := filepath.Join(normalParentPath, "Rancher Desktop.exe") + _, err = os.Stat(candidatePath) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return "", fmt.Errorf("failed to check existence of %q: %w", candidatePath, err) + } + if err == nil { + return candidatePath, nil + } + + homedir, err := os.UserHomeDir() + if err != nil { + homedir = "" + } + dataPaths := []string{} + // %LOCALAPPDATA% + dir, err := directories.GetLocalAppDataDirectory() + if err == nil { + dataPaths = append(dataPaths, dir) + } + // %APPDATA% + dir, err = directories.GetRoamingAppDataDirectory() + if err == nil { + dataPaths = append(dataPaths, dir) + } + dataPaths = append( + dataPaths, + filepath.Join(homedir, "AppData", "Local"), + filepath.Join(homedir, "AppData", "Roaming"), + ) + for _, dataDir := range dataPaths { + candidatePath := filepath.Join(dataDir, "Programs", "Rancher Desktop", "Rancher Desktop.exe") + _, err := os.Stat(candidatePath) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return "", fmt.Errorf("failed to check existence of %q: %w", candidatePath, err) + } + if err == nil { + return candidatePath, nil + } + } + + return "", errors.New("search locations exhausted") +}