1
1
import { AxiosInstance , InternalAxiosRequestConfig , isAxiosError } from "axios" ;
2
2
import { spawn } from "child_process" ;
3
3
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" ;
8
5
import fs from "fs/promises" ;
9
6
import { ProxyAgent } from "proxy-agent" ;
10
7
import * as vscode from "vscode" ;
11
- import * as ws from "ws" ;
12
8
import { errToStr } from "./api-helper" ;
13
9
import { CertificateError } from "./error" ;
14
10
import { FeatureSet } from "./featureSet" ;
15
11
import { getHeaderArgs } from "./headers" ;
16
12
import { getProxyForUrl } from "./proxy" ;
17
13
import { Storage } from "./storage" ;
18
14
import { expandPath } from "./util" ;
15
+ import { CoderWebSocketClient } from "./websocket/webSocketClient" ;
19
16
20
17
export const coderSessionTokenHeader = "Coder-Session-Token" ;
21
18
@@ -68,8 +65,12 @@ export async function createHttpAgent(): Promise<ProxyAgent> {
68
65
69
66
/**
70
67
* 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
72
69
* 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
73
74
*/
74
75
export function makeCoderSdk (
75
76
baseUrl : string ,
@@ -128,14 +129,14 @@ function addLoggingInterceptors(
128
129
) {
129
130
client . interceptors . request . use (
130
131
( config ) => {
131
- const requestId = crypto . randomUUID ( ) ;
132
+ const requestId = crypto . randomUUID ( ) . replace ( / - / g , "" ) ;
132
133
( config as RequestConfigWithMetadata ) . metadata = {
133
134
requestId,
134
135
startedAt : Date . now ( ) ,
135
136
} ;
136
137
137
138
logger . trace (
138
- `Request ${ requestId } : ${ config . method ?. toUpperCase ( ) } ${ config . url } ` ,
139
+ `req ${ requestId } : ${ config . method ?. toUpperCase ( ) } ${ config . url } ` ,
139
140
config . data ?? "" ,
140
141
) ;
141
142
@@ -146,9 +147,9 @@ function addLoggingInterceptors(
146
147
if ( isAxiosError ( error ) ) {
147
148
const meta = ( error . config as RequestConfigWithMetadata ) ?. metadata ;
148
149
const requestId = meta ?. requestId ?? "n/a" ;
149
- message = `Request ${ requestId } error` ;
150
+ message = `req ${ requestId } error` ;
150
151
}
151
- logger . warn ( message , error ) ;
152
+ logger . error ( message , error ) ;
152
153
153
154
return Promise . reject ( error ) ;
154
155
} ,
@@ -161,7 +162,7 @@ function addLoggingInterceptors(
161
162
const ms = startedAt ? Date . now ( ) - startedAt : undefined ;
162
163
163
164
logger . trace (
164
- `Response ${ requestId ?? "n/a" } : ${ response . status } ${
165
+ `res ${ requestId ?? "n/a" } : ${ response . status } ${
165
166
ms !== undefined ? ` in ${ ms } ms` : ""
166
167
} `,
167
168
// { responseBody: response.data }, // TODO too noisy
@@ -177,7 +178,7 @@ function addLoggingInterceptors(
177
178
const ms = startedAt ? Date . now ( ) - startedAt : undefined ;
178
179
179
180
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` : "" } ` ;
181
182
}
182
183
logger . warn ( message , error ) ;
183
184
@@ -264,70 +265,47 @@ export async function startWorkspaceIfStoppedOrFailed(
264
265
*/
265
266
export async function waitForBuild (
266
267
restClient : Api ,
268
+ webSocketClient : CoderWebSocketClient ,
267
269
writeEmitter : vscode . EventEmitter < string > ,
268
270
workspace : Workspace ,
269
271
) : 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
-
275
272
// This fetches the initial bunch of logs.
276
273
const logs = await restClient . getWorkspaceBuildLogs (
277
274
workspace . latest_build . id ,
278
275
) ;
279
276
logs . forEach ( ( log ) => writeEmitter . fire ( log . output + "\r\n" ) ) ;
280
277
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 ( ) ;
290
278
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
+
291
288
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 ! ;
312
299
writeEmitter . fire ( log . output + "\r\n" ) ;
313
300
} ) ;
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 ) ;
323
305
} ) ;
324
306
} catch ( error ) {
325
307
// 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 ) ;
331
309
}
332
310
} ) ;
333
311
0 commit comments