1- import { spawn } from "child_process" ;
21import { type Api } from "coder/site/src/api/api" ;
3- import { type Workspace } from "coder/site/src/api/typesGenerated" ;
2+ import {
3+ type WorkspaceAgentLog ,
4+ type ProvisionerJobLog ,
5+ type Workspace ,
6+ type WorkspaceAgent ,
7+ } from "coder/site/src/api/typesGenerated" ;
8+ import { spawn } from "node:child_process" ;
49import * as vscode from "vscode" ;
510
611import { type FeatureSet } from "../featureSet" ;
712import { getGlobalFlags } from "../globalFlags" ;
813import { escapeCommandArg } from "../util" ;
14+ import { type OneWayWebSocket } from "../websocket/oneWayWebSocket" ;
915
1016import { errToStr , createWorkspaceIdentifier } from "./api-helper" ;
1117import { type CoderApi } from "./coderApi" ;
@@ -36,35 +42,33 @@ export async function startWorkspaceIfStoppedOrFailed(
3642createWorkspaceIdentifier ( workspace ) ,
3743] ;
3844if ( featureSet . buildReason ) {
39- startArgs . push ( ... [ "--reason" , "vscode_connection" ] ) ;
45+ startArgs . push ( "--reason" , "vscode_connection" ) ;
4046}
4147
4248// { shell: true } requires one shell-safe command string, otherwise we lose all escaping
4349const cmd = `${ escapeCommandArg ( binPath ) } ${ startArgs . join ( " " ) } ` ;
4450const startProcess = spawn ( cmd , { shell :true } ) ;
4551
4652startProcess . stdout . on ( "data" , ( data :Buffer ) => {
47- data
53+ const lines = data
4854. toString ( )
4955. split ( / \r * \n / )
50- . forEach ( ( line :string ) => {
51- if ( line !== "" ) {
52- writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
53- }
54- } ) ;
56+ . filter ( ( line ) => line !== "" ) ;
57+ for ( const line of lines ) {
58+ writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
59+ }
5560} ) ;
5661
5762let capturedStderr = "" ;
5863startProcess . stderr . on ( "data" , ( data :Buffer ) => {
59- data
64+ const lines = data
6065. toString ( )
6166. split ( / \r * \n / )
62- . forEach ( ( line :string ) => {
63- if ( line !== "" ) {
64- writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
65- capturedStderr += line . toString ( ) + "\n" ;
66- }
67- } ) ;
67+ . filter ( ( line ) => line !== "" ) ;
68+ for ( const line of lines ) {
69+ writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
70+ capturedStderr += line . toString ( ) + "\n" ;
71+ }
6872} ) ;
6973
7074startProcess . on ( "close" , ( code :number ) => {
@@ -82,51 +86,72 @@ export async function startWorkspaceIfStoppedOrFailed(
8286}
8387
8488/**
85- * Wait for the latest build to finish while streaming logs to the emitter.
86- *
87- * Once completed, fetch the workspace again and return it.
89+ * Streams build logs to the emitter in real-time.
90+ * Returns the websocket for lifecycle management.
8891 */
89- export async function waitForBuild (
92+ export async function streamBuildLogs (
9093client :CoderApi ,
9194writeEmitter :vscode . EventEmitter < string > ,
9295workspace :Workspace ,
93- ) :Promise < Workspace > {
94- // This fetches the initial bunch of logs.
95- const logs = await client . getWorkspaceBuildLogs ( workspace . latest_build . id ) ;
96- logs . forEach ( ( log ) => writeEmitter . fire ( log . output + "\r\n" ) ) ;
97-
96+ ) :Promise < OneWayWebSocket < ProvisionerJobLog > > {
9897const socket = await client . watchBuildLogsByBuildId (
9998workspace . latest_build . id ,
100- logs ,
99+ [ ] ,
101100) ;
102101
103- await new Promise < void > ( ( resolve , reject ) => {
104- socket . addEventListener ( "message" , ( data ) => {
105- if ( data . parseError ) {
106- writeEmitter . fire (
107- errToStr ( data . parseError , "Failed to parse message" ) + "\r\n" ,
108- ) ;
109- } else {
110- writeEmitter . fire ( data . parsedMessage . output + "\r\n" ) ;
111- }
112- } ) ;
102+ socket . addEventListener ( "message" , ( data ) => {
103+ if ( data . parseError ) {
104+ writeEmitter . fire (
105+ errToStr ( data . parseError , "Failed to parse message" ) + "\r\n" ,
106+ ) ;
107+ } else {
108+ writeEmitter . fire ( data . parsedMessage . output + "\r\n" ) ;
109+ }
110+ } ) ;
111+
112+ socket . addEventListener ( "error" , ( error ) => {
113+ const baseUrlRaw = client . getAxiosInstance ( ) . defaults . baseURL ;
114+ writeEmitter . fire (
115+ `Error watching workspace build logs on${ baseUrlRaw } :${ errToStr ( error , "no further details" ) } \r\n` ,
116+ ) ;
117+ } ) ;
118+
119+ socket . addEventListener ( "close" , ( ) => {
120+ writeEmitter . fire ( "Build complete\r\n" ) ;
121+ } ) ;
122+
123+ return socket ;
124+ }
113125
114- socket . addEventListener ( "error" , ( error ) => {
115- const baseUrlRaw = client . getAxiosInstance ( ) . defaults . baseURL ;
116- return reject (
117- new Error (
118- `Failed to watch workspace build on${ baseUrlRaw } :${ errToStr ( error , "no further details" ) } ` ,
119- ) ,
126+ /**
127+ * Streams agent logs to the emitter in real-time.
128+ * Returns the websocket for lifecycle management.
129+ */
130+ export async function streamAgentLogs (
131+ client :CoderApi ,
132+ writeEmitter :vscode . EventEmitter < string > ,
133+ agent :WorkspaceAgent ,
134+ ) :Promise < OneWayWebSocket < WorkspaceAgentLog [ ] > > {
135+ const socket = await client . watchWorkspaceAgentLogs ( agent . id , [ ] ) ;
136+
137+ socket . addEventListener ( "message" , ( data ) => {
138+ if ( data . parseError ) {
139+ writeEmitter . fire (
140+ errToStr ( data . parseError , "Failed to parse message" ) + "\r\n" ,
120141) ;
121- } ) ;
142+ } else {
143+ for ( const log of data . parsedMessage ) {
144+ writeEmitter . fire ( log . output + "\r\n" ) ;
145+ }
146+ }
147+ } ) ;
122148
123- socket . addEventListener ( "close" , ( ) => resolve ( ) ) ;
149+ socket . addEventListener ( "error" , ( error ) => {
150+ const baseUrlRaw = client . getAxiosInstance ( ) . defaults . baseURL ;
151+ writeEmitter . fire (
152+ `Error watching agent logs on${ baseUrlRaw } :${ errToStr ( error , "no further details" ) } \r\n` ,
153+ ) ;
124154} ) ;
125155
126- writeEmitter . fire ( "Build complete\r\n" ) ;
127- const updatedWorkspace = await client . getWorkspace ( workspace . id ) ;
128- writeEmitter . fire (
129- `Workspace is now${ updatedWorkspace . latest_build . status } \r\n` ,
130- ) ;
131- return updatedWorkspace ;
156+ return socket ;
132157}