Files
uncloud/internal/sshexec/sshcli.go

87 lines
1.9 KiB
Go

package sshexec
import (
"bytes"
"context"
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
type SSHCLIRemote struct {
user string
host string
port int
keyPath string
}
func NewSSHCLIRemote(user, host string, port int, keyPath string) *SSHCLIRemote {
return &SSHCLIRemote{
user: user,
host: host,
port: port,
keyPath: keyPath,
}
}
// newSSHCommand creates an exec.Cmd for ssh that sends SIGINT on context cancellation, giving the
// remote process a chance to exit gracefully before being killed.
func (r *SSHCLIRemote) newSSHCommand(ctx context.Context, cmd string) *exec.Cmd {
args := []string{
"-o", "ConnectTimeout=5",
// Disable pseudo-terminal allocation to prevent SSH from executing as a login shell.
"-T",
}
if r.port != 0 {
args = append(args, "-p", strconv.Itoa(r.port))
}
if r.keyPath != "" {
args = append(args, "-i", r.keyPath)
}
dst := r.host
if r.user != "" {
dst = fmt.Sprintf("%s@%s", r.user, dst)
}
args = append(args, dst, cmd)
execCmd := exec.CommandContext(ctx, "ssh", args...)
execCmd.Cancel = func() error {
return execCmd.Process.Signal(os.Interrupt)
}
execCmd.WaitDelay = 5 * time.Second
return execCmd
}
func (r *SSHCLIRemote) Run(ctx context.Context, cmd string) (string, error) {
var stdout, stderr bytes.Buffer
err := r.Stream(ctx, cmd, &stdout, &stderr)
out := strings.TrimSpace(stdout.String())
if err != nil {
return out, fmt.Errorf("%w: %s", err, stderr.String())
}
return out, nil
}
func (r *SSHCLIRemote) Stream(ctx context.Context, cmd string, stdout, stderr io.Writer) error {
sshCmd := r.newSSHCommand(ctx, cmd)
sshCmd.Stdout = stdout
sshCmd.Stderr = stderr
if err := sshCmd.Run(); err != nil {
return fmt.Errorf("run command on remote host: %w", err)
}
return nil
}
// Close is no-op as there is no persistent connection.
func (r *SSHCLIRemote) Close() error {
return nil
}