@@ -21,18 +21,21 @@ import (
21
21
)
22
22
23
23
const codeServerPath = "~/.cache/sshcode/sshcode-server"
24
- const sshDirectory = "~/.ssh"
25
- const sshDirectoryUnsafeModeMask = 0022
26
- const sshControlPath = sshDirectory + "/control-%h-%p-%r"
24
+
25
+ const (
26
+ sshDirectory = "~/.ssh"
27
+ sshDirectoryUnsafeModeMask = 0022
28
+ sshControlPath = sshDirectory + "/control-%h-%p-%r"
29
+ )
27
30
28
31
type options struct {
29
- skipSync bool
30
- syncBack bool
31
- noOpen bool
32
- noReuseConnection bool
33
- bindAddr string
34
- remotePort string
35
- sshFlags string
32
+ skipSync bool
33
+ syncBack bool
34
+ noOpen bool
35
+ reuseConnection bool
36
+ bindAddr string
37
+ remotePort string
38
+ sshFlags string
36
39
}
37
40
38
41
func sshCode (host , dir string , o options ) error {
@@ -57,68 +60,19 @@ func sshCode(host, dir string, o options) error {
57
60
}
58
61
59
62
// Check the SSH directory's permissions and warn the user if it is not safe.
60
- sshDirectoryMode , err := os .Lstat (expandPath (sshDirectory ))
61
- if err != nil {
62
- if ! o .noReuseConnection {
63
- flog .Info ("failed to stat %v directory, disabling connection reuse feature: %v" , sshDirectory , err )
64
- o .noReuseConnection = true
65
- }
66
- } else {
67
- if ! sshDirectoryMode .IsDir () {
68
- if ! o .noReuseConnection {
69
- flog .Info ("%v is not a directory, disabling connection reuse feature" , sshDirectory )
70
- o .noReuseConnection = true
71
- } else {
72
- flog .Info ("warning: %v is not a directory" , sshDirectory )
73
- }
74
- }
75
- if sshDirectoryMode .Mode ().Perm ()& sshDirectoryUnsafeModeMask != 0 {
76
- flog .Info ("warning: the %v directory has unsafe permissions, they should only be writable by " +
77
- "the owner (and files inside should be set to 0600)" , sshDirectory )
78
- }
79
- }
63
+ o .reuseConnection = checkSSHDirectory (sshDirectory , o .reuseConnection )
80
64
81
65
// Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication
82
66
// only happens on the initial connection.
83
- if ! o .noReuseConnection {
84
- newSSHFlags := fmt .Sprintf (`%v -o "ControlPath=%v"` , o .sshFlags , sshControlPath )
85
-
86
- // -MN means "start a master socket and don't open a session, just connect".
87
- sshCmdStr := fmt .Sprintf (`exec ssh %v -MN %v` , newSSHFlags , host )
88
- sshMasterCmd := exec .Command ("sh" , "-c" , sshCmdStr )
89
- sshMasterCmd .Stdin = os .Stdin
90
- sshMasterCmd .Stdout = os .Stdout
91
- sshMasterCmd .Stderr = os .Stderr
92
- stopSSHMaster := func () {
93
- if sshMasterCmd .Process != nil {
94
- err := sshMasterCmd .Process .Signal (syscall .Signal (0 ))
95
- if err != nil {
96
- return
97
- }
98
- err = sshMasterCmd .Process .Signal (syscall .SIGTERM )
99
- if err != nil {
100
- flog .Error ("failed to send SIGTERM to SSH master process: %v" , err )
101
- }
102
- }
103
- }
104
- defer stopSSHMaster ()
105
-
106
- err = sshMasterCmd .Start ()
107
- go sshMasterCmd .Wait ()
67
+ if o .reuseConnection {
68
+ flog .Info ("starting SSH master connection..." )
69
+ newSSHFlags , cancel , err := startSSHMaster (o .sshFlags , sshControlPath , host )
70
+ defer cancel ()
108
71
if err != nil {
109
- flog .Error ("failed to start SSH master connection, disabling connection reuse feature: %v" , err )
110
- o .noReuseConnection = true
111
- stopSSHMaster ()
72
+ flog .Error ("failed to start SSH master connection: %v" , err )
73
+ o .reuseConnection = false
112
74
} else {
113
- err = checkSSHMaster (sshMasterCmd , newSSHFlags , host )
114
- if err != nil {
115
- flog .Error ("SSH master failed to be ready in time, disabling connection reuse feature: %v" , err )
116
- o .noReuseConnection = true
117
- stopSSHMaster ()
118
- } else {
119
- sshMasterCmd .Stdin = nil
120
- o .sshFlags = newSSHFlags
121
- }
75
+ o .sshFlags = newSSHFlags
122
76
}
123
77
}
124
78
@@ -226,12 +180,12 @@ func sshCode(host, dir string, o options) error {
226
180
227
181
err = syncExtensions (o .sshFlags , host , true )
228
182
if err != nil {
229
- return xerrors .Errorf ("failed to sync extensions back: %v " , err )
183
+ return xerrors .Errorf ("failed to sync extensions back: %w " , err )
230
184
}
231
185
232
186
err = syncUserSettings (o .sshFlags , host , true )
233
187
if err != nil {
234
- return xerrors .Errorf ("failed to sync user settings settings back: %v " , err )
188
+ return xerrors .Errorf ("failed to sync user settings settings back: %w " , err )
235
189
}
236
190
237
191
return nil
@@ -350,6 +304,74 @@ func randomPort() (string, error) {
350
304
return "" , xerrors .Errorf ("max number of tries exceeded: %d" , maxTries )
351
305
}
352
306
307
+ // checkSSHDirectory performs sanity and safety checks on sshDirectory, and
308
+ // returns a new value for o.reuseConnection depending on the checks.
309
+ func checkSSHDirectory (sshDirectory string , reuseConnection bool ) bool {
310
+ sshDirectoryMode , err := os .Lstat (expandPath (sshDirectory ))
311
+ if err != nil {
312
+ if reuseConnection {
313
+ flog .Info ("failed to stat %v directory, disabling connection reuse feature: %v" , sshDirectory , err )
314
+ }
315
+ reuseConnection = false
316
+ } else {
317
+ if ! sshDirectoryMode .IsDir () {
318
+ if reuseConnection {
319
+ flog .Info ("%v is not a directory, disabling connection reuse feature" , sshDirectory )
320
+ } else {
321
+ flog .Info ("warning: %v is not a directory" , sshDirectory )
322
+ }
323
+ reuseConnection = false
324
+ }
325
+ if sshDirectoryMode .Mode ().Perm ()& sshDirectoryUnsafeModeMask != 0 {
326
+ flog .Info ("warning: the %v directory has unsafe permissions, they should only be writable by " +
327
+ "the owner (and files inside should be set to 0600)" , sshDirectory )
328
+ }
329
+ }
330
+ return reuseConnection
331
+ }
332
+
333
+ // startSSHMaster starts an SSH master connection and waits for it to be ready.
334
+ // It returns a new set of SSH flags for child SSH processes to use.
335
+ func startSSHMaster (sshFlags string , sshControlPath string , host string ) (string , func (), error ) {
336
+ ctx , cancel := context .WithCancel (context .Background ())
337
+
338
+ newSSHFlags := fmt .Sprintf (`%v -o "ControlPath=%v"` , sshFlags , sshControlPath )
339
+
340
+ // -MN means "start a master socket and don't open a session, just connect".
341
+ sshCmdStr := fmt .Sprintf (`exec ssh %v -MNq %v` , newSSHFlags , host )
342
+ sshMasterCmd := exec .CommandContext (ctx , "sh" , "-c" , sshCmdStr )
343
+ sshMasterCmd .Stdin = os .Stdin
344
+ sshMasterCmd .Stderr = os .Stderr
345
+
346
+ // Gracefully stop the SSH master.
347
+ stopSSHMaster := func () {
348
+ if sshMasterCmd .Process != nil {
349
+ if sshMasterCmd .ProcessState != nil && sshMasterCmd .ProcessState .Exited () {
350
+ return
351
+ }
352
+ err := sshMasterCmd .Process .Signal (syscall .SIGTERM )
353
+ if err != nil {
354
+ flog .Error ("failed to send SIGTERM to SSH master process: %v" , err )
355
+ }
356
+ }
357
+ cancel ()
358
+ }
359
+
360
+ // Start ssh master and wait. Waiting prevents the process from becoming a zombie process if it dies before
361
+ // sshcode does, and allows sshMasterCmd.ProcessState to be populated.
362
+ err := sshMasterCmd .Start ()
363
+ go sshMasterCmd .Wait ()
364
+ if err != nil {
365
+ return "" , stopSSHMaster , err
366
+ }
367
+ err = checkSSHMaster (sshMasterCmd , newSSHFlags , host )
368
+ if err != nil {
369
+ stopSSHMaster ()
370
+ return "" , stopSSHMaster , xerrors .Errorf ("SSH master wasn't ready on time: %w" , err )
371
+ }
372
+ return newSSHFlags , stopSSHMaster , nil
373
+ }
374
+
353
375
// checkSSHMaster polls every second for 30 seconds to check if the SSH master
354
376
// is ready.
355
377
func checkSSHMaster (sshMasterCmd * exec.Cmd , sshFlags string , host string ) error {
@@ -359,16 +381,12 @@ func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error
359
381
err error
360
382
)
361
383
for i := 0 ; i < maxTries ; i ++ {
362
- // Check if the master is running
363
- if sshMasterCmd .Process == nil {
364
- return xerrors .Errorf ("SSH master process not running" )
365
- }
366
- err = sshMasterCmd .Process .Signal (syscall .Signal (0 ))
367
- if err != nil {
368
- return xerrors .Errorf ("failed to check if SSH master process was alive: %v" , err )
384
+ // Check if the master is running.
385
+ if sshMasterCmd .Process == nil || (sshMasterCmd .ProcessState != nil && sshMasterCmd .ProcessState .Exited ()) {
386
+ return xerrors .Errorf ("SSH master process is not running" )
369
387
}
370
388
371
- // Check if it's ready
389
+ // Check if it's ready.
372
390
sshCmdStr := fmt .Sprintf (`ssh %v -O check %v` , sshFlags , host )
373
391
sshCmd := exec .Command ("sh" , "-c" , sshCmdStr )
374
392
err = sshCmd .Run ()
0 commit comments