@@ -2485,6 +2485,181 @@ public function onAlarm(ConsoleAlarmEvent $event): void
24852485 $ this ->assertSame ([SignalEventSubscriber::class, AlarmEventSubscriber::class], $ command ->signalHandlers );
24862486 }
24872487
2488+ /**
2489+ * @requires extension pcntl
2490+ */
2491+ public function testSignalHandlersAreCleanedUpAfterCommandRuns ()
2492+ {
2493+ $ application = new Application ();
2494+ $ application ->setAutoExit (false );
2495+ $ application ->setCatchExceptions (false );
2496+ $ application ->add (new SignableCommand (false ));
2497+
2498+ $ signalRegistry = $ application ->getSignalRegistry ();
2499+ $ tester = new ApplicationTester ($ application );
2500+
2501+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry should be empty initially. ' );
2502+
2503+ $ tester ->run (['command ' => 'signal ' ]);
2504+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry should be empty after first run. ' );
2505+
2506+ $ tester ->run (['command ' => 'signal ' ]);
2507+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry should still be empty after second run. ' );
2508+ }
2509+
2510+ /**
2511+ * @requires extension pcntl
2512+ */
2513+ public function testSignalHandlersCleanupOnException ()
2514+ {
2515+ $ command = new class ('signal:exception ' ) extends Command implements SignalableCommandInterface {
2516+ public function getSubscribedSignals (): array
2517+ {
2518+ return [\SIGUSR1 ];
2519+ }
2520+
2521+ public function handleSignal (int $ signal , int |false $ previousExitCode = 0 ): int |false
2522+ {
2523+ return false ;
2524+ }
2525+
2526+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
2527+ {
2528+ throw new \RuntimeException ('Test exception ' );
2529+ }
2530+ };
2531+
2532+ $ application = new Application ();
2533+ $ application ->setAutoExit (false );
2534+ $ application ->setCatchExceptions (true );
2535+ $ application ->add ($ command );
2536+
2537+ $ signalRegistry = $ application ->getSignalRegistry ();
2538+ $ tester = new ApplicationTester ($ application );
2539+
2540+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Pre-condition: Registry must be empty. ' );
2541+
2542+ $ tester ->run (['command ' => 'signal:exception ' ]);
2543+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Signal handlers must be cleaned up even on exception. ' );
2544+ }
2545+
2546+ /**
2547+ * @requires extension pcntl
2548+ */
2549+ public function testNestedCommandsIsolateSignalHandlers ()
2550+ {
2551+ $ application = new Application ();
2552+ $ application ->setAutoExit (false );
2553+ $ application ->setCatchExceptions (false );
2554+
2555+ $ signalRegistry = $ application ->getSignalRegistry ();
2556+ $ self = $ this ;
2557+
2558+ $ innerCommand = new class ('signal:inner ' ) extends Command implements SignalableCommandInterface {
2559+ public $ signalRegistry ;
2560+ public $ self ;
2561+
2562+ public function getSubscribedSignals (): array
2563+ {
2564+ return [\SIGUSR1 ];
2565+ }
2566+
2567+ public function handleSignal (int $ signal , int |false $ previousExitCode = 0 ): int |false
2568+ {
2569+ return false ;
2570+ }
2571+
2572+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
2573+ {
2574+ $ handlers = $ this ->self ->getHandlersForSignal ($ this ->signalRegistry , \SIGUSR1 );
2575+ $ this ->self ->assertCount (1 , $ handlers , 'Inner command should only see its own handler. ' );
2576+ $ output ->write ('Inner execute. ' );
2577+
2578+ return 0 ;
2579+ }
2580+ };
2581+
2582+ $ outerCommand = new class ('signal:outer ' ) extends Command implements SignalableCommandInterface {
2583+ public $ signalRegistry ;
2584+ public $ self ;
2585+
2586+ public function getSubscribedSignals (): array
2587+ {
2588+ return [\SIGUSR1 ];
2589+ }
2590+
2591+ public function handleSignal (int $ signal , int |false $ previousExitCode = 0 ): int |false
2592+ {
2593+ return false ;
2594+ }
2595+
2596+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
2597+ {
2598+ $ handlersBefore = $ this ->self ->getHandlersForSignal ($ this ->signalRegistry , \SIGUSR1 );
2599+ $ this ->self ->assertCount (1 , $ handlersBefore , 'Outer command must have its handler registered. ' );
2600+
2601+ $ output ->write ('Outer pre-run. ' );
2602+
2603+ $ this ->getApplication ()->find ('signal:inner ' )->run (new ArrayInput ([]), $ output );
2604+
2605+ $ output ->write ('Outer post-run. ' );
2606+
2607+ $ handlersAfter = $ this ->self ->getHandlersForSignal ($ this ->signalRegistry , \SIGUSR1 );
2608+ $ this ->self ->assertCount (1 , $ handlersAfter , 'Outer command \'s handler must be restored. ' );
2609+ $ this ->self ->assertSame ($ handlersBefore , $ handlersAfter , 'Handler stack must be identical after pop. ' );
2610+
2611+ return 0 ;
2612+ }
2613+ };
2614+
2615+ $ innerCommand ->self = $ self ;
2616+ $ innerCommand ->signalRegistry = $ signalRegistry ;
2617+ $ outerCommand ->self = $ self ;
2618+ $ outerCommand ->signalRegistry = $ signalRegistry ;
2619+
2620+ $ application ->add ($ innerCommand );
2621+ $ application ->add ($ outerCommand );
2622+
2623+ $ tester = new ApplicationTester ($ application );
2624+
2625+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Pre-condition: Registry must be empty. ' );
2626+ $ tester ->run (['command ' => 'signal:outer ' ]);
2627+ $ this ->assertStringContainsString ('Outer pre-run.Inner execute.Outer post-run. ' , $ tester ->getDisplay ());
2628+
2629+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry must be empty after all commands are finished. ' );
2630+ }
2631+
2632+ /**
2633+ * @requires extension pcntl
2634+ */
2635+ public function testOriginalHandlerRestoredAfterPop ()
2636+ {
2637+ $ this ->assertSame (\SIG_DFL , pcntl_signal_get_handler (\SIGUSR1 ), 'Pre-condition: Original handler for SIGUSR1 must be SIG_DFL. ' );
2638+
2639+ $ application = new Application ();
2640+ $ application ->setAutoExit (false );
2641+ $ application ->setCatchExceptions (false );
2642+ $ application ->add (new SignableCommand (false ));
2643+
2644+ $ tester = new ApplicationTester ($ application );
2645+ $ tester ->run (['command ' => 'signal ' ]);
2646+
2647+ $ this ->assertSame (\SIG_DFL , pcntl_signal_get_handler (\SIGUSR1 ), 'OS-level handler for SIGUSR1 must be restored to SIG_DFL. ' );
2648+
2649+ $ tester ->run (['command ' => 'signal ' ]);
2650+ $ this ->assertSame (\SIG_DFL , pcntl_signal_get_handler (\SIGUSR1 ), 'OS-level handler must remain SIG_DFL after a second run. ' );
2651+ }
2652+
2653+ /**
2654+ * Reads the private "signalHandlers" property of the SignalRegistry for assertions.
2655+ */
2656+ public function getHandlersForSignal (SignalRegistry $ registry , int $ signal ): array
2657+ {
2658+ $ handlers = (\Closure::bind (fn () => $ this ->signalHandlers , $ registry , SignalRegistry::class))();
2659+
2660+ return $ handlers [$ signal ] ?? [];
2661+ }
2662+
24882663 private function createSignalableApplication (Command $ command , ?EventDispatcherInterface $ dispatcher ): Application
24892664 {
24902665 $ application = new Application ();
0 commit comments