Skip to content

Commit 59ac05c

Browse files
committed
Start workspaces by shelling out to CLI
Replace the REST-API-based start flow with one that shells out to the coder CLI. Signed-off-by: Aaron Lehmann <alehmann@netflix.com>
1 parent da1aaed commit 59ac05c

File tree

2 files changed

+76
-37
lines changed

2 files changed

+76
-37
lines changed

src/api.ts

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spawn, ChildProcessWithoutNullStreams } from "child_process"
12
import { Api } from "coder/site/src/api/api"
23
import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated"
34
import fs from "fs/promises"
@@ -122,29 +123,56 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s
122123
/**
123124
* Start or update a workspace and return the updated workspace.
124125
*/
125-
export async function startWorkspaceIfStoppedOrFailed(restClient: Api, workspace: Workspace): Promise<Workspace> {
126-
// If the workspace requires the latest active template version, we should attempt
127-
// to update that here.
128-
// TODO: If param set changes, what do we do??
129-
const versionID = workspace.template_require_active_version
130-
? // Use the latest template version
131-
workspace.template_active_version_id
132-
: // Default to not updating the workspace if not required.
133-
workspace.latest_build.template_version_id
134-
126+
export async function startWorkspaceIfStoppedOrFailed(
127+
restClient: Api,
128+
binPath: string,
129+
workspace: Workspace,
130+
writeEmitter: vscode.EventEmitter<string>,
131+
): Promise<Workspace> {
135132
// Before we start a workspace, we make an initial request to check it's not already started
136133
const updatedWorkspace = await restClient.getWorkspace(workspace.id)
137134

138135
if (!["stopped", "failed"].includes(updatedWorkspace.latest_build.status)) {
139136
return updatedWorkspace
140137
}
141138

142-
const latestBuild = await restClient.startWorkspace(updatedWorkspace.id, versionID)
139+
return new Promise((resolve, reject) => {
140+
const startProcess: ChildProcessWithoutNullStreams = spawn(binPath, [
141+
"start",
142+
"--yes",
143+
workspace.owner_name + "/" + workspace.name,
144+
])
145+
146+
startProcess.stdout.on("data", (data: Buffer) => {
147+
data
148+
.toString()
149+
.split(/\r*\n/)
150+
.forEach((line: string) => {
151+
if (line !== "") {
152+
writeEmitter.fire(line.toString() + "\r\n")
153+
}
154+
})
155+
})
156+
157+
startProcess.stderr.on("data", (data: Buffer) => {
158+
data
159+
.toString()
160+
.split(/\r*\n/)
161+
.forEach((line: string) => {
162+
if (line !== "") {
163+
writeEmitter.fire(line.toString() + "\r\n")
164+
}
165+
})
166+
})
143167

144-
return {
145-
...updatedWorkspace,
146-
latest_build: latestBuild,
147-
}
168+
startProcess.on("close", (code: number) => {
169+
if (code === 0) {
170+
resolve(restClient.getWorkspace(workspace.id))
171+
} else {
172+
reject(new Error(`"coder start" process exited with code ${code}`))
173+
}
174+
})
175+
})
148176
}
149177

150178
/**

src/remote.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ export class Remote {
5050
/**
5151
* Try to get the workspace running. Return undefined if the user canceled.
5252
*/
53-
private async maybeWaitForRunning(restClient: Api, workspace: Workspace): Promise<Workspace | undefined> {
53+
private async maybeWaitForRunning(
54+
restClient: Api,
55+
workspace: Workspace,
56+
binPath: string,
57+
): Promise<Workspace | undefined> {
5458
// Maybe already running?
5559
if (workspace.latest_build.status === "running") {
5660
return workspace
@@ -63,6 +67,28 @@ export class Remote {
6367
let terminal: undefined | vscode.Terminal
6468
let attempts = 0
6569

70+
function initWriteEmitterAndTerminal(): vscode.EventEmitter<string> {
71+
if (!writeEmitter) {
72+
writeEmitter = new vscode.EventEmitter<string>()
73+
}
74+
if (!terminal) {
75+
terminal = vscode.window.createTerminal({
76+
name: "Build Log",
77+
location: vscode.TerminalLocation.Panel,
78+
// Spin makes this gear icon spin!
79+
iconPath: new vscode.ThemeIcon("gear~spin"),
80+
pty: {
81+
onDidWrite: writeEmitter.event,
82+
close: () => undefined,
83+
open: () => undefined,
84+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
85+
} as Partial<vscode.Pseudoterminal> as any,
86+
})
87+
terminal.show(true)
88+
}
89+
return writeEmitter
90+
}
91+
6692
try {
6793
// Show a notification while we wait.
6894
return await this.vscodeProposed.window.withProgress(
@@ -78,33 +104,17 @@ export class Remote {
78104
case "pending":
79105
case "starting":
80106
case "stopping":
81-
if (!writeEmitter) {
82-
writeEmitter = new vscode.EventEmitter<string>()
83-
}
84-
if (!terminal) {
85-
terminal = vscode.window.createTerminal({
86-
name: "Build Log",
87-
location: vscode.TerminalLocation.Panel,
88-
// Spin makes this gear icon spin!
89-
iconPath: new vscode.ThemeIcon("gear~spin"),
90-
pty: {
91-
onDidWrite: writeEmitter.event,
92-
close: () => undefined,
93-
open: () => undefined,
94-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
95-
} as Partial<vscode.Pseudoterminal> as any,
96-
})
97-
terminal.show(true)
98-
}
107+
writeEmitter = initWriteEmitterAndTerminal()
99108
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}...`)
100109
workspace = await waitForBuild(restClient, writeEmitter, workspace)
101110
break
102111
case "stopped":
103112
if (!(await this.confirmStart(workspaceName))) {
104113
return undefined
105114
}
115+
writeEmitter = initWriteEmitterAndTerminal()
106116
this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`)
107-
workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace)
117+
workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter)
108118
break
109119
case "failed":
110120
// On a first attempt, we will try starting a failed workspace
@@ -113,8 +123,9 @@ export class Remote {
113123
if (!(await this.confirmStart(workspaceName))) {
114124
return undefined
115125
}
126+
writeEmitter = initWriteEmitterAndTerminal()
116127
this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`)
117-
workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace)
128+
workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter)
118129
break
119130
}
120131
// Otherwise fall through and error.
@@ -292,7 +303,7 @@ export class Remote {
292303
disposables.push(this.registerLabelFormatter(remoteAuthority, workspace.owner_name, workspace.name))
293304

294305
// If the workspace is not in a running state, try to get it running.
295-
const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace)
306+
const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace, binaryPath)
296307
if (!updatedWorkspace) {
297308
// User declined to start the workspace.
298309
await this.closeRemote()

0 commit comments

Comments
 (0)