@@ -20,14 +20,16 @@ import (
20
20
)
21
21
22
22
const codeServerPath = "~/.cache/sshcode/sshcode-server"
23
+ const sshControlPath = "~/.ssh/control-%h-%p-%r"
23
24
24
25
type 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
31
33
}
32
34
33
35
func sshCode (host , dir string , o options ) error {
@@ -53,6 +55,41 @@ func sshCode(host, dir string, o options) error {
53
55
return xerrors .Errorf ("failed to find available remote port: %w" , err )
54
56
}
55
57
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
+
56
93
dlScript := downloadScript (codeServerPath )
57
94
58
95
// Downloads the latest code-server and allows it to be executed.
@@ -146,22 +183,39 @@ func sshCode(host, dir string, o options) error {
146
183
case <- ctx .Done ():
147
184
case <- c :
148
185
}
186
+ flog .Info ("exiting" )
149
187
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" )
154
190
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
+ }
156
195
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
+ }
160
200
}
161
201
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
+ }
165
219
}
166
220
167
221
return nil
@@ -263,6 +317,26 @@ func randomPort() (string, error) {
263
317
return "" , xerrors .Errorf ("max number of tries exceeded: %d" , maxTries )
264
318
}
265
319
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
+
266
340
func syncUserSettings (sshFlags string , host string , back bool ) error {
267
341
localConfDir , err := configDir ()
268
342
if err != nil {
0 commit comments