11import { isAxiosError } from "axios"
22import { Api } from "coder/site/src/api/api"
3- import { ProvisionerJobLog , Workspace , WorkspaceAgent } from "coder/site/src/api/typesGenerated"
3+ import { Workspace , WorkspaceAgent } from "coder/site/src/api/typesGenerated"
44import EventSource from "eventsource"
55import find from "find-process"
66import * as fs from "fs/promises"
@@ -10,9 +10,7 @@ import * as path from "path"
1010import prettyBytes from "pretty-bytes"
1111import * as semver from "semver"
1212import * as vscode from "vscode"
13- import * as ws from "ws"
14- import { makeCoderSdk } from "./api"
15- import { errToStr } from "./api-helper"
13+ import { makeCoderSdk , startWorkspace , waitForBuild } from "./api"
1614import { Commands } from "./commands"
1715import { getHeaderCommand } from "./headers"
1816import { SSHConfig , SSHValues , mergeSSHConfigValues } from "./sshConfig"
@@ -35,6 +33,92 @@ export class Remote {
3533 private readonly mode : vscode . ExtensionMode ,
3634 ) { }
3735
36+ private async waitForRunning ( restClient : Api , workspace : Workspace ) : Promise < Workspace > {
37+ // Maybe already running?
38+ if ( workspace . latest_build . status === "running" ) {
39+ return workspace
40+ }
41+
42+ const workspaceName = `${ workspace . owner_name } /${ workspace . name } `
43+
44+ // A terminal will be used to stream the build, if one is necessary.
45+ let writeEmitter : undefined | vscode . EventEmitter < string >
46+ let terminal : undefined | vscode . Terminal
47+ let attempts = 0
48+
49+ try {
50+ // Show a notification while we wait.
51+ return await this . vscodeProposed . window . withProgress (
52+ {
53+ location : vscode . ProgressLocation . Notification ,
54+ cancellable : false ,
55+ title : "Waiting for workspace build..." ,
56+ } ,
57+ async ( ) => {
58+ while ( workspace . latest_build . status !== "running" ) {
59+ ++ attempts
60+ switch ( workspace . latest_build . status ) {
61+ case "pending" :
62+ case "starting" :
63+ case "stopping" :
64+ if ( ! writeEmitter ) {
65+ writeEmitter = new vscode . EventEmitter < string > ( )
66+ }
67+ if ( ! terminal ) {
68+ terminal = vscode . window . createTerminal ( {
69+ name : "Build Log" ,
70+ location : vscode . TerminalLocation . Panel ,
71+ // Spin makes this gear icon spin!
72+ iconPath : new vscode . ThemeIcon ( "gear~spin" ) ,
73+ pty : {
74+ onDidWrite : writeEmitter . event ,
75+ close : ( ) => undefined ,
76+ open : ( ) => undefined ,
77+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78+ } as Partial < vscode . Pseudoterminal > as any ,
79+ } )
80+ terminal . show ( true )
81+ }
82+ this . storage . writeToCoderOutputChannel ( `Waiting for ${ workspaceName } ...` )
83+ workspace = await waitForBuild ( restClient , writeEmitter , workspace )
84+ break
85+ case "stopped" :
86+ this . storage . writeToCoderOutputChannel ( `Starting ${ workspaceName } ...` )
87+ workspace = await startWorkspace ( restClient , workspace )
88+ break
89+ case "failed" :
90+ // On a first attempt, we will try starting a failed workspace
91+ // (for example canceling a start seems to cause this state).
92+ if ( attempts === 1 ) {
93+ this . storage . writeToCoderOutputChannel ( `Starting ${ workspaceName } ...` )
94+ workspace = await startWorkspace ( restClient , workspace )
95+ break
96+ }
97+ // Otherwise fall through and error.
98+ case "canceled" :
99+ case "canceling" :
100+ case "deleted" :
101+ case "deleting" :
102+ default : {
103+ const is = workspace . latest_build . status === "failed" ? "has" : "is"
104+ throw new Error ( `${ workspaceName } ${ is } ${ workspace . latest_build . status } ` )
105+ }
106+ }
107+ this . storage . writeToCoderOutputChannel ( `${ workspaceName } status is now ${ workspace . latest_build . status } ` )
108+ }
109+ return workspace
110+ } ,
111+ )
112+ } finally {
113+ if ( writeEmitter ) {
114+ writeEmitter . dispose ( )
115+ }
116+ if ( terminal ) {
117+ terminal . dispose ( )
118+ }
119+ }
120+ }
121+
38122 /**
39123 * Ensure the workspace specified by the remote authority is ready to receive
40124 * SSH connections. Return undefined if the authority is not for a Coder
@@ -170,135 +254,9 @@ export class Remote {
170254 // Initialize any WorkspaceAction notifications (auto-off, upcoming deletion)
171255 const action = await WorkspaceAction . init ( this . vscodeProposed , workspaceRestClient , this . storage )
172256
173- // Make sure the workspace has started.
174- let buildComplete : undefined | ( ( ) => void )
175- if ( workspace . latest_build . status === "stopped" ) {
176- // If the workspace requires the latest active template version, we should attempt
177- // to update that here.
178- // TODO: If param set changes, what do we do??
179- const versionID = workspace . template_require_active_version
180- ? // Use the latest template version
181- workspace . template_active_version_id
182- : // Default to not updating the workspace if not required.
183- workspace . latest_build . template_version_id
184-
185- this . vscodeProposed . window . withProgress (
186- {
187- location : vscode . ProgressLocation . Notification ,
188- cancellable : false ,
189- title : workspace . template_require_active_version ? "Updating workspace..." : "Starting workspace..." ,
190- } ,
191- ( ) =>
192- new Promise < void > ( ( r ) => {
193- buildComplete = r
194- } ) ,
195- )
196-
197- this . storage . writeToCoderOutputChannel ( `Trying to start ${ workspaceName } ...` )
198- const latestBuild = await workspaceRestClient . startWorkspace ( workspace . id , versionID )
199- workspace = {
200- ...workspace ,
201- latest_build : latestBuild ,
202- }
203- this . storage . writeToCoderOutputChannel ( `${ workspaceName } is now ${ workspace . latest_build . status } ` )
204- this . commands . workspace = workspace
205- }
206-
207- // If a build is running we should stream the logs to the user so they can
208- // watch what's going on!
209- if (
210- workspace . latest_build . status === "pending" ||
211- workspace . latest_build . status === "starting" ||
212- workspace . latest_build . status === "stopping"
213- ) {
214- this . storage . writeToCoderOutputChannel ( `Waiting for ${ workspaceName } ...` )
215- const writeEmitter = new vscode . EventEmitter < string > ( )
216- // We use a terminal instead of an output channel because it feels more
217- // familiar to a user!
218- const terminal = vscode . window . createTerminal ( {
219- name : "Build Log" ,
220- location : vscode . TerminalLocation . Panel ,
221- // Spin makes this gear icon spin!
222- iconPath : new vscode . ThemeIcon ( "gear~spin" ) ,
223- pty : {
224- onDidWrite : writeEmitter . event ,
225- close : ( ) => undefined ,
226- open : ( ) => undefined ,
227- // eslint-disable-next-line @typescript-eslint/no-explicit-any
228- } as Partial < vscode . Pseudoterminal > as any ,
229- } )
230- // This fetches the initial bunch of logs.
231- const logs = await workspaceRestClient . getWorkspaceBuildLogs ( workspace . latest_build . id , new Date ( ) )
232- logs . forEach ( ( log ) => writeEmitter . fire ( log . output + "\r\n" ) )
233- terminal . show ( true )
234- // This follows the logs for new activity!
235- // TODO: watchBuildLogsByBuildId exists, but it uses `location`.
236- // Would be nice if we could use it here.
237- let path = `/api/v2/workspacebuilds/${ workspace . latest_build . id } /logs?follow=true`
238- if ( logs . length ) {
239- path += `&after=${ logs [ logs . length - 1 ] . id } `
240- }
241- await new Promise < void > ( ( resolve , reject ) => {
242- try {
243- const baseUrl = new URL ( baseUrlRaw )
244- const proto = baseUrl . protocol === "https:" ? "wss:" : "ws:"
245- const socketUrlRaw = `${ proto } //${ baseUrl . host } ${ path } `
246- const socket = new ws . WebSocket ( new URL ( socketUrlRaw ) , {
247- headers : {
248- "Coder-Session-Token" : token ,
249- } ,
250- followRedirects : true ,
251- } )
252- socket . binaryType = "nodebuffer"
253- socket . on ( "message" , ( data ) => {
254- const buf = data as Buffer
255- const log = JSON . parse ( buf . toString ( ) ) as ProvisionerJobLog
256- writeEmitter . fire ( log . output + "\r\n" )
257- } )
258- socket . on ( "error" , ( error ) => {
259- reject (
260- new Error (
261- `Failed to watch workspace build using ${ socketUrlRaw } : ${ errToStr ( error , "no further details" ) } ` ,
262- ) ,
263- )
264- } )
265- socket . on ( "close" , ( ) => {
266- resolve ( )
267- } )
268- } catch ( error ) {
269- // If this errors, it is probably a malformed URL.
270- reject ( new Error ( `Failed to open web socket to ${ baseUrlRaw } : ${ errToStr ( error , "no further details" ) } ` ) )
271- }
272- } )
273- writeEmitter . fire ( "Build complete" )
274- workspace = await workspaceRestClient . getWorkspace ( workspace . id )
275- this . commands . workspace = workspace
276- terminal . dispose ( )
277- }
278-
279- if ( buildComplete ) {
280- buildComplete ( )
281- }
282-
283- // The workspace should now be running, but it could be stopped if the user
284- // stopped the workspace while connected.
285- if ( workspace . latest_build . status !== "running" ) {
286- const result = await this . vscodeProposed . window . showInformationMessage (
287- `${ workspaceName } is ${ workspace . latest_build . status } ` ,
288- {
289- modal : true ,
290- detail : `Click below to start the workspace and reconnect.` ,
291- useCustom : true ,
292- } ,
293- "Start Workspace" ,
294- )
295- if ( ! result ) {
296- await this . closeRemote ( )
297- } else {
298- await this . reloadWindow ( )
299- }
300- return
301- }
257+ // If the workspace is not in a running state, try to get it running.
258+ workspace = await this . waitForRunning ( workspaceRestClient , workspace )
259+ this . commands . workspace = workspace
302260
303261 // Pick an agent.
304262 this . storage . writeToCoderOutputChannel ( `Finding agent for ${ workspaceName } ...` )
0 commit comments