Skip to content

Commit f8319a8

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.
1 parent da1aaed commit f8319a8

File tree

2 files changed

+77
-37
lines changed

2 files changed

+77
-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: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
2020
import { Storage } from "./storage"
2121
import { AuthorityPrefix, expandPath, parseRemoteAuthority } from "./util"
2222
import { WorkspaceMonitor } from "./workspaceMonitor"
23+
import { write } from "fs"
2324

2425
export interface RemoteDetails extends vscode.Disposable {
2526
url: string
@@ -50,7 +51,11 @@ export class Remote {
5051
/**
5152
* Try to get the workspace running. Return undefined if the user canceled.
5253
*/
53-
private async maybeWaitForRunning(restClient: Api, workspace: Workspace): Promise<Workspace | undefined> {
54+
private async maybeWaitForRunning(
55+
restClient: Api,
56+
workspace: Workspace,
57+
binPath: string,
58+
): Promise<Workspace | undefined> {
5459
// Maybe already running?
5560
if (workspace.latest_build.status === "running") {
5661
return workspace
@@ -63,6 +68,28 @@ export class Remote {
6368
let terminal: undefined | vscode.Terminal
6469
let attempts = 0
6570

71+
function initWriteEmitterAndTerminal(): vscode.EventEmitter<string> {
72+
if (!writeEmitter) {
73+
writeEmitter = new vscode.EventEmitter<string>()
74+
}
75+
if (!terminal) {
76+
terminal = vscode.window.createTerminal({
77+
name: "Build Log",
78+
location: vscode.TerminalLocation.Panel,
79+
// Spin makes this gear icon spin!
80+
iconPath: new vscode.ThemeIcon("gear~spin"),
81+
pty: {
82+
onDidWrite: writeEmitter.event,
83+
close: () => undefined,
84+
open: () => undefined,
85+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
86+
} as Partial<vscode.Pseudoterminal> as any,
87+
})
88+
terminal.show(true)
89+
}
90+
return writeEmitter
91+
}
92+
6693
try {
6794
// Show a notification while we wait.
6895
return await this.vscodeProposed.window.withProgress(
@@ -78,33 +105,17 @@ export class Remote {
78105
case "pending":
79106
case "starting":
80107
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-
}
108+
writeEmitter = initWriteEmitterAndTerminal()
99109
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}...`)
100110
workspace = await waitForBuild(restClient, writeEmitter, workspace)
101111
break
102112
case "stopped":
103113
if (!(await this.confirmStart(workspaceName))) {
104114
return undefined
105115
}
116+
writeEmitter = initWriteEmitterAndTerminal()
106117
this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`)
107-
workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace)
118+
workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter)
108119
break
109120
case "failed":
110121
// On a first attempt, we will try starting a failed workspace
@@ -113,8 +124,9 @@ export class Remote {
113124
if (!(await this.confirmStart(workspaceName))) {
114125
return undefined
115126
}
127+
writeEmitter = initWriteEmitterAndTerminal()
116128
this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`)
117-
workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace)
129+
workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter)
118130
break
119131
}
120132
// Otherwise fall through and error.
@@ -292,7 +304,7 @@ export class Remote {
292304
disposables.push(this.registerLabelFormatter(remoteAuthority, workspace.owner_name, workspace.name))
293305

294306
// If the workspace is not in a running state, try to get it running.
295-
const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace)
307+
const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace, binaryPath)
296308
if (!updatedWorkspace) {
297309
// User declined to start the workspace.
298310
await this.closeRemote()

0 commit comments

Comments
 (0)