77use Evenement \EventEmitter ;
88use React \EventLoop \LoopInterface ;
99use React \Stream \DuplexStreamInterface ;
10+ use React \Stream \ReadableResourceStream ;
1011use React \Stream \ReadableStreamInterface ;
1112use React \Stream \Util ;
13+ use React \Stream \WritableResourceStream ;
1214use React \Stream \WritableStreamInterface ;
1315
1416class Stdio extends EventEmitter implements DuplexStreamInterface
@@ -20,15 +22,16 @@ class Stdio extends EventEmitter implements DuplexStreamInterface
2022 private $ ending = false ;
2123 private $ closed = false ;
2224 private $ incompleteLine = '' ;
25+ private $ originalTtyMode = null ;
2326
2427 public function __construct (LoopInterface $ loop , ReadableStreamInterface $ input = null , WritableStreamInterface $ output = null , Readline $ readline = null )
2528 {
2629 if ($ input === null ) {
27- $ input = new Stdin ($ loop );
30+ $ input = $ this -> createStdin ($ loop );
2831 }
2932
3033 if ($ output === null ) {
31- $ output = new Stdout ( );
34+ $ output = $ this -> createStdout ( $ loop );
3235 }
3336
3437 if ($ readline === null ) {
@@ -59,6 +62,11 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
5962 $ this ->output ->on ('close ' , array ($ this , 'handleCloseOutput ' ));
6063 }
6164
65+ public function __destruct ()
66+ {
67+ $ this ->restoreTtyMode ();
68+ }
69+
6270 public function pause ()
6371 {
6472 $ this ->input ->pause ();
@@ -173,6 +181,7 @@ public function end($data = null)
173181
174182 // clear readline output, close input and end output
175183 $ this ->readline ->setInput ('' )->setPrompt ('' )->clear ();
184+ $ this ->restoreTtyMode ();
176185 $ this ->input ->close ();
177186 $ this ->output ->end ();
178187 }
@@ -188,6 +197,7 @@ public function close()
188197
189198 // clear readline output and then close
190199 $ this ->readline ->setInput ('' )->setPrompt ('' )->clear ()->close ();
200+ $ this ->restoreTtyMode ();
191201 $ this ->input ->close ();
192202 $ this ->output ->close ();
193203 }
@@ -230,4 +240,116 @@ public function handleCloseOutput()
230240 $ this ->close ();
231241 }
232242 }
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+ }
233355}
0 commit comments