Skip to content

Commit 09cc0e0

Browse files
committed
Replace internal Stdin class with proper Stream handling
1 parent c205e2b commit 09cc0e0

File tree

3 files changed

+127
-127
lines changed

3 files changed

+127
-127
lines changed

src/Io/Stdin.php

Lines changed: 0 additions & 117 deletions
This file was deleted.

src/Stdio.php

Lines changed: 126 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

tests/StdioTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ public function setUp()
2020
*/
2121
public function testCtorDefaultArgs()
2222
{
23-
new Stdio($this->loop);
23+
$stdio = new Stdio($this->loop);
2424

2525
// Closing STDIN/STDOUT is not a good idea for reproducible tests
2626
// $stdio->close();

0 commit comments

Comments
 (0)