8000 ssh: explicitly set ~ for home directory unless using cmd.exe by spikecurtis · Pull Request #1 · coder/mutagen · GitHub
[go: up one dir, main page]

Skip to content

ssh: explicitly set ~ for home directory unless using cmd.exe #1

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

Merged
merged 6 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
ssh: explicitly set ~ for home directory unless using cmd.exe
  • Loading branch information
spikecurtis committed Mar 7, 2025
commit c8b9b65c1f5d04bc28def22ff0fc8c86a47abba2
16 changes: 13 additions & 3 deletions pkg/agent/dial.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,30 @@ func connect(logger *logging.Logger, transport Transport, mode, prompter string,
// environment or cmd.exe), we can leave the "exe" suffix off the target
// name. Fortunately this allows us to also avoid having to try the
// combination of forward slashes + ".exe" for Windows POSIX environments.
//
// HACK: When invoking on cmd.exe, we leave off the ~ prefix, since cmd.exe
// doesn't recognize it. In most cases the initial working directory for SSH
// commands is the home directory, but when possible we try to be explicit,
// to work around systems that use a different directory, such as Coder
// workspaces, which allow different initial working directories to be
// configured.
pathSeparator := "/"
pathComponents := []string{filesystem.HomeDirectorySpecial}
if cmdExe {
pathSeparator = "\\"
pathComponents = nil
}
dataDirectoryName := filesystem.MutagenDataDirectoryName
if mutagen.DevelopmentModeEnabled {
dataDirectoryName = filesystem.MutagenDataDirectoryDevelopmentName
}
agentInvocationPath := strings.Join([]string{
pathComponents = append(pathComponents,
dataDirectoryName,
filesystem.MutagenAgentsDirectoryName,
mutagen.Version,
BaseName,
}, pathSeparator)
)
agentInvocationPath := strings.Join(pathComponents, pathSeparator)

// Compute the command to invoke.
command := fmt.Sprintf("%s %s --%s=%s", agentInvocationPath, mode, FlagLogLevel, logger.Level())
Expand Down Expand Up @@ -204,7 +214,7 @@ func Dial(logger *logging.Logger, transport Transport, mode, prompter string) (i
}

// Attempt to install.
if err := install(logger, transport, prompter); err != nil {
if err := install(logger, transport, prompter, cmdExe); err != nil {
return nil, fmt.Errorf("unable to install agent: %w", err)
}

Expand Down
31 changes: 22 additions & 9 deletions pkg/agent/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"runtime"
"strings"

"github.com/google/uuid"

Expand Down Expand Up @@ -38,7 +39,7 @@ func Install() error {

// install attempts to probe an endpoint and install the appropriate agent
// binary over the specified transport.
func install(logger *logging.Logger, transport Transport, prompter string) error {
func install(logger *logging.Logger, transport Transport, prompter string, cmdExe bool) error {
// Detect the target platform.
goos, goarch, posix, err := probe(transport, prompter)
if err != nil {
Expand Down Expand Up @@ -68,14 +69,25 @@ func install(logger *logging.Logger, transport Transport, prompter string) error
if err != nil {
return fmt.Errorf("unable to generate UUID for agent copying: %w", err)
}
destination := BaseName + randomUUID.String()
remoteFileName := BaseName + randomUUID.String()
if goos == "windows" {
destination += ".exe"
remoteFileName += ".exe"
}
if posix {
destination = "." + destination
remoteFileName = "." + remoteFileName
}
if err = transport.Copy(agentExecutable, destination); err != nil {
// HACK: On cmd.exe systems, the ~ special character is not understood to mean the home directory, so we leave it
// off, and hope that the default copy directory is the home directory.
pathSeparator := "/"
pathComponents := []string{filesystem.HomeDirectorySpecial}
if cmdExe {
pathSeparator = "\\"
pathComponents = nil
}
pathComponents = append(pathComponents, remoteFileName)
fullRemotePath := strings.Join(pathComponents, pathSeparator)

if err = transport.Copy(agentExecutable, fullRemotePath); err != nil {
return fmt.Errorf("unable to copy agent binary: %w", err)
}

Expand All @@ -89,7 +101,7 @@ func install(logger *logging.Logger, transport Transport, prompter string) error
if err := prompting.Message(prompter, "Setting agent executability..."); err != nil {
return fmt.Errorf("unable to message prompter: %w", err)
}
executabilityCommand := fmt.Sprintf("chmod +x %s", destination)
executabilityCommand := fmt.Sprintf("chmod +x %s", fullRemotePath)
if err := run(transport, executabilityCommand); err != nil {
return fmt.Errorf("unable to set agent executability: %w", err)
}
Expand All @@ -100,10 +112,11 @@ func install(logger *logging.Logger, transport Transport, prompter string) error
return fmt.Errorf("unable to message prompter: %w", err)
}
var installCommand string
if posix {
installCommand = fmt.Sprintf("./%s %s", destination, CommandInstall)
if posix && cmdExe {
// TODO: is this path even possible?
installCommand = fmt.Sprintf("./%s %s", fullRemotePath, CommandInstall)
} else {
installCommand = fmt.Sprintf("%s %s", destination, CommandInstall)
installCommand = fmt.Sprintf("%s %s", fullRemotePath, CommandInstall)
}
if err := run(transport, installCommand); err != nil {
return fmt.Errorf("unable to invoke agent installation: %w", err)
Expand Down
11 changes: 11 additions & 0 deletions pkg/agent/transport/ssh/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"path/filepath"
"strconv"
"strings"

"github.com/mutagen-io/mutagen/pkg/agent"
"github.com/mutagen-io/mutagen/pkg/agent/transport"
Expand Down Expand Up @@ -192,6 +193,12 @@ func (t *sshTransport) Command(command string) (*exec.Cmd, error) {

// ClassifyError implements the ClassifyError method of agent.Transport.
func (t *sshTransport) ClassifyError(processState *os.ProcessState, errorOutput string) (bool, bool, error) {
// Windows Powershell introduces line breaks in the errorOutput, which get
// escaped before arriving at this function into a literal `\r` followed by
// a newline. Strip these out so that line breaks don't screw with our
// matching.
errorOutput = strings.ReplaceAll(errorOutput, "\\r\n", "")

// SSH faithfully returns exit codes and error output, so we can use direct
// methods for testing and classification. Note that we may get POSIX-like
// error codes back even from Windows remotes, but that indicates a POSIX
Expand Down Expand Up @@ -228,6 +235,10 @@ func (t *sshTransport) ClassifyError(processState *os.ProcessState, errorOutput
return false, true, nil
} else if process.OutputIsWindowsCommandNotFound(errorOutput) {
return true, true, nil
} else if process.OutputIsWindowsPowershellCommandNotFound(errorOutput) {
// It's Windows Powershell, not cmd.exe, so try (re)installing, but set cmdExe
// to false.
return true, false, nil
}

// Just bail if we weren't able to determine the nature of the error.
Expand Down
2 changes: 2 additions & 0 deletions pkg/filesystem/mutagen.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ const (
// MutagenLicensingDirectoryName is the name of the licensing data directory
// within the Mutagen data directory.
MutagenLicensingDirectoryName = "licensing"

HomeDirectorySpecial = "~"
)

// Mutagen computes (and optionally creates) subdirectories inside the Mutagen
Expand Down
11 changes: 11 additions & 0 deletions pkg/process/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ const (
// windowsCommandNotFoundFragment is a fragment of the error output returned
// on Windows systems when a command cannot be found.
windowsCommandNotFoundFragment = "The system cannot find the path specified"
// windowsPowershellCommandNotFoundFragment is a fragment of the error output
// returned on Windows systems running Powershell when a command cannot be
// found.
windowsPowershellCommandNotFoundFragment = "is not recognized as the name of a cmdlet, function, script file, or operable program."
Copy link
Member

Choose a reason for hiding this comment

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

This seems like it would break if you have a different OS language, but I guess mutagen was already doing similar error string checks previously and I can't really think of a better way to accomplish this. So it's probably fine for now

)

// OutputIsPOSIXCommandNotFound returns whether or not a process' error output
Expand All @@ -38,6 +42,13 @@ func OutputIsWindowsInvalidCommand(output string) bool {
// represents a command not found error on Windows.
func OutputIsWindowsCommandNotFound(output string) bool {
return strings.Contains(output, windowsCommandNotFoundFragment)

}

// OutputIsWindowsPowershellCommandNotFound returns whether or not a process' error
// output represents a command not found error from Windows running Powershell.
func OutputIsWindowsPowershellCommandNotFound(output string) bool {
return strings.Contains(output, windowsPowershellCommandNotFoundFragment)
}

// ExtractExitErrorMessage is a utility function that will attempt to extract
Expand Down
2 changes: 1 addition & 1 deletion pkg/synchronization/controller.go
4D7B
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func newSession(
)
if err != nil {
logger.Info("Beta connection failure:", err)
return nil, fmt.Errorf("unable to connect to beta: %w", err)
return nil, fmt.Errorf("unable to connect to beta_: %w", err)
}
}

Expand Down
0