@@ -21,18 +21,21 @@ import (
2121)
2222
2323const 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+ )
2730
2831type 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
3639}
3740
3841func sshCode (host , dir string , o options ) error {
@@ -57,68 +60,19 @@ func sshCode(host, dir string, o options) error {
5760 }
5861
5962 // 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 )
8064
8165 // Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication
8266 // 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 ()
10871 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
11274 } 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
12276 }
12377 }
12478
@@ -226,12 +180,12 @@ func sshCode(host, dir string, o options) error {
226180
227181 err = syncExtensions (o .sshFlags , host , true )
228182 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 )
230184 }
231185
232186 err = syncUserSettings (o .sshFlags , host , true )
233187 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 )
235189 }
236190
237191 return nil
@@ -350,6 +304,74 @@ func randomPort() (string, error) {
350304 return "" , xerrors .Errorf ("max number of tries exceeded: %d" , maxTries )
351305}
352306
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+
353375// checkSSHMaster polls every second for 30 seconds to check if the SSH master
354376// is ready.
355377func checkSSHMaster (sshMasterCmd * exec.Cmd , sshFlags string , host string ) error {
@@ -359,16 +381,12 @@ func checkSSHMaster(sshMasterCmd *exec.Cmd, sshFlags string, host string) error
359381 err error
360382 )
361383 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" )
369387 }
370388
371- // Check if it's ready
389+ // Check if it's ready.
372390 sshCmdStr := fmt .Sprintf (`ssh %v -O check %v` , sshFlags , host )
373391 sshCmd := exec .Command ("sh" , "-c" , sshCmdStr )
374392 err = sshCmd .Run ()
0 commit comments