-
-
Notifications
You must be signed in to change notification settings - Fork 29
Description
I have identified a critical blocking issue when using amphp/process on Windows with the uv extension enabled (using Revolt\EventLoop\Driver\UvDriver).
Any attempt to spawn a process results in a 60-second hang, rendering the library unusable. This also breaks amphp/dns out-of-the-box on Windows, as its default WindowsDnsConfigLoader spawns a PowerShell process to retrieve system DNS settings.
Environment:
- OS: Windows 11
- PHP: 8.5.2
- Extensions:
uv(enabled) - Driver:
Revolt\EventLoop\Driver\UvDriver - Package:
amphp/process(latest 2.0.3) &hp/windows-process-wrapper(v1.2.0)
Reproduction Script:
The hang occurs only when UvDriver is active. Using StreamDriver works instantly.
<?php
require __DIR__ . '/vendor/autoload.php';
use Amp\Process\Process;
use Revolt\EventLoop;
echo "Driver: " . get_class(EventLoop::getDriver()) . "\n";
$start = microtime(true);
echo "Starting process...\n";
// This line hangs for exactly 60 seconds
$process = Process::start('cmd /c echo Hello');
$process->join();
echo "Done in " . (microtime(true) - $start) . "s\n";Deep Analysis & Debugging:
I performed extensive debugging by modifying both the PHP SocketConnector and recompiling the C ProcessWrapper.exe with logging.
Trace Findings:
- TCP Connection:
ProcessWrapper.exesuccessfully connects to the PHP server socket (127.0.0.1:0). - Handshake: The security token exchange completes instantly.
- The Deadlock: The data transmission hangs exactly when the Wrapper calls
CreateProcessWto spawn the actual command (e.g.,cmd.exe). - Wrapper Internal State: My custom logs inside the Wrapper C-code show that it successfully sends the PID signal and calls
send(). It finishes its work instantly (< 1s). - PHP State:
UvDriverdoes not receive theonReadableevent for the PID data until exactly 60 seconds later (Windows TCP timeout).
Root Cause: IOCP Handle Inheritance Deadlock
The issue is likely caused by Socket Handle Inheritance combined with libuv's IOCP implementation.
- The Leak: PHP's
proc_openon Windows hardcodesbInheritHandles = TRUE. This causes theProcessWrapper.exeto inherit the parent's (PHP) TCP server socket handle. - The Deadlock:
ext-uvties all sockets to an IOCP port. When theProcessWrapper.exespawns the actual child process, that "grandchild" also inherits the server socket handle. - The Conflict: The server socket handle is now held by a grandchild process while being bound to the parent's IOCP. The Windows kernel detects a conflict or deadlock in the I/O completion mechanism for this socket. The
send()call in the Wrapper blocks (or the OS buffers the data but doesn't notify the parent) until the system TCP timeout (60s) forces a reset/flush.
Verification of Potential Fixes
I tried to fix this by modifying windows-process-wrapper (C-code) to use Selective Handle Inheritance:
- I utilized
STARTUPINFOEXwithUpdateProcThreadAttributeandPROC_THREAD_ATTRIBUTE_HANDLE_LIST. - I explicitly whitelisted only the 3 pipes (STDIN/STDOUT/STDERR) for inheritance.
- Result: The 60-second hang persisted.
Even though the Wrapper logic ensured the socket handle wasn't passed to the grandchild, the deadlock remained. This suggests that the conflict is deeper within libuv/PHP interaction or that proc_open's initial handle leakage cannot be fully mitigated by the child process wrapper alone.
Conclusion & Solution
Since patching the Wrapper to filter handles did not resolve the issue, and fixing proc_open in PHP core is out of scope, the architecture of amphp/process on Windows appears incompatible with ext-uv when using TCP sockets for IPC.
Proposed Long-Term Fix:
When UvDriver is detected on Windows, the library should ideally switch to using Named Pipes (\\.\pipe\...) instead of TCP sockets. Named Pipes are natively supported by libuv (and IOCP) and handle inheritance/duplication much more gracefully than TCP sockets in this context.