-
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathFactory.php
456 lines (412 loc) · 18.6 KB
/
Factory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
<?php
namespace Clue\React\SQLite;
use Clue\React\SQLite\Io\BlockingDatabase;
use Clue\React\SQLite\Io\LazyDatabase;
use Clue\React\SQLite\Io\ProcessIoDatabase;
use React\ChildProcess\Process;
use React\EventLoop\Loop;
use React\EventLoop\LoopInterface;
use React\Promise\Deferred;
use React\Stream\DuplexResourceStream;
class Factory
{
/** @var LoopInterface */
private $loop;
/** @var string */
private $bin;
/** @var bool */
private $useSocket;
/**
* The `Factory` is responsible for opening your [`DatabaseInterface`](#databaseinterface) instance.
*
* ```php
* $factory = new Clue\React\SQLite\Factory();
* ```
*
* This class takes an optional `LoopInterface|null $loop` parameter that can be used to
* pass the event loop instance to use for this object. You can use a `null` value
* here in order to use the [default loop](https://github.com/reactphp/event-loop#loop).
* This value SHOULD NOT be given unless you're sure you want to explicitly use a
* given event loop instance.
*
* This class takes an optional `?string $binary` parameter that can be used to
* pass a custom PHP binary to use when spawning a child process. You can use a
* `null` value here in order to automatically detect the current PHP binary. You
* may want to pass a custom executable path if this automatic detection fails or
* if you explicitly want to run the child process with a different PHP version or
* environment than your parent process.
*
* ```php
* // advanced usage: pass custom PHP binary to use when spawning child process
* $factory = new Clue\React\SQLite\Factory(null, '/usr/bin/php6.0');
* ```
*
* Or you may use this parameter to pass an empty PHP binary path which will
* cause this project to not spawn a PHP child process for any database
* interactions at all. In this case, using SQLite will block the main
* process, but continues to provide the exact same async API. This can be
* useful if concurrent execution is not needed, especially when running
* behind a traditional web server (non-CLI SAPI).
*
* ```php
* // advanced usage: empty binary path runs blocking SQLite in same process
* $factory = new Clue\React\SQLite\Factory(null, '');
* ```
*
* @param ?LoopInterface $loop
* @param ?string $binary
*/
public function __construct($loop = null, $binary = null)
{
if ($loop !== null && !$loop instanceof LoopInterface) { // manual type check to support legacy PHP < 7.1
throw new \InvalidArgumentException('Argument #1 ($loop) expected null|React\EventLoop\LoopInterface');
}
$this->loop = $loop ?: Loop::get();
$this->bin = $binary === null ? $this->php() : $binary;
// use socket I/O for Windows only, use faster process pipes everywhere else
$this->useSocket = \DIRECTORY_SEPARATOR === '\\';
}
/**
* Opens a new database connection for the given SQLite database file.
*
* This method returns a promise that will resolve with a `DatabaseInterface` on
* success or will reject with an `Exception` on error. The SQLite extension
* is inherently blocking, so this method will spawn an SQLite worker process
* to run all SQLite commands and queries in a separate process without
* blocking the main process. On Windows, it uses a temporary network socket
* for this communication, on all other platforms, it communicates over
* standard process I/O pipes.
*
* ```php
* $factory->open('users.db')->then(function (DatabaseInterface $db) {
* // database ready
* // $db->query('INSERT INTO users (name) VALUES ("test")');
* // $db->quit();
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* The `$filename` parameter is the path to the SQLite database file or
* `:memory:` to create a temporary in-memory database. As of PHP 7.0.10, an
* empty string can be given to create a private, temporary on-disk database.
* Relative paths will be resolved relative to the current working directory,
* so it's usually recommended to pass absolute paths instead to avoid any
* ambiguity.
*
* ```php
* $promise = $factory->open(__DIR__ . '/users.db');
* ```
*
* The optional `$flags` parameter is used to determine how to open the
* SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
*
* ```php
* $factory->open('users.db', SQLITE3_OPEN_READONLY)->then(function (DatabaseInterface $db) {
* // database ready (read-only)
* // $db->quit();
* }, function (Exception $e) {
* echo 'Error: ' . $e->getMessage() . PHP_EOL;
* });
* ```
*
* @param string $filename
* @param ?int $flags
* @return PromiseInterface<DatabaseInterface> Resolves with DatabaseInterface instance or rejects with Exception
*/
public function open($filename, $flags = null)
{
$filename = $this->resolve($filename);
if ($this->bin === '') {
try {
return \React\Promise\resolve(new BlockingDatabase($filename, $flags));
} catch (\Exception $e) {
return \React\Promise\reject(new \RuntimeException($e->getMessage()) );
} catch (\Error $e) { // @codeCoverageIgnore
assert(\PHP_VERSION_ID >= 70000); // @codeCoverageIgnore
return \React\Promise\reject(new \RuntimeException($e->getMessage())); // @codeCoverageIgnore
}
}
return $this->useSocket ? $this->openSocketIo($filename, $flags) : $this->openProcessIo($filename, $flags);
}
/**
* Opens a new database connection for the given SQLite database file.
*
* ```php
* $db = $factory->openLazy('users.db');
*
* $db->query('INSERT INTO users (name) VALUES ("test")');
* $db->quit();
* ```
*
* This method immediately returns a "virtual" connection implementing the
* [`DatabaseInterface`](#databaseinterface) that can be used to
* interface with your SQLite database. Internally, it lazily creates the
* underlying database process only on demand once the first request is
* invoked on this instance and will queue all outstanding requests until
* the underlying database is ready. Additionally, it will only keep this
* underlying database in an "idle" state for 60s by default and will
* automatically end the underlying database when it is no longer needed.
*
* From a consumer side this means that you can start sending queries to the
* database right away while the underlying database process may still be
* outstanding. Because creating this underlying process may take some
* time, it will enqueue all outstanding commands and will ensure that all
* commands will be executed in correct order once the database is ready.
* In other words, this "virtual" database behaves just like a "real"
* database as described in the `DatabaseInterface` and frees you from
* having to deal with its async resolution.
*
* If the underlying database process fails, it will reject all
* outstanding commands and will return to the initial "idle" state. This
* means that you can keep sending additional commands at a later time which
* will again try to open a new underlying database. Note that this may
* require special care if you're using transactions that are kept open for
* longer than the idle period.
*
* Note that creating the underlying database will be deferred until the
* first request is invoked. Accordingly, any eventual connection issues
* will be detected once this instance is first used. You can use the
* `quit()` method to ensure that the "virtual" connection will be soft-closed
* and no further commands can be enqueued. Similarly, calling `quit()` on
* this instance when not currently connected will succeed immediately and
* will not have to wait for an actual underlying connection.
*
* Depending on your particular use case, you may prefer this method or the
* underlying `open()` method which resolves with a promise. For many
* simple use cases, it may be easier to create a lazy connection.
*
* The `$filename` parameter is the path to the SQLite database file or
* `:memory:` to create a temporary in-memory database. As of PHP 7.0.10, an
* empty string can be given to create a private, temporary on-disk database.
* Relative paths will be resolved relative to the current working directory,
* so it's usually recommended to pass absolute paths instead to avoid any
* ambiguity.
*
* ```php
* $db = $factory->openLazy(__DIR__ . '/users.db');
* ```
*
* The optional `$flags` parameter is used to determine how to open the
* SQLite database. By default, open uses `SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE`.
*
* ```php
* $db = $factory->openLazy('users.db', SQLITE3_OPEN_READONLY);
* ```
*
* By default, this method will keep "idle" connection open for 60s and will
* then end the underlying connection. The next request after an "idle"
* connection ended will automatically create a new underlying connection.
* This ensure you always get a "fresh" connection and as such should not be
* confused with a "keepalive" or "heartbeat" mechanism, as this will not
* actively try to probe the connection. You can explicitly pass a custom
* idle timeout value in seconds (or use a negative number to not apply a
* timeout) like this:
*
* ```php
* $db = $factory->openLazy('users.db', null, ['idle' => 0.1]);
* ```
*
* @param string $filename
* @param ?int $flags
* @param array $options
* @return DatabaseInterface
*/
public function openLazy($filename, $flags = null, array $options = [])
{
return new LazyDatabase($this->resolve($filename), $flags, $options, $this, $this->loop);
}
private function openProcessIo($filename, $flags = null)
{
$cwd = null;
$worker = \dirname(__DIR__) . '/res/sqlite-worker.php';
// launch worker process directly or inside Phar by mapping relative paths (covered by functional test suite)
// @codeCoverageIgnoreStart
if (\class_exists('Phar', false) && ($phar = \Phar::running(false)) !== '') {
$worker = '-r' . 'Phar::loadPhar(' . var_export($phar, true) . ');require(' . \var_export($worker, true) . ');';
} else {
$cwd = __DIR__ . '/../res';
$worker = \basename($worker);
}
// @codeCoverageIgnoreEnd
$command = 'exec ' . \escapeshellarg($this->bin) . ' ' . escapeshellarg($worker);
// 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, but inherit STDERR
$pipes = array(
array('pipe', 'r'),
array('pipe', 'w'),
\defined('STDERR') ? \STDERR : \fopen('php://stderr', 'w')
);
// do not inherit open FDs by explicitly overwriting existing FDs with dummy files.
// Accessing /dev/null with null spec requires PHP 7.4+, older PHP versions may be restricted due to open_basedir, so let's reuse STDERR here.
// additionally, close all dummy files in the child process again
foreach ($fds as $fd) {
if ($fd > 2) {
$pipes[$fd] = \PHP_VERSION_ID >= 70400 ? ['null'] : $pipes[2];
$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, $cwd, null, $pipes);
$process->start($this->loop);
$db = new ProcessIoDatabase($process);
$args = array($filename, $flags);
return $db->send('open', $args)->then(function () use ($db) {
return $db;
}, function ($e) use ($db) {
$db->close();
throw $e;
});
}
private function openSocketIo($filename, $flags = null)
{
$cwd = null;
$worker = \dirname(__DIR__) . '/res/sqlite-worker.php';
// launch worker process directly or inside Phar by mapping relative paths (covered by functional test suite)
// @codeCoverageIgnoreStart
if (\class_exists('Phar', false) && ($phar = \Phar::running(false)) !== '') {
$worker = '-r' . 'Phar::loadPhar(' . var_export($phar, true) . ');require(' . \var_export($worker, true) . ');';
} else {
$cwd = __DIR__ . '/../res';
$worker = \basename($worker);
}
// @codeCoverageIgnoreEnd
$command = \escapeshellarg($this->bin) . ' ' . escapeshellarg($worker);
// launch process without default STDIO pipes, but inherit STDERR
$null = \DIRECTORY_SEPARATOR === '\\' ? 'nul' : '/dev/null';
$pipes = array(
array('file', $null, 'r'),
array('file', $null, 'w'),
\defined('STDERR') ? \STDERR : \fopen('php://stderr', 'w')
);
// start temporary socket on random address
$server = @stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
if ($server === false) {
// report error if temporary socket server can not be started (unlikely)
// @codeCoverageIgnoreStart
return \React\Promise\reject(
new \RuntimeException('Unable to start temporary socket I/O server: ' . $errstr, $errno)
);
// @codeCoverageIgnoreEnd
}
// pass random server address to child process to connect back to parent process
stream_set_blocking($server, false);
$command .= ' ' . stream_socket_get_name($server, false);
$process = new Process($command, $cwd, null, $pipes);
$process->start($this->loop);
$deferred = new Deferred(function () use ($process, $server) {
$this->loop->removeReadStream($server);
fclose($server);
$process->terminate();
throw new \RuntimeException('Opening database cancelled');
});
// time out after a few seconds if we don't receive a connection
$timeout = $this->loop->addTimer(5.0, function () use ($server, $deferred, $process) {
$this->loop->removeReadStream($server);
fclose($server);
$process->terminate();
$deferred->reject(new \RuntimeException('Opening database socket timed out'));
});
$process->on('exit', function () use ($deferred, $server, $timeout) {
$this->loop->cancelTimer($timeout);
if (is_resource($server)) {
$this->loop->removeReadStream($server);
fclose($server);
}
$deferred->reject(new \RuntimeException('Database process died while setting up connection'));
});
$this->loop->addReadStream($server, function () use ($server, $timeout, $filename, $flags, $deferred, $process) {
// accept once connection on server socket and stop server socket
$this->loop->cancelTimer($timeout);
$peer = stream_socket_accept($server, 0);
$this->loop->removeReadStream($server);
fclose($server);
// use this one connection as fake process I/O streams
$connection = new DuplexResourceStream($peer, $this->loop, -1);
$process->stdin = $process->stdout = $connection;
$connection->on('close', function () use ($process) {
$process->terminate();
});
$process->on('exit', function () use ($connection) {
$connection->close();
});
$db = new ProcessIoDatabase($process);
$args = array($filename, $flags);
$db->send('open', $args)->then(function () use ($deferred, $db) {
$deferred->resolve($db);
}, function ($e) use ($deferred, $db) {
$db->close();
$deferred->reject($e);
});
});
return $deferred->promise();
}
/**
* @param string $bin
* @return string|null
* @codeCoverageIgnore
*/
private function which($bin)
{
foreach (\explode(\PATH_SEPARATOR, \getenv('PATH')) as $path) {
if (@\is_executable($path . \DIRECTORY_SEPARATOR . $bin)) {
return $path . \DIRECTORY_SEPARATOR . $bin;
}
}
return null;
}
/**
* @param string $filename
* @return string
*/
private function resolve($filename)
{
if ($filename !== '' && $filename !== ':memory:' && !\preg_match('/^\/|\w+\:\\\\/', $filename)) {
$filename = \getcwd() . \DIRECTORY_SEPARATOR . $filename;
}
return $filename;
}
/**
* @return string
* @codeCoverageIgnore Covered by `/tests/FunctionalExampleTest.php` instead.
*/
private function php()
{
$binary = 'php';
if (\PHP_SAPI === 'cli' || \PHP_SAPI === 'cli-server') {
// use same PHP_BINARY in CLI mode, but do not use same binary for CGI/FPM
$binary = \PHP_BINARY;
} else {
// if this is the php-cgi binary, check if we can execute the php binary instead
$candidate = \str_replace('-cgi', '', \PHP_BINARY);
if ($candidate !== \PHP_BINARY && @\is_executable($candidate)) {
$binary = $candidate;
}
}
// if `php` is a symlink to the php binary, use the shorter `php` name
// this is purely cosmetic feature for the process list
if ($binary !== 'php' && \realpath((string) $this->which('php')) === $binary) {
$binary = 'php';
}
return $binary;
}
}