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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy