Skip to content

Commit

Permalink
Merge pull request #4005 from adamkpickering/3953-refactor-rdctl-getR…
Browse files Browse the repository at this point in the history
…DPath

Refactor `rdctl` code that gets path to main Rancher Desktop executable
  • Loading branch information
adamkpickering authored Mar 2, 2023
2 parents 49890b0 + 4092dc1 commit 5e2e12d
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 121 deletions.
30 changes: 3 additions & 27 deletions src/go/rdctl/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
9 changes: 2 additions & 7 deletions src/go/rdctl/pkg/autostart/autostart_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions src/go/rdctl/pkg/autostart/autostart_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand Down
83 changes: 3 additions & 80 deletions src/go/rdctl/pkg/utils/utils.go
Original file line number Diff line number Diff line change
@@ -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)
}
46 changes: 46 additions & 0 deletions src/go/rdctl/pkg/utils/utils_darwin.go
Original file line number Diff line number Diff line change
@@ -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")
}
38 changes: 38 additions & 0 deletions src/go/rdctl/pkg/utils/utils_linux.go
Original file line number Diff line number Diff line change
@@ -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 <installDir>/resources/resources/linux/bin/rdctl.
// rancher-desktop should be 5 directories up from that, at <installDir>/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")
}
39 changes: 39 additions & 0 deletions src/go/rdctl/pkg/utils/utils_unix.go
Original file line number Diff line number Diff line change
@@ -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
}
68 changes: 68 additions & 0 deletions src/go/rdctl/pkg/utils/utils_windows.go
Original file line number Diff line number Diff line change
@@ -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 <installDir>/resources/resources/win32/bin/rdctl.exe.
// rancher-desktop should be 5 directories up from that, at <installDir>/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")
}

0 comments on commit 5e2e12d

Please sign in to comment.