tl;dr
- This shouldn't happen on PHP 8.3.
- This can happen if you call some methods, such as
isRunning
or getExitCode
inside the output callback of the process.
Long explanation
If we look under the hood into what Symfony Process is doing, we can see it's using proc_open.
In most cases, an exit status code of -1
is more likely to come from a process manager than the process itself, so we can be almost certain this isn't the real exit status code.
Before PHP 8.3, if we invoke proc_get_status()
against the process resource, it will return the actual exit code only on the first time. Then, the process is discarded and any subsequent calls will return -1
.
We can see this behavior by adding a breakpoint after proc_open
and running proc_get_status($this->process)
manually.
Long story short, Symfony Process is good at avoiding this issue, as it bails immediately after the process is no longer running, which prevents it from invoking that method more than once on the process and therefore discarding the actual exit status code.
I don't know all the ways that it's possible to trigger this but, but I was able to consistently reproduce it in at least one way, and it's a combination of factors and racing conditions.
The way that I was able to reproduce it is: If the output callback of the process invokes on itself a method such as isRunning
or getExitCode
, you incur in a racing condition that can trigger the bug. The racing condition depends on the time between the output being generated and the process exiting.
Here's an isolated PoC:
<?php
if ( getenv( 'OUTPUT' ) ) {
#echo 'This should fail when "wait" gets large';
sleep( 2 );
echo 'This should fail even when "wait" is small';
die( 0 );
}
use Symfony\Component\Process\Process;
require_once __DIR__ . '/vendor/autoload.php';
do {
// Increasingly bigger waits.
static $wait = 0;
$wait += 100000;
echo sprintf( 'Wait: %s', $wait ) . PHP_EOL;
$p = new Process( [ 'php', __FILE__ ], __DIR__, [
'OUTPUT' => 1,
] );
$p->start( function ( string $type, string $out ) use ( $p ) {
echo $out . PHP_EOL;
/**
* Calling most methods in Symfony Process that triggers
* updateStatus() can potentially trigger the -1 bug.
*
* @see Process::updateStatus()
*/
echo sprintf( 'Is Running: %s', $p->isRunning() ? 'Yes' : 'No' ) . PHP_EOL;
echo sprintf( 'Exit Code: %s', $p->getExitCode() ) . PHP_EOL;
} );
while ( $p->isRunning() ) {
usleep( $wait );
}
if ( ! $p->isSuccessful() ) {
break;
}
} while ( true );
$is_started = $p->isStarted();
$is_running = $p->isRunning();
$exit_code = $p->getExitCode();
echo sprintf( 'Started: %s, Running: %s, Exit code: %s', $is_started, $is_running, $exit_code ) . PHP_EOL;
This shouldn't happen on PHP 8.3, as this issue with get_proc_status
has been fixed, as per changelog notes:
Executing proc_get_status() multiple times
Executing proc_get_status()
multiple times will now always return the
right value on POSIX systems. Previously, only the first call of the
function returned the right value. Executing proc_close()
after
proc_get_status()
will now also return the right exit code. Previously
this would return -1.
PS: I don't have time to contribute a PR to Symfony Process right now, but if anyone does, I just make a pledge to keep backwards PHP compatibility.