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 { getGlobalFlags } from "./globalFlags" ;
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,9 +178,9 @@ 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
- logger . warn ( message , error ) ;
183
+ logger . error ( message , error ) ;
183
184
184
185
return Promise . reject ( error ) ;
185
186
} ,
@@ -262,71 +263,44 @@ export async function startWorkspaceIfStoppedOrFailed(
262
263
*/
263
264
export async function waitForBuild (
264
265
restClient : Api ,
266
+ webSocketClient : CoderWebSocketClient ,
265
267
writeEmitter : vscode . EventEmitter < string > ,
266
268
workspace : Workspace ,
267
269
) : 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
-
273
270
// This fetches the initial bunch of logs.
274
271
const logs = await restClient . getWorkspaceBuildLogs (
275
272
workspace . latest_build . id ,
276
273
) ;
277
274
logs . forEach ( ( log ) => writeEmitter . fire ( log . output + "\r\n" ) ) ;
278
275
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 ( ) ;
288
276
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 (
325
280
new Error (
326
281
`Failed to watch workspace build on ${ baseUrlRaw } : ${ errToStr ( error , "no further details" ) } ` ,
327
282
) ,
328
283
) ;
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
+ } ) ;
330
304
} ) ;
331
305
332
306
writeEmitter . fire ( "Build complete\r\n" ) ;
0 commit comments