8000 Start workspaces by shelling out to CLI by aaronlehmann · Pull Request #400 · coder/vscode-coder · GitHub
[go: up one dir, main page]

Skip to content

Start workspaces by shelling out to CLI #400

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -122,29 +123,56 @@ 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<Workspace> {
// 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<string>,
): Promise<Workspace> {
// Before we start a workspace, we make an initial request to check it's not already started
const updatedWorkspace = await restClient.getWorkspace(workspace.id)

if (!["stopped", "failed"].includes(updatedWorkspace.latest_build.status)) {
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}`))
}
})
})
}

/**
Expand Down
55 changes: 33 additions & 22 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Workspace | undefined> {
private async maybeWaitForRunning(
restClient: Api,
workspace: Workspace,
binPath: string,
): Promise<Workspace | undefined> {
// Maybe already running?
if (workspace.latest_build.status === "running") {
return workspace
Expand All @@ -63,6 +67,28 @@ export class Remote {
let terminal: undefined | vscode.Terminal
let attempts = 0

function initWriteEmitterAndTerminal(): vscode.EventEmitter<string> {
if (!writeEmitter) {
writeEmitter = new vscode.EventEmitter<string>()
}
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<vscode.Pseudoterminal> as any,
})
terminal.show(true)
}
return writeEmitter
}

try {
// Show a notification while we wait.
return await this.vscodeProposed.window.withProgress(
Expand All @@ -78,33 +104,17 @@ export class Remote {
case "pending":
case "starting":
case "stopping":
if (!writeEmitter) {
writeEmitter = new vscode.EventEmitter<string>()
}
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<vscode.Pseudoterminal> as any,
})
terminal.show(true)
}
writeEmitter = initWriteEmitterAndTerminal()
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}...`)
workspace = await waitForBuild(restClient, writeEmitter, workspace)
break
case "stopped":
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
Expand All @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
0