Closes GH-5777.
$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
# 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
}
}
- desc->is_pipe = 1;
+ desc->type = DESCRIPTOR_TYPE_PIPE;
desc->childend = dup(*slave_fd);
desc->parentend = dup(*master_fd);
desc->mode_flags = O_RDWR;
#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];
return FAILURE;
}
- desc->is_pipe = 1;
+ desc->type = DESCRIPTOR_TYPE_PIPE;
if (strncmp(ZSTR_VAL(zmode), "w", 1) != 0) {
desc->parentend = newpipe[1];
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)
{
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) {
* 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) {
/* 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:
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);
}
}
--- /dev/null
+<?php
+
+echo "hello";
+sleep(1);
+fwrite(STDERR, "SOME ERROR");
+sleep(1);
+echo "world";
--- /dev/null
+--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
--- /dev/null
+<?php
+
+echo "hello";
+sleep(1);
+echo "world";
+
+echo strtoupper(trim(fgets(STDIN)));
--- /dev/null
+--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
--- /dev/null
+--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
self->is_seekable = !(file_type == FILE_TYPE_PIPE || file_type == FILE_TYPE_CHAR);
self->is_pipe = file_type == FILE_TYPE_PIPE;
+
+ /* Additional check needed to distinguish between pipes and sockets. */
+ if (self->is_pipe && !GetNamedPipeInfo((HANDLE) handle, NULL, NULL, NULL, NULL)) {
+ self->is_pipe = 0;
+ }
}
#endif
}
#include "php.h"
-PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2])
+PHPAPI int socketpair_win32(int domain, int type, int protocol, SOCKET sock[2], int overlapped)
{
struct sockaddr_in address;
SOCKET redirect;
int size = sizeof(address);
- if(domain != AF_INET) {
+ if (domain != AF_INET) {
WSASetLastError(WSAENOPROTOOPT);
return -1;
}
- sock[0] = sock[1] = redirect = INVALID_SOCKET;
-
+ sock[1] = redirect = INVALID_SOCKET;
- sock[0] = socket(domain, type, protocol);
+ sock[0] = socket(domain, type, protocol);
if (INVALID_SOCKET == sock[0]) {
goto error;
}
address.sin_addr.s_addr = INADDR_ANY;
- address.sin_family = AF_INET;
- address.sin_port = 0;
+ address.sin_family = AF_INET;
+ address.sin_port = 0;
- if (bind(sock[0], (struct sockaddr*)&address, sizeof(address)) != 0) {
+ if (bind(sock[0], (struct sockaddr *) &address, sizeof(address)) != 0) {
goto error;
}
- if(getsockname(sock[0], (struct sockaddr *)&address, &size) != 0) {
+ if (getsockname(sock[0], (struct sockaddr *) &address, &size) != 0) {
goto error;
}
goto error;
}
- sock[1] = socket(domain, type, protocol);
+ if (overlapped) {
+ sock[1] = socket(domain, type, protocol);
+ } else {
+ sock[1] = WSASocket(domain, type, protocol, NULL, 0, 0);
+ }
+
if (INVALID_SOCKET == sock[1]) {
goto error;
}
address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
- if(connect(sock[1], (struct sockaddr*)&address, sizeof(address)) != 0) {
+ if (connect(sock[1], (struct sockaddr *) &address, sizeof(address)) != 0) {
goto error;
}
- redirect = accept(sock[0],(struct sockaddr*)&address, &size);
+ redirect = accept(sock[0], (struct sockaddr *) &address, &size);
if (INVALID_SOCKET == redirect) {
goto error;
}
WSASetLastError(WSAECONNABORTED);
return -1;
}
+
+PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2])
+{
+ return socketpair_win32(domain, type, protocol, sock, 1);
+}
#ifndef PHP_WIN32_SOCKETS_H
#define PHP_WIN32_SOCKETS_H
+PHPAPI int socketpair_win32(int domain, int type, int protocol, SOCKET sock[2], int overlapped);
PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2]);
#endif