diff --git a/src/Database.php b/src/Database.php index d1235af..f6ebf76 100644 --- a/src/Database.php +++ b/src/Database.php @@ -83,7 +83,49 @@ class Database extends EventEmitter */ public static function open(LoopInterface $loop, $filename, $flags = null) { - $process = new Process('exec ' . \escapeshellarg(\PHP_BINARY) . ' ' . \escapeshellarg(__DIR__ . '/../res/sqlite-worker.php')); + $command = 'exec ' . \escapeshellarg(\PHP_BINARY) . ' ' . \escapeshellarg(__DIR__ . '/../res/sqlite-worker.php'); + + // Try to get list of all open FDs (Linux/Mac and others) + $fds = @\scandir('/dev/fd'); + + // Otherwise try temporarily duplicating file descriptors in the range 0-1024 (FD_SETSIZE). + // This is known to work on more exotic platforms and also inside chroot + // environments without /dev/fd. Causes many syscalls, but still rather fast. + // @codeCoverageIgnoreStart + if ($fds === false) { + $fds = array(); + for ($i = 0; $i <= 1024; ++$i) { + $copy = @\fopen('php://fd/' . $i, 'r'); + if ($copy !== false) { + $fds[] = $i; + \fclose($copy); + } + } + } + // @codeCoverageIgnoreEnd + + // launch process with default STDIO pipes + $pipes = array( + array('pipe', 'r'), + array('pipe', 'w'), + array('pipe', 'w') + ); + + // do not inherit open FDs by explicitly overwriting existing FDs with dummy files + // additionally, close all dummy files in the child process again + foreach ($fds as $fd) { + if ($fd > 2) { + $pipes[$fd] = array('file', '/dev/null', 'r'); + $command .= ' ' . $fd . '>&-'; + } + } + + // default `sh` only accepts single-digit FDs, so run in bash if needed + if ($fds && \max($fds) > 9) { + $command = 'exec bash -c ' . \escapeshellarg($command); + } + + $process = new Process($command, null, null, $pipes); $process->start($loop); $db = new Database($process); diff --git a/tests/FunctionalDatabaseTest.php b/tests/FunctionalDatabaseTest.php index 431951e..448116e 100644 --- a/tests/FunctionalDatabaseTest.php +++ b/tests/FunctionalDatabaseTest.php @@ -41,6 +41,33 @@ public function testOpenMemoryDatabaseResolvesWithDatabaseAndRunsUntilQuit() $loop->run(); } + public function testOpenMemoryDatabaseShouldNotInheritActiveFileDescriptors() + { + $server = stream_socket_server('tcp://127.0.0.1:0'); + $address = stream_socket_get_name($server, false); + + if (@stream_socket_server('tcp://' . $address) !== false) { + $this->markTestSkipped('Platform does not prevent binding to same address (Windows?)'); + } + + $loop = Factory::create(); + + $promise = Database::open($loop, ':memory:'); + + // close server and ensure we can start a new server on the previous address + // the pending SQLite process should not inherit the existing server socket + fclose($server); + $server = stream_socket_server('tcp://' . $address); + $this->assertTrue(is_resource($server)); + fclose($server); + + $promise->then(function (Database $db) { + $db->close(); + }); + + $loop->run(); + } + public function testOpenInvalidPathRejects() { $loop = Factory::create(); @@ -83,6 +110,30 @@ public function testQuitResolvesAndRunsUntilQuit() $loop->run(); } + + public function testQuitResolvesAndRunsUntilQuitWhenParentHasManyFileDescriptors() + { + $servers = array(); + for ($i = 0; $i < 100; ++$i) { + $servers[] = stream_socket_server('tcp://127.0.0.1:0'); + } + + $loop = Factory::create(); + + $promise = Database::open($loop, ':memory:'); + + $once = $this->expectCallableOnce(); + $promise->then(function (Database $db) use ($once){ + $db->quit()->then($once); + }); + + $loop->run(); + + foreach ($servers as $server) { + fclose($server); + } + } + public function testQuitTwiceWillRejectSecondCall() { $loop = Factory::create();