Skip to content

Commit

Permalink
Support socketpairs in proc_open()
Browse files Browse the repository at this point in the history
Closes GH-5777.
  • Loading branch information
kooldev authored and nikic committed Jul 14, 2020
1 parent 1a00d01 commit 547d98b
Show file tree
Hide file tree
Showing 10 changed files with 300 additions and 37 deletions.
8 changes: 8 additions & 0 deletions UPGRADING
Original file line number Diff line number Diff line change
Expand Up @@ -631,6 +631,14 @@ PHP 8.0 UPGRADE NOTES

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

. proc_open() now supports socket pair descriptors. The following attaches
a distinct socket pair to stdin, stdout and stderr:

$proc = proc_open(
$command, [['socket'], ['socket'], ['socket']], $pipes);

Unlike pipes, sockets do not suffer from blocking I/O issues on Windows.
However, not all programs may work correctly with stdio sockets.
. Sorting functions are now stable, which means that equal-comparing elements
will retain their original order.
RFC: https://wiki.php.net/rfc/stable_sorting
Expand Down
98 changes: 73 additions & 25 deletions ext/standard/proc_open.c
Original file line number Diff line number Diff line change
Expand Up @@ -444,11 +444,18 @@ static inline HANDLE dup_fd_as_handle(int fd)
# define close_descriptor(fd) close(fd)
#endif

/* Determines the type of a descriptor item. */
typedef enum _descriptor_type {
DESCRIPTOR_TYPE_STD,
DESCRIPTOR_TYPE_PIPE,
DESCRIPTOR_TYPE_SOCKET
} descriptor_type;

/* One instance of this struct is created for each item in `$descriptorspec` argument to `proc_open`
* They are used within `proc_open` and freed before it returns */
typedef struct _descriptorspec_item {
int index; /* desired FD # in child process */
int is_pipe;
descriptor_type type;
php_file_descriptor_t childend; /* FD # opened for use in child
* (will be copied to `index` in child) */
php_file_descriptor_t parentend; /* FD # opened for use in parent
Expand Down Expand Up @@ -679,7 +686,7 @@ static int set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd,
}
}

desc->is_pipe = 1;
desc->type = DESCRIPTOR_TYPE_PIPE;
desc->childend = dup(*slave_fd);
desc->parentend = dup(*master_fd);
desc->mode_flags = O_RDWR;
Expand All @@ -690,6 +697,19 @@ static int set_proc_descriptor_to_pty(descriptorspec_item *desc, int *master_fd,
#endif
}

/* Mark the descriptor close-on-exec, so it won't be inherited by children */
static php_file_descriptor_t make_descriptor_cloexec(php_file_descriptor_t fd)
{
#ifdef PHP_WIN32
return dup_handle(fd, FALSE, TRUE);
#else
#if defined(F_SETFD) && defined(FD_CLOEXEC)
fcntl(fd, F_SETFD, FD_CLOEXEC);
#endif
return fd;
#endif
}

static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *zmode)
{
php_file_descriptor_t newpipe[2];
Expand All @@ -699,7 +719,7 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z
return FAILURE;
}

desc->is_pipe = 1;
desc->type = DESCRIPTOR_TYPE_PIPE;

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

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

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

return SUCCESS;
}

#ifdef PHP_WIN32
#define create_socketpair(socks) socketpair_win32(AF_INET, SOCK_STREAM, 0, (socks), 0)
#else
#define create_socketpair(socks) socketpair(AF_UNIX, SOCK_STREAM, 0, (socks))
#endif

static int set_proc_descriptor_to_socket(descriptorspec_item *desc)
{
php_socket_t sock[2];

if (create_socketpair(sock)) {
zend_string *err = php_socket_error_str(php_socket_errno());
php_error_docref(NULL, E_WARNING, "Unable to create socket pair: %s", ZSTR_VAL(err));
zend_string_release(err);
return FAILURE;
}

desc->type = DESCRIPTOR_TYPE_SOCKET;
desc->parentend = make_descriptor_cloexec((php_file_descriptor_t) sock[0]);

/* Pass sock[1] to child because it will never use overlapped IO on Windows. */
desc->childend = (php_file_descriptor_t) sock[1];

return SUCCESS;
}

static int set_proc_descriptor_to_file(descriptorspec_item *desc, zend_string *file_path,
zend_string *file_mode)
{
Expand Down Expand Up @@ -827,6 +872,9 @@ static int set_proc_descriptor_from_array(zval *descitem, descriptorspec_item *d
goto finish;
}
retval = set_proc_descriptor_to_pipe(&descriptors[ndesc], zmode);
} else if (zend_string_equals_literal(ztype, "socket")) {
/* Set descriptor to socketpair */
retval = set_proc_descriptor_to_socket(&descriptors[ndesc]);
} else if (zend_string_equals_literal(ztype, "file")) {
/* Set descriptor to file */
if ((zfile = get_string_parameter(descitem, 1, "file name parameter for 'file'")) == NULL) {
Expand Down Expand Up @@ -903,7 +951,7 @@ static int close_parentends_of_pipes(descriptorspec_item *descriptors, int ndesc
* Also, dup() the child end of all pipes as necessary so they will use the FD
* number which the user requested */
for (int i = 0; i < ndesc; i++) {
if (descriptors[i].is_pipe) {
if (descriptors[i].type != DESCRIPTOR_TYPE_STD) {
close(descriptors[i].parentend);
}
if (descriptors[i].childend != descriptors[i].index) {
Expand Down Expand Up @@ -1194,12 +1242,13 @@ PHP_FUNCTION(proc_open)
/* Clean up all the child ends and then open streams on the parent
* ends, where appropriate */
for (i = 0; i < ndesc; i++) {
char *mode_string = NULL;
php_stream *stream = NULL;

close_descriptor(descriptors[i].childend);

if (descriptors[i].is_pipe) {
if (descriptors[i].type == DESCRIPTOR_TYPE_PIPE) {
char *mode_string = NULL;

switch (descriptors[i].mode_flags) {
#ifdef PHP_WIN32
case O_WRONLY|O_BINARY:
Expand All @@ -1219,32 +1268,31 @@ PHP_FUNCTION(proc_open)
mode_string = "r+";
break;
}

#ifdef PHP_WIN32
stream = php_stream_fopen_from_fd(_open_osfhandle((zend_intptr_t)descriptors[i].parentend,
descriptors[i].mode_flags), mode_string, NULL);
php_stream_set_option(stream, PHP_STREAM_OPTION_PIPE_BLOCKING, blocking_pipes, NULL);
#else
stream = php_stream_fopen_from_fd(descriptors[i].parentend, mode_string, NULL);
# if defined(F_SETFD) && defined(FD_CLOEXEC)
/* Mark the descriptor close-on-exec, so it won't be inherited by
* potential other children */
fcntl(descriptors[i].parentend, F_SETFD, FD_CLOEXEC);
# endif
#endif
if (stream) {
zval retfp;
} else if (descriptors[i].type == DESCRIPTOR_TYPE_SOCKET) {
stream = php_stream_sock_open_from_socket((php_socket_t) descriptors[i].parentend, NULL);
} else {
proc->pipes[i] = NULL;
}

/* nasty hack; don't copy it */
stream->flags |= PHP_STREAM_FLAG_NO_SEEK;
if (stream) {
zval retfp;

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

proc->pipes[i] = Z_RES(retfp);
Z_ADDREF(retfp);
}
} else {
proc->pipes[i] = NULL;
php_stream_to_zval(stream, &retfp);
add_index_zval(pipes, descriptors[i].index, &retfp);

proc->pipes[i] = Z_RES(retfp);
Z_ADDREF(retfp);
}
}

Expand Down
7 changes: 7 additions & 0 deletions ext/standard/tests/general_functions/proc_open_sockets1.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

echo "hello";
sleep(1);
fwrite(STDERR, "SOME ERROR");
sleep(1);
echo "world";
56 changes: 56 additions & 0 deletions ext/standard/tests/general_functions/proc_open_sockets1.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
--TEST--
proc_open() with output socketpairs
--FILE--
<?php

$cmd = [
getenv("TEST_PHP_EXECUTABLE"),
__DIR__ . '/proc_open_sockets1.inc'
];

$spec = [
['null'],
['socket'],
['socket']
];

$proc = proc_open($cmd, $spec, $pipes);

foreach ($pipes as $pipe) {
var_dump(stream_set_blocking($pipe, false));
}

while ($pipes) {
$r = $pipes;
$w = null;
$e = null;

if (!stream_select($r, $w, $e, null, 0)) {
throw new Error("Select failed");
}

foreach ($r as $i => $pipe) {
if (!is_resource($pipe) || feof($pipe)) {
unset($pipes[$i]);
continue;
}

$chunk = @fread($pipe, 8192);

if ($chunk === false) {
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
}

if ($chunk !== '') {
echo "PIPE {$i} << {$chunk}\n";
}
}
}

?>
--EXPECTF--
bool(true)
bool(true)
PIPE 1 << hello
PIPE 2 << SOME ERROR
PIPE 1 << world
7 changes: 7 additions & 0 deletions ext/standard/tests/general_functions/proc_open_sockets2.inc
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

echo "hello";
sleep(1);
echo "world";

echo strtoupper(trim(fgets(STDIN)));
67 changes: 67 additions & 0 deletions ext/standard/tests/general_functions/proc_open_sockets2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
--TEST--
proc_open() with IO socketpairs
--FILE--
<?php

function poll($pipe, $read = true)
{
$r = ($read == true) ? [$pipe] : null;
$w = ($read == false) ? [$pipe] : null;
$e = null;

if (!stream_select($r, $w, $e, null, 0)) {
throw new \Error("Select failed");
}
}

function read_pipe($pipe): string
{
poll($pipe);

if (false === ($chunk = @fread($pipe, 8192))) {
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
}

return $chunk;
}

function write_pipe($pipe, $data)
{
poll($pipe, false);

if (false == @fwrite($pipe, $data)) {
throw new Error("Failed to write: " . (error_get_last()['message'] ?? 'N/A'));
}
}

$cmd = [
getenv("TEST_PHP_EXECUTABLE"),
__DIR__ . '/proc_open_sockets2.inc'
];

$spec = [
['socket'],
['socket']
];

$proc = proc_open($cmd, $spec, $pipes);

foreach ($pipes as $pipe) {
var_dump(stream_set_blocking($pipe, false));
}

printf("STDOUT << %s\n", read_pipe($pipes[1]));
printf("STDOUT << %s\n", read_pipe($pipes[1]));

write_pipe($pipes[0], 'done');
fclose($pipes[0]);

printf("STDOUT << %s\n", read_pipe($pipes[1]));

?>
--EXPECTF--
bool(true)
bool(true)
STDOUT << hello
STDOUT << world
STDOUT << DONE
55 changes: 55 additions & 0 deletions ext/standard/tests/general_functions/proc_open_sockets3.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
--TEST--
proc_open() with socket and pipe
--FILE--
<?php

function poll($pipe, $read = true)
{
$r = ($read == true) ? [$pipe] : null;
$w = ($read == false) ? [$pipe] : null;
$e = null;

if (!stream_select($r, $w, $e, null, 0)) {
throw new \Error("Select failed");
}
}

function read_pipe($pipe): string
{
poll($pipe);

if (false === ($chunk = @fread($pipe, 8192))) {
throw new Error("Failed to read: " . (error_get_last()['message'] ?? 'N/A'));
}

return $chunk;
}

$cmd = [
getenv("TEST_PHP_EXECUTABLE"),
__DIR__ . '/proc_open_sockets2.inc'
];

$spec = [
['pipe', 'r'],
['socket']
];

$proc = proc_open($cmd, $spec, $pipes);

var_dump(stream_set_blocking($pipes[1], false));

printf("STDOUT << %s\n", read_pipe($pipes[1]));
printf("STDOUT << %s\n", read_pipe($pipes[1]));

fwrite($pipes[0], 'done');
fclose($pipes[0]);

printf("STDOUT << %s\n", read_pipe($pipes[1]));

?>
--EXPECTF--
bool(true)
STDOUT << hello
STDOUT << world
STDOUT << DONE

0 comments on commit 547d98b

Please sign in to comment.