5
5
use React \ChildProcess \Process ;
6
6
use React \EventLoop \LoopInterface ;
7
7
use Clue \React \SQLite \Io \ProcessIoDatabase ;
8
+ use React \Stream \DuplexResourceStream ;
9
+ use React \Promise \Deferred ;
10
+ use React \Stream \ThroughStream ;
8
11
9
12
class Factory
10
13
{
11
14
private $ loop ;
12
15
16
+ private $ useSocket ;
17
+
13
18
/**
14
19
* The `Factory` is responsible for opening your [`DatabaseInterface`](#databaseinterface) instance.
15
20
* It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage).
@@ -24,6 +29,9 @@ class Factory
24
29
public function __construct (LoopInterface $ loop )
25
30
{
26
31
$ this ->loop = $ loop ;
32
+
33
+ // use socket I/O for Windows only, use faster process pipes everywhere else
34
+ $ this ->useSocket = DIRECTORY_SEPARATOR === '\\' ;
27
35
}
28
36
29
37
/**
@@ -33,7 +41,9 @@ public function __construct(LoopInterface $loop)
33
41
* success or will reject with an `Exception` on error. The SQLite extension
34
42
* is inherently blocking, so this method will spawn an SQLite worker process
35
43
* 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.
37
47
*
38
48
* ```php
39
49
* $factory->open('users.db')->then(function (DatabaseInterface $db) {
@@ -62,6 +72,11 @@ public function __construct(LoopInterface $loop)
62
72
* @return PromiseInterface<DatabaseInterface> Resolves with DatabaseInterface instance or rejects with Exception
63
73
*/
64
74
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 )
65
80
{
66
81
$ command = 'exec ' . \escapeshellarg (\PHP_BINARY ) . ' ' . \escapeshellarg (__DIR__ . '/../res/sqlite-worker.php ' );
67
82
@@ -121,4 +136,82 @@ public function open($filename, $flags = null)
121
136
throw $ e ;
122
137
});
123
138
}
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
+ }
124
217
}
0 commit comments