7
7
use Evenement \EventEmitter ;
8
8
use React \EventLoop \LoopInterface ;
9
9
use React \Stream \DuplexStreamInterface ;
10
+ use React \Stream \ReadableResourceStream ;
10
11
use React \Stream \ReadableStreamInterface ;
11
12
use React \Stream \Util ;
13
+ use React \Stream \WritableResourceStream ;
12
14
use React \Stream \WritableStreamInterface ;
13
15
14
16
class Stdio extends EventEmitter implements DuplexStreamInterface
@@ -20,15 +22,16 @@ class Stdio extends EventEmitter implements DuplexStreamInterface
20
22
private $ ending = false ;
21
23
private $ closed = false ;
22
24
private $ incompleteLine = '' ;
25
+ private $ originalTtyMode = null ;
23
26
24
27
public function __construct (LoopInterface $ loop , ReadableStreamInterface $ input = null , WritableStreamInterface $ output = null , Readline $ readline = null )
25
28
{
26
29
if ($ input === null ) {
27
- $ input = new Stdin ($ loop );
30
+ $ input = $ this -> createStdin ($ loop );
28
31
}
29
32
30
33
if ($ output === null ) {
31
- $ output = new Stdout ( );
34
+ $ output = $ this -> createStdout ( $ loop );
32
35
}
33
36
34
37
if ($ readline === null ) {
@@ -59,6 +62,11 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
59
62
$ this ->output ->on ('close ' , array ($ this , 'handleCloseOutput ' ));
60
63
}
61
64
65
+ public function __destruct ()
66
+ {
67
+ $ this ->restoreTtyMode ();
68
+ }
69
+
62
70
public function pause ()
63
71
{
64
72
$ this ->input ->pause ();
@@ -173,6 +181,7 @@ public function end($data = null)
173
181
174
182
// clear readline output, close input and end output
175
183
$ this ->readline ->setInput ('' )->setPrompt ('' )->clear ();
184
+ $ this ->restoreTtyMode ();
176
185
$ this ->input ->close ();
177
186
$ this ->output ->end ();
178
187
}
@@ -188,6 +197,7 @@ public function close()
188
197
189
198
// clear readline output and then close
190
199
$ this ->readline ->setInput ('' )->setPrompt ('' )->clear ()->close ();
200
+ $ this ->restoreTtyMode ();
191
201
$ this ->input ->close ();
192
202
$ this ->output ->close ();
193
203
}
@@ -230,4 +240,116 @@ public function handleCloseOutput()
230
240
$ this ->close ();
231
241
}
232
242
}
243
+
244
+ /**
245
+ * @codeCoverageIgnore this is covered by functional tests with/without ext-readline
246
+ */
247
+ private function restoreTtyMode ()
248
+ {
249
+ if (function_exists ('readline_callback_handler_remove ' )) {
250
+ // remove dummy readline handler to turn to default input mode
251
+ readline_callback_handler_remove ();
252
+ } elseif ($ this ->originalTtyMode !== null && $ this ->isTty ()) {
253
+ // Reset stty so it behaves normally again
254
+ shell_exec (sprintf ('stty %s ' , $ this ->originalTtyMode ));
255
+ $ this ->originalTtyMode = null ;
256
+ }
257
+
258
+ // restore blocking mode so following programs behave normally
259
+ if (defined ('STDIN ' ) && is_resource (STDIN )) {
260
+ stream_set_blocking (STDIN , true );
261
+ }
262
+ }
263
+
264
+ /**
265
+ * @param LoopInterface $loop
266
+ * @return ReadableStreamInterface
267
+ * @codeCoverageIgnore this is covered by functional tests with/without ext-readline
268
+ */
269
+ private function createStdin (LoopInterface $ loop )
270
+ {
271
+ // STDIN not defined ("php -a") or already closed (`fclose(STDIN)`)
272
+ // also support starting program with closed STDIN ("example.php 0<&-")
273
+ // the stream is a valid resource and is not EOF, but fstat fails
274
+ if (!defined ('STDIN ' ) || !is_resource (STDIN ) || fstat (STDIN ) === false ) {
275
+ $ stream = new ReadableResourceStream (fopen ('php://memory ' , 'r ' ), $ loop );
276
+ $ stream ->close ();
277
+ return $ stream ;
278
+ }
279
+
280
+ $ stream = new ReadableResourceStream (STDIN , $ loop );
281
+
282
+ if (function_exists ('readline_callback_handler_install ' )) {
283
+ // Prefer `ext-readline` to install dummy handler to turn on raw input mode.
284
+ // We will nevery actually feed the readline handler and instead
285
+ // handle all input in our `Readline` implementation.
286
+ readline_callback_handler_install ('' , function () { });
287
+ return $ stream ;
288
+ }
289
+
290
+ if ($ this ->isTty ()) {
291
+ $ this ->originalTtyMode = shell_exec ('stty -g ' );
292
+
293
+ // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
294
+ shell_exec ('stty -icanon -echo ' );
295
+ }
296
+
297
+ // register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception)
298
+ // this will not trigger on SIGKILL etc., but the terminal should take care of this
299
+ register_shutdown_function (array ($ this , 'close ' ));
300
+
301
+ return $ stream ;
302
+ }
303
+
304
+ /**
305
+ * @param LoopInterface $loop
306
+ * @return WritableStreamInterface
307
+ * @codeCoverageIgnore this is covered by functional tests
308
+ */
309
+ private function createStdout (LoopInterface $ loop )
310
+ {
311
+ // STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
312
+ // also support starting program with closed STDOUT ("example.php >&-")
313
+ // the stream is a valid resource and is not EOF, but fstat fails
314
+ if (!defined ('STDOUT ' ) || !is_resource (STDOUT ) || fstat (STDOUT ) === false ) {
315
+ $ output = new WritableResourceStream (fopen ('php://memory ' , 'r+ ' ), $ loop );
316
+ $ output ->close ();
317
+ } else {
318
+ $ output = new WritableResourceStream (STDOUT , $ loop );
319
+ }
320
+
321
+ return $ output ;
322
+ }
323
+
324
+ /**
325
+ * @return bool
326
+ * @codeCoverageIgnore
327
+ */
328
+ private function isTty ()
329
+ {
330
+ if (PHP_VERSION_ID >= 70200 ) {
331
+ // Prefer `stream_isatty()` (available as of PHP 7.2 only)
332
+ return stream_isatty (STDIN );
333
+ } elseif (function_exists ('posix_isatty ' )) {
334
+ // Otherwise use `posix_isatty` if available (requires `ext-posix`)
335
+ return posix_isatty (STDIN );
336
+ }
337
+
338
+ // otherwise try to guess based on stat file mode and device major number
339
+ // Must be special character device: ($mode & S_IFMT) === S_IFCHR
340
+ // And device major number must be allocated to TTYs (2-5 and 128-143)
341
+ // For what it's worth, checking for device gid 5 (tty) is less reliable.
342
+ // @link http://man7.org/linux/man-pages/man7/inode.7.html
343
+ // @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices
344
+ if (is_resource (STDIN )) {
345
+ $ stat = fstat (STDIN );
346
+ $ mode = isset ($ stat ['mode ' ]) ? ($ stat ['mode ' ] & 0170000 ) : 0 ;
347
+ $ major = isset ($ stat ['dev ' ]) ? (($ stat ['dev ' ] >> 8 ) & 0xff ) : 0 ;
348
+
349
+ if ($ mode === 0020000 && $ major >= 2 && $ major <= 143 && ($ major <=5 || $ major >= 128 )) {
350
+ return true ;
351
+ }
352
+ }
353
+ return false ;
354
+ }
233
355
}
0 commit comments