Skip to content
This repository was archived by the owner on Jan 17, 2021. It is now read-only.

Commit e646d57

Browse files
committed
add SSH master connection feature
By default, sshcode will now start a master connection with no command so that users only need to authenticate once and multiple connections don't need to be established. This speeds up load times significantly as there are less handshakes required. To disable this behaviour you can use `--no-reuse-connection`.
1 parent 5bbc39f commit e646d57

File tree

2 files changed

+104
-27
lines changed

2 files changed

+104
-27
lines changed

main.go

+13-10
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,12 @@ var _ interface {
3434
} = new(rootCmd)
3535

3636
type rootCmd struct {
37-
skipSync bool
38-
syncBack bool
39-
printVersion bool
40-
bindAddr string
41-
sshFlags string
37+
skipSync bool
38+
syncBack bool
39+
printVersion bool
40+
noReuseConnection bool
41+
bindAddr string
42+
sshFlags string
4243
}
4344

4445
func (c *rootCmd) Spec() cli.CommandSpec {
@@ -53,6 +54,7 @@ func (c *rootCmd) RegisterFlags(fl *flag.FlagSet) {
5354
fl.BoolVar(&c.skipSync, "skipsync", false, "skip syncing local settings and extensions to remote host")
5455
fl.BoolVar(&c.syncBack, "b", false, "sync extensions back on termination")
5556
fl.BoolVar(&c.printVersion, "version", false, "print version information and exit")
57+
fl.BoolVar(&c.noReuseConnection, "no-reuse-connection", false, "do not reuse SSH connection via control socket")
5658
fl.StringVar(&c.bindAddr, "bind", "", "local bind address for ssh tunnel")
5759
fl.StringVar(&c.sshFlags, "ssh-flags", "", "custom SSH flags")
5860
}
@@ -76,10 +78,11 @@ func (c *rootCmd) Run(fl *flag.FlagSet) {
7678
}
7779

7880
err := sshCode(host, dir, options{
79-
skipSync: c.skipSync,
80-
sshFlags: c.sshFlags,
81-
bindAddr: c.bindAddr,
82-
syncBack: c.syncBack,
81+
skipSync: c.skipSync,
82+
sshFlags: c.sshFlags,
83+
bindAddr: c.bindAddr,
84+
syncBack: c.syncBack,
85+
noReuseConnection: c.noReuseConnection,
8386
})
8487

8588
if err != nil {
@@ -101,7 +104,7 @@ Environment variables:
101104
More info: https://github.com/cdr/sshcode
102105
103106
Arguments:
104-
%vHOST is passed into the ssh command. Valid formats are '<ip-address>' or 'gcp:<instance-name>'.
107+
%vHOST is passed into the ssh command. Valid formats are '<ip-address>' or 'gcp:<instance-name>'.
105108
%vDIR is optional.`,
106109
helpTab, vsCodeConfigDirEnv,
107110
helpTab, vsCodeExtensionsDirEnv,

sshcode.go

+91-17
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,16 @@ import (
2020
)
2121

2222
const codeServerPath = "~/.cache/sshcode/sshcode-server"
23+
const sshControlPath = "~/.ssh/control-%h-%p-%r"
2324

2425
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
3133
}
3234

3335
func 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+
266340
func syncUserSettings(sshFlags string, host string, back bool) error {
267341
localConfDir, err := configDir()
268342
if err != nil {

0 commit comments

Comments
 (0)