@@ -21,22 +21,16 @@ class Stdio extends EventEmitter implements DuplexStreamInterface
21
21
private $ ending = false ;
22
22
private $ closed = false ;
23
23
private $ incompleteLine = '' ;
24
+ private $ originalTtyMode = null ;
24
25
25
26
public function __construct (LoopInterface $ loop , ReadableStreamInterface $ input = null , WritableStreamInterface $ output = null , Readline $ readline = null )
26
27
{
27
28
if ($ input === null ) {
28
- $ input = new Stdin ($ loop );
29
+ $ input = $ this -> createStdin ($ loop );
29
30
}
30
31
31
32
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 );
40
34
}
41
35
42
36
if ($ readline === null ) {
@@ -67,6 +61,11 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
67
61
$ this ->output ->on ('close ' , array ($ this , 'handleCloseOutput ' ));
68
62
}
69
63
64
+ public function __destruct ()
65
+ {
66
+ $ this ->restoreTtyMode ();
67
+ }
68
+
70
69
public function pause ()
71
70
{
72
71
$ this ->input ->pause ();
@@ -181,6 +180,7 @@ public function end($data = null)
181
180
182
181
// clear readline output, close input and end output
183
182
$ this ->readline ->setInput ('' )->setPrompt ('' )->clear ();
183
+ $ this ->restoreTtyMode ();
184
184
$ this ->input ->close ();
185
185
$ this ->output ->end ();
186
186
}
@@ -196,6 +196,7 @@ public function close()
196
196
197
197
// clear readline output and then close
198
198
$ this ->readline ->setInput ('' )->setPrompt ('' )->clear ()->close ();
199
+ $ this ->restoreTtyMode ();
199
200
$ this ->input ->close ();
200
201
$ this ->output ->close ();
201
202
}
@@ -238,4 +239,120 @@ public function handleCloseOutput()
238
239
$ this ->close ();
239
240
}
240
241
}
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
+ }
241
358
}
0 commit comments