Skip to content

Commit 547d98b

Browse files
kooldevnikic
authored andcommitted
Support socketpairs in proc_open()
Closes GH-5777.
1 parent 1a00d01 commit 547d98b

File tree

10 files changed

+300
-37
lines changed

10 files changed

+300
-37
lines changed

UPGRADING

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,14 @@ PHP 8.0 UPGRADE NOTES
631631

632632
$proc = proc_open($command, [['pty'], ['pty'], ['pty']], $pipes);
633633

634+
. proc_open() now supports socket pair descriptors. The following attaches
635+
a distinct socket pair to stdin, stdout and stderr:
636+
637+
$proc = proc_open(
638+
$command, [['socket'], ['socket'], ['socket']], $pipes);
639+
640+
Unlike pipes, sockets do not suffer from blocking I/O issues on Windows.
641+
However, not all programs may work correctly with stdio sockets.
634642
. Sorting functions are now stable, which means that equal-comparing elements
635643
will retain their original order.
636644
RFC: https://wiki.php.net/rfc/stable_sorting

ext/standard/proc_open.c

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -444,11 +444,18 @@ static inline HANDLE dup_fd_as_handle(int fd)
444444
# define close_descriptor(fd) close(fd)
445445
#endif
446446

447+
/* Determines the type of a descriptor item. */
448+
typedef enum _descriptor_type {
449+
DESCRIPTOR_TYPE_STD,
450+
DESCRIPTOR_TYPE_PIPE,
451+
DESCRIPTOR_TYPE_SOCKET
452+
} descriptor_type;
453+
447454
/* One instance of this struct is created for each item in `$descriptorspec` argument to `proc_open`
448455
* They are used within `proc_open` and freed before it returns */
449456
typedef struct _descriptorspec_item {
450457
int index; /* desired FD # in child process */
451-
int is_pipe;
458+
descriptor_type type;
452459
php_file_descriptor_t childend; /* FD # opened for use in child
453460
* (will be copied to `index` in child) */
454461
php_file_descriptor_t parentend; /* FD # opened for use in parent
@@ -679,7 +686,7 @@ static int set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd,
679686
}
680687
}
681688

682-
desc->is_pipe = 1;
689+
desc->type = DESCRIPTOR_TYPE_PIPE;
683690
desc->childend = dup(*slave_fd);
684691
desc->parentend = dup(*master_fd);
685692
desc->mode_flags = O_RDWR;
@@ -690,6 +697,19 @@ static int set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd,
690697
#endif
691698
}
692699

700+
/* Mark the descriptor close-on-exec, so it won't be inherited by children */
701+
static php_file_descriptor_t make_descriptor_cloexec(php_file_descriptor_t fd)
702+
{
703+
#ifdef PHP_WIN32
704+
return dup_handle(fd, FALSE, TRUE);
705+
#else
706+
#if defined(F_SETFD) && defined(FD_CLOEXEC)
707+
fcntl(fd, F_SETFD, FD_CLOEXEC);
708+
#endif
709+
return fd;
710+
#endif
711+
}
712+
693713
static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *zmode)
694714
{
695715
php_file_descriptor_t newpipe[2];
@@ -699,7 +719,7 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z
699719
return FAILURE;
700720
}
701721

702-
desc->is_pipe = 1;
722+
desc->type = DESCRIPTOR_TYPE_PIPE;
703723

704724
if (strncmp(ZSTR_VAL(zmode), "w", 1) != 0) {
705725
desc->parentend = newpipe[1];
@@ -711,17 +731,42 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z
711731
desc->mode_flags = O_RDONLY;
712732
}
713733

714-
#ifdef PHP_WIN32
715-
/* don't let the child inherit the parent side of the pipe */
716-
desc->parentend = dup_handle(desc->parentend, FALSE, TRUE);
734+
desc->parentend = make_descriptor_cloexec(desc->parentend);
717735

736+
#ifdef PHP_WIN32
718737
if (ZSTR_LEN(zmode) >= 2 && ZSTR_VAL(zmode)[1] == 'b')
719738
desc->mode_flags |= O_BINARY;
720739
#endif
721740

722741
return SUCCESS;
723742
}
724743

744+
#ifdef PHP_WIN32
745+
#define create_socketpair(socks) socketpair_win32(AF_INET, SOCK_STREAM, 0, (socks), 0)
746+
#else
747+
#define create_socketpair(socks) socketpair(AF_UNIX, SOCK_STREAM, 0, (socks))
748+
#endif
749+
750+
static int set_proc_descriptor_to_socket(descriptorspec_item *desc)
751+
{
752+
php_socket_t sock[2];
753+
754+
if (create_socketpair(sock)) {
755+
zend_string *err = php_socket_error_str(php_socket_errno());
756+
php_error_docref(NULL, E_WARNING, "Unable to create socket pair: %s", ZSTR_VAL(err));
757+
zend_string_release(err);
758+
return FAILURE;
759+
}
760+
761+
desc->type = DESCRIPTOR_TYPE_SOCKET;
762+
desc->parentend = make_descriptor_cloexec((php_file_descriptor_t) sock[0]);
763+
764+
/* Pass sock[1] to child because it will never use overlapped IO on Windows. */
765+
desc->childend = (php_file_descriptor_t) sock[1];
766+
767+
return SUCCESS;
768+
}
769+
725770
static int set_proc_descriptor_to_file(descriptorspec_item *desc, zend_string *file_path,
726771
zend_string *file_mode)
727772
{
@@ -827,6 +872,9 @@ static int set_proc_descriptor_from_array(zval *descitem, descriptorspec_item *d
827872
goto finish;
828873
}
829874
retval = set_proc_descriptor_to_pipe(&descriptors[ndesc], zmode);
875+
} else if (zend_string_equals_literal(ztype, "socket")) {
876+
/* Set descriptor to socketpair */
877+
retval = set_proc_descriptor_to_socket(&descriptors[ndesc]);
830878
} else if (zend_string_equals_literal(ztype, "file")) {
831879
/* Set descriptor to file */
832880
if ((zfile = get_string_parameter(descitem, 1, "file name parameter for 'file'")) == NULL) {
@@ -903,7 +951,7 @@ static int close_parentends_of_pipes(descriptorspec_item *descriptors, int ndesc
903951
* Also, dup() the child end of all pipes as necessary so they will use the FD
904952
* number which the user requested */
905953
for (int i = 0; i < ndesc; i++) {
906-
if (descriptors[i].is_pipe) {
954+
if (descriptors[i].type != DESCRIPTOR_TYPE_STD) {
907955
close(descriptors[i].parentend);
908956
}
909957
if (descriptors[i].childend != descriptors[i].index) {
@@ -1194,12 +1242,13 @@ PHP_FUNCTION(proc_open)
11941242
/* Clean up all the child ends and then open streams on the parent
11951243
* ends, where appropriate */
11961244
for (i = 0; i < ndesc; i++) {
1197-
char *mode_string = NULL;
11981245
php_stream *stream = NULL;
11991246

12001247
close_descriptor(descriptors[i].childend);
12011248

1202-
if (descriptors[i].is_pipe) {
1249+
if (descriptors[i].type == DESCRIPTOR_TYPE_PIPE) {
1250+
char *mode_string = NULL;
1251+
12031252
switch (descriptors[i].mode_flags) {
12041253
#ifdef PHP_WIN32
12051254
case O_WRONLY|O_BINARY:
@@ -1219,32 +1268,31 @@ PHP_FUNCTION(proc_open)
12191268
mode_string = "r+";
12201269
break;
12211270
}
1271+
12221272
#ifdef PHP_WIN32
12231273
stream = php_stream_fopen_from_fd(_open_osfhandle((zend_intptr_t)descriptors[i].parentend,
12241274
descriptors[i].mode_flags), mode_string, NULL);
12251275
php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, blocking_pipes, NULL);
12261276
#else
12271277
stream = php_stream_fopen_from_fd(descriptors[i].parentend, mode_string, NULL);
1228-
# if defined(F_SETFD) && defined(FD_CLOEXEC)
1229-
/* Mark the descriptor close-on-exec, so it won't be inherited by
1230-
* potential other children */
1231-
fcntl(descriptors[i].parentend, F_SETFD, FD_CLOEXEC);
1232-
# endif
12331278
#endif
1234-
if (stream) {
1235-
zval retfp;
1279+
} else if (descriptors[i].type == DESCRIPTOR_TYPE_SOCKET) {
1280+
stream = php_stream_sock_open_from_socket((php_socket_t) descriptors[i].parentend, NULL);
1281+
} else {
1282+
proc->pipes[i] = NULL;
1283+
}
12361284

1237-
/* nasty hack; don't copy it */
1238-
stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
1285+
if (stream) {
1286+
zval retfp;
12391287

1240-
php_stream_to_zval(stream, &retfp);
1241-
add_index_zval(pipes, descriptors[i].index, &retfp);
1288+
/* nasty hack; don't copy it */
1289+
stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
12421290

1243-
proc->pipes[i] = Z_RES(retfp);
1244-
Z_ADDREF(retfp);
1245-
}
1246-
} else {
1247-
proc->pipes[i] = NULL;
1291+
php_stream_to_zval(stream, &retfp);
1292+
add_index_zval(pipes, descriptors[i].index, &retfp);
1293+
1294+
proc->pipes[i] = Z_RES(retfp);
1295+
Z_ADDREF(retfp);
12481296
}
12491297
}
12501298

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
echo "hello";
4+
sleep(1);
5+
fwrite(STDERR, "SOME ERROR");
6+
sleep(1);
7+
echo "world";
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
--TEST--
2+
proc_open() with output socketpairs
3+
--FILE--
4+
<?php
5+
6+
$cmd = [
7+
getenv("TEST_PHP_EXECUTABLE"),
8+
__DIR__ . '/proc_open_sockets1.inc'
9+
];
10+
11+
$spec = [
12+
['null'],
13+
['socket'],
14+
['socket']
15+
];
16+
17+
$proc = proc_open($cmd, $spec, $pipes);
18+
19+
foreach ($pipes as $pipe) {
20+
var_dump(stream_set_blocking($pipe, false));
21+
}
22+
23+
while ($pipes) {
24+
$r = $pipes;
25+
$w = null;
26+
$e = null;
27+
28+
if (!stream_select($r, $w, $e, null, 0)) {
29+
throw new Error("Select failed");
30+
}
31+
32+
foreach ($r as $i => $pipe) {
33+
if (!is_resource($pipe) || feof($pipe)) {
34+
unset($pipes[$i]);
35+
continue;
36+
}
37+
38+
$chunk = @fread($pipe, 8192);
39+
40+
if ($chunk === false) {
41+
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
42+
}
43+
44+
if ($chunk !== '') {
45+
echo "PIPE {$i} << {$chunk}\n";
46+
}
47+
}
48+
}
49+
50+
?>
51+
--EXPECTF--
52+
bool(true)
53+
bool(true)
54+
PIPE 1 << hello
55+
PIPE 2 << SOME ERROR
56+
PIPE 1 << world
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
echo "hello";
4+
sleep(1);
5+
echo "world";
6+
7+
echo strtoupper(trim(fgets(STDIN)));
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
--TEST--
2+
proc_open() with IO socketpairs
3+
--FILE--
4+
<?php
5+
6+
function poll($pipe, $read = true)
7+
{
8+
$r = ($read == true) ? [$pipe] : null;
9+
$w = ($read == false) ? [$pipe] : null;
10+
$e = null;
11+
12+
if (!stream_select($r, $w, $e, null, 0)) {
13+
throw new \Error("Select failed");
14+
}
15+
}
16+
17+
function read_pipe($pipe): string
18+
{
19+
poll($pipe);
20+
21+
if (false === ($chunk = @fread($pipe, 8192))) {
22+
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
23+
}
24+
25+
return $chunk;
26+
}
27+
28+
function write_pipe($pipe, $data)
29+
{
30+
poll($pipe, false);
31+
32+
if (false == @fwrite($pipe, $data)) {
33+
throw new Error("Failed to write: " . (error_get_last()['message'] ?? 'N/A'));
34+
}
35+
}
36+
37+
$cmd = [
38+
getenv("TEST_PHP_EXECUTABLE"),
39+
__DIR__ . '/proc_open_sockets2.inc'
40+
];
41+
42+
$spec = [
43+
['socket'],
44+
['socket']
45+
];
46+
47+
$proc = proc_open($cmd, $spec, $pipes);
48+
49+
foreach ($pipes as $pipe) {
50+
var_dump(stream_set_blocking($pipe, false));
51+
}
52+
53+
printf("STDOUT << %s\n", read_pipe($pipes[1]));
54+
printf("STDOUT << %s\n", read_pipe($pipes[1]));
55+
56+
write_pipe($pipes[0], 'done');
57+
fclose($pipes[0]);
58+
59+
printf("STDOUT << %s\n", read_pipe($pipes[1]));
60+
61+
?>
62+
--EXPECTF--
63+
bool(true)
64+
bool(true)
65+
STDOUT << hello
66+
STDOUT << world
67+
STDOUT << DONE
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
proc_open() with socket and pipe
3+
--FILE--
4+
<?php
5+
6+
function poll($pipe, $read = true)
7+
{
8+
$r = ($read == true) ? [$pipe] : null;
9+
$w = ($read == false) ? [$pipe] : null;
10+
$e = null;
11+
12+
if (!stream_select($r, $w, $e, null, 0)) {
13+
throw new \Error("Select failed");
14+
}
15+
}
16+
17+
function read_pipe($pipe): string
18+
{
19+
poll($pipe);
20+
21+
if (false === ($chunk = @fread($pipe, 8192))) {
22+
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
23+
}
24+
25+
return $chunk;
26+
}
27+
28+
$cmd = [
29+
getenv("TEST_PHP_EXECUTABLE"),
30+
__DIR__ . '/proc_open_sockets2.inc'
31+
];
32+
33+
$spec = [
34+
['pipe', 'r'],
35+
['socket']
36+
];
37+
38+
$proc = proc_open($cmd, $spec, $pipes);
39+
40+
var_dump(stream_set_blocking($pipes[1], false));
41+
42+
printf("STDOUT << %s\n", read_pipe($pipes[1]));
43+
printf("STDOUT << %s\n", read_pipe($pipes[1]));
44+
45+
fwrite($pipes[0], 'done');
46+
fclose($pipes[0]);
47+
48+
printf("STDOUT << %s\n", read_pipe($pipes[1]));
49+
50+
?>
51+
--EXPECTF--
52+
bool(true)
53+
STDOUT << hello
54+
STDOUT << world
55+
STDOUT << DONE

0 commit comments

Comments
 (0)