Skip to content

Forward compatibility with latest and upcoming ReactPHP packages #68

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
],
"require": {
"php": ">=5.3",
"react/event-loop": "0.3.*|0.4.*",
"react/stream": "^0.4.2",
"clue/term-react": "^1.0 || ^0.1.1",
"clue/utf8-react": "^1.0 || ^0.1",
"clue/term-react": "^1.0 || ^0.1.1"
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
"react/stream": "^1.0 || ^0.7 || ^0.6"
},
"suggest": {
"ext-mbstring": "Using ext-mbstring should provide slightly better performance for handling I/O"
Expand Down
15 changes: 7 additions & 8 deletions examples/11-login.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,16 @@
$first = false;
} else {
$password = $line;
$stdio->end();
}
});

$loop->run();

echo <<<EOT
$stdio->end(<<<EOT
---------------------
Confirmation:
---------------------
Username: $username
Password: $password
EOT;
EOT
);
}
});

$loop->run();
117 changes: 0 additions & 117 deletions src/Io/Stdin.php

This file was deleted.

31 changes: 0 additions & 31 deletions src/Io/Stdout.php

This file was deleted.

126 changes: 124 additions & 2 deletions src/Stdio.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
use Evenement\EventEmitter;
use React\EventLoop\LoopInterface;
use React\Stream\DuplexStreamInterface;
use React\Stream\ReadableResourceStream;
use React\Stream\ReadableStreamInterface;
use React\Stream\Util;
use React\Stream\WritableResourceStream;
use React\Stream\WritableStreamInterface;

class Stdio extends EventEmitter implements DuplexStreamInterface
Expand All @@ -20,15 +22,16 @@ class Stdio extends EventEmitter implements DuplexStreamInterface
private $ending = false;
private $closed = false;
private $incompleteLine = '';
private $originalTtyMode = null;

public function __construct(LoopInterface $loop, ReadableStreamInterface $input = null, WritableStreamInterface $output = null, Readline $readline = null)
{
if ($input === null) {
$input = new Stdin($loop);
$input = $this->createStdin($loop);
}

if ($output === null) {
$output = new Stdout();
$output = $this->createStdout($loop);
}

if ($readline === null) {
Expand Down Expand Up @@ -59,6 +62,11 @@ public function __construct(LoopInterface $loop, ReadableStreamInterface $input
$this->output->on('close', array($this, 'handleCloseOutput'));
}

public function __destruct()
{
$this->restoreTtyMode();
}

public function pause()
{
$this->input->pause();
Expand Down Expand Up @@ -173,6 +181,7 @@ public function end($data = null)

// clear readline output, close input and end output
$this->readline->setInput('')->setPrompt('')->clear();
$this->restoreTtyMode();
$this->input->close();
$this->output->end();
}
Expand All @@ -188,6 +197,7 @@ public function close()

// clear readline output and then close
$this->readline->setInput('')->setPrompt('')->clear()->close();
$this->restoreTtyMode();
$this->input->close();
$this->output->close();
}
Expand Down Expand Up @@ -230,4 +240,116 @@ public function handleCloseOutput()
$this->close();
}
}

/**
* @codeCoverageIgnore this is covered by functional tests with/without ext-readline
*/
private function restoreTtyMode()
{
if (function_exists('readline_callback_handler_remove')) {
// remove dummy readline handler to turn to default input mode
readline_callback_handler_remove();
} elseif ($this->originalTtyMode !== null && $this->isTty()) {
// Reset stty so it behaves normally again
shell_exec(sprintf('stty %s', $this->originalTtyMode));
$this->originalTtyMode = null;
}

// restore blocking mode so following programs behave normally
if (defined('STDIN') && is_resource(STDIN)) {
stream_set_blocking(STDIN, true);
}
}

/**
* @param LoopInterface $loop
* @return ReadableStreamInterface
* @codeCoverageIgnore this is covered by functional tests with/without ext-readline
*/
private function createStdin(LoopInterface $loop)
{
// STDIN not defined ("php -a") or already closed (`fclose(STDIN)`)
// also support starting program with closed STDIN ("example.php 0<&-")
// the stream is a valid resource and is not EOF, but fstat fails
if (!defined('STDIN') || !is_resource(STDIN) || fstat(STDIN) === false) {
$stream = new ReadableResourceStream(fopen('php://memory', 'r'), $loop);
$stream->close();
return $stream;
}

$stream = new ReadableResourceStream(STDIN, $loop);

if (function_exists('readline_callback_handler_install')) {
// Prefer `ext-readline` to install dummy handler to turn on raw input mode.
// We will nevery actually feed the readline handler and instead
// handle all input in our `Readline` implementation.
readline_callback_handler_install('', function () { });
return $stream;
}

if ($this->isTty()) {
$this->originalTtyMode = shell_exec('stty -g');

// Disable icanon (so we can fread each keypress) and echo (we'll do echoing here instead)
shell_exec('stty -icanon -echo');
}

// register shutdown function to restore TTY mode in case of unclean shutdown (uncaught exception)
// this will not trigger on SIGKILL etc., but the terminal should take care of this
register_shutdown_function(array($this, 'close'));

return $stream;
}

/**
* @param LoopInterface $loop
* @return WritableStreamInterface
* @codeCoverageIgnore this is covered by functional tests
*/
private function createStdout(LoopInterface $loop)
{
// STDOUT not defined ("php -a") or already closed (`fclose(STDOUT)`)
// also support starting program with closed STDOUT ("example.php >&-")
// the stream is a valid resource and is not EOF, but fstat fails
if (!defined('STDOUT') || !is_resource(STDOUT) || fstat(STDOUT) === false) {
$output = new WritableResourceStream(fopen('php://memory', 'r+'), $loop);
$output->close();
} else {
$output = new WritableResourceStream(STDOUT, $loop);
}

return $output;
}

/**
* @return bool
* @codeCoverageIgnore
*/
private function isTty()
{
if (PHP_VERSION_ID >= 70200) {
// Prefer `stream_isatty()` (available as of PHP 7.2 only)
return stream_isatty(STDIN);
} elseif (function_exists('posix_isatty')) {
// Otherwise use `posix_isatty` if available (requires `ext-posix`)
return posix_isatty(STDIN);
}

// otherwise try to guess based on stat file mode and device major number
// Must be special character device: ($mode & S_IFMT) === S_IFCHR
// And device major number must be allocated to TTYs (2-5 and 128-143)
// For what it's worth, checking for device gid 5 (tty) is less reliable.
// @link http://man7.org/linux/man-pages/man7/inode.7.html
// @link https://www.kernel.org/doc/html/v4.11/admin-guide/devices.html#terminal-devices
if (is_resource(STDIN)) {
$stat = fstat(STDIN);
$mode = isset($stat['mode']) ? ($stat['mode'] & 0170000) : 0;
$major = isset($stat['dev']) ? (($stat['dev'] >> 8) & 0xff) : 0;

if ($mode === 0020000 && $major >= 2 && $major <= 143 && ($major <=5 || $major >= 128)) {
return true;
}
}
return false;
}
}
11 changes: 11 additions & 0 deletions tests/FunctionalExampleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ public function testPeriodicExampleWithClosedInputQuitsImmediately()
$this->assertNotContains('you just said:', $output);
}

public function testPeriodicExampleWithClosedInputAndOutputQuitsImmediatelyWithoutOutput()
{
$output = $this->execExample('php 01-periodic.php <&- >&- 2>&1');

if (strpos($output, 'said') !== false) {
$this->markTestIncomplete('Your platform exhibits a closed STDIN bug, this may need some further debugging');
}

$this->assertEquals('', $output);
}

public function testStubShowStdinIsReadableByDefault()
{
$output = $this->execExample('php ../tests/stub/01-check-stdin.php');
Expand Down
Loading