package main import ( "fmt" "os" "os/exec" "strings" ) // shellQuote wraps s in single quotes so it survives the remote shell that // OpenSSH hands its non-option arguments to (ssh joins them itself; it does // not exec the remote command with a distinct argv the way os/exec does). func shellQuote(s string) string { return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" } func shellJoin(args []string) string { parts := make([]string, len(args)) for i, a := range args { parts[i] = shellQuote(a) } return strings.Join(parts, " ") } // sshCommand builds an *exec.Cmd that runs remoteArgs on host via ssh. // batchMode disables interactive prompts (used for the push/pull direction // probe, so an unreachable/unauthenticated attempt fails fast instead of // hanging); it is left off for the manager's own control connections so a // password prompt still works when the user is present at a terminal. func sshCommand(sshBin string, extraOpts []string, batchMode bool, connectTimeoutSec int, user, host string, remoteArgs []string) *exec.Cmd { args := []string{"-o", "StrictHostKeyChecking=accept-new"} if batchMode { args = append(args, "-o", "BatchMode=yes") } if connectTimeoutSec > 0 { args = append(args, "-o", fmt.Sprintf("ConnectTimeout=%d", connectTimeoutSec)) } for _, o := range extraOpts { args = append(args, "-o", o) } userHost := host if user != "" { userHost = user + "@" + host } args = append(args, userHost, shellJoin(remoteArgs)) return exec.Command(sshBin, args...) } // localAgentCommand re-execs this same binary as an agent, for a spec with // no host part. func localAgentCommand(remoteArgs []string) *exec.Cmd { self, err := os.Executable() if err != nil { self = os.Args[0] } return exec.Command(self, remoteArgs...) }