Skip to content

Commit e6bb283

Browse files
committed
Refactoring and cleanup of agent startup script waiting
1 parent c49dc60 commit e6bb283

File tree

3 files changed

+280
-149
lines changed

3 files changed

+280
-149
lines changed

src/api/workspace.ts

Lines changed: 69 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { spawn } from "child_process";
21
import { type Api } from "coder/site/src/api/api";
32
import {
43
type WorkspaceAgentLog,
54
type Workspace,
5+
type WorkspaceAgent,
66
} from "coder/site/src/api/typesGenerated";
7+
import { spawn } from "node:child_process";
78
import * as vscode from "vscode";
89

910
import { type FeatureSet } from "../featureSet";
@@ -40,35 +41,33 @@ export async function startWorkspaceIfStoppedOrFailed(
4041
createWorkspaceIdentifier(workspace),
4142
];
4243
if (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
4748
const cmd = `${escapeCommandArg(binPath)} ${startArgs.join(" ")}`;
4849
const startProcess = spawn(cmd, { shell: true });
4950

5051
startProcess.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

6161
let capturedStderr = "";
6262
startProcess.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

7473
startProcess.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.
13698
const 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

139103
const socket = await client.watchBuildLogsByBuildId(
140104
workspace.latest_build.id,
@@ -171,3 +135,55 @@ export async function waitForBuild(
171135
);
172136
return 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+
}

0 commit comments

Comments
 (0)