diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4b455e76..cd59f212 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ contains the `coder-vscode` prefix, and if so we delay activation to: ```text Host coder-vscode.dev.coder.com--* - 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 + 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 ConnectTimeout 0 StrictHostKeyChecking no UserKnownHostsFile /dev/null @@ -50,8 +50,8 @@ specified port. This port is printed to the `Remote - SSH` log file in the VS Code Output panel in the format `-> socksPort ->`. We use this port to find the SSH process ID that is being used by the remote session. -The `vscodessh` subcommand on the `coder` binary periodically flushes its -network information to `network-info-dir + "/" + process.ppid`. SSH executes +The `ssh` subcommand on the `coder` binary periodically flushes its network +information to `network-info-dir + "/" + process.ppid`. SSH executes `ProxyCommand`, which means the `process.ppid` will always be the matching SSH command. diff --git a/src/commands.ts b/src/commands.ts index 8ddd6f51..3506d822 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -538,7 +538,7 @@ async function openWorkspace( // when opening a workspace unless explicitly specified. let remoteAuthority = `ssh-remote+${AuthorityPrefix}.${toSafeHost(baseUrl)}--${workspaceOwner}--${workspaceName}` if (workspaceAgent) { - remoteAuthority += `--${workspaceAgent}` + remoteAuthority += `.${workspaceAgent}` } let newWindow = true diff --git a/src/featureSet.ts b/src/featureSet.ts index 62ff0c2b..6d1195a6 100644 --- a/src/featureSet.ts +++ b/src/featureSet.ts @@ -3,6 +3,7 @@ import * as semver from "semver" export type FeatureSet = { vscodessh: boolean proxyLogDirectory: boolean + wildcardSSH: boolean } /** @@ -21,5 +22,6 @@ export function featureSetForVersion(version: semver.SemVer | null): FeatureSet // If this check didn't exist, VS Code connections would fail on // older versions because of an unknown CLI argument. proxyLogDirectory: (version?.compare("2.3.3") || 0) > 0 || version?.prerelease[0] === "devel", + wildcardSSH: (version?.compare("2.19.0") || 0) > 0 || version?.prerelease[0] === "devel", } } diff --git a/src/remote.ts b/src/remote.ts index abe93e1f..6f5d051b 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -467,20 +467,27 @@ export class Remote { // "Host not found". try { this.storage.writeToCoderOutputChannel("Updating SSH config...") - await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir) + await this.updateSSHConfig(workspaceRestClient, parts.label, parts.host, binaryPath, logDir, featureSet) } catch (error) { this.storage.writeToCoderOutputChannel(`Failed to configure SSH: ${error}`) throw error } // TODO: This needs to be reworked; it fails to pick up reconnects. - this.findSSHProcessID().then((pid) => { + this.findSSHProcessID().then(async (pid) => { if (!pid) { // TODO: Show an error here! return } disposables.push(this.showNetworkUpdates(pid)) - this.commands.workspaceLogPath = logDir ? path.join(logDir, `${pid}.log`) : undefined + if (logDir) { + const logFiles = await fs.readdir(logDir) + this.commands.workspaceLogPath = logFiles + .reverse() + .find((file) => file === `${pid}.log` || file.endsWith(`-${pid}.log`)) + } else { + this.commands.workspaceLogPath = undefined + } }) // Register the label formatter again because SSH overrides it! @@ -532,7 +539,14 @@ export class Remote { // updateSSHConfig updates the SSH configuration with a wildcard that handles // all Coder entries. - private async updateSSHConfig(restClient: Api, label: string, hostName: string, binaryPath: string, logDir: string) { + private async updateSSHConfig( + restClient: Api, + label: string, + hostName: string, + binaryPath: string, + logDir: string, + featureSet: FeatureSet, + ) { let deploymentSSHConfig = {} try { const deploymentConfig = await restClient.getDeploymentSSHConfig() @@ -610,13 +624,21 @@ export class Remote { headerArg = ` --header-command ${escapeSubcommand(headerCommand)}` } + const hostPrefix = label ? `${AuthorityPrefix}.${label}--` : `${AuthorityPrefix}--` + + const proxyCommand = featureSet.wildcardSSH + ? `${escape(binaryPath)}${headerArg} --global-config ${escape( + path.dirname(this.storage.getSessionTokenPath(label)), + )} ssh --stdio --network-info-dir ${escape(this.storage.getNetworkInfoPath())}${await this.formatLogArg(logDir)} --ssh-host-prefix ${hostPrefix} %h` + : `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape( + this.storage.getNetworkInfoPath(), + )}${await this.formatLogArg(logDir)} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape( + this.storage.getUrlPath(label), + )} %h` + const sshValues: SSHValues = { - Host: label ? `${AuthorityPrefix}.${label}--*` : `${AuthorityPrefix}--*`, - ProxyCommand: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape( - this.storage.getNetworkInfoPath(), - )}${await this.formatLogArg(logDir)} --session-token-file ${escape(this.storage.getSessionTokenPath(label))} --url-file ${escape( - this.storage.getUrlPath(label), - )} %h`, + Host: hostPrefix + `*`, + ProxyCommand: proxyCommand, ConnectTimeout: "0", StrictHostKeyChecking: "no", UserKnownHostsFile: "/dev/null", diff --git a/src/util.test.ts b/src/util.test.ts index a9890d34..4fffcc75 100644 --- a/src/util.test.ts +++ b/src/util.test.ts @@ -56,6 +56,13 @@ it("should parse authority", async () => { username: "foo", workspace: "bar", }) + expect(parseRemoteAuthority("vscode://ssh-remote+coder-vscode.dev.coder.com--foo--bar.baz")).toStrictEqual({ + agent: "baz", + host: "coder-vscode.dev.coder.com--foo--bar.baz", + label: "dev.coder.com", + username: "foo", + workspace: "bar", + }) }) it("escapes url host", async () => { diff --git a/src/util.ts b/src/util.ts index 19837d6a..fd5af748 100644 --- a/src/util.ts +++ b/src/util.ts @@ -24,9 +24,8 @@ export function parseRemoteAuthority(authority: string): AuthorityParts | null { // The authority looks like: vscode://ssh-remote+ const authorityParts = authority.split("+") - // We create SSH host names in one of two formats: - // coder-vscode------ (old style) - // coder-vscode.