Skip to content

Commit a9962e7

Browse files
committed
Use coder ssh in place of coder vscodessh
Build on top of recent coder PRs to make coder ssh provide equivalent functionality to vscodessh, avoiding the need for a VSCode-specific ssh subcommand.
1 parent 4dd0d70 commit a9962e7

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)