You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add PTY support to proc_open (again after 16 long years)
Back in 2004, a feature was added to proc_open which allowed it to open a PTY,
connecting specific FDs in the child process to the slave end of the PTY and returning
the master end of the PTY (wrapped as a PHP stream) in the `$pipes` array. However,
this feature was disabled just about a month later. Little information is available
about why this was done, but from talking to the original implementer, it seems there
were portability problems with some rare flavors of Unix.
Re-enable this feature with a simplified implementation which uses openpty(). No
attempt is made to support PTYs if the platform does not have openpty(). The configure
script checks if linking with -lutil is necessary to use openpty(), but if anything
else is required, like including some special header or linking with some other library,
PTY support will be disabled.
The original PTY support for proc_open automatically daemonized the child process
(disassociating it from the TTY session and process group of the parent). However,
I don't think this is a good idea. Just because a user opens a child process in a
PTY, it doesn't mean they want it to continue running even when the parent process
is killed. Of course, if the child process is some kind of server, it will likely
daemonize itself; but we have no reason to preempt that decision.
It turns out that since 2015, there has been one test case for PTY support in
proc_open() in the test suite. This test was added in GitHub PR #1588
(#1588). That PR mentioned that the PHP
binary in the Debian/Ubuntu repositories is patched to *enable* PTY support. Checking
the Debian PHP repository (https://salsa.debian.org/php-team/php.git) shows that this
is still true. Debian's patch does not modify the implementation from 2004 in any
way; it just removes the #if 0 line which disables it.
Naturally, the test case is skipped if PTY support is not enabled. This means that ever
since it was added, every test run against the 'vanilla' PHP codebase has skipped it.
Interestingly, the test case which was added in 2015 fails on my Linux Mint PC... both
with this simplified implementation *and* when enabling the original implementation.
Investigation reveals the reason: when the child process using the slave end of the
PTY exits and its FDs are all closed, and all buffered data is read from the master
end of the PTY, any further attempt to read from the master end fails with EIO. The
test case seems to expect that reading from the master end will always return an
empty string if no data is available.
Could this mean that on the platform where this test case was originally developed,
the PHP parent process ran *faster* than the child process, so that all the calls to
fread() complete before the child process exits and closes its FDs? Or could something
else about the platform have been different, such that read() in the parent does not fail
with EIO even after the child process exits? Another question: Are the Debian packagers
even running this test case every time they build PHP binaries? Intriguing questions, these!
There may be another way out of this sticky dilemma: IF at least one FD referring to the
slave end of the PTY is kept open *in the parent process*, the failure with EIO will not
occur even after the child process exits. This might be a good approach, though we would
need a way to ensure the FD will be closed eventually in long-running programs. Perhaps
associate it with the proc resource and close it when the proc is freed or explicitly closed
with proc_close?
And the rabbit hole goes deeper. Although (for now) the test case from 2015 has been
updated so it does not attempt to fread() multiple times from the PTY, it still fails
intermittently. The reason? When the child process writes "foo\n" to the PTY, the parent
sometimes receives "foo" (3 bytes) and sometimes "foo\r\n" (5 bytes). The "\r" is obviously
from the TTY line discipline converting "\n" to "\r\n", but why on earth would the parent
sometimes read the newline and sometimes not? What is more, strace clearly shows that this
is happening *in the kernel*. The child process always executes a single write("foo\n")
syscall, but when the blocking read() syscall issued by the parent process returns,
sometimes it returns "foo" and sometimes "foo\r\n". This doesn't happen when I extract the
test code and run it directly from the PHP CLI, only when the test case is run by
run-tests.php.
Thanks to Nikita Popov for suggesting that we should just use openpty() rather than
grantpt(), unlockpt(), etc.
0 commit comments