Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 5de1084

Browse files
authored
feat(cli/ssh): simplify log file flags (#7863)
And, fix a race condition.
1 parent ec7b117 commit 5de1084

File tree

5 files changed

+92
-77
lines changed

5 files changed

+92
-77
lines changed

cli/clibase/cmd.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,19 @@ func (inv *Invocation) Run() (err error) {
448448
panic(err)
449449
}
450450
}()
451+
// We close Stdin to prevent deadlocks, e.g. when the command
452+
// has ended but an io.Copy is still reading from Stdin.
453+
defer func() {
454+
if inv.Stdin == nil {
455+
return
456+
}
457+
rc, ok := inv.Stdin.(io.ReadCloser)
458+
if !ok {
459+
return
460+
}
461+
e := rc.Close()
462+
err = errors.Join(err, e)
463+
}()
451464
err = inv.run(&runState{
452465
allArgs: inv.Args,
453466
})

cli/ssh.go

Lines changed: 68 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import (
1010
"net/url"
1111
"os"
1212
"os/exec"
13-
"path"
1413
"path/filepath"
1514
"strings"
15+
"sync"
1616
"time"
1717

1818
"github.com/gen2brain/beeep"
@@ -52,8 +52,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
5252
wsPollInterval time.Duration
5353
waitEnum string
5454
noWait bool
55-
logDir string
56-
logToFile bool
55+
logDirPath string
5756
)
5857
client := new(codersdk.Client)
5958
cmd := &clibase.Cmd{
@@ -76,24 +75,45 @@ func (r *RootCmd) ssh() *clibase.Cmd {
7675
logger.Error(ctx, "command exit", slog.Error(retErr))
7776
}
7877
}()
79-
if logToFile {
80-
// we need a way to ensure different ssh invocations don't clobber
81-
// each other's logs. Date-time strings will likely have collisions
82-
// in unit tests and/or scripts unless we extend precision out to
83-
// sub-millisecond, which seems unwieldy. A simple 5-character random
84-
// string will do it, since the operating system already tracks
85-
// dates and times for file IO.
86-
qual, err := cryptorand.String(5)
78+
79+
// This WaitGroup solves for a race condition where we were logging
80+
// while closing the log file in a defer. It probably solves
81+
// others too.
82+
var wg sync.WaitGroup
83+
wg.Add(1)
84+
defer wg.Done()
85+
86+
if logDirPath != "" {
87+
nonce, err := cryptorand.StringCharset(cryptorand.Lower, 5)
8788
if err != nil {
88-
return xerrors.Errorf("generate random qualifier: %w", err)
89+
return xerrors.Errorf("generate nonce: %w", err)
8990
}
90-
logPth := path.Join(logDir, fmt.Sprintf("coder-ssh-%s.log", qual))
91-
logFile, err := os.Create(logPth)
91+
logFilePath := filepath.Join(
92+
logDirPath,
93+
fmt.Sprintf(
94+
"coder-ssh-%s-%s.log",
95+
// The time portion makes it easier to find the right
96+
// log file.
97+
time.Now().Format("20060102-150405"),
98+
// The nonce prevents collisions, as SSH invocations
99+
// frequently happen in parallel.
100+
nonce,
101+
),
102+
)
103+
logFile, err := os.OpenFile(
104+
logFilePath,
105+
os.O_CREATE|os.O_APPEND|os.O_WRONLY|os.O_EXCL,
106+
0o600,
107+
)
92108
if err != nil {
93-
return xerrors.Errorf("error opening %s for logging: %w", logPth, err)
109+
return xerrors.Errorf("error opening %s for logging: %w", logDirPath, err)
94110
}
111+
go func() {
112+
wg.Wait()
113+
logFile.Close()
114+
}()
115+
95116
logger = slog.Make(sloghuman.Sink(logFile))
96-
defer logFile.Close()
97117
if r.verbose {
98118
logger = logger.Leveled(slog.LevelDebug)
99119
}
@@ -192,9 +212,18 @@ func (r *RootCmd) ssh() *clibase.Cmd {
192212
return xerrors.Errorf("connect SSH: %w", err)
193213
}
194214
defer rawSSH.Close()
195-
go watchAndClose(ctx, rawSSH.Close, logger, client, workspace)
196215

216+
wg.Add(1)
217+
go func() {
218+
defer wg.Done()
219+
watchAndClose(ctx, func() error {
220+
return rawSSH.Close()
221+
}, logger, client, workspace)
222+
}()
223+
224+
wg.Add(1)
197225
go func() {
226+
defer wg.Done()
198227
// Ensure stdout copy closes incase stdin is closed
199228
// unexpectedly. Typically we wouldn't worry about
200229
// this since OpenSSH should kill the proxy command.
@@ -227,19 +256,24 @@ func (r *RootCmd) ssh() *clibase.Cmd {
227256
return xerrors.Errorf("ssh session: %w", err)
228257
}
229258
defer sshSession.Close()
230-
go watchAndClose(
231-
ctx,
232-
func() error {
233-
err := sshSession.Close()
234-
logger.Debug(ctx, "session close", slog.Error(err))
235-
err = sshClient.Close()
236-
logger.Debug(ctx, "client close", slog.Error(err))
237-
return nil
238-
},
239-
logger,
240-
client,
241-
workspace,
242-
)
259+
260+
wg.Add(1)
261+
go func() {
262+
defer wg.Done()
263+
watchAndClose(
264+
ctx,
265+
func() error {
266+
err := sshSession.Close()
267+
logger.Debug(ctx, "session close", slog.Error(err))
268+
err = sshClient.Close()
269+
logger.Debug(ctx, "client close", slog.Error(err))
270+
return nil
271+
},
272+
logger,
273+
client,
274+
workspace,
275+
)
276+
}()
243277

244278
if identityAgent == "" {
245279
identityAgent = os.Getenv("SSH_AUTH_SOCK")
@@ -389,18 +423,11 @@ func (r *RootCmd) ssh() *clibase.Cmd {
389423
UseInstead: []clibase.Option{waitOption},
390424
},
391425
{
392-
Flag: "log-dir",
393-
Default: os.TempDir(),
394-
Description: "Specify the location for the log files.",
395-
Env: "CODER_SSH_LOG_DIR",
396-
Value: clibase.StringOf(&logDir),
397-
},
398-
{
399-
Flag: "log-to-file",
426+
Flag: "log-dir",
427+
Description: "Specify the directory containing SSH diagnostic log files.",
428+
Env: "CODER_SSH_LOG_DIR",
400429
FlagShorthand: "l",
401-
Env: "CODER_SSH_LOG_TO_FILE",
402-
Description: "Enable diagnostic logging to file.",
403-
Value: clibase.BoolOf(&logToFile),
430+
Value: clibase.StringOf(&logDirPath),
404431
},
405432
}
406433
return cmd

cli/ssh_test.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ func TestSSH(t *testing.T) {
261261
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
262262
_, _ = tGoContext(t, func(ctx context.Context) {
263263
// Run this async so the SSH command has to wait for
264-
// the build and agent to connect!
264+
// the build and agent to connect.
265265
agentClient := agentsdk.New(client.URL)
266266
agentClient.SetSessionToken(agentToken)
267267
agentCloser := agent.New(agent.Options{
@@ -411,20 +411,14 @@ func TestSSH(t *testing.T) {
411411
t.Run("FileLogging", func(t *testing.T) {
412412
t.Parallel()
413413

414-
dir := t.TempDir()
414+
logDir := t.TempDir()
415415

416416
client, workspace, agentToken := setupWorkspaceForAgent(t, nil)
417-
inv, root := clitest.New(t, "ssh", workspace.Name, "-l", "--log-dir", dir)
417+
inv, root := clitest.New(t, "ssh", "-l", logDir, workspace.Name)
418418
clitest.SetupConfig(t, client, root)
419419
pty := ptytest.New(t).Attach(inv)
420+
w := clitest.StartWithWaiter(t, inv)
420421

421-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
422-
defer cancel()
423-
424-
cmdDone := tGo(t, func() {
425-
err := inv.WithContext(ctx).Run()
426-
assert.NoError(t, err)
427-
})
428422
pty.ExpectMatch("Waiting")
429423

430424
agentClient := agentsdk.New(client.URL)
@@ -439,17 +433,11 @@ func TestSSH(t *testing.T) {
439433

440434
// Shells on Mac, Windows, and Linux all exit shells with the "exit" command.
441435
pty.WriteLine("exit")
442-
<-cmdDone
436+
w.RequireSuccess()
443437

444-
entries, err := os.ReadDir(dir)
438+
ents, err := os.ReadDir(logDir)
445439
require.NoError(t, err)
446-
for _, e := range entries {
447-
t.Logf("logdir entry: %s", e.Name())
448-
if strings.HasPrefix(e.Name(), "coder-ssh") {
449-
return
450-
}
451-
}
452-
t.Fatal("failed to find ssh logfile")
440+
require.Len(t, ents, 1, "expected one file in logdir %s", logDir)
453441
})
454442
}
455443

cli/testdata/coder_ssh_--help.golden

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@ Start a shell into a workspace
1818
Specifies which identity agent to use (overrides $SSH_AUTH_SOCK),
1919
forward agent must also be enabled.
2020

21-
--log-dir string, $CODER_SSH_LOG_DIR (default: /tmp)
22-
Specify the location for the log files.
23-
24-
-l, --log-to-file bool, $CODER_SSH_LOG_TO_FILE
25-
Enable diagnostic logging to file.
21+
-l, --log-dir string, $CODER_SSH_LOG_DIR
22+
Specify the directory containing SSH diagnostic log files.
2623

2724
--no-wait bool, $CODER_SSH_NO_WAIT
2825
Enter workspace immediately after the agent has connected. This is the

docs/cli/ssh.md

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,14 @@ Specifies whether to forward the GPG agent. Unsupported on Windows workspaces, b
3939

4040
Specifies which identity agent to use (overrides $SSH_AUTH_SOCK), forward agent must also be enabled.
4141

42-
### --log-dir
42+
### -l, --log-dir
4343

4444
| | |
4545
| ----------- | ------------------------------- |
4646
| Type | <code>string</code> |
4747
| Environment | <code>$CODER_SSH_LOG_DIR</code> |
48-
| Default | <code>/tmp</code> |
4948

50-
Specify the location for the log files.
51-
52-
### -l, --log-to-file
53-
54-
| | |
55-
| ----------- | ----------------------------------- |
56-
| Type | <code>bool</code> |
57-
| Environment | <code>$CODER_SSH_LOG_TO_FILE</code> |
58-
59-
Enable diagnostic logging to file.
49+
Specify the directory containing SSH diagnostic log files.
6050

6151
### --no-wait
6252

0 commit comments

Comments
 (0)