@@ -21,22 +21,16 @@ class Stdio extends EventEmitter implements DuplexStreamInterface
2121 private $ ending = false ;
2222 private $ closed = false ;
2323 private $ incompleteLine = '' ;
24+ private $ originalTtyMode = null ;
2425
2526 public function __construct (LoopInterface $ loop , ReadableStreamInterface $ input = null , WritableStreamInterface $ output = null , Readline $ readline = null )
2627 {
2728 if ($ input === null ) {
28- $ input = new Stdin ($ loop );
29+ $ input = $ this -> createStdin ($ loop );
2930 }
3031
3132 if ($ output === null ) {
32- // STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
33- if (!defined ('STDOUT ' ) || !is_resource (STDOUT )) {
34- $ output = new Stream (fopen ('php://memory ' , 'r+ ' ), $ loop );
35- $ output ->close ();
36- } else {
37- $ output = new Stream (STDOUT , $ loop );
38- $ output ->pause ();
39- }
33+ $ output = $ this ->createStdout ($ loop );
4034 }
4135
4236 if ($ readline === null ) {
@@ -67,6 +61,11 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
6761 $ this ->output ->on ('close ' , array ($ this , 'handleCloseOutput ' ));
6862 }
6963
64+ public function __destruct ()
65+ {
66+ $ this ->restoreTtyMode ();
67+ }
68+
7069 public function pause ()
7170 {
7271 $ this ->input ->pause ();
@@ -181,6 +180,7 @@ public function end($data = null)
181180
182181 // clear readline output, close input and end output
183182 $ this ->readline ->setInput ('' )->setPrompt ('' )->clear ();
183+ $ this ->restoreTtyMode ();
184184 $ this ->input ->close ();
185185 $ this ->output ->end ();
186186 }
@@ -196,6 +196,7 @@ public function close()
196196
197197 // clear readline output and then close
198198 $ this ->readline ->setInput ('' )->setPrompt ('' )->clear ()->close ();
199+ $ this ->restoreTtyMode ();
199200 $ this ->input ->close ();
200201 $ this ->output ->close ();
201202 }
@@ -238,4 +239,120 @@ public function handleCloseOutput()
238239 $ this ->close ();
239240 }
240241 }
242+
243+ /**
244+ * @codeCoverageIgnore this is covered by functional tests with/without ext-readline
245+ */
246+ private function restoreTtyMode ()
247+ {
248+ if (function_exists ('readline_callback_handler_remove ' )) {
249+ // remove dummy readline handler to turn to default input mode
250+ readline_callback_handler_remove ();
251+ } elseif ($ this ->originalTtyMode !== null && $ this ->isTty ()) {
252+ // Reset stty so it behaves normally again
253+ shell_exec (sprintf ('stty %s ' , $ this ->originalTtyMode ));
254+ $ this ->originalTtyMode = null ;
255+ }
256+
257+ // restore blocking mode so following programs behave normally
258+ if (defined ('STDIN ' ) && is_resource (STDIN )) {
259+ stream_set_blocking (STDIN , true );
260+ }
261+ }
262+
263+ /**
264+ * @param LoopInterface $loop
265+ * @return ReadableStreamInterface
266+ * @codeCoverageIgnore this is covered by functional tests with/without ext-readline
267+ */
268+ private function createStdin (LoopInterface $ loop )
269+ {
270+ // STDIN not defined ("php -a") or already closed (`fclose(STDIN)`)
271+ if (!defined ('STDIN ' ) || !is_resource (STDIN )) {
272+ $ stream = new Stream (fopen ('php://memory ' , 'r ' ), $ loop );
273+ $ stream ->close ();
274+ return $ stream ;
275+ }
276+
277+ $ stream = new Stream (STDIN , $ loop );
278+
279+ // support starting program with closed STDIN ("example.php 0<&-")
280+ // the stream is a valid resource and is not EOF, but fstat fails
281+ if (fstat (STDIN ) === false ) {
282+ $ stream ->close ();
283+ return $ stream ;
284+ }
285+
286+ if (function_exists ('readline_callback_handler_install ' )) {
287+ // Prefer `ext-readline` to install dummy handler to turn on raw input mode.
288+ // We will nevery actually feed the readline handler and instead
289+ // handle all input in our `Readline` implementation.
290+ readline_callback_handler_install ('' , function () { });
291+ return $ stream ;
292+ }
293+
294+ if ($ this ->isTty ()) {
295+ $ this ->originalTtyMode = shell_exec ('stty -g ' );
296+
297+ // Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
298+ shell_exec ('stty -icanon -echo ' );
299+ }
300+
301+ // register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception)
302+ // this will not trigger on SIGKILL etc., but the terminal should take care of this
303+ register_shutdown_function (array ($ this , 'close ' ));
304+
305+ return $ stream ;
306+ }
307+
308+ /**
309+ * @param LoopInterface $loop
310+ * @return WritableStreamInterface
311+ * @codeCoverageIgnore this is covered by functional tests
312+ */
313+ private function createStdout (LoopInterface $ loop )
314+ {
315+ // STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
316+ if (!defined ('STDOUT ' ) || !is_resource (STDOUT )) {
317+ $ output = new Stream (fopen ('php://memory ' , 'r+ ' ), $ loop );
318+ $ output ->close ();
319+ } else {
320+ $ output = new Stream (STDOUT , $ loop );
321+ $ output ->pause ();
322+ }
323+
324+ return $ output ;
325+ }
326+
327+ /**
328+ * @return bool
329+ * @codeCoverageIgnore
330+ */
331+ private function isTty ()
332+ {
333+ if (PHP_VERSION_ID >= 70200 ) {
334+ // Prefer `stream_isatty()` (available as of PHP 7.2 only)
335+ return stream_isatty (STDIN );
336+ } elseif (function_exists ('posix_isatty ' )) {
337+ // Otherwise use `posix_isatty` if available (requires `ext-posix`)
338+ return posix_isatty (STDIN );
339+ }
340+
341+ // otherwise try to guess based on stat file mode and device major number
342+ // Must be special character device: ($mode & S_IFMT) === S_IFCHR
343+ // And device major number must be allocated to TTYs (2-5 and 128-143)
344+ // For what it's worth, checking for device gid 5 (tty) is less reliable.
345+ // @link http://man7.org/linux/man-pages/man7/inode.7.html
346+ // @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices
347+ if (is_resource (STDIN )) {
348+ $ stat = fstat (STDIN );
349+ $ mode = isset ($ stat ['mode ' ]) ? ($ stat ['mode ' ] & 0170000 ) : 0 ;
350+ $ major = isset ($ stat ['dev ' ]) ? (($ stat ['dev ' ] >> 8 ) & 0xff ) : 0 ;
351+
352+ if ($ mode === 0020000 && $ major >= 2 && $ major <= 143 && ($ major <=5 || $ major >= 128 )) {
353+ return true ;
354+ }
355+ }
356+ return false ;
357+ }
241358}
0 commit comments