@@ -46,6 +46,7 @@ import {
4646} from '../support/process'
4747import { acquireE2eRunLock } from '../support/run-lock'
4848import { createE2eRuntimeSecrets , runtimeSecretValues } from '../support/runtime-secrets'
49+ import { createSingleFlightSignalCleanup } from '../support/signal-cleanup'
4950import {
5051 buildApp ,
5152 capturePersonaAuthStates ,
@@ -111,7 +112,6 @@ async function main(): Promise<void> {
111112 let canaryCoverageComplete = true
112113 let diagnosticsRetained = true
113114 let failed = false
114- let signalCleanupStarted = false
115115
116116 const cleanup = ( ) : Promise < void > => {
117117 if ( cleanupPromise ) return cleanupPromise
@@ -163,81 +163,95 @@ async function main(): Promise<void> {
163163 return cleanupPromise
164164 }
165165
166- const handleSignal = async ( signal : NodeJS . Signals ) : Promise < void > => {
167- if ( signalCleanupStarted ) return
168- signalCleanupStarted = true
166+ const signalCleanup = createSingleFlightSignalCleanup ( async ( signal ) => {
169167 failed = true
170168 const exitCode = signal === 'SIGINT' ? 130 : 143
171169 process . exitCode = exitCode
172170 console . error ( `Received ${ signal } ; cleaning up the E2E run` )
173- if ( stripeFake ) {
174- try {
175- writeFileSync (
176- path . join ( logsDirectory , 'stripe-requests.json' ) ,
177- JSON . stringify ( stripeFake . requestLog , null , 2 )
178- )
179- } catch { }
180- }
181171 let lockTransferred = false
182- if ( runDatabase ) {
183- const cleanupLogFd = openSync ( path . join ( logsDirectory , 'signal-cleanup.log' ) , 'a' )
184- try {
185- const cleanupProcess = spawn (
186- bunExecutable ,
187- [ '--no-env-file' , path . join ( SIM_APP_DIR , 'e2e/scripts/signal-cleanup.ts' ) ] ,
188- {
189- cwd : SIM_APP_DIR ,
190- detached : true ,
191- env : {
192- NODE_ENV : 'test' ,
193- PATH : process . env . PATH ?? '' ,
194- HOME : setupHomeDirectory ,
195- E2E_PG_ADMIN_URL : adminDatabaseUrl ,
196- E2E_DATABASE_NAME : runDatabase . name ,
197- E2E_DATABASE_CREATION_COMPLETE : String ( databaseCreationComplete ) ,
198- E2E_CLEANUP_PROCESS_GROUPS : getActiveManagedProcessGroupIds ( ) . join ( ',' ) ,
199- E2E_CLEANUP_DIRECTORIES : JSON . stringify ( [
200- storageStateDirectory ,
201- privateDirectory ,
202- homesDirectory ,
203- ] ) ,
204- E2E_RUN_LOCK_PATH : runLock . path ,
205- E2E_RUN_LOCK_TOKEN : runLock . token ,
206- } ,
207- stdio : [ 'ignore' , cleanupLogFd , cleanupLogFd ] ,
208- }
209- )
210- await new Promise < void > ( ( resolve , reject ) => {
211- cleanupProcess . once ( 'spawn' , resolve )
212- cleanupProcess . once ( 'error' , reject )
213- } )
214- if ( ! cleanupProcess . pid ) {
215- throw new Error ( 'Detached E2E cleanup supervisor started without a process ID' )
172+ try {
173+ if ( stripeFake ) {
174+ try {
175+ writeFileSync (
176+ path . join ( logsDirectory , 'stripe-requests.json' ) ,
177+ JSON . stringify ( stripeFake . requestLog , null , 2 )
178+ )
179+ } catch ( error ) {
180+ console . error ( error )
216181 }
217- if ( ! runLock . transfer ( cleanupProcess . pid ) ) {
218- throw new Error ( 'Detached E2E cleanup supervisor could not acquire run-lock ownership' )
182+ }
183+ if ( runDatabase ) {
184+ let cleanupLogFd : number | null = null
185+ try {
186+ cleanupLogFd = openSync ( path . join ( logsDirectory , 'signal-cleanup.log' ) , 'a' )
187+ const cleanupProcess = spawn (
188+ bunExecutable ,
189+ [ '--no-env-file' , path . join ( SIM_APP_DIR , 'e2e/scripts/signal-cleanup.ts' ) ] ,
190+ {
191+ cwd : SIM_APP_DIR ,
192+ detached : true ,
193+ env : {
194+ NODE_ENV : 'test' ,
195+ PATH : process . env . PATH ?? '' ,
196+ HOME : setupHomeDirectory ,
197+ E2E_PG_ADMIN_URL : adminDatabaseUrl ,
198+ E2E_DATABASE_NAME : runDatabase . name ,
199+ E2E_DATABASE_CREATION_COMPLETE : String ( databaseCreationComplete ) ,
200+ E2E_CLEANUP_PROCESS_GROUPS : getActiveManagedProcessGroupIds ( ) . join ( ',' ) ,
201+ E2E_CLEANUP_DIRECTORIES : JSON . stringify ( [
202+ storageStateDirectory ,
203+ privateDirectory ,
204+ homesDirectory ,
205+ ] ) ,
206+ E2E_RUN_LOCK_PATH : runLock . path ,
207+ E2E_RUN_LOCK_TOKEN : runLock . token ,
208+ } ,
209+ stdio : [ 'ignore' , cleanupLogFd , cleanupLogFd ] ,
210+ }
211+ )
212+ await new Promise < void > ( ( resolve , reject ) => {
213+ cleanupProcess . once ( 'spawn' , resolve )
214+ cleanupProcess . once ( 'error' , reject )
215+ } )
216+ if ( ! cleanupProcess . pid ) {
217+ throw new Error ( 'Detached E2E cleanup supervisor started without a process ID' )
218+ }
219+ if ( ! runLock . transfer ( cleanupProcess . pid ) ) {
220+ throw new Error ( 'Detached E2E cleanup supervisor could not acquire run-lock ownership' )
221+ }
222+ lockTransferred = true
223+ cleanupProcess . unref ( )
224+ console . error ( `Detached E2E cleanup supervisor started as PID ${ cleanupProcess . pid } ` )
225+ } finally {
226+ if ( cleanupLogFd !== null ) {
227+ try {
228+ closeSync ( cleanupLogFd )
229+ } catch ( error ) {
230+ console . error ( error )
231+ }
232+ }
219233 }
220- lockTransferred = true
221- cleanupProcess . unref ( )
222- console . error ( `Detached E2E cleanup supervisor started as PID ${ cleanupProcess . pid } ` )
223- } catch ( error ) {
224- console . error ( error )
225- } finally {
226- closeSync ( cleanupLogFd )
227234 }
235+ } catch ( error ) {
236+ console . error ( error )
228237 }
229- if ( ! lockTransferred ) {
230- if ( runDatabase ) runLock . retain ( 'signal cleanup supervisor failed to start' )
231- else runLock . release ( )
238+ try {
239+ if ( ! lockTransferred ) {
240+ if ( runDatabase ) runLock . retain ( 'signal cleanup supervisor failed to start' )
241+ else runLock . release ( )
242+ }
243+ } catch ( error ) {
244+ console . error ( 'Unable to update E2E run-lock after signal cleanup startup failure' , error )
245+ } finally {
246+ process . exit ( exitCode )
232247 }
233- process . exit ( exitCode )
234- }
235- // Both signal types share a synchronous single-flight guard so opposite signals cannot launch
236- // competing supervisors while the first handler awaits child-process startup.
237- const handleSigint = ( ) : void => void handleSignal ( 'SIGINT' )
238- const handleSigterm = ( ) : void => void handleSignal ( 'SIGTERM' )
239- process . once ( 'SIGINT' , handleSigint )
240- process . once ( 'SIGTERM' , handleSigterm )
248+ } )
249+ // Persistent handlers keep repeated or opposite signals from invoking the OS default while
250+ // the first handler awaits ownership transfer. The synchronous guard keeps cleanup single-flight.
251+ const handleSigint = ( ) : void => void signalCleanup . start ( 'SIGINT' )
252+ const handleSigterm = ( ) : void => void signalCleanup . start ( 'SIGTERM' )
253+ process . on ( 'SIGINT' , handleSigint )
254+ process . on ( 'SIGTERM' , handleSigterm )
241255
242256 try {
243257 const hostAddresses = await assertE2eHostResolvesToLoopback ( )
@@ -415,10 +429,10 @@ async function main(): Promise<void> {
415429 diagnosticsRetained = scrubDiagnostics ( diagnosticRoots )
416430 if ( diagnosticsRetained ) cleanupSucceeded = false
417431 }
418- process . off ( 'SIGINT' , handleSigint )
419- process . off ( 'SIGTERM ' , handleSigterm )
420- setManagedProcessGroupObserver ( null )
421- if ( ! signalCleanupStarted ) {
432+ if ( ! signalCleanup . isStarted ( ) ) {
433+ process . off ( 'SIGINT ' , handleSigint )
434+ process . off ( 'SIGTERM' , handleSigterm )
435+ setManagedProcessGroupObserver ( null )
422436 if ( cleanupSucceeded ) runLock . release ( )
423437 else runLock . retain ( 'normal cleanup failed; inspect diagnostics and clean resources manually' )
424438 }
0 commit comments