Skip to content

Commit 1298dba

Browse files
authored
Merge pull request #68 from clue-labs/deps
Forward compatibility with latest and upcoming ReactPHP packages
2 parents 21bf3c0 + 6f6c5f7 commit 1298dba

File tree

8 files changed

+161
-170
lines changed

8 files changed

+161
-170
lines changed

composer.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
],
1313
"require": {
1414
"php": ">=5.3",
15-
"react/event-loop": "0.3.*|0.4.*",
16-
"react/stream": "^0.4.2",
15+
"clue/term-react": "^1.0 || ^0.1.1",
1716
"clue/utf8-react": "^1.0 || ^0.1",
18-
"clue/term-react": "^1.0 || ^0.1.1"
17+
"react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3",
18+
"react/stream": "^1.0 || ^0.7 || ^0.6"
1919
},
2020
"suggest": {
2121
"ext-mbstring": "Using ext-mbstring should provide slightly better performance for handling I/O"

examples/11-login.php

+7-8
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,16 @@
2323
$first = false;
2424
} else {
2525
$password = $line;
26-
$stdio->end();
27-
}
28-
});
29-
30-
$loop->run();
31-
32-
echo <<<EOT
26+
$stdio->end(<<<EOT
3327
---------------------
3428
Confirmation:
3529
---------------------
3630
Username: $username
3731
Password: $password
3832
39-
EOT;
33+
EOT
34+
);
35+
}
36+
});
37+
38+
$loop->run();

src/Io/Stdin.php

-117
This file was deleted.

src/Io/Stdout.php

-31
This file was deleted.

src/Stdio.php

+124-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use Evenement\EventEmitter;
88
use React\EventLoop\LoopInterface;
99
use React\Stream\DuplexStreamInterface;
10+
use React\Stream\ReadableResourceStream;
1011
use React\Stream\ReadableStreamInterface;
1112
use React\Stream\Util;
13+
use React\Stream\WritableResourceStream;
1214
use React\Stream\WritableStreamInterface;
1315

1416
class 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
}

tests/FunctionalExampleTest.php

+11
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,17 @@ public function testPeriodicExampleWithClosedInputQuitsImmediately()
4141
$this->assertNotContains('you just said:', $output);
4242
}
4343

44+
public function testPeriodicExampleWithClosedInputAndOutputQuitsImmediatelyWithoutOutput()
45+
{
46+
$output = $this->execExample('php 01-periodic.php <&- >&- 2>&1');
47+
48+
if (strpos($output, 'said') !== false) {
49+
$this->markTestIncomplete('Your platform exhibits a closed STDIN bug, this may need some further debugging');
50+
}
51+
52+
$this->assertEquals('', $output);
53+
}
54+
4455
public function testStubShowStdinIsReadableByDefault()
4556
{
4657
$output = $this->execExample('php ../tests/stub/01-check-stdin.php');

0 commit comments

Comments
 (0)