diff --git a/src/api.ts b/src/api.ts index fafeaf5..2e57410 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,3 +1,4 @@ +import { spawn, ChildProcessWithoutNullStreams } from "child_process" import { Api } from "coder/site/src/api/api" import { ProvisionerJobLog, Workspace } from "coder/site/src/api/typesGenerated" import fs from "fs/promises" @@ -122,16 +123,12 @@ export async function makeCoderSdk(baseUrl: string, token: string | undefined, s /** * Start or update a workspace and return the updated workspace. */ -export async function startWorkspaceIfStoppedOrFailed(restClient: Api, workspace: Workspace): Promise { - // If the workspace requires the latest active template version, we should attempt - // to update that here. - // TODO: If param set changes, what do we do?? - const versionID = workspace.template_require_active_version - ? // Use the latest template version - workspace.template_active_version_id - : // Default to not updating the workspace if not required. - workspace.latest_build.template_version_id - +export async function startWorkspaceIfStoppedOrFailed( + restClient: Api, + binPath: string, + workspace: Workspace, + writeEmitter: vscode.EventEmitter, +): Promise { // Before we start a workspace, we make an initial request to check it's not already started const updatedWorkspace = await restClient.getWorkspace(workspace.id) @@ -139,12 +136,43 @@ export async function startWorkspaceIfStoppedOrFailed(restClient: Api, workspace return updatedWorkspace } - const latestBuild = await restClient.startWorkspace(updatedWorkspace.id, versionID) + return new Promise((resolve, reject) => { + const startProcess: ChildProcessWithoutNullStreams = spawn(binPath, [ + "start", + "--yes", + workspace.owner_name + "/" + workspace.name, + ]) + + startProcess.stdout.on("data", (data: Buffer) => { + data + .toString() + .split(/\r*\n/) + .forEach((line: string) => { + if (line !== "") { + writeEmitter.fire(line.toString() + "\r\n") + } + }) + }) + + startProcess.stderr.on("data", (data: Buffer) => { + data + .toString() + .split(/\r*\n/) + .forEach((line: string) => { + if (line !== "") { + writeEmitter.fire(line.toString() + "\r\n") + } + }) + }) - return { - ...updatedWorkspace, - latest_build: latestBuild, - } + startProcess.on("close", (code: number) => { + if (code === 0) { + resolve(restClient.getWorkspace(workspace.id)) + } else { + reject(new Error(`"coder start" process exited with code ${code}`)) + } + }) + }) } /** diff --git a/src/remote.ts b/src/remote.ts index cd0c391..1977dd8 100644 --- a/src/remote.ts +++ b/src/remote.ts @@ -50,7 +50,11 @@ export class Remote { /** * Try to get the workspace running. Return undefined if the user canceled. */ - private async maybeWaitForRunning(restClient: Api, workspace: Workspace): Promise { + private async maybeWaitForRunning( + restClient: Api, + workspace: Workspace, + binPath: string, + ): Promise { // Maybe already running? if (workspace.latest_build.status === "running") { return workspace @@ -63,6 +67,28 @@ export class Remote { let terminal: undefined | vscode.Terminal let attempts = 0 + function initWriteEmitterAndTerminal(): vscode.EventEmitter { + if (!writeEmitter) { + writeEmitter = new vscode.EventEmitter() + } + if (!terminal) { + terminal = vscode.window.createTerminal({ + name: "Build Log", + location: vscode.TerminalLocation.Panel, + // Spin makes this gear icon spin! + iconPath: new vscode.ThemeIcon("gear~spin"), + pty: { + onDidWrite: writeEmitter.event, + close: () => undefined, + open: () => undefined, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as Partial as any, + }) + terminal.show(true) + } + return writeEmitter + } + try { // Show a notification while we wait. return await this.vscodeProposed.window.withProgress( @@ -78,24 +104,7 @@ export class Remote { case "pending": case "starting": case "stopping": - if (!writeEmitter) { - writeEmitter = new vscode.EventEmitter() - } - if (!terminal) { - terminal = vscode.window.createTerminal({ - name: "Build Log", - location: vscode.TerminalLocation.Panel, - // Spin makes this gear icon spin! - iconPath: new vscode.ThemeIcon("gear~spin"), - pty: { - onDidWrite: writeEmitter.event, - close: () => undefined, - open: () => undefined, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as Partial as any, - }) - terminal.show(true) - } + writeEmitter = initWriteEmitterAndTerminal() this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}...`) workspace = await waitForBuild(restClient, writeEmitter, workspace) break @@ -103,8 +112,9 @@ export class Remote { if (!(await this.confirmStart(workspaceName))) { return undefined } + writeEmitter = initWriteEmitterAndTerminal() this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`) - workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace) + workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter) break case "failed": // On a first attempt, we will try starting a failed workspace @@ -113,8 +123,9 @@ export class Remote { if (!(await this.confirmStart(workspaceName))) { return undefined } + writeEmitter = initWriteEmitterAndTerminal() this.storage.writeToCoderOutputChannel(`Starting ${workspaceName}...`) - workspace = await startWorkspaceIfStoppedOrFailed(restClient, workspace) + workspace = await startWorkspaceIfStoppedOrFailed(restClient, binPath, workspace, writeEmitter) break } // Otherwise fall through and error. @@ -292,7 +303,7 @@ export class Remote { disposables.push(this.registerLabelFormatter(remoteAuthority, workspace.owner_name, workspace.name)) // If the workspace is not in a running state, try to get it running. - const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace) + const updatedWorkspace = await this.maybeWaitForRunning(workspaceRestClient, workspace, binaryPath) if (!updatedWorkspace) { // User declined to start the workspace. await this.closeRemote()