// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package wsl import ( "bytes" "context" "errors" "fmt" "html/template" "io" "log" "os" "path/filepath" "strings" "time" ) func DetectShell(ctx context.Context, client *Distro) (string, error) { wshPath := GetWshPath(ctx, client) cmd := client.WslCommand(ctx, wshPath+" shell") log.Printf("shell detecting using command: %s shell", wshPath) out, err := cmd.Output() if err != nil { log.Printf("unable to determine shell. defaulting to /bin/bash: %s", err) return "/bin/bash", nil } log.Printf("detecting shell: %s", out) // quoting breaks this particular case return strings.TrimSpace(string(out)), nil } func GetWshVersion(ctx context.Context, client *Distro) (string, error) { wshPath := GetWshPath(ctx, client) cmd := client.WslCommand(ctx, wshPath+" version") out, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } func GetWshPath(ctx context.Context, client *Distro) string { defaultPath := "~/.waveterm/bin/wsh" cmd := client.WslCommand(ctx, "which wsh") out, whichErr := cmd.Output() if whichErr == nil { return strings.TrimSpace(string(out)) } cmd = client.WslCommand(ctx, "where.exe wsh") out, whereErr := cmd.Output() if whereErr == nil { return strings.TrimSpace(string(out)) } // check cmd on windows since it requires an absolute path with backslashes cmd = client.WslCommand(ctx, "(dir 2>&1 *``|echo %userprofile%\\.waveterm%\\.waveterm\\bin\\wsh.exe);&<# rem #>echo none") out, cmdErr := cmd.Output() if cmdErr == nil && strings.TrimSpace(string(out)) != "none" { return strings.TrimSpace(string(out)) } // no custom install, use default path return defaultPath } func hasBashInstalled(ctx context.Context, client *Distro) (bool, error) { cmd := client.WslCommand(ctx, "which bash") out, whichErr := cmd.Output() if whichErr == nil && len(out) != 0 { return true, nil } cmd = client.WslCommand(ctx, "where.exe bash") out, whereErr := cmd.Output() if whereErr == nil && len(out) != 0 { return true, nil } // note: we could also check in /bin/bash explicitly // just in case that wasn't added to the path. but if // that's true, we will most likely have worse // problems going forward return false, nil } func GetClientOs(ctx context.Context, client *Distro) (string, error) { cmd := client.WslCommand(ctx, "uname -s") out, unixErr := cmd.Output() if unixErr == nil { formatted := strings.ToLower(string(out)) formatted = strings.TrimSpace(formatted) return formatted, nil } cmd = client.WslCommand(ctx, "echo %OS%") out, cmdErr := cmd.Output() if cmdErr == nil && strings.TrimSpace(string(out)) != "%OS%" { formatted := strings.ToLower(string(out)) formatted = strings.TrimSpace(formatted) return strings.Split(formatted, "_")[0], nil } cmd = client.WslCommand(ctx, "echo $env:OS") out, psErr := cmd.Output() if psErr == nil && strings.TrimSpace(string(out)) != "$env:OS" { formatted := strings.ToLower(string(out)) formatted = strings.TrimSpace(formatted) return strings.Split(formatted, "_")[0], nil } return "", fmt.Errorf("unable to determine os: {unix: %s, cmd: %s, powershell: %s}", unixErr, cmdErr, psErr) } func GetClientArch(ctx context.Context, client *Distro) (string, error) { cmd := client.WslCommand(ctx, "uname -m") out, unixErr := cmd.Output() if unixErr == nil { formatted := strings.ToLower(string(out)) formatted = strings.TrimSpace(formatted) if formatted == "x86_64" { return "x64", nil } return formatted, nil } cmd = client.WslCommand(ctx, "echo %PROCESSOR_ARCHITECTURE%") out, cmdErr := cmd.Output() if cmdErr == nil && strings.TrimSpace(string(out)) != "%PROCESSOR_ARCHITECTURE%" { formatted := strings.ToLower(string(out)) return strings.TrimSpace(formatted), nil } cmd = client.WslCommand(ctx, "echo $env:PROCESSOR_ARCHITECTURE") out, psErr := cmd.Output() if psErr == nil && strings.TrimSpace(string(out)) != "$env:PROCESSOR_ARCHITECTURE" { formatted := strings.ToLower(string(out)) return strings.TrimSpace(formatted), nil } return "", fmt.Errorf("unable to determine architecture: {unix: %s, cmd: %s, powershell: %s}", unixErr, cmdErr, psErr) } type CancellableCmd struct { Cmd *WslCmd Cancel func() } var installTemplatesRawBash = map[string]string{ "mkdir": `bash -c 'mkdir -p {{.installDir}}'`, "cat": `bash -c 'cat > {{.tempPath}}'`, "mv": `bash -c 'mv {{.tempPath}} {{.installPath}}'`, "chmod": `bash -c 'chmod a+x {{.installPath}}'`, } var installTemplatesRawDefault = map[string]string{ "mkdir": `mkdir -p {{.installDir}}`, "cat": `cat > {{.tempPath}}`, "mv": `mv {{.tempPath}} {{.installPath}}`, "chmod": `chmod a+x {{.installPath}}`, } func makeCancellableCommand(ctx context.Context, client *Distro, cmdTemplateRaw string, words map[string]string) (*CancellableCmd, error) { cmdContext, cmdCancel := context.WithCancel(ctx) cmdStr := &bytes.Buffer{} cmdTemplate, err := template.New("").Parse(cmdTemplateRaw) if err != nil { cmdCancel() return nil, err } cmdTemplate.Execute(cmdStr, words) cmd := client.WslCommand(cmdContext, cmdStr.String()) return &CancellableCmd{cmd, cmdCancel}, nil } func CpHostToRemote(ctx context.Context, client *Distro, sourcePath string, destPath string) error { // warning: does not work on windows remote yet bashInstalled, err := hasBashInstalled(ctx, client) if err != nil { return err } var selectedTemplatesRaw map[string]string if bashInstalled { selectedTemplatesRaw = installTemplatesRawBash } else { log.Printf("bash is not installed on remote. attempting with default shell") selectedTemplatesRaw = installTemplatesRawDefault } // I need to use toSlash here to force unix keybindings // this means we can't guarantee it will work on a remote windows machine var installWords = map[string]string{ "installDir": filepath.ToSlash(filepath.Dir(destPath)), "tempPath": destPath + ".temp", "installPath": destPath, } installStepCmds := make(map[string]*CancellableCmd) for cmdName, selectedTemplateRaw := range selectedTemplatesRaw { cancellableCmd, err := makeCancellableCommand(ctx, client, selectedTemplateRaw, installWords) if err != nil { return err } installStepCmds[cmdName] = cancellableCmd } _, err = installStepCmds["mkdir"].Cmd.Output() if err != nil { return err } // the cat part of this is complicated since it requires stdin catCmd := installStepCmds["cat"].Cmd catStdin, err := catCmd.StdinPipe() if err != nil { return err } err = catCmd.Start() if err != nil { return err } input, err := os.Open(sourcePath) if err != nil { return fmt.Errorf("cannot open local file %s to send to host: %v", sourcePath, err) } go func() { io.Copy(catStdin, input) installStepCmds["cat"].Cancel() // backup just in case something weird happens // could cause potential race condition, but very // unlikely time.Sleep(time.Second * 1) process := catCmd.GetProcess() if process != nil { process.Kill() } }() catErr := catCmd.Wait() if catErr != nil && !errors.Is(catErr, context.Canceled) { return catErr } _, err = installStepCmds["mv"].Cmd.Output() if err != nil { return err } _, err = installStepCmds["chmod"].Cmd.Output() if err != nil { return err } return nil } func InstallClientRcFiles(ctx context.Context, client *Distro) error { path := GetWshPath(ctx, client) log.Printf("path to wsh searched is: %s", path) cmd := client.WslCommand(ctx, path+" rcfiles") _, err := cmd.Output() return err } func GetHomeDir(ctx context.Context, client *Distro) string { // note: also works for powershell cmd := client.WslCommand(ctx, `echo "$HOME"`) out, err := cmd.Output() if err == nil { return strings.TrimSpace(string(out)) } cmd = client.WslCommand(ctx, `echo %userprofile%`) out, err = cmd.Output() if err == nil { return strings.TrimSpace(string(out)) } return "~" } func IsPowershell(shellPath string) bool { // get the base path, and then check contains shellBase := filepath.Base(shellPath) return strings.Contains(shellBase, "powershell") || strings.Contains(shellBase, "pwsh") }