@@ -10,9 +10,9 @@ import (
10
10
"net/url"
11
11
"os"
12
12
"os/exec"
13
- "path"
14
13
"path/filepath"
15
14
"strings"
15
+ "sync"
16
16
"time"
17
17
18
18
"github.com/gen2brain/beeep"
@@ -52,8 +52,7 @@ func (r *RootCmd) ssh() *clibase.Cmd {
52
52
wsPollInterval time.Duration
53
53
waitEnum string
54
54
noWait bool
55
- logDir string
56
- logToFile bool
55
+ logDirPath string
57
56
)
58
57
client := new (codersdk.Client )
59
58
cmd := & clibase.Cmd {
@@ -76,24 +75,45 @@ func (r *RootCmd) ssh() *clibase.Cmd {
76
75
logger .Error (ctx , "command exit" , slog .Error (retErr ))
77
76
}
78
77
}()
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 )
87
88
if err != nil {
88
- return xerrors .Errorf ("generate random qualifier : %w" , err )
89
+ return xerrors .Errorf ("generate nonce : %w" , err )
89
90
}
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
+ )
92
108
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 )
94
110
}
111
+ go func () {
112
+ wg .Wait ()
113
+ logFile .Close ()
114
+ }()
115
+
95
116
logger = slog .Make (sloghuman .Sink (logFile ))
96
- defer logFile .Close ()
97
117
if r .verbose {
98
118
logger = logger .Leveled (slog .LevelDebug )
99
119
}
@@ -192,9 +212,18 @@ func (r *RootCmd) ssh() *clibase.Cmd {
192
212
return xerrors .Errorf ("connect SSH: %w" , err )
193
213
}
194
214
defer rawSSH .Close ()
195
- go watchAndClose (ctx , rawSSH .Close , logger , client , workspace )
196
215
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 )
197
225
go func () {
226
+ defer wg .Done ()
198
227
// Ensure stdout copy closes incase stdin is closed
199
228
// unexpectedly. Typically we wouldn't worry about
200
229
// this since OpenSSH should kill the proxy command.
@@ -227,19 +256,24 @@ func (r *RootCmd) ssh() *clibase.Cmd {
227
256
return xerrors .Errorf ("ssh session: %w" , err )
228
257
}
229
258
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
+ }()
243
277
244
278
if identityAgent == "" {
245
279
identityAgent = os .Getenv ("SSH_AUTH_SOCK" )
@@ -389,18 +423,11 @@ func (r *RootCmd) ssh() *clibase.Cmd {
389
423
UseInstead : []clibase.Option {waitOption },
390
424
},
391
425
{
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" ,
400
429
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 ),
404
431
},
405
432
}
406
433
return cmd
0 commit comments