Skip to content

Commit 41c5a2b

Browse files
authored
openssh: improve command errors and debug logging (#377)
Consistently return a more informative error message when an SSH command fails. The errors now contain the pid, the quoted command, and exit code. When debug logging is enabled, also write the full command when the process starts and when it exits. If the command fails, indent and log its stdout and stderr buffers. If it succeeds, only log stderr to avoid logging private VM keys. Instead, the caller will redact, format, and log a successful JSON response.
1 parent 46eafa8 commit 41c5a2b

File tree

2 files changed

+74
-26
lines changed

2 files changed

+74
-26
lines changed

internal/cloud/cloud.go

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ package cloud
55

66
import (
77
"encoding/json"
8-
"errors"
98
"fmt"
109
"log"
1110
"os"
12-
"os/exec"
1311
"path/filepath"
1412
"strings"
1513
"time"
@@ -90,6 +88,11 @@ type vm struct {
9088
VMPrivateKey string `json:"vm_private_key"`
9189
}
9290

91+
func (vm vm) redact() *vm {
92+
vm.VMPrivateKey = "***"
93+
return &vm
94+
}
95+
9396
func getVirtualMachine(username string) string {
9497
client := openssh.Client{
9598
Username: username,
@@ -101,26 +104,24 @@ func getVirtualMachine(username string) string {
101104
if envGateway := os.Getenv("DEVBOX_GATEWAY"); envGateway != "" {
102105
client.Hostname = envGateway
103106
}
104-
bytes, err := client.Exec("auth")
107+
sshOut, err := client.Exec("auth")
105108
if err != nil {
106-
log.Println(err)
107-
var exitErr *exec.ExitError
108-
if errors.As(err, &exitErr) {
109-
log.Printf("ssh %s stderr:\n%s", client.Hostname, string(exitErr.Stderr))
110-
}
111-
os.Exit(1)
112-
}
113-
debug.Log("ssh %s stdout:\n%s", client.Hostname, string(bytes))
109+
log.Fatalln("error requesting VM:", err)
110+
}
114111
resp := &vm{}
115-
err = json.Unmarshal(bytes, resp)
116-
if err != nil {
117-
log.Fatal(err)
112+
if err := json.Unmarshal(sshOut, resp); err != nil {
113+
log.Fatalf("error unmarshaling gateway response %q: %v", sshOut, err)
118114
}
119-
if resp.VMPrivateKey != "" {
120-
err := openssh.AddVMKey(resp.VMHost, resp.VMPrivateKey)
121-
if err != nil {
122-
log.Fatal(err)
123-
}
115+
if redacted, err := json.MarshalIndent(resp.redact(), "\t", " "); err == nil {
116+
debug.Log("got gateway response:\n\t%s", redacted)
117+
}
118+
if resp.VMPrivateKey == "" {
119+
return resp.VMHost
120+
}
121+
122+
err = openssh.AddVMKey(resp.VMHost, resp.VMPrivateKey)
123+
if err != nil {
124+
log.Fatalf("error adding new VM key: %v", err)
124125
}
125126
return resp.VMHost
126127
}

internal/cloud/openssh/client.go

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package openssh
55

66
import (
7+
"bytes"
78
"fmt"
89
"net"
910
"os"
@@ -23,21 +24,29 @@ func (c *Client) Shell() error {
2324
cmd := c.cmd("-t")
2425
remoteCmd := fmt.Sprintf(`bash -l -c "start_devbox_shell.sh \"%s\""`, c.ProjectDirName)
2526
cmd.Args = append(cmd.Args, remoteCmd)
26-
debug.Log("running command: %s", cmd)
27-
28-
// Setup stdin, stdout, stderr
2927
cmd.Stdin = os.Stdin
3028
cmd.Stdout = os.Stdout
3129
cmd.Stderr = os.Stderr
32-
33-
return cmd.Run()
30+
return logCmdRun(cmd)
3431
}
3532

3633
func (c *Client) Exec(remoteCmd string) ([]byte, error) {
3734
sshCmd := c.cmd()
3835
sshCmd.Args = append(sshCmd.Args, remoteCmd)
39-
debug.Log("cmd/exec: %s", sshCmd)
40-
return sshCmd.Output()
36+
37+
var stdout, stderr bytes.Buffer
38+
sshCmd.Stdout = &stdout
39+
sshCmd.Stderr = &stderr
40+
41+
err := logCmdRun(sshCmd)
42+
logCmdOutput(sshCmd, "stderr", stderr.Bytes())
43+
if err != nil {
44+
// Only log output if there was an error, otherwise we might log
45+
// a VM's private key.
46+
logCmdOutput(sshCmd, "stdout", stdout.Bytes())
47+
return nil, err
48+
}
49+
return stdout.Bytes(), nil
4150
}
4251

4352
func (c *Client) cmd(sshArgs ...string) *exec.Cmd {
@@ -73,3 +82,41 @@ func destination(username, hostname string) string {
7382

7483
return result
7584
}
85+
86+
func logCmdRun(cmd *exec.Cmd) error {
87+
// Use cmd.Start so we can log the pid. Don't bother writing errors to
88+
// the debug log since those will be printed anyway.
89+
if err := cmd.Start(); err != nil {
90+
return fmt.Errorf("openssh: start command %q: %w", cmd, err)
91+
}
92+
debug.Log("openssh: started process %d with command %q", cmd.Process.Pid, cmd)
93+
94+
if err := cmd.Wait(); err != nil {
95+
return fmt.Errorf("openssh: process %d with command %q: %w",
96+
cmd.Process.Pid, cmd, err)
97+
}
98+
debug.Log("openssh: process %d with command %q: exit status 0", cmd.Process.Pid, cmd)
99+
return nil
100+
}
101+
102+
func logCmdOutput(cmd *exec.Cmd, stdstream string, out []byte) {
103+
out = bytes.TrimSpace(out)
104+
if len(out) == 0 {
105+
debug.Log("openssh: process %d with command %q: exit status %d: %s is empty",
106+
cmd.Process.Pid, cmd, cmd.ProcessState.ExitCode(), stdstream)
107+
return
108+
}
109+
110+
out = bytes.ReplaceAll(out, []byte{'\n'}, []byte{'\n', '\t'})
111+
max := 1 << 16 // 64 KiB
112+
if overflow := len(out) - max; overflow > 0 {
113+
out = bytes.TrimSpace(out[:max])
114+
if overflow == 1 {
115+
out = append(out, "...truncated 1 byte."...)
116+
} else {
117+
out = fmt.Appendf(out, "...truncated %d bytes.", overflow)
118+
}
119+
}
120+
debug.Log("openssh: process %d with command %q: exit status %d: %s text:\n\t%s",
121+
cmd.Process.Pid, cmd, cmd.ProcessState.ExitCode(), stdstream, out)
122+
}

0 commit comments

Comments
 (0)