Skip to content

Commit e58a26b

Browse files
committed
Merge branch 'main' into show-agent-metadata
2 parents 21cc7b6 + 4c37680 commit e58a26b

File tree

6 files changed

+147
-32
lines changed

6 files changed

+147
-32
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"displayName": "Coder Remote",
55
"description": "Open any workspace with a single click.",
66
"repository": "https://github.com/coder/vscode-coder",
7-
"version": "0.1.17",
7+
"version": "0.1.18",
88
"engines": {
99
"vscode": "^1.73.0"
1010
},

src/commands.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ export class Commands {
217217
if (agents.length === 1) {
218218
folderPath = agents[0].expanded_directory
219219
workspaceAgent = agents[0].name
220-
} else {
220+
} else if (agents.length > 0) {
221221
const agentQuickPick = vscode.window.createQuickPick()
222222
agentQuickPick.title = `Select an agent`
223223

src/remote.ts

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import * as vscode from "vscode"
2121
import * as ws from "ws"
2222
import { z } from "zod"
2323
import { SSHConfig, SSHValues, defaultSSHConfigResponse, mergeSSHConfigValues } from "./sshConfig"
24-
import { sshSupportsSetEnv } from "./sshSupport"
24+
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
2525
import { Storage } from "./storage"
2626

2727
export class Remote {
@@ -123,7 +123,9 @@ export class Remote {
123123

124124
const disposables: vscode.Disposable[] = []
125125
// Register before connection so the label still displays!
126-
disposables.push(this.registerLabelFormatter(`${this.storage.workspace.owner_name}/${this.storage.workspace.name}`))
126+
disposables.push(
127+
this.registerLabelFormatter(remoteAuthority, this.storage.workspace.owner_name, this.storage.workspace.name),
128+
)
127129

128130
let buildComplete: undefined | (() => void)
129131
if (this.storage.workspace.latest_build.status === "stopped") {
@@ -409,7 +411,7 @@ export class Remote {
409411
//
410412
// If we didn't write to the SSH config file, connecting would fail with
411413
// "Host not found".
412-
await this.updateSSHConfig()
414+
await this.updateSSHConfig(authorityParts[1])
413415

414416
this.findSSHProcessID().then((pid) => {
415417
if (!pid) {
@@ -420,14 +422,11 @@ export class Remote {
420422
})
421423

422424
// Register the label formatter again because SSH overrides it!
423-
let label = `${this.storage.workspace.owner_name}/${this.storage.workspace.name}`
424-
if (agents.length > 1) {
425-
label += `/${agent.name}`
426-
}
427-
425+
const workspace = this.storage.workspace
426+
const agentName = agents.length > 1 ? agent.name : undefined
428427
disposables.push(
429428
vscode.extensions.onDidChange(() => {
430-
disposables.push(this.registerLabelFormatter(label))
429+
disposables.push(this.registerLabelFormatter(remoteAuthority, workspace.owner_name, workspace.name, agentName))
431430
}),
432431
)
433432

@@ -506,7 +505,7 @@ export class Remote {
506505

507506
// updateSSHConfig updates the SSH configuration with a wildcard that handles
508507
// all Coder entries.
509-
private async updateSSHConfig() {
508+
private async updateSSHConfig(hostName: string) {
510509
let deploymentSSHConfig = defaultSSHConfigResponse
511510
try {
512511
const deploymentConfig = await getDeploymentSSHConfig()
@@ -594,6 +593,34 @@ export class Remote {
594593
}
595594

596595
await sshConfig.update(sshValues, sshConfigOverrides)
596+
597+
// A user can provide a "Host *" entry in their SSH config to add options
598+
// to all hosts. We need to ensure that the options we set are not
599+
// overridden by the user's config.
600+
const computedProperties = computeSSHProperties(hostName, sshConfig.getRaw())
601+
const keysToMatch: Array<keyof SSHValues> = ["ProxyCommand", "UserKnownHostsFile", "StrictHostKeyChecking"]
602+
for (let i = 0; i < keysToMatch.length; i++) {
603+
const key = keysToMatch[i]
604+
if (computedProperties[key] === sshValues[key]) {
605+
continue
606+
}
607+
608+
const result = await this.vscodeProposed.window.showErrorMessage(
609+
"Unexpected SSH Config Option",
610+
{
611+
useCustom: true,
612+
modal: true,
613+
detail: `Your SSH config is overriding the "${key}" property to "${computedProperties[key]}" when it expected "${sshValues[key]}" for the "${hostName}" host. Please fix this and try again!`,
614+
},
615+
"Reload Window",
616+
)
617+
if (result === "Reload Window") {
618+
await this.reloadWindow()
619+
}
620+
await this.closeRemote()
621+
}
622+
623+
return sshConfig.getRaw()
597624
}
598625

599626
// showNetworkUpdates finds the SSH process ID that is being used by this
@@ -744,14 +771,34 @@ export class Remote {
744771
await vscode.commands.executeCommand("workbench.action.reloadWindow")
745772
}
746773

747-
private registerLabelFormatter(suffix: string): vscode.Disposable {
774+
private registerLabelFormatter(
775+
remoteAuthority: string,
776+
owner: string,
777+
workspace: string,
778+
agent?: string,
779+
): vscode.Disposable {
780+
// VS Code splits based on the separator when displaying the label
781+
// in a recently opened dialog. If the workspace suffix contains /,
782+
// then it'll visually display weird:
783+
// "/home/kyle [Coder: kyle/workspace]" displays as "workspace] /home/kyle [Coder: kyle"
784+
// For this reason, we use a different / that visually appears the
785+
// same on non-monospace fonts "∕".
786+
let suffix = `Coder: ${owner}${workspace}`
787+
if (agent) {
788+
suffix += `∕${agent}`
789+
}
790+
// VS Code caches resource label formatters in it's global storage SQLite database
791+
// under the key "memento/cachedResourceLabelFormatters2".
748792
return this.vscodeProposed.workspace.registerResourceLabelFormatter({
749793
scheme: "vscode-remote",
794+
// authority is optional but VS Code prefers formatters that most
795+
// accurately match the requested authority, so we include it.
796+
authority: remoteAuthority,
750797
formatting: {
751798
label: "${path}",
752799
separator: "/",
753800
tildify: true,
754-
workspaceSuffix: `Coder: ${suffix}`,
801+
workspaceSuffix: suffix,
755802
},
756803
})
757804
}

src/sshSupport.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { it, expect } from "vitest"
2-
import { sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport"
2+
import { computeSSHProperties, sshSupportsSetEnv, sshVersionSupportsSetEnv } from "./sshSupport"
33

44
const supports = {
55
"OpenSSH_8.9p1 Ubuntu-3ubuntu0.1, OpenSSL 3.0.2 15 Mar 2022": true,
6+
"OpenSSH_9.0p1, LibreSSL 3.3.6": true,
67
"OpenSSH_7.6p1 Ubuntu-4ubuntu0.7, OpenSSL 1.0.2n 7 Dec 2017": false,
78
"OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017": false,
89
}
@@ -16,3 +17,23 @@ Object.entries(supports).forEach(([version, expected]) => {
1617
it("current shell supports ssh", () => {
1718
expect(sshSupportsSetEnv()).toBeTruthy()
1819
})
20+
21+
it("computes the config for a host", () => {
22+
const properties = computeSSHProperties(
23+
"coder-vscode--testing",
24+
`Host *
25+
StrictHostKeyChecking yes
26+
27+
# --- START CODER VSCODE ---
28+
Host coder-vscode--*
29+
StrictHostKeyChecking no
30+
Another=true
31+
# --- END CODER VSCODE ---
32+
`,
33+
)
34+
35+
expect(properties).toEqual({
36+
Another: "true",
37+
StrictHostKeyChecking: "yes",
38+
})
39+
})

src/sshSupport.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,67 @@ export function sshVersionSupportsSetEnv(sshVersionString: string): boolean {
2424
return false
2525
}
2626
// 7.8 is the first version that supports SetEnv
27-
if (Number.parseInt(parts[0], 10) < 7) {
27+
const major = Number.parseInt(parts[0], 10)
28+
const minor = Number.parseInt(parts[1], 10)
29+
if (major < 7) {
2830
return false
2931
}
30-
if (Number.parseInt(parts[1], 10) < 8) {
32+
if (major === 7 && minor < 8) {
3133
return false
3234
}
3335
return true
3436
}
3537
return false
3638
}
39+
40+
// computeSSHProperties accepts an SSH config and a host name and returns
41+
// the properties that should be set for that host.
42+
export function computeSSHProperties(host: string, config: string): Record<string, string> {
43+
let currentConfig:
44+
| {
45+
Host: string
46+
properties: Record<string, string>
47+
}
48+
| undefined
49+
const configs: Array<typeof currentConfig> = []
50+
config.split("\n").forEach((line) => {
51+
line = line.trim()
52+
if (line === "") {
53+
return
54+
}
55+
const [key, ...valueParts] = line.split(/\s+|=/)
56+
if (key.startsWith("#")) {
57+
// Ignore comments!
58+
return
59+
}
60+
if (key === "Host") {
61+
if (currentConfig) {
62+
configs.push(currentConfig)
63+
}
64+
currentConfig = {
65+
Host: valueParts.join(" "),
66+
properties: {},
67+
}
68+
return
69+
}
70+
if (!currentConfig) {
71+
return
72+
}
73+
currentConfig.properties[key] = valueParts.join(" ")
74+
})
75+
if (currentConfig) {
76+
configs.push(currentConfig)
77+
}
78+
79+
const merged: Record<string, string> = {}
80+
configs.reverse().forEach((config) => {
81+
if (!config) {
82+
return
83+
}
84+
if (!new RegExp("^" + config?.Host.replace(/\*/g, ".*") + "$").test(host)) {
85+
return
86+
}
87+
Object.assign(merged, config.properties)
88+
})
89+
return merged
90+
}

src/storage.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,13 @@ export class Storage {
5252
}
5353

5454
public async getSessionToken(): Promise<string | undefined> {
55-
return this.secrets.get("sessionToken")
55+
try {
56+
return await this.secrets.get("sessionToken")
57+
} catch (ex) {
58+
// The VS Code session store has become corrupt before, and
59+
// will fail to get the session token...
60+
return undefined
61+
}
5662
}
5763

5864
// getRemoteSSHLogPath returns the log path for the "Remote - SSH" output panel.
@@ -264,7 +270,7 @@ export class Storage {
264270
}
265271

266272
public getUserSettingsPath(): string {
267-
return path.join(this.appDataDir(), "Code", "User", "settings.json")
273+
return path.join(this.globalStorageUri.fsPath, "..", "..", "..", "User", "settings.json")
268274
}
269275

270276
public getSessionTokenPath(): string {
@@ -292,19 +298,6 @@ export class Storage {
292298
})
293299
}
294300

295-
private appDataDir(): string {
296-
switch (process.platform) {
297-
case "darwin":
298-
return `${os.homedir()}/Library/Application Support`
299-
case "linux":
300-
return `${os.homedir()}/.config`
301-
case "win32":
302-
return process.env.APPDATA || ""
303-
default:
304-
return "/var/local"
305-
}
306-
}
307-
308301
private async updateURL(): Promise<void> {
309302
const url = this.getURL()
310303
axios.defaults.baseURL = url

0 commit comments

Comments
 (0)