1- import { spawn } from "child_process" ;
21import { type Api } from "coder/site/src/api/api" ;
32import {
43type WorkspaceAgentLog ,
54type Workspace ,
5+ type WorkspaceAgent ,
66} from "coder/site/src/api/typesGenerated" ;
7+ import { spawn } from "node:child_process" ;
78import * as vscode from "vscode" ;
89
910import { type FeatureSet } from "../featureSet" ;
@@ -40,35 +41,33 @@ export async function startWorkspaceIfStoppedOrFailed(
4041createWorkspaceIdentifier ( workspace ) ,
4142] ;
4243if ( featureSet . buildReason ) {
43- startArgs . push ( ... [ "--reason" , "vscode_connection" ] ) ;
44+ startArgs . push ( "--reason" , "vscode_connection" ) ;
4445}
4546
4647// { shell: true } requires one shell-safe command string, otherwise we lose all escaping
4748const cmd = `${ escapeCommandArg ( binPath ) } ${ startArgs . join ( " " ) } ` ;
4849const startProcess = spawn ( cmd , { shell :true } ) ;
4950
5051startProcess . stdout . on ( "data" , ( data :Buffer ) => {
51- data
52+ const lines = data
5253. toString ( )
5354. split ( / \r * \n / )
54- . forEach ( ( line :string ) => {
55- if ( line !== "" ) {
56- writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
57- }
58- } ) ;
55+ . filter ( ( line ) => line !== "" ) ;
56+ for ( const line of lines ) {
57+ writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
58+ }
5959} ) ;
6060
6161let capturedStderr = "" ;
6262startProcess . stderr . on ( "data" , ( data :Buffer ) => {
63- data
63+ const lines = data
6464. toString ( )
6565. split ( / \r * \n / )
66- . forEach ( ( line :string ) => {
67- if ( line !== "" ) {
68- writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
69- capturedStderr += line . toString ( ) + "\n" ;
70- }
71- } ) ;
66+ . filter ( ( line ) => line !== "" ) ;
67+ for ( const line of lines ) {
68+ writeEmitter . fire ( line . toString ( ) + "\r\n" ) ;
69+ capturedStderr += line . toString ( ) + "\n" ;
70+ }
7271} ) ;
7372
7473startProcess . on ( "close" , ( code :number ) => {
@@ -85,43 +84,6 @@ export async function startWorkspaceIfStoppedOrFailed(
8584} ) ;
8685}
8786
88- /**
89- * Wait for the latest build to finish while streaming logs to the emitter.
90- *
91- * Once completed, fetch the workspace again and return it.
92- */
93- export async function writeAgentLogs (
94- client :CoderApi ,
95- writeEmitter :vscode . EventEmitter < string > ,
96- agentId :string ,
97- ) :Promise < OneWayWebSocket < WorkspaceAgentLog [ ] > > {
98- // This fetches the initial bunch of logs.
99- const logs = await client . getWorkspaceAgentLogs ( agentId ) ;
100- logs . forEach ( ( log ) => writeEmitter . fire ( log . output + "\r\n" ) ) ;
101-
102- const socket = await client . watchWorkspaceAgentLogs ( agentId , logs ) ;
103-
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- data . parsedMessage . forEach ( ( message ) =>
111- writeEmitter . fire ( message . output + "\r\n" ) ,
112- ) ;
113- }
114- } ) ;
115-
116- socket . addEventListener ( "error" , ( error ) => {
117- const baseUrlRaw = client . getAxiosInstance ( ) . defaults . baseURL ;
118- throw new Error (
119- `Failed to watch workspace build on${ baseUrlRaw } :${ errToStr ( error , "no further details" ) } ` ,
120- ) ;
121- } ) ;
122- return socket ;
123- }
124-
12587/**
12688 * Wait for the latest build to finish while streaming logs to the emitter.
12789 *
@@ -134,7 +96,9 @@ export async function waitForBuild(
13496) :Promise < Workspace > {
13597// This fetches the initial bunch of logs.
13698const logs = await client . getWorkspaceBuildLogs ( workspace . latest_build . id ) ;
137- logs . forEach ( ( log ) => writeEmitter . fire ( log . output + "\r\n" ) ) ;
99+ for ( const log of logs ) {
100+ writeEmitter . fire ( log . output + "\r\n" ) ;
101+ }
138102
139103const socket = await client . watchBuildLogsByBuildId (
140104workspace . latest_build . id ,
@@ -171,3 +135,55 @@ export async function waitForBuild(
171135) ;
172136return updatedWorkspace ;
173137}
138+
139+ /**
140+ * Streams agent logs to the emitter in real-time.
141+ * Fetches existing logs and subscribes to new logs via websocket.
142+ * Returns the websocket and a completion promise that rejects on error.
143+ */
144+ export async function streamAgentLogs (
145+ client :CoderApi ,
146+ writeEmitter :vscode . EventEmitter < string > ,
147+ agent :WorkspaceAgent ,
148+ ) :Promise < {
149+ socket :OneWayWebSocket < WorkspaceAgentLog [ ] > ;
150+ completion :Promise < void > ;
151+ } > {
152+ // This fetches the initial bunch of logs.
153+ const logs = await client . getWorkspaceAgentLogs ( agent . id ) ;
154+ for ( const log of logs ) {
155+ writeEmitter . fire ( log . output + "\r\n" ) ;
156+ }
157+
158+ const socket = await client . watchWorkspaceAgentLogs ( agent . id , logs ) ;
159+
160+ const completion = new Promise < void > ( ( resolve , reject ) => {
161+ socket . addEventListener ( "message" , ( data ) => {
162+ if ( data . parseError ) {
163+ writeEmitter . fire (
164+ errToStr ( data . parseError , "Failed to parse message" ) + "\r\n" ,
165+ ) ;
166+ } else {
167+ for ( const log of data . parsedMessage ) {
168+ writeEmitter . fire ( log . output + "\r\n" ) ;
169+ }
170+ }
171+ } ) ;
172+
173+ socket . addEventListener ( "error" , ( error ) => {
174+ const baseUrlRaw = client . getAxiosInstance ( ) . defaults . baseURL ;
175+ writeEmitter . fire (
176+ `Error watching agent logs on${ baseUrlRaw } :${ errToStr ( error , "no further details" ) } \r\n` ,
177+ ) ;
178+ return reject (
179+ new Error (
180+ `Failed to watch agent logs on${ baseUrlRaw } :${ errToStr ( error , "no further details" ) } ` ,
181+ ) ,
182+ ) ;
183+ } ) ;
184+
185+ socket . addEventListener ( "close" , ( ) => resolve ( ) ) ;
186+ } ) ;
187+
188+ return { socket, completion} ;
189+ }