markTestSkipped('pcntl and posix are required'); } ChildProcesses::flush(); } public function tearDown(): void { foreach ($this->spawned as $pid) { @posix_kill($pid, SIGKILL); @pcntl_waitpid($pid, $status); } ChildProcesses::flush(); parent::tearDown(); } public function test_hard_shutdown_stops_running_children() { $pid = $this->spawnTrackedChild(); $command = $this->command(LoopFactory::create()); (new \ReflectionMethod(StartServer::class, 'triggerHardShutdown'))->invoke($command); $this->assertFalse(@posix_kill($pid, 0), 'The child must not outlive the server.'); $this->assertSame(0, ChildProcesses::count()); } public function test_soft_shutdown_stops_running_children() { $pid = $this->spawnTrackedChild(); $loop = LoopFactory::create(); $this->app->instance(ChannelManager::class, new class { public function declineNewConnections(): void {} public function getLocalConnections(): PromiseInterface { return Helpers::createFulfilledPromise([]); } }); $command = $this->command($loop); $loop->addTimer(8, fn () => $loop->stop()); $loop->futureTick(fn () => (new \ReflectionMethod(StartServer::class, 'triggerSoftShutdown'))->invoke($command)); $loop->run(); $this->assertFalse(@posix_kill($pid, 0), 'The child must not outlive the server.'); $this->assertSame(0, ChildProcesses::count()); } public function test_the_server_registers_its_listening_socket_for_children_to_close() { // No broadcast socket file left behind in /tmp by this test. config(['websockets.broadcast_socket_enabled' => false]); $registered = null; $this->loop->futureTick(function () use (&$registered) { $registered = ChildProcesses::listenerCount(); $this->loop->stop(); }); $this->artisan('websockets:serve', ['--loop' => $this->loop, '--host' => '127.0.0.1', '--port' => 0]); $this->assertSame(1, $registered); } public function test_children_are_left_alone_when_disabled() { config(['websockets.shutdown.terminate_children' => false]); $pid = $this->spawnTrackedChild(); $command = $this->command(LoopFactory::create()); (new \ReflectionMethod(StartServer::class, 'triggerHardShutdown'))->invoke($command); $this->assertTrue(posix_kill($pid, 0), 'Opted out: the child keeps running.'); } private function command($loop): StartServer { $command = $this->app->make(StartServer::class); $command->setLaravel($this->app); $command->setOutput(new OutputStyle(new ArrayInput([]), new BufferedOutput)); (new \ReflectionProperty(StartServer::class, 'loop'))->setValue($command, $loop); return $command; } private function spawnTrackedChild(): int { $pid = pcntl_fork(); $this->assertNotSame(-1, $pid); if ($pid === 0) { // A busy message child: default SIGTERM disposition, as after // ChildProcesses::prepareChild(). Never returns into PHPUnit. ChildProcesses::restoreDefaultSignals(); while (true) { usleep(50_000); } } $this->spawned[] = $pid; ChildProcesses::track($pid, 'child-crash.hold'); return $pid; } }