Skip to content

Commit eb8a0a3

Browse files
committed
Unify WS implementations across VS Code
1 parent 80d2958 commit eb8a0a3

File tree

10 files changed

+213
-203
lines changed

10 files changed

+213
-203
lines changed

src/agentMetadataHelper.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { Api } from "coder/site/src/api/api";
21
import { WorkspaceAgent } from "coder/site/src/api/typesGenerated";
3-
import { ProxyAgent } from "proxy-agent";
42
import * as vscode from "vscode";
53
import {
64
AgentMetadataEvent,
75
AgentMetadataEventSchemaArray,
86
errToStr,
97
} from "./api-helper";
10-
import { watchAgentMetadata } from "./websocket/ws-helper";
8+
import { CoderWebSocketClient } from "./websocket/webSocketClient";
119

1210
export type AgentMetadataWatcher = {
1311
onChange: vscode.EventEmitter<null>["event"];
@@ -22,10 +20,9 @@ export type AgentMetadataWatcher = {
2220
*/
2321
export function createAgentMetadataWatcher(
2422
agentId: WorkspaceAgent["id"],
25-
restClient: Api,
26-
httpAgent: ProxyAgent,
23+
webSocketClient: CoderWebSocketClient,
2724
): AgentMetadataWatcher {
28-
const socket = watchAgentMetadata(restClient, httpAgent, agentId);
25+
const socket = webSocketClient.watchAgentMetadata(agentId);
2926

3027
let disposed = false;
3128
const onChange = new vscode.EventEmitter<null>();

src/api.ts

Lines changed: 38 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
11
import { AxiosInstance, InternalAxiosRequestConfig, isAxiosError } from "axios";
22
import { spawn } from "child_process";
33
import { Api } from "coder/site/src/api/api";
4-
import {
5-
ProvisionerJobLog,
6-
Workspace,
7-
} from "coder/site/src/api/typesGenerated";
4+
import { Workspace } from "coder/site/src/api/typesGenerated";
85
import fs from "fs/promises";
96
import { ProxyAgent } from "proxy-agent";
107
import * as vscode from "vscode";
11-
import * as ws from "ws";
128
import { errToStr } from "./api-helper";
139
import { CertificateError } from "./error";
1410
import { FeatureSet } from "./featureSet";
1511
import { getHeaderArgs } from "./headers";
1612
import { getProxyForUrl } from "./proxy";
1713
import { Storage } from "./storage";
1814
import { expandPath } from "./util";
15+
import { CoderWebSocketClient } from "./websocket/webSocketClient";
1916

2017
export const coderSessionTokenHeader = "Coder-Session-Token";
2118

@@ -68,8 +65,12 @@ export async function createHttpAgent(): Promise<ProxyAgent> {
6865

6966
/**
7067
* Create an sdk instance using the provided URL and token and hook it up to
71-
* configuration. The token may be undefined if some other form of
68+
* configuration. The token may be undefined if some other form of
7269
* authentication is being used.
70+
*
71+
* Automatically configures logging interceptors that log:
72+
* - Requests and responses at the trace level
73+
* - Errors at the error level
7374
*/
7475
export function makeCoderSdk(
7576
baseUrl: string,
@@ -128,14 +129,14 @@ function addLoggingInterceptors(
128129
) {
129130
client.interceptors.request.use(
130131
(config) => {
131-
const requestId = crypto.randomUUID();
132+
const requestId = crypto.randomUUID().replace(/-/g, "");
132133
(config as RequestConfigWithMetadata).metadata = {
133134
requestId,
134135
startedAt: Date.now(),
135136
};
136137

137138
logger.trace(
138-
`Request ${requestId}: ${config.method?.toUpperCase()} ${config.url}`,
139+
`req ${requestId}: ${config.method?.toUpperCase()} ${config.url}`,
139140
config.data ?? "",
140141
);
141142

@@ -146,9 +147,9 @@ function addLoggingInterceptors(
146147
if (isAxiosError(error)) {
147148
const meta = (error.config as RequestConfigWithMetadata)?.metadata;
148149
const requestId = meta?.requestId ?? "n/a";
149-
message = `Request ${requestId} error`;
150+
message = `req ${requestId} error`;
150151
}
151-
logger.warn(message, error);
152+
logger.error(message, error);
152153

153154
return Promise.reject(error);
154155
},
@@ -161,7 +162,7 @@ function addLoggingInterceptors(
161162
const ms = startedAt ? Date.now() - startedAt : undefined;
162163

163164
logger.trace(
164-
`Response ${requestId ?? "n/a"}: ${response.status}${
165+
`res ${requestId ?? "n/a"}: ${response.status}${
165166
ms !== undefined ? ` in ${ms}ms` : ""
166167
}`,
167168
// { responseBody: response.data }, // TODO too noisy
@@ -177,7 +178,7 @@ function addLoggingInterceptors(
177178
const ms = startedAt ? Date.now() - startedAt : undefined;
178179

179180
const status = error.response?.status ?? "unknown";
180-
message = `Response ${requestId}: ${status}${ms !== undefined ? ` in ${ms}ms` : ""}`;
181+
message = `res ${requestId}: ${status}${ms !== undefined ? ` in ${ms}ms` : ""}`;
181182
}
182183
logger.warn(message, error);
183184

@@ -264,70 +265,47 @@ export async function startWorkspaceIfStoppedOrFailed(
264265
*/
265266
export async function waitForBuild(
266267
restClient: Api,
268+
webSocketClient: CoderWebSocketClient,
267269
writeEmitter: vscode.EventEmitter<string>,
268270
workspace: Workspace,
269271
): Promise<Workspace> {
270-
const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL;
271-
if (!baseUrlRaw) {
272-
throw new Error("No base URL set on REST client");
273-
}
274-
275272
// This fetches the initial bunch of logs.
276273
const logs = await restClient.getWorkspaceBuildLogs(
277274
workspace.latest_build.id,
278275
);
279276
logs.forEach((log) => writeEmitter.fire(log.output + "\r\n"));
280277

281-
// This follows the logs for new activity!
282-
// TODO: watchBuildLogsByBuildId exists, but it uses `location`.
283-
// Would be nice if we could use it here.
284-
let path = `/api/v2/workspacebuilds/${workspace.latest_build.id}/logs?follow=true`;
285-
if (logs.length) {
286-
path += `&after=${logs[logs.length - 1].id}`;
287-
}
288-
289-
const agent = await createHttpAgent();
290278
await new Promise<void>((resolve, reject) => {
279+
const rejectError = (error: unknown) => {
280+
const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL!;
281+
return reject(
282+
new Error(
283+
`Failed to watch workspace build on ${baseUrlRaw}: ${errToStr(error, "no further details")}`,
284+
),
285+
);
286+
};
287+
291288
try {
292-
// TODO move to `ws-helper`
293-
const baseUrl = new URL(baseUrlRaw);
294-
const proto = baseUrl.protocol === "https:" ? "wss:" : "ws:";
295-
const socketUrlRaw = `${proto}//${baseUrl.host}${path}`;
296-
const token = restClient.getAxiosInstance().defaults.headers.common[
297-
coderSessionTokenHeader
298-
] as string | undefined;
299-
const socket = new ws.WebSocket(new URL(socketUrlRaw), {
300-
agent: agent,
301-
followRedirects: true,
302-
headers: token
303-
? {
304-
[coderSessionTokenHeader]: token,
305-
}
306-
: undefined,
307-
});
308-
socket.binaryType = "nodebuffer";
309-
socket.on("message", (data) => {
310-
const buf = data as Buffer;
311-
const log = JSON.parse(buf.toString()) as ProvisionerJobLog;
289+
const socket = webSocketClient.watchBuildLogsByBuildId(
290+
workspace.latest_build.id,
291+
logs,
292+
);
293+
const closeHandler = () => {
294+
resolve();
295+
};
296+
socket.addEventListener("close", closeHandler);
297+
socket.addEventListener("message", (data) => {
298+
const log = data.parsedMessage!;
312299
writeEmitter.fire(log.output + "\r\n");
313300
});
314-
socket.on("error", (error) => {
315-
reject(
316-
new Error(
317-
`Failed to watch workspace build using ${socketUrlRaw}: ${errToStr(error, "no further details")}`,
318-
),
319-
);
320-
});
321-
socket.on("close", () => {
322-
resolve();
301+
socket.addEventListener("error", (error) => {
302+
socket.removeEventListener("close", closeHandler);
303+
socket.close();
304+
rejectError(error);
323305
});
324306
} catch (error) {
325307
// If this errors, it is probably a malformed URL.
326-
reject(
327-
new Error(
328-
`Failed to watch workspace build on ${baseUrlRaw}: ${errToStr(error, "no further details")}`,
329-
),
330-
);
308+
reject(rejectError);
331309
}
332310
});
333311

src/extension.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CertificateError, getErrorDetail } from "./error";
1010
import { Remote } from "./remote";
1111
import { Storage } from "./storage";
1212
import { toSafeHost } from "./util";
13+
import { CoderWebSocketClient } from "./websocket/webSocketClient";
1314
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider";
1415

1516
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
@@ -69,18 +70,23 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
6970

7071
// TODO this won't get updated when users change their settings; Listen to changes and update this
7172
const httpAgent = await createHttpAgent();
73+
const webSocketClient = new CoderWebSocketClient(
74+
restClient,
75+
httpAgent,
76+
storage,
77+
);
7278
const myWorkspacesProvider = new WorkspaceProvider(
7379
WorkspaceQuery.Mine,
7480
restClient,
7581
storage,
76-
httpAgent,
82+
webSocketClient,
7783
5,
7884
);
7985
const allWorkspacesProvider = new WorkspaceProvider(
8086
WorkspaceQuery.All,
8187
restClient,
8288
storage,
83-
httpAgent,
89+
webSocketClient,
8490
);
8591

8692
// createTreeView, unlike registerTreeDataProvider, gives us the tree view API

src/inbox.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
import { Api } from "coder/site/src/api/api";
21
import {
32
Workspace,
43
GetInboxNotificationResponse,
54
} from "coder/site/src/api/typesGenerated";
6-
import { ProxyAgent } from "proxy-agent";
75
import * as vscode from "vscode";
86
import { errToStr } from "./api-helper";
97
import { type Storage } from "./storage";
10-
import { OneWayCodeWebSocket } from "./websocket/OneWayCodeWebSocket";
11-
import { watchInboxNotifications } from "./websocket/ws-helper";
8+
import { OneWayCodeWebSocket } from "./websocket/oneWayCodeWebSocket";
9+
import { CoderWebSocketClient } from "./websocket/webSocketClient";
1210

1311
// These are the template IDs of our notifications.
1412
// Maybe in the future we should avoid hardcoding
@@ -23,8 +21,7 @@ export class Inbox implements vscode.Disposable {
2321

2422
constructor(
2523
workspace: Workspace,
26-
httpAgent: ProxyAgent,
27-
restClient: Api,
24+
webSocketClient: CoderWebSocketClient,
2825
storage: Storage,
2926
) {
3027
this.#storage = storage;
@@ -36,9 +33,7 @@ export class Inbox implements vscode.Disposable {
3633

3734
const watchTargets = [workspace.id];
3835

39-
this.#socket = watchInboxNotifications(
40-
restClient,
41-
httpAgent,
36+
this.#socket = webSocketClient.watchInboxNotifications(
4237
watchTemplates,
4338
watchTargets,
4439
);
@@ -47,8 +42,8 @@ export class Inbox implements vscode.Disposable {
4742
this.#storage.output.info("Listening to Coder Inbox");
4843
});
4944

50-
this.#socket.addEventListener("error", (error) => {
51-
this.notifyError(error);
45+
this.#socket.addEventListener("error", () => {
46+
// Errors are already logged internally
5247
this.dispose();
5348
});
5449

src/remote.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as jsonc from "jsonc-parser";
77
import * as os from "os";
88
import * as path from "path";
99
import prettyBytes from "pretty-bytes";
10-
import { ProxyAgent } from "proxy-agent";
1110
import * as semver from "semver";
1211
import * as vscode from "vscode";
1312
import {
@@ -39,6 +38,7 @@ import {
3938
findPort,
4039
parseRemoteAuthority,
4140
} from "./util";
41+
import { CoderWebSocketClient } from "./websocket/webSocketClient";
4242
import { WorkspaceMonitor } from "./workspaceMonitor";
4343

4444
export interface RemoteDetails extends vscode.Disposable {
@@ -493,27 +493,27 @@ export class Remote {
493493
}
494494

495495
const httpAgent = await createHttpAgent();
496+
const webSocketClient = new CoderWebSocketClient(
497+
workspaceRestClient,
498+
httpAgent,
499+
this.storage,
500+
);
496501

497502
// Watch the workspace for changes.
498503
const monitor = new WorkspaceMonitor(
499504
workspace,
500505
workspaceRestClient,
501506
this.storage,
502507
this.vscodeProposed,
503-
httpAgent,
508+
webSocketClient,
504509
);
505510
disposables.push(monitor);
506511
disposables.push(
507512
monitor.onChange.event((w) => (this.commands.workspace = w)),
508513
);
509514

510515
// Watch coder inbox for messages
511-
const inbox = new Inbox(
512-
workspace,
513-
httpAgent,
514-
workspaceRestClient,
515-
this.storage,
516-
);
516+
const inbox = new Inbox(workspace, webSocketClient, this.storage);
517517
disposables.push(inbox);
518518

519519
// Wait for the agent to connect.
@@ -631,11 +631,7 @@ export class Remote {
631631
);
632632

633633
disposables.push(
634-
...this.createAgentMetadataStatusBar(
635-
agent,
636-
workspaceRestClient,
637-
httpAgent,
638-
),
634+
...this.createAgentMetadataStatusBar(agent, webSocketClient),
639635
);
640636

641637
this.storage.output.info("Remote setup complete");
@@ -987,19 +983,14 @@ export class Remote {
987983
*/
988984
private createAgentMetadataStatusBar(
989985
agent: WorkspaceAgent,
990-
restClient: Api,
991-
httpAgent: ProxyAgent,
986+
webSocketClient: CoderWebSocketClient,
992987
): vscode.Disposable[] {
993988
const statusBarItem = vscode.window.createStatusBarItem(
994989
"agentMetadata",
995990
vscode.StatusBarAlignment.Left,
996991
);
997992

998-
const agentWatcher = createAgentMetadataWatcher(
999-
agent.id,
1000-
restClient,
1001-
httpAgent,
1002-
);
993+
const agentWatcher = createAgentMetadataWatcher(agent.id, webSocketClient);
1003994

1004995
const onChangeDisposable = agentWatcher.onChange(() => {
1005996
if (agentWatcher.error) {
File renamed without changes.

0 commit comments

Comments
 (0)