amphp / amphp/process

Exception "ProcessException: Received 0 of 5 expected bytes" when reading from stdout out of time

Open
#69 10 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug windows
Dominant language
PHP
Stars
260
Forks
29
PR merge metrics
No merged PRs in 30d

Description

I've been experimenting with Amp\Process to better understand how it works. I launched several processes, connecting them stdout -> stdin, simulated various situations in order to understand where errors could occur and how to handle them correctly, including so that there were no zombie processes left. I have encountered one problem, the nature of which is not clear to me.

Important: I am using the Windows 10 operating system and what I am working on will run on the same operating system.

I will give some synthetic examples. Most of them work correctly, but one works unexpectedly. In all examples, I will use the ImageMagick command to convert a PNG image to a JPEG.

Example 1 (works well):

\Amp\Process\Process::start([
  'magick',
  'image.png', // input file
  '-quality', '100',
  'JPEG:image.jpg' // output file
]);

\Revolt\EventLoop::run();

Now I modify this example to pass the input image to stdin.

Example 2 (works well):

$content = \file_get_contents('image.png');

$process = \Amp\Process\Process::start([
  'magick',
  '-', // input stdin
  '-quality', '100',
  'JPEG:image.jpg' // output file
]);

$process->getStdin()->write($content);
$process->getStdin()->end();

\Revolt\EventLoop::run();

Next, I change the example so that the output image is sent to stdout.

Example 3 (works well):

$content = \file_get_contents('image.png');

$process = \Amp\Process\Process::start([
  'magick',
  '-', // input stdin
  '-quality', '100',
  'JPEG:-' // output stdout
]);

$process->getStdin()->write($content);
$process->getStdin()->end();

$content = \Amp\ByteStream\buffer($process->getStdout());
\file_put_contents('image.jpg', $content);

\Revolt\EventLoop::run();

Now I remove the code that reads from stdout. I expect the script to hang forever on the \Revolt\EventLoop::run(); call. I realize this is stupid code, but it demonstrates the problem.

Example 4 (not working correctly):

$content = \file_get_contents('image.png');

$process = \Amp\Process\Process::start([
  'magick',
  '-', // input stdin
  '-quality', '100',
  'JPEG:-' // output stdout
]);

$process->getStdin()->write($content);
$process->getStdin()->end();

\Revolt\EventLoop::run();

The script does hang at the line \Revolt\EventLoop::run();, but every time after exactly 120 seconds, I get an exception:

Uncaught Amp\Process\ProcessException: Received 0 of 5 expected bytes in C:\test\vendor\amphp\process\src\Internal\Windows\SocketConnector.php:245

The exception text is always identical, it reports the expected 5 bytes.

Then I additionally remove the code that sends the input image to stdin.

Example 5 (works well):

\Amp\Process\Process::start([
  'magick',
  '-', // input stdin
  '-quality', '100',
  'JPEG:-' // output stdout
]);

\Revolt\EventLoop::run();

In this case, the script hangs indefinitely, as expected (perhaps not indefinitely, but certainly much longer than 120 seconds).

Then I try again to specify the input file as the command argument.

Example 6 (works well):

\Amp\Process\Process::start([
  'magick',
  'image.png', // input file
  '-quality', '100',
  'JPEG:-' // output stdout
]);

\Revolt\EventLoop::run();

In this example, the script still hangs forever, as expected.

In each of the examples I tried replacing \Revolt\EventLoop::run(); with $process->join();, this did not affect the results in any way.

Below I provide the full stack trace of the exception from example 4, but with $process->join();:

PHP Fatal error:  Uncaught Amp\Process\ProcessException: Received 0 of 5 expected bytes in C:\test\vendor\amphp\process\src\Internal\Windows\SocketConnector.php:245
Stack trace:
#0 C:\test\vendor\amphp\process\src\Internal\Windows\SocketConnector.php(219): Amp\Process\Internal\Windows\SocketConnector->read(Object(Amp\ByteStream\ReadableResourceStream), 5)
#1 C:\test\vendor\amphp\process\src\Internal\Windows\SocketConnector.php(102): Amp\Process\Internal\Windows\SocketConnector->readExitCode(Object(Amp\ByteStream\ReadableResourceStream))
#2 C:\test\vendor\amphp\amp\src\functions.php(34): Amp\Process\Internal\Windows\SocketConnector->Amp\Process\Internal\Windows\{closure}()
#3 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(425): Amp\{closure}(NULL, NULL, Array)
#4 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(616): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#5 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#6 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\DriverSuspension.php(64): Fiber->resume(NULL)
#7 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(425): Revolt\EventLoop\Internal\DriverSuspension::Revolt\EventLoop\Internal\{closure}()
#8 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(616): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#9 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#10 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(498): Fiber->start()
#11 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(553): Revolt\EventLoop\Internal\AbstractDriver->invokeCallbacks()
#12 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#13 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(94): Fiber->resume()
#14 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\DriverSuspension.php(117): Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#15 C:\test\vendor\amphp\amp\src\Future.php(251): Revolt\EventLoop\Internal\DriverSuspension->suspend()
#16 C:\test\vendor\amphp\process\src\Internal\Windows\WindowsRunner.php(128): Amp\Future->await(NULL)
#17 C:\test\vendor\amphp\process\src\Process.php(123): Amp\Process\Internal\Windows\WindowsRunner->join(Object(Amp\Process\Internal\Windows\WindowsHandle), NULL)
#18 C:\test\test.php(19): Amp\Process\Process->join()
#19 {main}

Next Amp\Future\UnhandledFutureError: Unhandled future: Amp\Process\ProcessException: "Received 0 of 5 expected bytes"; Await the Future with Future::await() before the future is destroyed or use Future::ignore() to suppress this exception. The future has been created at #0 C:\test\vendor\amphp\amp\src\functions.php:40 Amp\Internal\FutureState->__construct()
#1 C:\test\vendor\amphp\process\src\Internal\Windows\SocketConnector.php:100 Amp\async()
#2 C:\test\vendor\amphp\process\src\Internal\Windows\WindowsRunner.php:101 Amp\Process\Internal\Windows\SocketConnector->connectPipes()
#3 C:\test\vendor\amphp\process\src\Process.php:80 Amp\Process\Internal\Windows\WindowsRunner->start()
#4 C:\test\test.php:9 Amp\Process\Process::start() in C:\test\vendor\amphp\amp\src\Internal\FutureState.php:53
Stack trace:
#0 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(425): Amp\Internal\FutureState->__destruct()
#1 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(616): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#2 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#3 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\DriverSuspension.php(64): Fiber->resume(NULL)
#4 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(425): Revolt\EventLoop\Internal\DriverSuspension::Revolt\EventLoop\Internal\{closure}()
#5 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(616): Revolt\EventLoop\Internal\AbstractDriver->invokeMicrotasks()
#6 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#7 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(498): Fiber->start()
#8 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(553): Revolt\EventLoop\Internal\AbstractDriver->invokeCallbacks()
#9 [internal function]: Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#10 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(94): Fiber->resume()
#11 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\DriverSuspension.php(117): Revolt\EventLoop\Internal\AbstractDriver->Revolt\EventLoop\Internal\{closure}()
#12 C:\test\vendor\amphp\amp\src\Future.php(251): Revolt\EventLoop\Internal\DriverSuspension->suspend()
#13 C:\test\vendor\amphp\process\src\Internal\Windows\WindowsRunner.php(128): Amp\Future->await(NULL)
#14 C:\test\vendor\amphp\process\src\Process.php(123): Amp\Process\Internal\Windows\WindowsRunner->join(Object(Amp\Process\Internal\Windows\WindowsHandle), NULL)
#15 C:\test\test.php(19): Amp\Process\Process->join()
#16 {main}

Next Revolt\EventLoop\UncaughtThrowable: Uncaught Amp\Future\UnhandledFutureError thrown in event loop callback Amp\Internal\FutureState::Amp\Internal\{closure} defined in C:\test\vendor\amphp\amp\src\Internal\FutureState.php:54; use Revolt\EventLoop::setErrorHandler() to gracefully handle such exceptions: Unhandled future: Amp\Process\ProcessException: "Received 0 of 5 expected bytes"; Await the Future with Future::await() before the future is destroyed or use Future::ignore() to suppress this exception. The future has been created at #0 C:\test\vendor\amphp\amp\src\functions.php:40 Amp\Internal\FutureState->__construct()
#1 C:\test\vendor\amphp\process\src\Internal\Windows\SocketConnector.php:100 Amp\async()
#2 C:\test\vendor\amphp\process\src\Internal\Windows\WindowsRunner.php:101 Amp\Process\Internal\Windows\SocketConnector->connectPipes()
#3 C:\test\vendor\amphp\process\src\Process.php:80 Amp\Process\Internal\Windows\WindowsRunner->start()
#4 C:\test\test.php:9 Amp\Process\Process::start() in C:\test\vendor\revolt\event-loop\src\EventLoop\UncaughtThrowable.php:13
Stack trace:
#0 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\AbstractDriver.php(400): Revolt\EventLoop\UncaughtThrowable::throwingCallback(Object(Closure), Object(Amp\Future\UnhandledFutureError))
#1 C:\test\vendor\revolt\event-loop\src\EventLoop\Internal\DriverSuspension.php(127): Revolt\EventLoop\Internal\AbstractDriver::Revolt\EventLoop\Internal\{closure}()
#2 C:\test\vendor\amphp\amp\src\Future.php(251): Revolt\EventLoop\Internal\DriverSuspension->suspend()
#3 C:\test\vendor\amphp\process\src\Internal\Windows\WindowsRunner.php(128): Amp\Future->await(NULL)
#4 C:\test\vendor\amphp\process\src\Process.php(123): Amp\Process\Internal\Windows\WindowsRunner->join(Object(Amp\Process\Internal\Windows\WindowsHandle), NULL)
#5 C:\test\test.php(19): Amp\Process\Process->join()
#6 {main}
  thrown in C:\test\vendor\revolt\event-loop\src\EventLoop\UncaughtThrowable.php on line 13

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Internal/Windows/SocketConnector.php, especially read() and readExitCode(), then trace how WindowsRunner connects pipes and joins the process. Reproduce examples 4 and 5 on Windows 10 and compare the stdout, stdin, and exit-code paths. Done means the 120-second failure is explained and the reported behavior is corrected or clearly documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
php
Domain
backend, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.