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