Skip to content

Commit a84cd96

Browse files
alexdowadnikic
authored andcommitted
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. Likely this is because PHP's fread() was updated to report errors from the underlying system calls only recently. One way out of this 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. However, that would raise another issue: we would need a way to ensure the FD will be closed eventually in long-running programs. Another discovery made while testing this code is that fread() does not always return all the data written to the slave end of the PTY in a single call, even if the data was written with a single syscall and it is only a few bytes long. Specifically, when the child process in the test case writes "foo\n" to the PTY, the parent sometimes receives "foo" (3 bytes) and sometimes "foo\r\n" (5 bytes). (The "\r" is from the TTY line discipline converting "\n" to "\r\n".) A second call to fread() does return the remaining bytes, though sometimes all the data is read in the first call, and by the time the second call is made, the child process has already exited. It seems that liberal use of the @ operator is needed when using fread() on pipes. Thanks to Nikita Popov for suggesting that we should just use openpty() rather than grantpt(), unlockpt(), etc.
1 parent ae5ca54 commit a84cd96

File tree

3 files changed

+75
-85
lines changed

3 files changed

+75
-85
lines changed

configure.ac

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ malloc.h \
392392
monetary.h \
393393
netdb.h \
394394
poll.h \
395+
pty.h \
395396
pwd.h \
396397
resolv.h \
397398
strings.h \
@@ -560,7 +561,6 @@ getgrnam_r \
560561
getpwuid_r \
561562
getwd \
562563
glob \
563-
grantpt \
564564
inet_ntoa \
565565
inet_ntop \
566566
inet_pton \
@@ -572,7 +572,6 @@ mmap \
572572
nice \
573573
nl_langinfo \
574574
poll \
575-
ptsname \
576575
putenv \
577576
scandir \
578577
setitimer \
@@ -588,7 +587,6 @@ strptime \
588587
strtok_r \
589588
symlink \
590589
tzset \
591-
unlockpt \
592590
unsetenv \
593591
usleep \
594592
utime \
@@ -713,6 +711,9 @@ if test "$PHP_VALGRIND" != "no"; then
713711
fi
714712
fi
715713

714+
dnl Check for openpty. It may require linking against libutil.
715+
PHP_CHECK_FUNC(openpty, util)
716+
716717
dnl General settings.
717718
dnl ----------------------------------------------------------------------------
718719
PHP_CONFIGURE_PART(General settings)

ext/standard/proc_open.c

Lines changed: 47 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,13 @@
4343
* */
4444
#ifdef PHP_CAN_SUPPORT_PROC_OPEN
4545

46-
#if 0 && HAVE_PTSNAME && HAVE_GRANTPT && HAVE_UNLOCKPT && HAVE_SYS_IOCTL_H && HAVE_TERMIOS_H
47-
# include <sys/ioctl.h>
48-
# include <termios.h>
49-
# define PHP_CAN_DO_PTS 1
46+
#if HAVE_OPENPTY
47+
# if HAVE_PTY_H
48+
# include <pty.h>
49+
# else
50+
/* Mac OS X defines openpty() in <util.h> */
51+
# include <util.h>
52+
# endif
5053
#endif
5154

5255
#include "proc_open.h"
@@ -592,6 +595,27 @@ static int set_proc_descriptor_to_blackhole(struct php_proc_open_descriptor_item
592595
return SUCCESS;
593596
}
594597

598+
static int set_proc_descriptor_to_pty(struct php_proc_open_descriptor_item *desc, int *master_fd, int *slave_fd)
599+
{
600+
#if HAVE_OPENPTY
601+
if (*master_fd == -1) {
602+
if (openpty(master_fd, slave_fd, NULL, NULL, NULL)) {
603+
php_error_docref(NULL, E_WARNING, "Could not open PTY (pseudoterminal) - %s", strerror(errno));
604+
return FAILURE;
605+
}
606+
}
607+
608+
desc->is_pipe = 1;
609+
desc->childend = dup(*slave_fd);
610+
desc->parentend = dup(*master_fd);
611+
desc->mode_flags = O_RDWR;
612+
return SUCCESS;
613+
#else
614+
php_error_docref(NULL, E_WARNING, "PTY (pseudoterminal) not supported on this system");
615+
return FAILURE;
616+
#endif
617+
}
618+
595619
static int set_proc_descriptor_to_pipe(struct php_proc_open_descriptor_item *desc, zend_string *zmode)
596620
{
597621
php_file_descriptor_t newpipe[2];
@@ -708,9 +732,9 @@ static int redirect_proc_descriptor(struct php_proc_open_descriptor_item *desc,
708732
return dup_proc_descriptor(redirect_to, &desc->childend, nindex);
709733
}
710734

711-
712735
int set_proc_descriptor_from_array(
713-
zval *descitem, struct php_proc_open_descriptor_item *descriptors, int ndesc, int nindex) {
736+
zval *descitem, struct php_proc_open_descriptor_item *descriptors, int ndesc,
737+
int nindex, int *pty_master_fd, int *pty_slave_fd) {
714738
zend_string *ztype = get_string_parameter(descitem, 0, "handle qualifier");
715739
if (!ztype) {
716740
return FAILURE;
@@ -749,32 +773,7 @@ int set_proc_descriptor_from_array(
749773
} else if (zend_string_equals_literal(ztype, "null")) {
750774
retval = set_proc_descriptor_to_blackhole(&descriptors[ndesc]);
751775
} else if (zend_string_equals_literal(ztype, "pty")) {
752-
#if PHP_CAN_DO_PTS
753-
if (dev_ptmx == -1) {
754-
/* open things up */
755-
dev_ptmx = open("/dev/ptmx", O_RDWR);
756-
if (dev_ptmx == -1) {
757-
php_error_docref(NULL, E_WARNING, "Failed to open /dev/ptmx, errno %d", errno);
758-
goto finish;
759-
}
760-
grantpt(dev_ptmx);
761-
unlockpt(dev_ptmx);
762-
slave_pty = open(ptsname(dev_ptmx), O_RDWR);
763-
764-
if (slave_pty == -1) {
765-
php_error_docref(NULL, E_WARNING, "Failed to open slave pty, errno %d", errno);
766-
goto finish;
767-
}
768-
}
769-
descriptors[ndesc].is_pipe = 1;
770-
descriptors[ndesc].childend = dup(slave_pty);
771-
descriptors[ndesc].parentend = dup(dev_ptmx);
772-
descriptors[ndesc].mode_flags = O_RDWR;
773-
retval = SUCCESS;
774-
#else
775-
php_error_docref(NULL, E_WARNING, "PTY pseudo terminal not supported on this system");
776-
goto finish;
777-
#endif
776+
retval = set_proc_descriptor_to_pty(&descriptors[ndesc], pty_master_fd, pty_slave_fd);
778777
} else {
779778
php_error_docref(NULL, E_WARNING, "%s is not a valid descriptor spec/mode", ZSTR_VAL(ztype));
780779
goto finish;
@@ -867,14 +866,10 @@ PHP_FUNCTION(proc_open)
867866
#else
868867
char **argv = NULL;
869868
#endif
869+
int pty_master_fd = -1, pty_slave_fd = -1;
870870
php_process_id_t child;
871871
struct php_process_handle *proc;
872872

873-
#if PHP_CAN_DO_PTS
874-
php_file_descriptor_t dev_ptmx = -1; /* master */
875-
php_file_descriptor_t slave_pty = -1;
876-
#endif
877-
878873
ZEND_PARSE_PARAMETERS_START(3, 6)
879874
Z_PARAM_ZVAL(command_zv)
880875
Z_PARAM_ARRAY(descriptorspec)
@@ -957,7 +952,8 @@ PHP_FUNCTION(proc_open)
957952
goto exit_fail;
958953
}
959954
} else if (Z_TYPE_P(descitem) == IS_ARRAY) {
960-
if (set_proc_descriptor_from_array(descitem, descriptors, ndesc, nindex) == FAILURE) {
955+
if (set_proc_descriptor_from_array(descitem, descriptors, ndesc, nindex,
956+
&pty_master_fd, &pty_slave_fd) == FAILURE) {
961957
goto exit_fail;
962958
}
963959
} else {
@@ -1044,27 +1040,6 @@ PHP_FUNCTION(proc_open)
10441040
if (child == 0) {
10451041
/* this is the child process */
10461042

1047-
#if PHP_CAN_DO_PTS
1048-
if (dev_ptmx >= 0) {
1049-
int my_pid = getpid();
1050-
1051-
#ifdef TIOCNOTTY
1052-
/* detach from original tty. Might only need this if isatty(0) is true */
1053-
ioctl(0,TIOCNOTTY,NULL);
1054-
#else
1055-
setsid();
1056-
#endif
1057-
/* become process group leader */
1058-
setpgid(my_pid, my_pid);
1059-
tcsetpgrp(0, my_pid);
1060-
}
1061-
1062-
if (dev_ptmx >= 0) {
1063-
close(dev_ptmx);
1064-
close(slave_pty);
1065-
}
1066-
#endif
1067-
10681043
if (close_parent_ends_of_pipes_in_child(descriptors, ndesc) == FAILURE) {
10691044
/* We are already in child process and can't do anything to make
10701045
* proc_open() return an error in the parent
@@ -1121,13 +1096,6 @@ PHP_FUNCTION(proc_open)
11211096
#endif
11221097
proc->env = env;
11231098

1124-
#if PHP_CAN_DO_PTS
1125-
if (dev_ptmx >= 0) {
1126-
close(dev_ptmx);
1127-
close(slave_pty);
1128-
}
1129-
#endif
1130-
11311099
/* clean up all the child ends and then open streams on the parent
11321100
* ends, where appropriate */
11331101
for (i = 0; i < ndesc; i++) {
@@ -1191,7 +1159,14 @@ PHP_FUNCTION(proc_open)
11911159
#else
11921160
efree_argv(argv);
11931161
#endif
1194-
1162+
#if HAVE_OPENPTY
1163+
if (pty_master_fd != -1) {
1164+
close(pty_master_fd);
1165+
}
1166+
if (pty_slave_fd != -1) {
1167+
close(pty_slave_fd);
1168+
}
1169+
#endif
11951170
efree(descriptors);
11961171
ZVAL_RES(return_value, zend_register_resource(proc, le_proc_open));
11971172
return;
@@ -1211,12 +1186,12 @@ PHP_FUNCTION(proc_open)
12111186
#else
12121187
efree_argv(argv);
12131188
#endif
1214-
#if PHP_CAN_DO_PTS
1215-
if (dev_ptmx >= 0) {
1216-
close(dev_ptmx);
1189+
#if HAVE_OPENPTY
1190+
if (pty_master_fd != -1) {
1191+
close(pty_master_fd);
12171192
}
1218-
if (slave_pty >= 0) {
1219-
close(slave_pty);
1193+
if (pty_slave_fd != -1) {
1194+
close(pty_slave_fd);
12201195
}
12211196
#endif
12221197
RETURN_FALSE;

ext/standard/tests/file/bug69442.phpt

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ EOC;
1717
$output = join("\n", $output);
1818
unlink($tmpFile);
1919

20-
if (strstr($output, "PTY pseudo terminal not supported on this system") !== false) {
20+
if (strstr($output, "PTY (pseudoterminal) not supported on this system") !== false) {
2121
die("skip PTY pseudo terminals are not supported");
2222
}
2323
--FILE--
@@ -28,17 +28,31 @@ $pipes = array();
2828

2929
$process = proc_open($cmd, $descriptors, $pipes);
3030

31-
foreach ($pipes as $type => $pipe) {
32-
$data = fread($pipe, 999);
33-
echo 'type ' . $type . ' ';
34-
var_dump($data);
35-
fclose($pipe);
31+
function read_from_pipe($pipe) {
32+
$result = fread($pipe, 1000);
33+
/* We can't guarantee that everything written to the pipe will be returned by a single call
34+
* to fread(), even if it was written with a single syscall and the number of bytes written
35+
* was small */
36+
$again = @fread($pipe, 1000);
37+
if ($again) {
38+
$result .= $again;
39+
}
40+
return $result;
3641
}
42+
43+
$data0 = read_from_pipe($pipes[0]);
44+
echo 'read from pipe 0: ';
45+
var_dump($data0);
46+
fclose($pipes[0]);
47+
48+
$data3 = read_from_pipe($pipes[3]);
49+
echo 'read from pipe 3: ';
50+
var_dump($data3);
51+
fclose($pipes[3]);
52+
3753
proc_close($process);
3854
--EXPECT--
39-
type 0 string(5) "foo
55+
read from pipe 0: string(5) "foo
4056
"
41-
type 1 string(0) ""
42-
type 2 string(0) ""
43-
type 3 string(3) "42
57+
read from pipe 3: string(3) "42
4458
"

0 commit comments

Comments
 (0)