Skip to content

Commit 022e832

Browse files
committed
Merge remote-tracking branch 'github/main' into asher/binary-verification
2 parents c4f3d0a + cb2a4ec commit 022e832

File tree

10 files changed

+1184
-268
lines changed

10 files changed

+1184
-268
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
and configFile are provided.
1414
- Add `coder.disableUpdateNotifications` setting to disable workspace template
1515
update notifications.
16+
- Coder output panel enhancements: All log entries now include timestamps, and you
17+
can filter messages by log level in the panel.
18+
- Consistently use the same session for each agent. Previously,
19+
depending on how you connected, it could be possible to get two
20+
different sessions for an agent. Existing connections may still
21+
have this problem, only new connections are fixed.
22+
- Added an agent metadata monitor status bar item, so you can view your active
23+
agent metadata at a glance.
1624
- Add binary signature verification. This can be disabled with
1725
`coder.disableSignatureVerification` if you purposefully run a binary that is
1826
not signed by Coder (for example a binary you built yourself).

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,8 @@
295295
"memfs": "^4.17.1",
296296
"node-forge": "^1.3.1",
297297
"openpgp": "^6.2.0",
298-
"pretty-bytes": "^6.1.1",
299-
"proxy-agent": "^6.4.0",
298+
"pretty-bytes": "^7.0.0",
299+
"proxy-agent": "^6.5.0",
300300
"semver": "^7.7.1",
301301
"ua-parser-js": "1.0.40",
302302
"ws": "^8.18.2",
@@ -314,7 +314,7 @@
314314
"@typescript-eslint/parser": "^6.21.0",
315315
"@vscode/test-cli": "^0.0.10",
316316
"@vscode/test-electron": "^2.5.2",
317-
"@vscode/vsce": "^2.21.1",
317+
"@vscode/vsce": "^3.6.0",
318318
"bufferutil": "^4.0.9",
319319
"coder": "https://github.com/coder/coder#main",
320320
"dayjs": "^1.11.13",
@@ -329,8 +329,7 @@
329329
"nyc": "^17.1.0",
330330
"prettier": "^3.5.3",
331331
"ts-loader": "^9.5.1",
332-
"tsc-watch": "^6.2.1",
333-
"typescript": "^5.4.5",
332+
"typescript": "^5.8.3",
334333
"utf-8-validate": "^6.0.5",
335334
"vitest": "^0.34.6",
336335
"vscode-test": "^1.5.0",

src/agentMetadataHelper.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { Api } from "coder/site/src/api/api";
2+
import { WorkspaceAgent } from "coder/site/src/api/typesGenerated";
3+
import { EventSource } from "eventsource";
4+
import * as vscode from "vscode";
5+
import { createStreamingFetchAdapter } from "./api";
6+
import {
7+
AgentMetadataEvent,
8+
AgentMetadataEventSchemaArray,
9+
errToStr,
10+
} from "./api-helper";
11+
12+
export type AgentMetadataWatcher = {
13+
onChange: vscode.EventEmitter<null>["event"];
14+
dispose: () => void;
15+
metadata?: AgentMetadataEvent[];
16+
error?: unknown;
17+
};
18+
19+
/**
20+
* Opens an SSE connection to watch metadata for a given workspace agent.
21+
* Emits onChange when metadata updates or an error occurs.
22+
*/
23+
export function createAgentMetadataWatcher(
24+
agentId: WorkspaceAgent["id"],
25+
restClient: Api,
26+
): AgentMetadataWatcher {
27+
// TODO: Is there a better way to grab the url and token?
28+
const url = restClient.getAxiosInstance().defaults.baseURL;
29+
const metadataUrl = new URL(
30+
`${url}/api/v2/workspaceagents/${agentId}/watch-metadata`,
31+
);
32+
const eventSource = new EventSource(metadataUrl.toString(), {
33+
fetch: createStreamingFetchAdapter(restClient.getAxiosInstance()),
34+
});
35+
36+
let disposed = false;
37+
const onChange = new vscode.EventEmitter<null>();
38+
const watcher: AgentMetadataWatcher = {
39+
onChange: onChange.event,
40+
dispose: () => {
41+
if (!disposed) {
42+
eventSource.close();
43+
disposed = true;
44+
}
45+
},
46+
};
47+
48+
eventSource.addEventListener("data", (event) => {
49+
try {
50+
const dataEvent = JSON.parse(event.data);
51+
const metadata = AgentMetadataEventSchemaArray.parse(dataEvent);
52+
53+
// Overwrite metadata if it changed.
54+
if (JSON.stringify(watcher.metadata) !== JSON.stringify(metadata)) {
55+
watcher.metadata = metadata;
56+
onChange.fire(null);
57+
}
58+
} catch (error) {
59+
watcher.error = error;
60+
onChange.fire(null);
61+
}
62+
});
63+
64+
return watcher;
65+
}
66+
67+
export function formatMetadataError(error: unknown): string {
68+
return "Failed to query metadata: " + errToStr(error, "no error provided");
69+
}
70+
71+
export function formatEventLabel(metadataEvent: AgentMetadataEvent): string {
72+
return getEventName(metadataEvent) + ": " + getEventValue(metadataEvent);
73+
}
74+
75+
export function getEventName(metadataEvent: AgentMetadataEvent): string {
76+
return metadataEvent.description.display_name.trim();
77+
}
78+
79+
export function getEventValue(metadataEvent: AgentMetadataEvent): string {
80+
return metadataEvent.result.value.replace(/\n/g, "").trim();
81+
}

src/api.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as vscode from "vscode";
1212
import * as ws from "ws";
1313
import { errToStr } from "./api-helper";
1414
import { CertificateError } from "./error";
15+
import { FeatureSet } from "./featureSet";
1516
import { getHeaderArgs } from "./headers";
1617
import { getProxyForUrl } from "./proxy";
1718
import { Storage } from "./storage";
@@ -174,6 +175,7 @@ export async function startWorkspaceIfStoppedOrFailed(
174175
binPath: string,
175176
workspace: Workspace,
176177
writeEmitter: vscode.EventEmitter<string>,
178+
featureSet: FeatureSet,
177179
): Promise<Workspace> {
178180
// Before we start a workspace, we make an initial request to check it's not already started
179181
const updatedWorkspace = await restClient.getWorkspace(workspace.id);
@@ -191,6 +193,10 @@ export async function startWorkspaceIfStoppedOrFailed(
191193
"--yes",
192194
workspace.owner_name + "/" + workspace.name,
193195
];
196+
if (featureSet.buildReason) {
197+
startArgs.push(...["--reason", "vscode_connection"]);
198+
}
199+
194200
const startProcess = spawn(binPath, startArgs);
195201

196202
startProcess.stdout.on("data", (data: Buffer) => {

src/commands.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -437,12 +437,15 @@ export class Commands {
437437
if (!baseUrl) {
438438
throw new Error("You are not logged in");
439439
}
440+
if (treeItem.primaryAgentName === undefined) {
441+
return;
442+
}
440443
await openWorkspace(
441444
baseUrl,
442445
treeItem.workspaceOwner,
443446
treeItem.workspaceName,
444-
treeItem.workspaceAgent,
445-
treeItem.workspaceFolderPath,
447+
treeItem.primaryAgentName,
448+
treeItem.primaryAgentFolderPath,
446449
true,
447450
);
448451
} else {
@@ -525,6 +528,8 @@ export class Commands {
525528
let folderPath: string | undefined;
526529
let openRecent: boolean | undefined;
527530

531+
let workspace: Workspace | undefined;
532+
528533
const baseUrl = this.restClient.getAxiosInstance().defaults.baseURL;
529534
if (!baseUrl) {
530535
throw new Error("You are not logged in");
@@ -571,7 +576,7 @@ export class Commands {
571576
});
572577
});
573578
quickPick.show();
574-
const workspace = await new Promise<Workspace | undefined>((resolve) => {
579+
workspace = await new Promise<Workspace | undefined>((resolve) => {
575580
quickPick.onDidHide(() => {
576581
resolve(undefined);
577582
});
@@ -590,20 +595,31 @@ export class Commands {
590595
}
591596
workspaceOwner = workspace.owner_name;
592597
workspaceName = workspace.name;
598+
} else {
599+
workspaceOwner = args[0] as string;
600+
workspaceName = args[1] as string;
601+
workspaceAgent = args[2] as string | undefined;
602+
folderPath = args[3] as string | undefined;
603+
openRecent = args[4] as boolean | undefined;
604+
}
605+
606+
if (!workspaceAgent) {
607+
if (workspace === undefined) {
608+
workspace = await this.restClient.getWorkspaceByOwnerAndName(
609+
workspaceOwner,
610+
workspaceName,
611+
);
612+
}
593613

594614
const agent = await this.maybeAskAgent(workspace);
595615
if (!agent) {
596616
// User declined to pick an agent.
597617
return;
598618
}
599-
folderPath = agent.expanded_directory;
619+
if (!folderPath) {
620+
folderPath = agent.expanded_directory;
621+
}
600622
workspaceAgent = agent.name;
601-
} else {
602-
workspaceOwner = args[0] as string;
603-
workspaceName = args[1] as string;
604-
workspaceAgent = args[2] as string | undefined;
605-
folderPath = args[3] as string | undefined;
606-
openRecent = args[4] as boolean | undefined;
607623
}
608624

609625
await openWorkspace(
@@ -655,14 +671,15 @@ export class Commands {
655671
if (!this.workspace || !this.workspaceRestClient) {
656672
return;
657673
}
658-
const action = await this.vscodeProposed.window.showInformationMessage(
674+
const action = await this.vscodeProposed.window.showWarningMessage(
659675
"Update Workspace",
660676
{
661677
useCustom: true,
662678
modal: true,
663-
detail: `Update ${this.workspace.owner_name}/${this.workspace.name} to the latest version?`,
679+
detail: `Update ${this.workspace.owner_name}/${this.workspace.name} to the latest version?\n\nUpdating will restart your workspace which stops any running processes and may result in the loss of unsaved work.`,
664680
},
665681
"Update",
682+
"Cancel",
666683
);
667684
if (action === "Update") {
668685
await this.workspaceRestClient.updateWorkspaceVersion(this.workspace);
@@ -678,7 +695,7 @@ async function openWorkspace(
678695
baseUrl: string,
679696
workspaceOwner: string,
680697
workspaceName: string,
681-
workspaceAgent: string | undefined,
698+
workspaceAgent: string,
682699
folderPath: string | undefined,
683700
openRecent: boolean | undefined,
684701
) {

src/featureSet.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export type FeatureSet = {
44
vscodessh: boolean;
55
proxyLogDirectory: boolean;
66
wildcardSSH: boolean;
7+
buildReason: boolean;
78
};
89

910
/**
@@ -29,5 +30,10 @@ export function featureSetForVersion(
2930
wildcardSSH:
3031
(version ? version.compare("2.19.0") : -1) >= 0 ||
3132
version?.prerelease[0] === "devel",
33+
34+
// The --reason flag was added to `coder start` in 2.25.0
35+
buildReason:
36+
(version?.compare("2.25.0") || 0) >= 0 ||
37+
version?.prerelease[0] === "devel",
3238
};
3339
}

src/remote.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { isAxiosError } from "axios";
22
import { Api } from "coder/site/src/api/api";
3-
import { Workspace } from "coder/site/src/api/typesGenerated";
3+
import { Workspace, WorkspaceAgent } from "coder/site/src/api/typesGenerated";
44
import find from "find-process";
55
import * as fs from "fs/promises";
66
import * as jsonc from "jsonc-parser";
@@ -9,6 +9,12 @@ import * as path from "path";
99
import prettyBytes from "pretty-bytes";
1010
import * as semver from "semver";
1111
import * as vscode from "vscode";
12+
import {
13+
createAgentMetadataWatcher,
14+
getEventValue,
15+
formatEventLabel,
16+
formatMetadataError,
17+
} from "./agentMetadataHelper";
1218
import {
1319
createHttpAgent,
1420
makeCoderSdk,
@@ -68,6 +74,7 @@ export class Remote {
6874
workspace: Workspace,
6975
label: string,
7076
binPath: string,
77+
featureSet: FeatureSet,
7178
): Promise<Workspace | undefined> {
7279
const workspaceName = `${workspace.owner_name}/${workspace.name}`;
7380

@@ -136,6 +143,7 @@ export class Remote {
136143
binPath,
137144
workspace,
138145
writeEmitter,
146+
featureSet,
139147
);
140148
break;
141149
case "failed":
@@ -153,6 +161,7 @@ export class Remote {
153161
binPath,
154162
workspace,
155163
writeEmitter,
164+
featureSet,
156165
);
157166
break;
158167
}
@@ -383,6 +392,7 @@ export class Remote {
383392
workspace,
384393
parts.label,
385394
binaryPath,
395+
featureSet,
386396
);
387397
if (!updatedWorkspace) {
388398
// User declined to start the workspace.
@@ -620,6 +630,10 @@ export class Remote {
620630
}),
621631
);
622632

633+
disposables.push(
634+
...this.createAgentMetadataStatusBar(agent, workspaceRestClient),
635+
);
636+
623637
this.storage.output.info("Remote setup complete");
624638

625639
// Returning the URL and token allows the plugin to authenticate its own
@@ -962,6 +976,56 @@ export class Remote {
962976
return loop();
963977
}
964978

979+
/**
980+
* Creates and manages a status bar item that displays metadata information for a given workspace agent.
981+
* The status bar item updates dynamically based on changes to the agent's metadata,
982+
* and hides itself if no metadata is available or an error occurs.
983+
*/
984+
private createAgentMetadataStatusBar(
985+
agent: WorkspaceAgent,
986+
restClient: Api,
987+
): vscode.Disposable[] {
988+
const statusBarItem = vscode.window.createStatusBarItem(
989+
"agentMetadata",
990+
vscode.StatusBarAlignment.Left,
991+
);
992+
993+
const agentWatcher = createAgentMetadataWatcher(agent.id, restClient);
994+
995+
const onChangeDisposable = agentWatcher.onChange(() => {
996+
if (agentWatcher.error) {
997+
const errMessage = formatMetadataError(agentWatcher.error);
998+
this.storage.output.warn(errMessage);
999+
1000+
statusBarItem.text = "$(warning) Agent Status Unavailable";
1001+
statusBarItem.tooltip = errMessage;
1002+
statusBarItem.color = new vscode.ThemeColor(
1003+
"statusBarItem.warningForeground",
1004+
);
1005+
statusBarItem.backgroundColor = new vscode.ThemeColor(
1006+
"statusBarItem.warningBackground",
1007+
);
1008+
statusBarItem.show();
1009+
return;
1010+
}
1011+
1012+
if (agentWatcher.metadata && agentWatcher.metadata.length > 0) {
1013+
statusBarItem.text =
1014+
"$(dashboard) " + getEventValue(agentWatcher.metadata[0]);
1015+
statusBarItem.tooltip = agentWatcher.metadata
1016+
.map((metadata) => formatEventLabel(metadata))
1017+
.join("\n");
1018+
statusBarItem.color = undefined;
1019+
statusBarItem.backgroundColor = undefined;
1020+
statusBarItem.show();
1021+
} else {
1022+
statusBarItem.hide();
1023+
}
1024+
});
1025+
1026+
return [statusBarItem, agentWatcher, onChangeDisposable];
1027+
}
1028+
9651029
// closeRemote ends the current remote session.
9661030
public async closeRemote() {
9671031
await vscode.commands.executeCommand("workbench.action.remote.close");

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy