@@ -9,6 +9,7 @@ const PM2_RESTART_DELAY_MS = 2000;
99const PM2_MIN_UPTIME = "5s" ;
1010const PM2_MAX_RESTARTS = 10 ;
1111const PM2_DELETE_WAIT_MS = 5000 ;
12+ const PM2_DISCONNECT_WAIT_MS = 1000 ;
1213const STARTUP_POLL_INTERVAL_MS = 100 ;
1314const STARTUP_FAILURE_GUIDANCE =
1415 "Run `coder-studio logs` for details or `coder-studio serve --foreground` for interactive debugging." ;
@@ -62,7 +63,7 @@ const isPm2BrokenStateError = (error: unknown): boolean => {
6263
6364type Pm2Module = {
6465 connect : ( cb : ( err : Error | null ) => void ) => void ;
65- disconnect : ( ) => void ;
66+ disconnect : ( cb ?: ( err : Error | null , data ?: unknown ) => void ) => void ;
6667 describe : ( name : string , cb : ( err : Error | null , result : unknown [ ] ) => void ) => void ;
6768 delete : ( name : string , cb : ( err : Error | null ) => void ) => void ;
6869 start : ( opts : unknown , cb : ( err : Error | null ) => void ) => void ;
@@ -111,36 +112,51 @@ const sleep = async (ms: number): Promise<void> =>
111112
112113const disconnectPm2 = async ( ) : Promise < void > => {
113114 const pm2 = await loadPm2 ( ) ;
114- pm2 . disconnect ( ) ;
115+ await new Promise < void > ( ( resolve ) => {
116+ let settled = false ;
117+ const finish = ( ) => {
118+ if ( settled ) {
119+ return ;
120+ }
121+
122+ settled = true ;
123+ resolve ( ) ;
124+ } ;
125+
126+ const timer = setTimeout ( finish , PM2_DISCONNECT_WAIT_MS ) ;
127+ try {
128+ pm2 . disconnect ( ( ) => {
129+ clearTimeout ( timer ) ;
130+ finish ( ) ;
131+ } ) ;
132+ } catch {
133+ clearTimeout ( timer ) ;
134+ finish ( ) ;
135+ }
136+ } ) ;
115137} ;
116138
117- const describeManagedServer = async ( ) : Promise < Pm2ProcessDescription [ ] > => {
118- const pm2 = await loadPm2 ( ) ;
119- return new Promise ( ( resolve , reject ) => {
139+ const describeManagedServer = async ( pm2 : Pm2Module ) : Promise < Pm2ProcessDescription [ ] > =>
140+ new Promise ( ( resolve , reject ) => {
120141 pm2 . describe ( MANAGED_SERVER_NAME , ( error , result ) => {
121142 if ( error ) {
122143 reject ( error ) ;
123144 return ;
124145 }
125-
126146 resolve ( ( result ?? [ ] ) as Pm2ProcessDescription [ ] ) ;
127147 } ) ;
128148 } ) ;
129- } ;
130149
131- const removeManagedServer = async ( ) : Promise < void > => {
132- const pm2 = await loadPm2 ( ) ;
133- return new Promise ( ( resolve , reject ) => {
150+ const removeManagedServer = async ( pm2 : Pm2Module ) : Promise < void > =>
151+ new Promise ( ( resolve , reject ) => {
134152 pm2 . delete ( MANAGED_SERVER_NAME , ( error ) => {
135153 if ( error ) {
136154 reject ( error ) ;
137155 return ;
138156 }
139-
140157 resolve ( ) ;
141158 } ) ;
142159 } ) ;
143- } ;
144160
145161/**
146162 * Kill the PM2 daemon to clear stale paths/caches.
@@ -159,9 +175,10 @@ const killPm2Daemon = async (): Promise<void> => {
159175 * Try to connect to PM2, and if it's in a broken state (stale worktree path),
160176 * kill the daemon and reconnect fresh.
161177 */
162- const connectWithRecovery = async ( ) : Promise < void > => {
178+ const connectWithRecovery = async ( ) : Promise < Pm2Module > => {
163179 try {
164180 await connectPm2 ( ) ;
181+ return loadPm2 ( ) ;
165182 } catch ( error ) {
166183 if ( isPm2BrokenStateError ( error ) ) {
167184 console . warn ( "PM2 daemon is in a stale state. Killing and reconnecting..." ) ;
@@ -174,23 +191,25 @@ const connectWithRecovery = async (): Promise<void> => {
174191 // Clear cached module so next loadPm2 gets a fresh instance
175192 cachedPm2 = null ;
176193 await connectPm2 ( ) ;
194+ return loadPm2 ( ) ;
177195 } else {
178196 throw error ;
179197 }
180198 }
181199} ;
182200
183- const withPm2Connection = async < T > ( operation : ( ) => Promise < T > ) : Promise < T > => {
184- await connectWithRecovery ( ) ;
201+ const withPm2Connection = async < T > ( operation : ( pm2 : Pm2Module ) => Promise < T > ) : Promise < T > => {
202+ const pm2 = await connectWithRecovery ( ) ;
185203
186204 try {
187- return await operation ( ) ;
205+ return await operation ( pm2 ) ;
188206 } finally {
189207 await disconnectPm2 ( ) ;
190208 }
191209} ;
192210
193211const waitForRuntimeReady = async (
212+ pm2 : Pm2Module ,
194213 waitMs : number ,
195214 logOffsets : StartupLogOffsets
196215) : Promise < void > => {
@@ -201,7 +220,7 @@ const waitForRuntimeReady = async (
201220 return ;
202221 }
203222
204- const processes = await describeManagedServer ( ) ;
223+ const processes = await describeManagedServer ( pm2 ) ;
205224 const process = processes [ 0 ] ;
206225 if ( ! process ) {
207226 throw createStartupError (
@@ -226,22 +245,11 @@ const waitForRuntimeReady = async (
226245 throw createStartupError ( `runtime readiness timed out after ${ waitMs } ms` , logOffsets ) ;
227246} ;
228247
229- const waitForManagedServerExit = async ( ) : Promise < void > => {
230- while ( true ) {
231- const processes = await describeManagedServer ( ) ;
232- if ( processes . length === 0 ) {
233- return ;
234- }
235-
236- await sleep ( STARTUP_POLL_INTERVAL_MS ) ;
237- }
238- } ;
239-
240- const waitForManagedServerDeletion = async ( waitMs : number ) : Promise < void > => {
248+ const waitForManagedServerDeletion = async ( pm2 : Pm2Module , waitMs : number ) : Promise < void > => {
241249 const deadline = Date . now ( ) + waitMs ;
242250
243251 while ( Date . now ( ) <= deadline ) {
244- const processes = await withPm2Connection ( describeManagedServer ) ;
252+ const processes = await describeManagedServer ( pm2 ) ;
245253 if ( processes . length === 0 ) {
246254 return ;
247255 }
@@ -257,6 +265,34 @@ const waitForManagedServerDeletion = async (waitMs: number): Promise<void> => {
257265 throw new Error ( `Timed out waiting for the managed server to stop after ${ waitMs } ms.` ) ;
258266} ;
259267
268+ const deleteManagedServerInSession = async (
269+ pm2 : Pm2Module ,
270+ {
271+ ignoreMissing = false ,
272+ } : {
273+ ignoreMissing ?: boolean ;
274+ } = { }
275+ ) : Promise < boolean > => {
276+ const processes = await describeManagedServer ( pm2 ) ;
277+ if ( processes . length === 0 ) {
278+ return false ;
279+ }
280+
281+ try {
282+ await removeManagedServer ( pm2 ) ;
283+ } catch ( error ) {
284+ if ( ignoreMissing && isMissingManagedServerError ( error ) ) {
285+ await waitForManagedServerDeletion ( pm2 , PM2_DELETE_WAIT_MS ) ;
286+ return false ;
287+ }
288+
289+ throw error ;
290+ }
291+
292+ await waitForManagedServerDeletion ( pm2 , PM2_DELETE_WAIT_MS ) ;
293+ return true ;
294+ } ;
295+
260296const ensureLogDirectory = ( ) : void => {
261297 mkdirSync ( join ( homedir ( ) , ".coder-studio" , "logs" ) , { recursive : true } ) ;
262298} ;
@@ -307,56 +343,26 @@ export const deleteManagedServer = async ({
307343 ignoreMissing = false ,
308344} : {
309345 ignoreMissing ?: boolean ;
310- } = { } ) : Promise < boolean > => {
311- const deleted = await withPm2Connection ( async ( ) => {
312- const processes = await describeManagedServer ( ) ;
313- if ( processes . length === 0 ) {
314- return false ;
315- }
316-
317- try {
318- await removeManagedServer ( ) ;
319- return true ;
320- } catch ( error ) {
321- if ( ignoreMissing && isMissingManagedServerError ( error ) ) {
322- return false ;
323- }
324-
325- throw error ;
326- }
327- } ) ;
328-
329- if ( ! deleted ) {
330- return false ;
331- }
332-
333- await waitForManagedServerDeletion ( PM2_DELETE_WAIT_MS ) ;
334- return true ;
335- } ;
346+ } = { } ) : Promise < boolean > =>
347+ withPm2Connection ( ( pm2 ) => deleteManagedServerInSession ( pm2 , { ignoreMissing } ) ) ;
336348
337349export const startManagedServer = async ( {
338350 script,
339351 cwd,
340352 waitMs,
341353 args,
342- } : StartManagedServerOptions ) : Promise < void > => {
343- // First try to delete any existing managed server
344- await deleteManagedServer ( { ignoreMissing : true } ) ;
354+ } : StartManagedServerOptions ) : Promise < void > =>
355+ withPm2Connection ( async ( pm2 ) => {
356+ await deleteManagedServerInSession ( pm2 , { ignoreMissing : true } ) ;
345357
346- // Wait for the old process to actually exit
347- await withPm2Connection ( waitForManagedServerExit ) ;
348-
349- // Clear stale runtime config
350- if ( readRuntimeConfig ( ) ) {
351- deleteRuntimeConfig ( ) ;
352- }
353-
354- ensureLogDirectory ( ) ;
355- const { outFile, errFile } = getLogPaths ( ) ;
356- const pm2 = await loadPm2 ( ) ;
358+ if ( readRuntimeConfig ( ) ) {
359+ deleteRuntimeConfig ( ) ;
360+ }
357361
358- await withPm2Connection ( async ( ) => {
362+ ensureLogDirectory ( ) ;
363+ const { outFile, errFile } = getLogPaths ( ) ;
359364 const logOffsets = captureStartupLogOffsets ( ) ;
365+
360366 await new Promise < void > ( ( resolve , reject ) => {
361367 pm2 . start (
362368 {
@@ -386,13 +392,12 @@ export const startManagedServer = async ({
386392 ) ;
387393 } ) ;
388394
389- await waitForRuntimeReady ( waitMs , logOffsets ) ;
395+ await waitForRuntimeReady ( pm2 , waitMs , logOffsets ) ;
390396 } ) ;
391- } ;
392397
393398export const getManagedServerStatus = async ( ) : Promise < ManagedServerStatus > =>
394- withPm2Connection ( async ( ) => {
395- const processes = await describeManagedServer ( ) ;
399+ withPm2Connection ( async ( pm2 ) => {
400+ const processes = await describeManagedServer ( pm2 ) ;
396401 const process = processes [ 0 ] ;
397402
398403 if ( ! process ) {
0 commit comments