@@ -20,14 +20,16 @@ import (
2020)
2121
2222const codeServerPath = "~/.cache/sshcode/sshcode-server"
23+ const sshControlPath = "~/.ssh/control-%h-%p-%r"
2324
2425type options struct {
25- skipSync bool
26- syncBack bool
27- noOpen bool
28- bindAddr string
29- remotePort string
30- sshFlags string
26+ skipSync bool
27+ syncBack bool
28+ noOpen bool
29+ noReuseConnection bool
30+ bindAddr string
31+ remotePort string
32+ sshFlags string
3133}
3234
3335func sshCode (host , dir string , o options ) error {
@@ -53,6 +55,41 @@ func sshCode(host, dir string, o options) error {
5355 return xerrors .Errorf ("failed to find available remote port: %w" , err )
5456 }
5557
58+ // Start SSH master connection socket. This prevents multiple password prompts from appearing as authentication
59+ // only happens on the initial connection.
60+ var sshMasterCmd * exec.Cmd
61+ if ! o .noReuseConnection {
62+ newSSHFlags := fmt .Sprintf (`%v -o "ControlPath=%v"` , o .sshFlags , sshControlPath )
63+
64+ // -MN means "start a master socket and don't open a session, just connect".
65+ sshMasterCmdStr := fmt .Sprintf (`ssh %v -MN %v` , newSSHFlags , host )
66+ sshMasterCmd = exec .Command ("sh" , "-c" , sshMasterCmdStr )
67+ sshMasterCmd .Stdin = os .Stdin
68+ sshMasterCmd .Stdout = os .Stdout
69+ sshMasterCmd .Stderr = os .Stderr
70+ err = sshMasterCmd .Start ()
71+ if err != nil {
72+ flog .Error ("failed to start SSH master connection, disabling connection reuse feature: %v" , err )
73+ o .noReuseConnection = true
74+ } else {
75+ // Wait for master to be ready.
76+ err = checkSSHMaster (newSSHFlags , host )
77+ if err != nil {
78+ flog .Error ("SSH master failed to start in time, disabling connection reuse feature: %v" , err )
79+ o .noReuseConnection = true
80+ if sshMasterCmd .Process != nil {
81+ err = sshMasterCmd .Process .Kill ()
82+ if err != nil {
83+ flog .Error ("failed to kill SSH master connection, ignoring: %v" , err )
84+ }
85+ }
86+ } else {
87+ sshMasterCmd .Stdin = nil
88+ o .sshFlags = newSSHFlags
89+ }
90+ }
91+ }
92+
5693 dlScript := downloadScript (codeServerPath )
5794
5895 // Downloads the latest code-server and allows it to be executed.
@@ -146,22 +183,39 @@ func sshCode(host, dir string, o options) error {
146183 case <- ctx .Done ():
147184 case <- c :
148185 }
186+ flog .Info ("exiting" )
149187
150- if ! o .syncBack || o .skipSync {
151- flog .Info ("shutting down" )
152- return nil
153- }
188+ if o .syncBack && ! o .skipSync {
189+ flog .Info ("synchronizing VS Code back to local" )
154190
155- flog .Info ("synchronizing VS Code back to local" )
191+ err = syncExtensions (o .sshFlags , host , true )
192+ if err != nil {
193+ flog .Error ("failed to sync extensions back: %v" , err )
194+ }
156195
157- err = syncExtensions (o .sshFlags , host , true )
158- if err != nil {
159- return xerrors .Errorf ("failed to sync extensions back: %w" , err )
196+ err = syncUserSettings (o .sshFlags , host , true )
197+ if err != nil {
198+ flog .Error ("failed to sync user settings settings back: %v" , err )
199+ }
160200 }
161201
162- err = syncUserSettings (o .sshFlags , host , true )
163- if err != nil {
164- return xerrors .Errorf ("failed to sync user settings settings back: %w" , err )
202+ // Kill the master connection if we made one.
203+ if ! o .noReuseConnection {
204+ // Try using the -O exit syntax first before killing the master.
205+ sshCmdStr = fmt .Sprintf (`ssh %v -O exit %v` , o .sshFlags , host )
206+ sshCmd = exec .Command ("sh" , "-c" , sshCmdStr )
207+ sshCmd .Stdout = os .Stdout
208+ sshCmd .Stderr = os .Stderr
209+ err = sshCmd .Run ()
210+ if err != nil {
211+ flog .Error ("failed to gracefully stop SSH master connection, killing: %v" , err )
212+ if sshMasterCmd .Process != nil {
213+ err = sshMasterCmd .Process .Kill ()
214+ if err != nil {
215+ flog .Error ("failed to kill SSH master connection, ignoring: %v" , err )
216+ }
217+ }
218+ }
165219 }
166220
167221 return nil
@@ -263,6 +317,26 @@ func randomPort() (string, error) {
263317 return "" , xerrors .Errorf ("max number of tries exceeded: %d" , maxTries )
264318}
265319
320+ // checkSSHMaster polls every second for 30 seconds to check if the SSH master
321+ // is ready.
322+ func checkSSHMaster (sshFlags string , host string ) (err error ) {
323+ maxTries := 30
324+ check := func () error {
325+ sshCmdStr := fmt .Sprintf (`ssh %v -O check %v` , sshFlags , host )
326+ sshCmd := exec .Command ("sh" , "-c" , sshCmdStr )
327+ return sshCmd .Run ()
328+ }
329+
330+ for i := 0 ; i < maxTries ; i ++ {
331+ err = check ()
332+ if err == nil {
333+ return nil
334+ }
335+ time .Sleep (time .Second )
336+ }
337+ return err
338+ }
339+
266340func syncUserSettings (sshFlags string , host string , back bool ) error {
267341 localConfDir , err := configDir ()
268342 if err != nil {
0 commit comments