@@ -428,6 +428,12 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
428428
429429 // Keep the command running until interrupted or dev server exits
430430 await new Promise < void > ( ( resolve ) => {
431+ const handleSignal = ( signal : string ) : void => {
432+ this . logger ?. debug ( `Received ${ signal } signal, initiating graceful shutdown` ) ;
433+ process . exitCode = 130 ; // Standard exit code for SIGINT/SIGTERM
434+ resolve ( ) ;
435+ } ;
436+
431437 // Exit if dev server exits with SIGINT (user pressed Ctrl+C)
432438 if ( this . devServerManager ) {
433439 this . devServerManager . on ( 'exit' , ( code : number | null , signal : string | null ) => {
@@ -438,19 +444,15 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
438444 } ) ;
439445 }
440446
441- // CRITICAL: Use prependOnceListener to add our handlers BEFORE sfCommand's handlers
447+ // CRITICAL: Remove sfCommand's signal handlers before adding our own.
442448 // sfCommand adds process.on('SIGINT', () => this.exit(130)) which throws ExitError
443- // By using prependOnceListener, our resolve() runs FIRST, allowing clean shutdown
444- // This is especially important when there's no dev server (explicit URL mode)
445- process . prependOnceListener ( 'SIGINT' , ( ) => {
446- this . logger ?. debug ( 'Received SIGINT signal, initiating graceful shutdown' ) ;
447- resolve ( ) ;
448- } ) ;
449-
450- process . prependOnceListener ( 'SIGTERM' , ( ) => {
451- this . logger ?. debug ( 'Received SIGTERM signal, initiating graceful shutdown' ) ;
452- resolve ( ) ;
453- } ) ;
449+ // and prints an ugly stack trace. By removing those handlers and handling signals
450+ // ourselves, we exit cleanly: resolve() -> run() returns -> finally() cleans up.
451+ const signalsToHandle = [ 'SIGINT' , 'SIGTERM' , 'SIGBREAK' , 'SIGHUP' ] as const ;
452+ for ( const signal of signalsToHandle ) {
453+ process . removeAllListeners ( signal ) ;
454+ process . once ( signal , ( ) => handleSignal ( signal ) ) ;
455+ }
454456 } ) ;
455457
456458 // Return result (never reached, but required for type safety)
@@ -482,8 +484,6 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
482484 * This is the proper way to handle cleanup in oclif commands
483485 */
484486 protected async finally ( ) : Promise < void > {
485- // Cleanup all resources silently
486- // Don't show messages here as this runs on ALL exits (errors, Ctrl+C, etc)
487487 await this . cleanup ( ) ;
488488 }
489489
@@ -514,11 +514,18 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
514514 * Cleanup all resources (proxy, dev server, file watcher)
515515 */
516516 private async cleanup ( ) : Promise < void > {
517- // Stop proxy server
517+ const hasProxy = ! ! this . proxyServer ;
518+ const hasDevServer = ! ! this . devServerManager ;
519+ const showShutdownLog = hasProxy || hasDevServer ;
520+
521+ if ( showShutdownLog ) {
522+ this . log ( '' ) ;
523+ }
524+
525+ // Stop proxy server first (closes connections, stops accepting new requests)
518526 if ( this . proxyServer ) {
519527 try {
520528 await this . proxyServer . stop ( ) ;
521- this . logger ?. debug ( 'Proxy server stopped' ) ;
522529 } catch ( error ) {
523530 this . logger ?. debug ( `Failed to stop proxy server: ${ ( error as Error ) . message } ` ) ;
524531 }
@@ -529,7 +536,6 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
529536 if ( this . devServerManager ) {
530537 try {
531538 await this . devServerManager . stop ( ) ;
532- this . logger ?. debug ( 'Dev server stopped' ) ;
533539 } catch ( error ) {
534540 this . logger ?. debug ( `Failed to stop dev server: ${ ( error as Error ) . message } ` ) ;
535541 }
@@ -540,13 +546,21 @@ export default class WebappDev extends SfCommand<WebAppDevResult> {
540546 if ( this . manifestWatcher ) {
541547 try {
542548 await this . manifestWatcher . stop ( ) ;
543- this . logger ?. debug ( 'Manifest watcher stopped' ) ;
544549 } catch ( error ) {
545550 this . logger ?. debug ( `Failed to stop manifest watcher: ${ ( error as Error ) . message } ` ) ;
546551 }
547552 this . manifestWatcher = null ;
548553 }
549554
555+ if ( showShutdownLog ) {
556+ if ( hasProxy && hasDevServer ) {
557+ this . log ( messages . getMessage ( 'info.stopped-dev-and-proxy' ) ) ;
558+ } else if ( hasProxy ) {
559+ this . log ( messages . getMessage ( 'info.stopped-proxy-only' ) ) ;
560+ } else {
561+ this . log ( messages . getMessage ( 'info.stopped-dev-only' ) ) ;
562+ }
563+ }
550564 this . logger ?. debug ( 'Cleanup complete' ) ;
551565 }
552566}
0 commit comments