@@ -824,7 +824,7 @@ public function testSetCatchErrors(bool $catchExceptions)
824824
825825 try {
826826 $ tester ->run (['command ' => 'boom ' ]);
827- $ this ->fail ('The exception is not catched . ' );
827+ $ this ->fail ('The exception is not caught . ' );
828828 } catch (\Throwable $ e ) {
829829 $ this ->assertInstanceOf (\Error::class, $ e );
830830 $ this ->assertSame ('This is an error. ' , $ e ->getMessage ());
@@ -2259,6 +2259,181 @@ private function runRestoresSttyTest(array $params, int $expectedExitCode, bool
22592259 }
22602260 }
22612261
2262+ /**
2263+ * @requires extension pcntl
2264+ */
2265+ public function testSignalHandlersAreCleanedUpAfterCommandRuns ()
2266+ {
2267+ $ application = new Application ();
2268+ $ application ->setAutoExit (false );
2269+ $ application ->setCatchExceptions (false );
2270+ $ application ->add (new SignableCommand (false ));
2271+
2272+ $ signalRegistry = $ application ->getSignalRegistry ();
2273+ $ tester = new ApplicationTester ($ application );
2274+
2275+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry should be empty initially. ' );
2276+
2277+ $ tester ->run (['command ' => 'signal ' ]);
2278+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry should be empty after first run. ' );
2279+
2280+ $ tester ->run (['command ' => 'signal ' ]);
2281+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry should still be empty after second run. ' );
2282+ }
2283+
2284+ /**
2285+ * @requires extension pcntl
2286+ */
2287+ public function testSignalHandlersCleanupOnException ()
2288+ {
2289+ $ command = new class ('signal:exception ' ) extends Command implements SignalableCommandInterface {
2290+ public function getSubscribedSignals (): array
2291+ {
2292+ return [\SIGUSR1 ];
2293+ }
2294+
2295+ public function handleSignal (int $ signal , int |false $ previousExitCode = 0 ): int |false
2296+ {
2297+ return false ;
2298+ }
2299+
2300+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
2301+ {
2302+ throw new \RuntimeException ('Test exception ' );
2303+ }
2304+ };
2305+
2306+ $ application = new Application ();
2307+ $ application ->setAutoExit (false );
2308+ $ application ->setCatchExceptions (true );
2309+ $ application ->add ($ command );
2310+
2311+ $ signalRegistry = $ application ->getSignalRegistry ();
2312+ $ tester = new ApplicationTester ($ application );
2313+
2314+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Pre-condition: Registry must be empty. ' );
2315+
2316+ $ tester ->run (['command ' => 'signal:exception ' ]);
2317+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Signal handlers must be cleaned up even on exception. ' );
2318+ }
2319+
2320+ /**
2321+ * @requires extension pcntl
2322+ */
2323+ public function testNestedCommandsIsolateSignalHandlers ()
2324+ {
2325+ $ application = new Application ();
2326+ $ application ->setAutoExit (false );
2327+ $ application ->setCatchExceptions (false );
2328+
2329+ $ signalRegistry = $ application ->getSignalRegistry ();
2330+ $ self = $ this ;
2331+
2332+ $ innerCommand = new class ('signal:inner ' ) extends Command implements SignalableCommandInterface {
2333+ public $ signalRegistry ;
2334+ public $ self ;
2335+
2336+ public function getSubscribedSignals (): array
2337+ {
2338+ return [\SIGUSR1 ];
2339+ }
2340+
2341+ public function handleSignal (int $ signal , int |false $ previousExitCode = 0 ): int |false
2342+ {
2343+ return false ;
2344+ }
2345+
2346+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
2347+ {
2348+ $ handlers = $ this ->self ->getHandlersForSignal ($ this ->signalRegistry , \SIGUSR1 );
2349+ $ this ->self ->assertCount (1 , $ handlers , 'Inner command should only see its own handler. ' );
2350+ $ output ->write ('Inner execute. ' );
2351+
2352+ return 0 ;
2353+ }
2354+ };
2355+
2356+ $ outerCommand = new class ('signal:outer ' ) extends Command implements SignalableCommandInterface {
2357+ public $ signalRegistry ;
2358+ public $ self ;
2359+
2360+ public function getSubscribedSignals (): array
2361+ {
2362+ return [\SIGUSR1 ];
2363+ }
2364+
2365+ public function handleSignal (int $ signal , int |false $ previousExitCode = 0 ): int |false
2366+ {
2367+ return false ;
2368+ }
2369+
2370+ protected function execute (InputInterface $ input , OutputInterface $ output ): int
2371+ {
2372+ $ handlersBefore = $ this ->self ->getHandlersForSignal ($ this ->signalRegistry , \SIGUSR1 );
2373+ $ this ->self ->assertCount (1 , $ handlersBefore , 'Outer command must have its handler registered. ' );
2374+
2375+ $ output ->write ('Outer pre-run. ' );
2376+
2377+ $ this ->getApplication ()->find ('signal:inner ' )->run (new ArrayInput ([]), $ output );
2378+
2379+ $ output ->write ('Outer post-run. ' );
2380+
2381+ $ handlersAfter = $ this ->self ->getHandlersForSignal ($ this ->signalRegistry , \SIGUSR1 );
2382+ $ this ->self ->assertCount (1 , $ handlersAfter , 'Outer command \'s handler must be restored. ' );
2383+ $ this ->self ->assertSame ($ handlersBefore , $ handlersAfter , 'Handler stack must be identical after pop. ' );
2384+
2385+ return 0 ;
2386+ }
2387+ };
2388+
2389+ $ innerCommand ->self = $ self ;
2390+ $ innerCommand ->signalRegistry = $ signalRegistry ;
2391+ $ outerCommand ->self = $ self ;
2392+ $ outerCommand ->signalRegistry = $ signalRegistry ;
2393+
2394+ $ application ->add ($ innerCommand );
2395+ $ application ->add ($ outerCommand );
2396+
2397+ $ tester = new ApplicationTester ($ application );
2398+
2399+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Pre-condition: Registry must be empty. ' );
2400+ $ tester ->run (['command ' => 'signal:outer ' ]);
2401+ $ this ->assertStringContainsString ('Outer pre-run.Inner execute.Outer post-run. ' , $ tester ->getDisplay ());
2402+
2403+ $ this ->assertCount (0 , $ this ->getHandlersForSignal ($ signalRegistry , \SIGUSR1 ), 'Registry must be empty after all commands are finished. ' );
2404+ }
2405+
2406+ /**
2407+ * @requires extension pcntl
2408+ */
2409+ public function testOriginalHandlerRestoredAfterPop ()
2410+ {
2411+ $ this ->assertSame (\SIG_DFL , pcntl_signal_get_handler (\SIGUSR1 ), 'Pre-condition: Original handler for SIGUSR1 must be SIG_DFL. ' );
2412+
2413+ $ application = new Application ();
2414+ $ application ->setAutoExit (false );
2415+ $ application ->setCatchExceptions (false );
2416+ $ application ->add (new SignableCommand (false ));
2417+
2418+ $ tester = new ApplicationTester ($ application );
2419+ $ tester ->run (['command ' => 'signal ' ]);
2420+
2421+ $ this ->assertSame (\SIG_DFL , pcntl_signal_get_handler (\SIGUSR1 ), 'OS-level handler for SIGUSR1 must be restored to SIG_DFL. ' );
2422+
2423+ $ tester ->run (['command ' => 'signal ' ]);
2424+ $ this ->assertSame (\SIG_DFL , pcntl_signal_get_handler (\SIGUSR1 ), 'OS-level handler must remain SIG_DFL after a second run. ' );
2425+ }
2426+
2427+ /**
2428+ * Reads the private "signalHandlers" property of the SignalRegistry for assertions.
2429+ */
2430+ public function getHandlersForSignal (SignalRegistry $ registry , int $ signal ): array
2431+ {
2432+ $ handlers = (\Closure::bind (fn () => $ this ->signalHandlers , $ registry , SignalRegistry::class))();
2433+
2434+ return $ handlers [$ signal ] ?? [];
2435+ }
2436+
22622437 private function createSignalableApplication (Command $ command , ?EventDispatcherInterface $ dispatcher ): Application
22632438 {
22642439 $ application = new Application ();
0 commit comments