@@ -13,9 +13,10 @@ const log = logger.scope("app-lifecycle");
1313
1414@injectable ( )
1515export class AppLifecycleService {
16+ private static readonly SHUTDOWN_TIMEOUT_MS = 3000 ;
17+
1618 private _isQuittingForUpdate = false ;
1719 private _isShuttingDown = false ;
18- private static readonly SHUTDOWN_TIMEOUT_MS = 3000 ;
1920
2021 get isQuittingForUpdate ( ) : boolean {
2122 return this . _isQuittingForUpdate ;
@@ -29,60 +30,21 @@ export class AppLifecycleService {
2930 this . _isQuittingForUpdate = true ;
3031 }
3132
32- forceExit ( ) : never {
33+ /**
34+ * Immediately kills the process. Used when shutdown is stuck or re-entrant.
35+ */
36+ forceKill ( ) : never {
3337 log . warn ( "Force-killing process" ) ;
3438 process . exit ( 1 ) ;
3539 }
3640
37- async cleanupForUpdate ( ) : Promise < void > {
38- log . info ( "Cleanup for update started" ) ;
39-
40- // Shut down watchers
41- log . info ( "Shutting down native watchers" ) ;
42- try {
43- const watcherRegistry = container . get < WatcherRegistryService > (
44- MAIN_TOKENS . WatcherRegistryService ,
45- ) ;
46- await watcherRegistry . shutdownAll ( ) ;
47- } catch ( error ) {
48- log . warn ( "Failed to shutdown watcher registry" , error ) ;
49- }
50-
51- // Kill all tracked processes
52- try {
53- const processTracking = container . get < ProcessTrackingService > (
54- MAIN_TOKENS . ProcessTrackingService ,
55- ) ;
56- const snapshot = await processTracking . getSnapshot ( true ) ;
57- log . info ( "Process snapshot before update" , {
58- tracked : {
59- shell : snapshot . tracked . shell . length ,
60- agent : snapshot . tracked . agent . length ,
61- child : snapshot . tracked . child . length ,
62- } ,
63- } ) ;
64-
65- if (
66- snapshot . tracked . shell . length +
67- snapshot . tracked . agent . length +
68- snapshot . tracked . child . length >
69- 0
70- ) {
71- log . info ( "Killing all tracked processes before update" ) ;
72- processTracking . killAll ( ) ;
73- }
74- } catch ( error ) {
75- log . warn ( "Failed to kill processes before update" , error ) ;
76- }
77-
78- // Skip container unbind, PostHog shutdown - app is restarting anyway
79- log . info ( "Cleanup for update complete" ) ;
80- }
81-
41+ /**
42+ * Full graceful shutdown with timeout. Force-kills if already in progress or times out.
43+ */
8244 async shutdown ( ) : Promise < void > {
8345 if ( this . _isShuttingDown ) {
8446 log . warn ( "Shutdown already in progress, forcing exit" ) ;
85- this . forceExit ( ) ;
47+ this . forceKill ( ) ;
8648 }
8749
8850 this . _isShuttingDown = true ;
@@ -96,14 +58,57 @@ export class AppLifecycleService {
9658 log . warn ( "Shutdown timeout reached, forcing exit" , {
9759 timeoutMs : AppLifecycleService . SHUTDOWN_TIMEOUT_MS ,
9860 } ) ;
99- this . forceExit ( ) ;
61+ this . forceKill ( ) ;
10062 }
10163 }
10264
65+ /**
66+ * Tears down watchers and processes but keeps the DI container alive
67+ * so the before-quit handler can still access services. Used before quitAndInstall.
68+ */
69+ async shutdownWithoutContainer ( ) : Promise < void > {
70+ log . info ( "Partial shutdown started (keeping container)" ) ;
71+ await this . teardownNativeResources ( ) ;
72+ }
73+
74+ /**
75+ * Runs a full shutdown then exits the Electron app.
76+ */
77+ async gracefulExit ( ) : Promise < void > {
78+ await this . shutdown ( ) ;
79+ app . exit ( 0 ) ;
80+ }
81+
82+ /**
83+ * Runs the full shutdown sequence: native resources, container, analytics.
84+ */
10385 private async doShutdown ( ) : Promise < void > {
10486 log . info ( "Shutdown started" ) ;
10587
106- log . info ( "Shutting down native watchers first" ) ;
88+ await this . teardownNativeResources ( ) ;
89+
90+ try {
91+ await container . unbindAll ( ) ;
92+ } catch ( error ) {
93+ log . warn ( "Failed to unbind container" , error ) ;
94+ }
95+
96+ trackAppEvent ( ANALYTICS_EVENTS . APP_QUIT ) ;
97+
98+ try {
99+ await shutdownPostHog ( ) ;
100+ } catch ( error ) {
101+ log . warn ( "Failed to shutdown PostHog" , error ) ;
102+ }
103+
104+ log . info ( "Shutdown complete" ) ;
105+ }
106+
107+ /**
108+ * Shuts down file watchers and kills child processes, then drains the
109+ * event loop so pending native callbacks fire while JS is still alive.
110+ */
111+ private async teardownNativeResources ( ) : Promise < void > {
107112 try {
108113 const watcherRegistry = container . get < WatcherRegistryService > (
109114 MAIN_TOKENS . WatcherRegistryService ,
@@ -118,54 +123,30 @@ export class AppLifecycleService {
118123 MAIN_TOKENS . ProcessTrackingService ,
119124 ) ;
120125 const snapshot = await processTracking . getSnapshot ( true ) ;
121- log . info ( "Process snapshot at shutdown " , {
126+ log . debug ( "Process snapshot" , {
122127 tracked : {
123128 shell : snapshot . tracked . shell . length ,
124129 agent : snapshot . tracked . agent . length ,
125130 child : snapshot . tracked . child . length ,
126131 } ,
127132 discovered : snapshot . discovered ?. length ?? 0 ,
128- untrackedDiscovered :
129- snapshot . discovered ?. filter ( ( p ) => ! p . tracked ) . length ?? 0 ,
130133 } ) ;
131134
132- if (
135+ const trackedCount =
133136 snapshot . tracked . shell . length +
134- snapshot . tracked . agent . length +
135- snapshot . tracked . child . length >
136- 0
137- ) {
138- log . info ( " Killing all tracked processes before container unbind" ) ;
137+ snapshot . tracked . agent . length +
138+ snapshot . tracked . child . length ;
139+
140+ if ( trackedCount > 0 ) {
141+ log . info ( ` Killing ${ trackedCount } tracked processes` ) ;
139142 processTracking . killAll ( ) ;
140143 }
141144 } catch ( error ) {
142- log . warn ( "Failed to get process snapshot at shutdown " , error ) ;
145+ log . warn ( "Failed to kill tracked processes " , error ) ;
143146 }
144147
145- log . info ( "Unbinding container" ) ;
146- try {
147- await container . unbindAll ( ) ;
148- log . info ( "Container unbound successfully" ) ;
149- } catch ( error ) {
150- log . error ( "Failed to unbind container" , error ) ;
151- }
152-
153- trackAppEvent ( ANALYTICS_EVENTS . APP_QUIT ) ;
154-
155- log . info ( "Shutting down PostHog" ) ;
156- try {
157- await shutdownPostHog ( ) ;
158- log . info ( "PostHog shutdown complete" ) ;
159- } catch ( error ) {
160- log . error ( "Failed to shutdown PostHog" , error ) ;
161- }
162-
163- log . info ( "Graceful shutdown complete" ) ;
164- }
165-
166- async shutdownAndExit ( ) : Promise < void > {
167- await this . shutdown ( ) ;
168- log . info ( "Calling app.exit(0)" ) ;
169- app . exit ( 0 ) ;
148+ // Drain pending native callbacks (e.g. @parcel/watcher ThreadSafeFunction)
149+ // so they fire while JS is still alive, not during FreeEnvironment teardown
150+ await new Promise ( ( resolve ) => setImmediate ( resolve ) ) ;
170151 }
171152}
0 commit comments