55use React \ChildProcess \Process ;
66use React \EventLoop \LoopInterface ;
77use Clue \React \SQLite \Io \ProcessIoDatabase ;
8+ use React \Stream \DuplexResourceStream ;
9+ use React \Promise \Deferred ;
10+ use React \Stream \ThroughStream ;
811
912class Factory
1013{
1114 private $ loop ;
1215
16+ private $ useSocket ;
17+
1318 /**
1419 * The `Factory` is responsible for opening your [`DatabaseInterface`](#databaseinterface) instance.
1520 * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage).
@@ -24,6 +29,9 @@ class Factory
2429 public function __construct (LoopInterface $ loop )
2530 {
2631 $ this ->loop = $ loop ;
32+
33+ // use socket I/O for Windows only, use faster process pipes everywhere else
34+ $ this ->useSocket = DIRECTORY_SEPARATOR === '\\' ;
2735 }
2836
2937 /**
@@ -33,7 +41,9 @@ public function __construct(LoopInterface $loop)
3341 * success or will reject with an `Exception` on error. The SQLite extension
3442 * is inherently blocking, so this method will spawn an SQLite worker process
3543 * to run all SQLite commands and queries in a separate process without
36- * blocking the main process.
44+ * blocking the main process. On Windows, it uses a temporary network socket
45+ * for this communication, on all other platforms it communicates over
46+ * standard process I/O pipes.
3747 *
3848 * ```php
3949 * $factory->open('users.db')->then(function (DatabaseInterface $db) {
@@ -62,6 +72,11 @@ public function __construct(LoopInterface $loop)
6272 * @return PromiseInterface<DatabaseInterface> Resolves with DatabaseInterface instance or rejects with Exception
6373 */
6474 public function open ($ filename , $ flags = null )
75+ {
76+ return $ this ->useSocket ? $ this ->openSocketIo ($ filename , $ flags ) : $ this ->openProcessIo ($ filename , $ flags );
77+ }
78+
79+ private function openProcessIo ($ filename , $ flags = null )
6580 {
6681 $ command = 'exec ' . \escapeshellarg (\PHP_BINARY ) . ' ' . \escapeshellarg (__DIR__ . '/../res/sqlite-worker.php ' );
6782
@@ -121,4 +136,82 @@ public function open($filename, $flags = null)
121136 throw $ e ;
122137 });
123138 }
139+
140+ private function openSocketIo ($ filename , $ flags = null )
141+ {
142+ $ command = \escapeshellarg (\PHP_BINARY ) . ' ' . \escapeshellarg (__DIR__ . '/../res/sqlite-worker.php ' );
143+
144+ // launch process without default STDIO pipes
145+ $ null = \DIRECTORY_SEPARATOR === '\\' ? 'nul ' : '/dev/null ' ;
146+ $ pipes = array (
147+ array ('file ' , $ null , 'r ' ),
148+ array ('file ' , $ null , 'w ' ),
149+ STDERR // array('file', $null, 'w'),
150+ );
151+
152+ // start temporary socket on random address
153+ $ server = @stream_socket_server ('tcp://127.0.0.1:0 ' , $ errno , $ errstr );
154+ if ($ server === false ) {
155+ return \React \Promise \reject (
156+ new \RuntimeException ('Unable to start temporary socket I/O server: ' . $ errstr , $ errno )
157+ );
158+ }
159+
160+ // pass random server address to child process to connect back to parent process
161+ stream_set_blocking ($ server , false );
162+ $ command .= ' ' . stream_socket_get_name ($ server , false );
163+
164+ $ process = new Process ($ command , null , null , $ pipes );
165+ $ process ->start ($ this ->loop );
166+
167+ $ deferred = new Deferred (function () use ($ process , $ server ) {
168+ $ this ->loop ->removeReadStream ($ server );
169+ fclose ($ server );
170+ $ process ->terminate ();
171+
172+ throw new \RuntimeException ('Opening database cancelled ' );
173+ });
174+
175+ // time out after a few seconds if we don't receive a connection
176+ $ timeout = $ this ->loop ->addTimer (5.0 , function () use ($ server , $ deferred , $ process ) {
177+ $ this ->loop ->removeReadStream ($ server );
178+ fclose ($ server );
179+ $ process ->terminate ();
180+
181+ $ deferred ->reject (new \RuntimeException ('No connection detected ' ));
182+ });
183+
184+ $ this ->loop ->addReadStream ($ server , function () use ($ server , $ timeout , $ filename , $ flags , $ deferred , $ process ) {
185+ // accept once connection on server socket and stop server socket
186+ $ this ->loop ->cancelTimer ($ timeout );
187+ $ peer = stream_socket_accept ($ server , 0 );
188+ $ this ->loop ->removeReadStream ($ server );
189+ fclose ($ server );
190+
191+ // use this one connection as fake process I/O streams
192+ $ connection = new DuplexResourceStream ($ peer , $ this ->loop , -1 );
193+ $ process ->stdin = $ process ->stdout = $ connection ;
194+ $ connection ->on ('close ' , function () use ($ process ) {
195+ $ process ->terminate ();
196+ });
197+ $ process ->on ('exit ' , function () use ($ connection ) {
198+ $ connection ->close ();
199+ });
200+
201+ $ db = new ProcessIoDatabase ($ process );
202+ $ args = array ($ filename );
203+ if ($ flags !== null ) {
204+ $ args [] = $ flags ;
205+ }
206+
207+ $ db ->send ('open ' , $ args )->then (function () use ($ deferred , $ db ) {
208+ $ deferred ->resolve ($ db );
209+ }, function ($ e ) use ($ deferred , $ db ) {
210+ $ db ->close ();
211+ $ deferred ->reject ($ e );
212+ });
213+ });
214+
215+ return $ deferred ->promise ();
216+ }
124217}
0 commit comments