139 lines
4.5 KiB
PHP
139 lines
4.5 KiB
PHP
<?php
|
|
|
|
namespace BlaxSoftware\LaravelWebSockets\Test\Commands;
|
|
|
|
use BlaxSoftware\LaravelWebSockets\Console\Commands\StartServer;
|
|
use BlaxSoftware\LaravelWebSockets\Contracts\ChannelManager;
|
|
use BlaxSoftware\LaravelWebSockets\Helpers;
|
|
use BlaxSoftware\LaravelWebSockets\Server\ChildProcesses;
|
|
use BlaxSoftware\LaravelWebSockets\Test\TestCase;
|
|
use Illuminate\Console\OutputStyle;
|
|
use React\EventLoop\Factory as LoopFactory;
|
|
use React\Promise\PromiseInterface;
|
|
use Symfony\Component\Console\Input\ArrayInput;
|
|
use Symfony\Component\Console\Output\BufferedOutput;
|
|
|
|
/**
|
|
* Both shutdown paths of websockets:serve stop the message children that are
|
|
* still running (#2465). A child that outlived the server used to keep the
|
|
* listening port bound, and the restarted server failed with EADDRINUSE.
|
|
*/
|
|
class ShutdownStopsChildrenTest extends TestCase
|
|
{
|
|
/** @var int[] */
|
|
private array $spawned = [];
|
|
|
|
public function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
if (! function_exists('pcntl_fork') || ! function_exists('posix_kill')) {
|
|
$this->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;
|
|
}
|
|
}
|