Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix (shell) : Improve CRC shell detection on unix environments (#3767) #4526

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 144 additions & 3 deletions pkg/os/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@ package shell

import (
"fmt"
"os"
"strconv"
"strings"

crcos "github.com/crc-org/crc/v2/pkg/os"
)

var (
CommandRunner = crcos.NewLocalCommandRunner()
WindowsSubsystemLinuxKernelMetadataFile = "/proc/version"
)

type Config struct {
Expand Down Expand Up @@ -65,9 +74,9 @@ func GetEnvString(userShell string, envName string, envValue string) string {
case "cmd":
return fmt.Sprintf("SET %s=%s", envName, envValue)
case "fish":
return fmt.Sprintf("contains %s $fish_user_paths; or set -U fish_user_paths %s $fish_user_paths", envValue, envValue)
return fmt.Sprintf("contains %s $fish_user_paths; or set -U fish_user_paths %s $fish_user_paths", convertToLinuxStylePath(userShell, envValue), convertToLinuxStylePath(userShell, envValue))
default:
return fmt.Sprintf("export %s=\"%s\"", envName, envValue)
return fmt.Sprintf("export %s=\"%s\"", envName, convertToLinuxStylePath(userShell, envValue))
}
}

Expand All @@ -81,8 +90,140 @@ func GetPathEnvString(userShell string, prependedPath string) string {
case "cmd":
pathStr = fmt.Sprintf("%s;%%PATH%%", prependedPath)
default:
pathStr = fmt.Sprintf("%s:$PATH", prependedPath)
pathStr = fmt.Sprintf("%s:$PATH", convertToLinuxStylePath(userShell, prependedPath))
}

return GetEnvString(userShell, "PATH", pathStr)
}

// convertToLinuxStylePath is a utility method to translate Windows paths to Linux environments (e.g. Git Bash).
//
// It receives two arguments:
// - userShell : currently active shell
// - path : Windows path to be converted
//
// It returns Linux equivalent of the Windows path.
//
// For example, a Windows path like `C:\Users\foo\.crc\bin\oc` is converted into `/C/Users/foo/.crc/bin/oc`.
func convertToLinuxStylePath(userShell string, path string) string {
if IsWindowsSubsystemLinux() {
return convertToWindowsSubsystemLinuxPath(path)
}
if strings.Contains(path, "\\") &&
(userShell == "bash" || userShell == "zsh" || userShell == "fish") {
path = strings.ReplaceAll(path, ":", "")
path = strings.ReplaceAll(path, "\\", "/")

return fmt.Sprintf("/%s", path)
}
return path
}

// convertToWindowsSubsystemLinuxPath is a utility method to translate between Windows and WSL(Windows Subsystem for
// Linux) paths. It relies on `wslpath` command to perform this conversion.
//
// It receives one argument:
// - path : Windows path to be converted to WSL path
//
// It returns translated WSL equivalent of provided windows path.
func convertToWindowsSubsystemLinuxPath(path string) string {
stdOut, _, err := CommandRunner.Run("wsl", "-e", "bash", "-c", fmt.Sprintf("wslpath -a '%s'", path))
if err != nil {
return path
}
return strings.TrimSpace(stdOut)
}

// IsWindowsSubsystemLinux detects whether current system is using Windows Subsystem for Linux or not
//
// It checks for these conditions to make sure that current system has WSL installed:
// - `/proc/version` file is present
// - `/proc/version` file contents contain keywords `Microsoft` and `WSL`
//
// It above conditions are met, then this method returns `true` otherwise `false`.
func IsWindowsSubsystemLinux() bool {
procVersionContent, err := os.ReadFile(WindowsSubsystemLinuxKernelMetadataFile)
if err != nil {
return false
}
if strings.Contains(string(procVersionContent), "Microsoft") ||
strings.Contains(string(procVersionContent), "WSL") {
return true
}
return false
}

// detectShellByInvokingCommand is a utility method that tries to detect current shell in use by invoking `ps` command.
// This method is extracted so that it could be used by unix systems as well as Windows (in case of WSL). It executes
// the command provided in the method arguments and then passes the output to inspectProcessOutputForRecentlyUsedShell
// for evaluation.
//
// It receives two arguments:
// - defaultShell : default shell to revert back to in case it's unable to detect.
// - command: command to be executed
// - args: a string array containing command arguments
//
// It returns a string value representing current shell.
func detectShellByInvokingCommand(defaultShell string, command string, args []string) string {
stdOut, _, err := CommandRunner.Run(command, args...)
if err != nil {
return defaultShell
}

detectedShell := inspectProcessOutputForRecentlyUsedShell(stdOut)
if detectedShell == "" {
return defaultShell
}
return detectedShell
}

// inspectProcessOutputForRecentlyUsedShell inspects output of ps command to detect currently active shell session.
//
// Note : This method assumes that ps command has already sorted the processes by `pid` in reverse order.
// It parses the output into a struct, filters process types by name and returns the first element.
//
// It takes one argument:
//
// - psCommandOutput: output of ps command executed on a particular shell session
//
// It returns:
//
// - a string value (one of `zsh`, `bash` or `fish`) for current shell environment in use. If it's not able to determine
// underlying shell type, it returns and empty string.
//
// This method tries to check all processes open and filters out shell sessions (one of `zsh`, `bash` or `fish)
// It then returns first shell process.
//
// For example, if ps command gives this output:
//
// 2908 ps
// 2889 fish
// 823 bash
//
// Then this method would return `fish` as it's the first shell process.
func inspectProcessOutputForRecentlyUsedShell(psCommandOutput string) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is doing lot of things, may be add a comment what actually this is doing with example so would be easy for review.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I'll update it as per your comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added comments to this method and other related methods.

type ProcessOutput struct {
processID int
output string
}
var processOutputs []ProcessOutput
lines := strings.Split(psCommandOutput, "\n")
for _, line := range lines {
lineParts := strings.Split(strings.TrimSpace(line), " ")
if len(lineParts) == 2 && (strings.Contains(lineParts[1], "zsh") ||
strings.Contains(lineParts[1], "bash") ||
strings.Contains(lineParts[1], "fish")) {
parsedProcessID, err := strconv.Atoi(lineParts[0])
if err == nil {
processOutputs = append(processOutputs, ProcessOutput{
processID: parsedProcessID,
output: lineParts[1],
})
}
}
}
if len(processOutputs) > 0 {
return processOutputs[0].output
}
return ""
}
Loading
Loading