Skip to content

Commit cf230ff

Browse files
committed
Unify WS implementations across VS Code
1 parent 67f7268 commit cf230ff

File tree

10 files changed

+227
-225
lines changed

10 files changed

+227
-225
lines changed

src/agentMetadataHelper.ts

Lines changed: 4 additions & 7 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>();
@@ -42,7 +39,7 @@ export function createAgentMetadataWatcher(
4239
socket.addEventListener("message", (event) => {
4340
try {
4441
const metadata = AgentMetadataEventSchemaArray.parse(
45-
event.parsedMessage?.data,
42+
event.parsedMessage!.data,
4643
);
4744

4845
// Overwrite metadata if it changed.

src/api.ts

Lines changed: 38 additions & 64 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 { getGlobalFlags } from "./globalFlags";
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,9 +178,9 @@ 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
}
182-
logger.warn(message, error);
183+
logger.error(message, error);
183184

184185
return Promise.reject(error);
185186
},
@@ -262,71 +263,44 @@ export async function startWorkspaceIfStoppedOrFailed(
262263
*/
263264
export async function waitForBuild(
264265
restClient: Api,
266+
webSocketClient: CoderWebSocketClient,
265267
writeEmitter: vscode.EventEmitter<string>,
266268
workspace: Workspace,
267269
): Promise<Workspace> {
268-
const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL;
269-
if (!baseUrlRaw) {
270-
throw new Error("No base URL set on REST client");
271-
}
272-
273270
// This fetches the initial bunch of logs.
274271
const logs = await restClient.getWorkspaceBuildLogs(
275272
workspace.latest_build.id,
276273
);
277274
logs.forEach((log) => writeEmitter.fire(log.output + "\r\n"));
278275

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

332306
writeEmitter.fire("Build complete\r\n");

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: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
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";
8-
import { errToStr } from "./api-helper";
96
import { type Storage } from "./storage";
10-
import { OneWayCodeWebSocket } from "./websocket/OneWayCodeWebSocket";
11-
import { watchInboxNotifications } from "./websocket/ws-helper";
7+
import { OneWayCodeWebSocket } from "./websocket/oneWayCodeWebSocket";
8+
import { CoderWebSocketClient } from "./websocket/webSocketClient";
129

1310
// These are the template IDs of our notifications.
1411
// Maybe in the future we should avoid hardcoding
@@ -23,8 +20,7 @@ export class Inbox implements vscode.Disposable {
2320

2421
constructor(
2522
workspace: Workspace,
26-
httpAgent: ProxyAgent,
27-
restClient: Api,
23+
webSocketClient: CoderWebSocketClient,
2824
storage: Storage,
2925
) {
3026
this.#storage = storage;
@@ -36,9 +32,7 @@ export class Inbox implements vscode.Disposable {
3632

3733
const watchTargets = [workspace.id];
3834

39-
this.#socket = watchInboxNotifications(
40-
restClient,
41-
httpAgent,
35+
this.#socket = webSocketClient.watchInboxNotifications(
4236
watchTemplates,
4337
watchTargets,
4438
);
@@ -47,18 +41,14 @@ export class Inbox implements vscode.Disposable {
4741
this.#storage.output.info("Listening to Coder Inbox");
4842
});
4943

50-
this.#socket.addEventListener("error", (error) => {
51-
this.notifyError(error);
44+
this.#socket.addEventListener("error", () => {
45+
// Errors are already logged internally
5246
this.dispose();
5347
});
5448

5549
this.#socket.addEventListener("message", (data) => {
56-
try {
57-
const inboxMessage = data.parsedMessage!;
58-
vscode.window.showInformationMessage(inboxMessage.notification.title);
59-
} catch (error) {
60-
this.notifyError(error);
61-
}
50+
const inboxMessage = data.parsedMessage!;
51+
vscode.window.showInformationMessage(inboxMessage.notification.title);
6252
});
6353
}
6454

@@ -69,12 +59,4 @@ export class Inbox implements vscode.Disposable {
6959
this.#disposed = true;
7060
}
7161
}
72-
73-
private notifyError(error: unknown) {
74-
const message = errToStr(
75-
error,
76-
"Got empty error while monitoring Coder Inbox",
77-
);
78-
this.#storage.output.error(message);
79-
}
8062
}

0 commit comments

Comments
 (0)