mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
fix: separate stdout and stderr in SSH CLI executor to prevent output contamination (closes #270)
This commit is contained in:
+32
-23
@@ -1,12 +1,15 @@
|
|||||||
package sshexec
|
package sshexec
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SSHCLIRemote struct {
|
type SSHCLIRemote struct {
|
||||||
@@ -25,15 +28,18 @@ func NewSSHCLIRemote(user, host string, port int, keyPath string) *SSHCLIRemote
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Refactor and reuse this with buildDialArgs and buildSSHArgs from
|
// newSSHCommand creates an exec.Cmd for ssh that sends SIGINT on context cancellation, giving the
|
||||||
// SSHCLI Connector.
|
// remote process a chance to exit gracefully before being killed.
|
||||||
func (r *SSHCLIRemote) buildSSHArgs() []string {
|
func (r *SSHCLIRemote) newSSHCommand(ctx context.Context, cmd string) *exec.Cmd {
|
||||||
args := []string{"-o", "ConnectTimeout=5"}
|
args := []string{
|
||||||
|
"-o", "ConnectTimeout=5",
|
||||||
|
// Disable pseudo-terminal allocation to prevent SSH from executing as a login shell.
|
||||||
|
"-T",
|
||||||
|
}
|
||||||
|
|
||||||
if r.port != 0 {
|
if r.port != 0 {
|
||||||
args = append(args, "-p", strconv.Itoa(r.port))
|
args = append(args, "-p", strconv.Itoa(r.port))
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.keyPath != "" {
|
if r.keyPath != "" {
|
||||||
args = append(args, "-i", r.keyPath)
|
args = append(args, "-i", r.keyPath)
|
||||||
}
|
}
|
||||||
@@ -42,36 +48,39 @@ func (r *SSHCLIRemote) buildSSHArgs() []string {
|
|||||||
if r.user != "" {
|
if r.user != "" {
|
||||||
dst = fmt.Sprintf("%s@%s", r.user, dst)
|
dst = fmt.Sprintf("%s@%s", r.user, dst)
|
||||||
}
|
}
|
||||||
args = append(args, dst)
|
args = append(args, dst, cmd)
|
||||||
|
|
||||||
return args
|
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) {
|
func (r *SSHCLIRemote) Run(ctx context.Context, cmd string) (string, error) {
|
||||||
args := r.buildSSHArgs()
|
var stdout, stderr bytes.Buffer
|
||||||
args = append(args, cmd)
|
err := r.Stream(ctx, cmd, &stdout, &stderr)
|
||||||
|
out := strings.TrimSpace(stdout.String())
|
||||||
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
|
||||||
output, err := execCmd.CombinedOutput()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return strings.TrimSpace(string(output)),
|
return out, fmt.Errorf("%w: %s", err, stderr.String())
|
||||||
fmt.Errorf("run command on remote host: %w: %s", err, string(output))
|
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(output)), nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *SSHCLIRemote) Stream(ctx context.Context, cmd string, stdout, stderr io.Writer) error {
|
func (r *SSHCLIRemote) Stream(ctx context.Context, cmd string, stdout, stderr io.Writer) error {
|
||||||
args := r.buildSSHArgs()
|
sshCmd := r.newSSHCommand(ctx, cmd)
|
||||||
args = append(args, cmd)
|
sshCmd.Stdout = stdout
|
||||||
|
sshCmd.Stderr = stderr
|
||||||
|
|
||||||
execCmd := exec.CommandContext(ctx, "ssh", args...)
|
if err := sshCmd.Run(); err != nil {
|
||||||
execCmd.Stdout = stdout
|
return fmt.Errorf("run command on remote host: %w", err)
|
||||||
execCmd.Stderr = stderr
|
}
|
||||||
|
return nil
|
||||||
return execCmd.Run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// no-op as there is no persistent connection.
|
// Close is no-op as there is no persistent connection.
|
||||||
func (r *SSHCLIRemote) Close() error {
|
func (r *SSHCLIRemote) Close() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user