Skip to content

Commit 9253a22

Browse files
authored
Use coder ssh in place of coder vscodessh (#420)
1 parent 4dd0d70 commit 9253a22

File tree

6 files changed

+62
-20
lines changed

6 files changed

+62
-20
lines changed

CONTRIBUTING.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ contains the `coder-vscode` prefix, and if so we delay activation to:
3434

3535
```text
3636
Host coder-vscode.dev.coder.com--*
37-
ProxyCommand "/tmp/coder" vscodessh --network-info-dir "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/net" --session-token-file "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com/session_token" --url-file "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com/url" %h
37+
ProxyCommand "/tmp/coder" --global-config "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/dev.coder.com" ssh --stdio --network-info-dir "/home/kyle/.config/Code/User/globalStorage/coder.coder-remote/net" --ssh-host-prefix coder-vscode.dev.coder.com-- %h
3838
ConnectTimeout 0
3939
StrictHostKeyChecking no
4040
UserKnownHostsFile /dev/null
@@ -50,8 +50,8 @@ specified port. This port is printed to the `Remote - SSH` log file in the VS
5050
Code Output panel in the format `-> socksPort <port> ->`. We use this port to
5151
find the SSH process ID that is being used by the remote session.
5252

53-
The `vscodessh` subcommand on the `coder` binary periodically flushes its
54-
network information to `network-info-dir + "/" + process.ppid`. SSH executes
53+
The `ssh` subcommand on the `coder` binary periodically flushes its network
54+
information to `network-info-dir + "/" + process.ppid`. SSH executes
5555
`ProxyCommand`, which means the `process.ppid` will always be the matching SSH
5656
command.
5757

src/commands.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,7 @@ async function openWorkspace(
538538
// when opening a workspace unless explicitly specified.
539539
let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}`
540540
if (workspaceAgent) {
541-
remoteAuthority += `--${workspaceAgent}`
541+
remoteAuthority += `.${workspaceAgent}`
542542
}
543543

544544
let newWindow = true

src/featureSet.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import * as semver from "semver"
33
export type FeatureSet = {
44
vscodessh: boolean
55
proxyLogDirectory: boolean
6+
wildcardSSH: boolean
67
}
78

89
/**
@@ -21,5 +22,6 @@ export function featureSetForVersion(version: semver.SemVer | null): FeatureSet
2122
// If this check didn't exist, VS Code connections would fail on
2223
// older versions because of an unknown CLI argument.
2324
proxyLogDirectory: (version?.compare("2.3.3") || 0) > 0 || version?.prerelease[0] === "devel",
25+
wildcardSSH: (version?.compare("2.19.0") || 0) > 0 || version?.prerelease[0] === "devel",
2426
}
2527
}

src/remote.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -467,20 +467,27 @@ export class Remote {
467467
// "Host not found".
468468
try {
469469
this.storage.writeToCoderOutputChannel("Updating SSH config...")
470-
await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir)
470+
await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir, featureSet)
471471
} catch (error) {
472472
this.storage.writeToCoderOutputChannel(`Failed to configure SSH: ${error}`)
473473
throw error
474474
}
475475

476476
// TODO: This needs to be reworked; it fails to pick up reconnects.
477-
this.findSSHProcessID().then((pid) => {
477+
this.findSSHProcessID().then(async (pid) => {
478478
if (!pid) {
479479
// TODO: Show an error here!
480480
return
481481
}
482482
disposables.push(this.showNetworkUpdates(pid))
483-
this.commands.workspaceLogPath = logDir ? path.join(logDir, `${pid}.log`) : undefined
483+
if (logDir) {
484+
const logFiles = await fs.readdir(logDir)
485+
this.commands.workspaceLogPath = logFiles
486+
.reverse()
487+
.find((file) => file === `${pid}.log` || file.endsWith(`-${pid}.log`))
488+
} else {
489+
this.commands.workspaceLogPath = undefined
490+
}
484491
})
485492

486493
// Register the label formatter again because SSH overrides it!
@@ -532,7 +539,14 @@ export class Remote {
532539

533540
// updateSSHConfig updates the SSH configuration with a wildcard that handles
534541
// all Coder entries.
535-
private async updateSSHConfig(restClient: Api, label: string, hostName: string, binaryPath: string, logDir: string) {
542+
private async updateSSHConfig(
543+
restClient: Api,
544+
label: string,
545+
hostName: string,
546+
binaryPath: string,
547+
logDir: string,
548+
featureSet: FeatureSet,
549+
) {
536550
let deploymentSSHConfig = {}
537551
try {
538552
const deploymentConfig = await restClient.getDeploymentSSHConfig()
@@ -610,13 +624,21 @@ export class Remote {
610624
headerArg = ` --header-command ${escapeSubcommand(headerCommand)}`
611625
}
612626

627+
const hostPrefix = label ? `${AuthorityPrefix}.${label}--` : `${AuthorityPrefix}--`
628+
629+
const proxyCommand = featureSet.wildcardSSH
630+
? `${escape(binaryPath)}${headerArg} --global-config ${escape(
631+
path.dirname(this.storage.getSessionTokenPath(label)),
632+
)} ssh --stdio --network-info-dir ${escape(this.storage.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h`
633+
: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape(
634+
this.storage.getNetworkInfoPath(),
635+
)}${await this.formatLogArg(logDir)} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape(
636+
this.storage.getUrlPath(label),
637+
)} %h`
638+
613639
const sshValues: SSHValues = {
614-
Host: label ? `${AuthorityPrefix}.${label}--*` : `${AuthorityPrefix}--*`,
615-
ProxyCommand: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape(
616-
this.storage.getNetworkInfoPath(),
617-
)}${await this.formatLogArg(logDir)} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape(
618-
this.storage.getUrlPath(label),
619-
)} %h`,
640+
Host: hostPrefix + `*`,
641+
ProxyCommand: proxyCommand,
620642
ConnectTimeout: "0",
621643
StrictHostKeyChecking: "no",
622644
UserKnownHostsFile: "/dev/null",

src/util.test.ts

+7
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ it("should parse authority", async () => {
5656
username: "foo",
5757
workspace: "bar",
5858
})
59+
expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({
60+
agent: "baz",
61+
host: "coder-vscode.dev.coder.com--foo--bar.baz",
62+
label: "dev.coder.com",
63+
username: "foo",
64+
workspace: "bar",
65+
})
5966
})
6067

6168
it("escapes url host", async () => {

src/util.ts

+17-6
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,8 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
2424
// The authority looks like: vscode://ssh-remote+<ssh host name>
2525
const authorityParts = authority.split("+")
2626

27-
// We create SSH host names in one of two formats:
28-
// coder-vscode--<username>--<workspace>--<agent?> (old style)
29-
// coder-vscode.<label>--<username>--<workspace>--<agent>
27+
// We create SSH host names in a format matching:
28+
// coder-vscode(--|.)<username>--<workspace>(--|.)<agent?>
3029
// The agent can be omitted; the user will be prompted for it instead.
3130
// Anything else is unrelated to Coder and can be ignored.
3231
const parts = authorityParts[1].split("--")
@@ -38,15 +37,27 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null {
3837
// Validate the SSH host name. Including the prefix, we expect at least
3938
// three parts, or four if including the agent.
4039
if ((parts.length !== 3 && parts.length !== 4) || parts.some((p) => !p)) {
41-
throw new Error(`Invalid Coder SSH authority. Must be: <username>--<workspace>--<agent?>`)
40+
throw new Error(`Invalid Coder SSH authority. Must be: <username>--<workspace>(--|.)<agent?>`)
41+
}
42+
43+
let workspace = parts[2]
44+
let agent = ""
45+
if (parts.length === 4) {
46+
agent = parts[3]
47+
} else if (parts.length === 3) {
48+
const workspaceParts = parts[2].split(".")
49+
if (workspaceParts.length === 2) {
50+
workspace = workspaceParts[0]
51+
agent = workspaceParts[1]
52+
}
4253
}
4354

4455
return {
45-
agent: parts[3] ?? "",
56+
agent: agent,
4657
host: authorityParts[1],
4758
label: parts[0].replace(/^coder-vscode\.?/, ""),
4859
username: parts[1],
49-
workspace: parts[2],
60+
workspace: workspace,
5061
}
5162
}
5263

0 commit comments

Comments
 (0)