diff options
-rw-r--r-- | UPGRADING | 8 | ||||
-rw-r--r-- | ext/standard/proc_open.c | 98 | ||||
-rw-r--r-- | ext/standard/tests/general_functions/proc_open_sockets1.inc | 7 | ||||
-rw-r--r-- | ext/standard/tests/general_functions/proc_open_sockets1.phpt | 56 | ||||
-rw-r--r-- | ext/standard/tests/general_functions/proc_open_sockets2.inc | 7 | ||||
-rw-r--r-- | ext/standard/tests/general_functions/proc_open_sockets2.phpt | 67 | ||||
-rw-r--r-- | ext/standard/tests/general_functions/proc_open_sockets3.phpt | 55 | ||||
-rw-r--r-- | main/streams/plain_wrapper.c | 5 | ||||
-rw-r--r-- | win32/sockets.c | 33 | ||||
-rw-r--r-- | win32/sockets.h | 1 |
10 files changed, 300 insertions, 37 deletions
@@ -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 diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index ac00dd318c..501d2c3f35 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -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 @@ -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; @@ -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]; @@ -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]; @@ -711,10 +731,9 @@ 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 @@ -722,6 +741,32 @@ static int set_proc_descriptor_to_pipe(descriptorspec_item *desc, zend_string *z 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) { @@ -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) { @@ -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) { @@ -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: @@ -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); } } diff --git a/ext/standard/tests/general_functions/proc_open_sockets1.inc b/ext/standard/tests/general_functions/proc_open_sockets1.inc new file mode 100644 index 0000000000..680f9cea5c --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_sockets1.inc @@ -0,0 +1,7 @@ +<?php + +echo "hello"; +sleep(1); +fwrite(STDERR, "SOME ERROR"); +sleep(1); +echo "world"; diff --git a/ext/standard/tests/general_functions/proc_open_sockets1.phpt b/ext/standard/tests/general_functions/proc_open_sockets1.phpt new file mode 100644 index 0000000000..baecc44827 --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_sockets1.phpt @@ -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 diff --git a/ext/standard/tests/general_functions/proc_open_sockets2.inc b/ext/standard/tests/general_functions/proc_open_sockets2.inc new file mode 100644 index 0000000000..e9a6c9e33d --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_sockets2.inc @@ -0,0 +1,7 @@ +<?php + +echo "hello"; +sleep(1); +echo "world"; + +echo strtoupper(trim(fgets(STDIN))); diff --git a/ext/standard/tests/general_functions/proc_open_sockets2.phpt b/ext/standard/tests/general_functions/proc_open_sockets2.phpt new file mode 100644 index 0000000000..25f3153ec4 --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_sockets2.phpt @@ -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 diff --git a/ext/standard/tests/general_functions/proc_open_sockets3.phpt b/ext/standard/tests/general_functions/proc_open_sockets3.phpt new file mode 100644 index 0000000000..5ee9e53b56 --- /dev/null +++ b/ext/standard/tests/general_functions/proc_open_sockets3.phpt @@ -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 diff --git a/main/streams/plain_wrapper.c b/main/streams/plain_wrapper.c index f33c65df68..b6db92ddad 100644 --- a/main/streams/plain_wrapper.c +++ b/main/streams/plain_wrapper.c @@ -257,6 +257,11 @@ static void detect_is_seekable(php_stdio_stream_data *self) { 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 } diff --git a/win32/sockets.c b/win32/sockets.c index c38d2bccbc..0a9a230df5 100644 --- a/win32/sockets.c +++ b/win32/sockets.c @@ -24,34 +24,33 @@ #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; } @@ -59,17 +58,22 @@ PHPAPI int socketpair(int domain, int type, int protocol, SOCKET sock[2]) 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; } @@ -86,3 +90,8 @@ 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); +} diff --git a/win32/sockets.h b/win32/sockets.h index f254133cc8..2b2b5c4712 100644 --- a/win32/sockets.h +++ b/win32/sockets.h @@ -22,6 +22,7 @@ #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 |