diff --git a/.github/actions/apt-x64/action.yml b/.github/actions/apt-x64/action.yml index 3e654ef9fdc4..30280581a7f4 100644 --- a/.github/actions/apt-x64/action.yml +++ b/.github/actions/apt-x64/action.yml @@ -36,6 +36,8 @@ runs: language-pack-de \ libgmp-dev \ libicu-dev \ + liburing-dev \ + cmake \ libtidy-dev \ libenchant-2-dev \ libbz2-dev \ diff --git a/.github/actions/build-ior-windows/action.yml b/.github/actions/build-ior-windows/action.yml new file mode 100644 index 000000000000..f600d84d1e59 --- /dev/null +++ b/.github/actions/build-ior-windows/action.yml @@ -0,0 +1,64 @@ +name: Build ior (Windows) +# Temporary, for the IO hooks proof of concept only, like build-ior: builds a +# pinned commit of ior into a cached prefix laid out as win32/build/config.w32 +# expects (include\ior\ior.h, lib\ior.lib) and exports it as IOR_PREFIX, which +# build_task.bat passes as --with-ior. The CI builds of PHP are release builds +# (/MD), so ior is built Release against the same DLL runtime. +inputs: + ref: + description: The ior commit to build, a full SHA + default: 2bfd23198962f5d6a0de8e3aa959998c644d4294 + required: false + arch: + description: x64 or x86 + required: true +runs: + using: composite + steps: + - name: ior cache key + id: key + shell: pwsh + env: + IOR_REF: ${{ inputs.ref }} + IOR_ARCH: ${{ inputs.arch }} + run: | + "key=ior-$env:RUNNER_OS-$env:ImageOS-$env:PHP_BUILD_CRT-$env:IOR_ARCH-$env:IOR_REF" >> $env:GITHUB_OUTPUT + - name: Restore ior + id: cache + uses: actions/cache@v5 + with: + path: C:\ior + key: ${{ steps.key.outputs.key }} + - name: Build ior + if: steps.cache.outputs.cache-hit != 'true' + shell: pwsh + env: + IOR_REF: ${{ inputs.ref }} + IOR_ARCH: ${{ inputs.arch }} + run: | + $ErrorActionPreference = 'Stop' + $PSNativeCommandUseErrorActionPreference = $true + Set-PSDebug -Trace 1 + $prefix = 'C:\ior' + $src = Join-Path $env:RUNNER_TEMP 'ior-src' + foreach ($dir in @($src, $prefix)) { + if (Test-Path $dir) { Remove-Item -Recurse -Force $dir } + } + git init -q $src + git -C $src fetch --depth 1 https://github.com/libior/ior.git $env:IOR_REF + git -C $src checkout -q FETCH_HEAD + $platform = if ($env:IOR_ARCH -eq 'x86') { 'Win32' } else { 'x64' } + # Static library, IOCP backend; tests and bench would need cmocka + cmake -S $src -B "$src\build" -A $platform ` + -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL ` + -DIOR_BUILD_TESTS=OFF ` + -DIOR_BUILD_BENCH=OFF + cmake --build "$src\build" --config Release --parallel + cmake --install "$src\build" --config Release --prefix $prefix + - name: Export the ior prefix + shell: pwsh + run: | + if (-not (Test-Path 'C:\ior\include\ior\ior.h') -or -not (Test-Path 'C:\ior\lib\ior.lib')) { + throw 'ior install prefix is incomplete' + } + "IOR_PREFIX=C:\ior" >> $env:GITHUB_ENV diff --git a/.github/actions/build-ior/action.yml b/.github/actions/build-ior/action.yml new file mode 100644 index 000000000000..dd536693d92f --- /dev/null +++ b/.github/actions/build-ior/action.yml @@ -0,0 +1,54 @@ +name: Build ior +# Temporary, for the IO hooks proof of concept only: the ring (main/io/php_io_ring.c) +# is built on the ior library, which will be bundled with php-src once the +# design settles. Until then CI builds a pinned commit of it into a cached +# prefix that ./configure finds through pkg-config (--with-ior). +inputs: + ref: + description: The ior commit to build, a full SHA + default: 2bfd23198962f5d6a0de8e3aa959998c644d4294 + required: false +runs: + using: composite + steps: + - name: ior cache key + id: key + shell: bash + env: + IOR_REF: ${{ inputs.ref }} + run: | + echo "key=ior-${RUNNER_OS}-${RUNNER_ARCH}-${ImageOS:-unknown}-${IOR_REF}" >> "$GITHUB_OUTPUT" + - name: Restore ior + id: cache + uses: actions/cache@v5 + with: + path: ~/ior + key: ${{ steps.key.outputs.key }} + - name: Build ior + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + env: + IOR_REF: ${{ inputs.ref }} + run: | + set -x + IOR_PREFIX="$HOME/ior" + IOR_SRC="$RUNNER_TEMP/ior-src" + rm -rf "$IOR_SRC" "$IOR_PREFIX" + git init -q "$IOR_SRC" + git -C "$IOR_SRC" fetch --depth 1 https://github.com/libior/ior.git "$IOR_REF" + git -C "$IOR_SRC" checkout -q FETCH_HEAD + # Both Linux backends in one library, so IOR_BACKEND=threads can + # select the thread pool at run time; a no-op elsewhere + cmake -S "$IOR_SRC" -B "$IOR_SRC/build" \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_INSTALL_PREFIX="$IOR_PREFIX" \ + -DIOR_WITH_THREADS=ON \ + -DIOR_BUILD_TESTS=OFF \ + -DIOR_BUILD_BENCH=OFF + cmake --build "$IOR_SRC/build" --parallel + cmake --install "$IOR_SRC/build" + - name: Point pkg-config at ior + shell: bash + run: | + IOR_PREFIX="$HOME/ior" + echo "PKG_CONFIG_PATH=$IOR_PREFIX/lib/pkgconfig:$IOR_PREFIX/lib64/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" >> "$GITHUB_ENV" diff --git a/.github/actions/configure-macos/action.yml b/.github/actions/configure-macos/action.yml index b9739a790530..06a9bf003df1 100644 --- a/.github/actions/configure-macos/action.yml +++ b/.github/actions/configure-macos/action.yml @@ -6,6 +6,9 @@ inputs: runs: using: composite steps: + # Temporary for the IO hooks proof of concept, see the action + - name: Build ior + uses: ./.github/actions/build-ior - shell: bash run: | set -x @@ -53,6 +56,7 @@ runs: --enable-sockets \ --with-bz2="$BREW_OPT"/bzip2 \ --with-openssl \ + --with-ior \ --with-gmp="$BREW_OPT"/gmp \ --with-iconv="$BREW_OPT"/libiconv \ --enable-bcmath \ diff --git a/.github/actions/configure-x64/action.yml b/.github/actions/configure-x64/action.yml index ff746791fb95..c666dffcfc20 100644 --- a/.github/actions/configure-x64/action.yml +++ b/.github/actions/configure-x64/action.yml @@ -12,6 +12,9 @@ inputs: runs: using: composite steps: + # Temporary for the IO hooks proof of concept, see the action + - name: Build ior + uses: ./.github/actions/build-ior - shell: bash run: | set -x @@ -78,6 +81,7 @@ runs: ${{ inputs.skipSlow == 'false' && '--with-pdo-odbc=unixODBC,/usr' || '' }} \ --with-config-file-path=/etc \ --with-config-file-scan-dir=/etc/php.d \ + --with-ior \ ${{ inputs.skipSlow == 'false' && '--with-pdo-firebird' || '' }} \ ${{ inputs.skipSlow == 'false' && '--with-pdo-dblib' || '' }} \ --enable-werror \ diff --git a/.github/matrix.php b/.github/matrix.php index 87486791cff5..9b35c32c2363 100644 --- a/.github/matrix.php +++ b/.github/matrix.php @@ -141,7 +141,7 @@ function select_jobs($repository, $trigger, $nightly, $labels, $php_version, $re $jobs['SOLARIS'] = true; } if ($all_jobs || !$no_jobs || $test_windows) { - $matrix = [['asan' => false, 'opcache' => true, 'x64' => true, 'zts' => true]]; + $matrix = [['asan' => false, 'opcache' => true, 'x64' => true, 'zts' => true, 'ior' => true]]; if ($all_variations) { $matrix[] = ['asan' => true, 'opcache' => true, 'x64' => true, 'zts' => true]; $matrix[] = ['asan' => false, 'opcache' => false, 'x64' => false, 'zts' => false]; diff --git a/.github/scripts/windows/build_task.bat b/.github/scripts/windows/build_task.bat index 1177cef3be4d..34db0f592b73 100644 --- a/.github/scripts/windows/build_task.bat +++ b/.github/scripts/windows/build_task.bat @@ -27,6 +27,7 @@ if "%THREAD_SAFE%" equ "0" set ADD_CONF=%ADD_CONF% --disable-zts if "%INTRINSICS%" neq "" set ADD_CONF=%ADD_CONF% --enable-native-intrinsics=%INTRINSICS% if "%ASAN%" equ "1" set ADD_CONF=%ADD_CONF% --enable-sanitizer --enable-debug-pack if "%CLANG_TOOLSET%" equ "1" set ADD_CONF=%ADD_CONF% --with-toolset=clang +if "%IOR_PREFIX%" neq "" set ADD_CONF=%ADD_CONF% --with-ior=%IOR_PREFIX% rem C4018: comparison: signed/unsigned mismatch rem C4146: unary minus operator applied to unsigned type diff --git a/.github/scripts/windows/test_task.bat b/.github/scripts/windows/test_task.bat index 77b0059fccf4..b380944b1aaa 100644 --- a/.github/scripts/windows/test_task.bat +++ b/.github/scripts/windows/test_task.bat @@ -154,6 +154,18 @@ nmake test TESTS="%OPCACHE_OPTS% -g FAIL,BORK,LEAK,XLEAK %ASAN_OPTS% --no-progre set EXIT_CODE=%errorlevel% +rem The IO hooks tests again, on the ring built on ior +if "%IOR_PREFIX%" neq "" ( + set IO_HOOKS_QUEUE=ring + nmake test TESTS="-g FAIL,BORK,LEAK,XLEAK --no-progress -q --offline --show-diff --set-timeout 120 --temp-source c:\tests_tmp --temp-target c:\tests_tmp %PARALLEL% ext\standard\tests\streams\hooks ext\standard\tests\poll ext\openssl\tests\stream_io_hooks.phpt ext\curl\tests" + if errorlevel 1 set EXIT_CODE=3 + set IO_HOOKS_CAPS=files,directData,directAccept + nmake test TESTS="-g FAIL,BORK,LEAK,XLEAK --no-progress -q --offline --show-diff --set-timeout 120 --temp-source c:\tests_tmp --temp-target c:\tests_tmp %PARALLEL% ext\standard\tests\streams\hooks ext\openssl\tests\stream_io_hooks.phpt ext\curl\tests" + if errorlevel 1 set EXIT_CODE=3 + set IO_HOOKS_CAPS= + set IO_HOOKS_QUEUE= +) + nmake unregister_comtest taskkill /f /im snmpd.exe diff --git a/.github/workflows/test-suite.yml b/.github/workflows/test-suite.yml index c9f61e6adfd5..a88e47e13453 100644 --- a/.github/workflows/test-suite.yml +++ b/.github/workflows/test-suite.yml @@ -871,6 +871,11 @@ jobs: ref: ${{ fromJson(inputs.branch).ref }} - name: Setup uses: ./.github/actions/setup-windows + - name: Build ior + if: ${{ matrix.ior }} + uses: ./.github/actions/build-ior-windows + with: + arch: ${{ matrix.x64 && 'x64' || 'x86' }} - name: Build run: .github/scripts/windows/build.bat - name: Test diff --git a/Zend/zend_hrtime.h b/Zend/zend_hrtime.h index 7cc0c0394cfb..ca514495b9d8 100644 --- a/Zend/zend_hrtime.h +++ b/Zend/zend_hrtime.h @@ -72,6 +72,7 @@ ZEND_API extern mach_timebase_info_data_t zend_hrtime_timerlib_info; #endif +#define ZEND_HRTIME_T_MAX UINT64_MAX #define ZEND_NANO_IN_SEC UINT64_C(1000000000) typedef uint64_t zend_hrtime_t; diff --git a/configure.ac b/configure.ac index f48f1e8de90e..92929845fccb 100644 --- a/configure.ac +++ b/configure.ac @@ -586,6 +586,7 @@ AC_CHECK_FUNCS([ setitimer shutdown sigprocmask + sigtimedwait splice statfs statvfs @@ -620,6 +621,8 @@ AC_FUNC_STRERROR_R dnl Check for functions inside their belonging headers. AC_CHECK_HEADER([sys/prctl.h], [AC_CHECK_FUNCS([prctl])]) +AC_CHECK_HEADERS([sys/signalfd.h sys/pidfd.h]) +AC_CHECK_FUNCS([pidfd_open signalfd pipe2]) AC_CHECK_HEADER([sys/procctl.h], [AC_CHECK_FUNCS([procctl])]) AX_FUNC_WHICH_GETHOSTBYNAME_R @@ -751,6 +754,30 @@ AS_VAR_IF([php_cv_have__asm_goto], [yes], [AC_DEFINE([HAVE_ASM_GOTO], [1], [Define to 1 if asm goto support is available.])]) +dnl Check for the ior library the IO ring is built on. +PHP_ARG_WITH([ior], + [for ior support], + [AS_HELP_STRING([[--with-ior[=DIR]]], + [Build the IO ring on the ior library, found through pkg-config. The + optional DIR is the ior install prefix to take its pkg-config file from])], + [no], + [no]) +AS_VAR_IF([PHP_IOR], [no],, [ + php_ior_pkg_config_path=$PKG_CONFIG_PATH + AS_VAR_IF([PHP_IOR], [yes],, [ + PKG_CONFIG_PATH="$PHP_IOR/lib/pkgconfig:$PHP_IOR/lib64/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" + export PKG_CONFIG_PATH + ]) + PKG_CHECK_MODULES([IOR], [ior]) + IOR_LIBS=$($PKG_CONFIG --static --libs ior) + PKG_CONFIG_PATH=$php_ior_pkg_config_path + PHP_EVAL_INCLINE([$IOR_CFLAGS]) + PHP_EVAL_LIBLINE([$IOR_LIBS]) + AC_DEFINE([HAVE_IOR], [1], [Define to 1 if the ior library is available.]) + PHP_ADD_SOURCES([main/io], [php_io_ring.c], + [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) +]) + dnl Check Valgrind support. PHP_ARG_WITH([valgrind], [whether to enable Valgrind support], @@ -1694,6 +1721,8 @@ PHP_ADD_SOURCES([main/io], m4_normalize([ php_io_copy_freebsd.c php_io_copy_solaris.c php_io_copy_macos.c + php_io_hooks.c + php_io_queue_poll.c ]), [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) @@ -1705,6 +1734,7 @@ PHP_ADD_SOURCES([main/poll], m4_normalize([ poll_core.c poll_fd_table.c poll_handle.c + poll_source.c ]), [-DZEND_ENABLE_STATIC_TSRMLS_CACHE=1]) diff --git a/ext/curl/curl_private.h b/ext/curl/curl_private.h index 25cc37b6e458..7c08906d8dec 100644 --- a/ext/curl/curl_private.h +++ b/ext/curl/curl_private.h @@ -26,6 +26,8 @@ #include #include +#include "main/php_deadline.h" + #define CURLOPT_RETURNTRANSFER 19913 #define CURLOPT_BINARYTRANSFER 19914 /* For Backward compatibility */ #define PHP_CURL_STDOUT 0 @@ -109,12 +111,20 @@ typedef struct { struct _php_curl_send_headers header; struct _php_curl_error err; bool in_callback; + bool in_exec; uint32_t* clone; zval postfields; /* For CURLOPT_PRIVATE */ zval private_data; /* CurlShareHandle object set using CURLOPT_SHARE. */ struct _php_curlsh *share; + /* Sockets libcurl wants watched, keyed by curl_socket_t */ + HashTable *io_sockets; + struct _php_curl_socket_entry *io_removed; + php_deadline io_timer; + /* Private multi handle driving curl_exec() */ + CURLM *multi; + long maxconnects; zend_object std; } php_curl; @@ -148,6 +158,7 @@ void _php_curl_cleanup_handle(php_curl *); void _php_curl_multi_cleanup_list(void *data); void _php_curl_verify_handlers(php_curl *ch, bool reporterror); void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source); +bool php_curl_check_not_in_exec(php_curl *ch); /* Consumes `zv` */ zend_long php_curl_get_long(zval *zv); diff --git a/ext/curl/curl_socket_handle.stub.php b/ext/curl/curl_socket_handle.stub.php new file mode 100644 index 000000000000..351556529ab4 --- /dev/null +++ b/ext/curl/curl_socket_handle.stub.php @@ -0,0 +1,18 @@ + #include "zend_smart_str.h" +#include "zend_enum.h" #include "ext/standard/info.h" #include "ext/standard/file.h" #include "ext/standard/url.h" +#include "ext/standard/io_poll.h" +#include "ext/standard/io_poll_decl.h" +#include "main/php_poll.h" +#include "php_io.h" #include "curl_private.h" +#include "curl_socket_handle_arginfo.h" #ifdef __GNUC__ /* don't complain about deprecated CURLOPT_* we're exposing to PHP; we @@ -194,8 +200,14 @@ void _php_curl_verify_handlers(php_curl *ch, bool reporterror) /* {{{ */ /* }}} */ /* {{{ curl_module_entry */ +static const zend_module_dep curl_deps[] = { + ZEND_MOD_REQUIRED("standard") + ZEND_MOD_END +}; + zend_module_entry curl_module_entry = { - STANDARD_MODULE_HEADER, + STANDARD_MODULE_HEADER_EX, NULL, + curl_deps, "curl", ext_functions, PHP_MINIT(curl), @@ -233,6 +245,9 @@ zend_class_entry *curl_ce; zend_class_entry *curl_share_ce; zend_class_entry *curl_share_persistent_ce; static zend_object_handlers curl_object_handlers; +static zend_class_entry *php_curl_socket_weak_handle_ce; +static zend_object_handlers php_curl_socket_handle_object_handlers; +static zend_object *php_curl_socket_handle_create_object(zend_class_entry *ce); static zend_object *curl_create_object(zend_class_entry *class_type); static void curl_free_obj(zend_object *object); @@ -399,6 +414,16 @@ PHP_MINIT_FUNCTION(curl) curlfile_register_class(); + php_curl_socket_weak_handle_ce = register_class_CurlSocketPollWeakHandle( + php_io_poll_weak_handle_class_entry); + php_curl_socket_weak_handle_ce->create_object = php_curl_socket_handle_create_object; + memcpy(&php_curl_socket_handle_object_handlers, &std_object_handlers, + sizeof(zend_object_handlers)); + php_curl_socket_handle_object_handlers.offset = offsetof(php_poll_handle_object, std); + php_curl_socket_handle_object_handlers.free_obj = php_poll_handle_object_free; + php_curl_socket_handle_object_handlers.clone_obj = NULL; + php_curl_socket_weak_handle_ce->default_object_handlers = &php_curl_socket_handle_object_handlers; + return SUCCESS; } /* }}} */ @@ -1161,6 +1186,11 @@ void init_curl_handle(php_curl *ch) zend_hash_init(&ch->to_free->slist, 4, NULL, curl_free_slist, 0); ZVAL_UNDEF(&ch->postfields); + ch->io_sockets = NULL; + ch->io_removed = NULL; + php_deadline_init_infinite(&ch->io_timer); + ch->multi = NULL; + ch->in_exec = false; } /* }}} */ @@ -1212,6 +1242,8 @@ static void _php_curl_set_default_options(php_curl *ch) curl_easy_setopt(ch->cp, CURLOPT_WRITEHEADER, (void *) ch); curl_easy_setopt(ch->cp, CURLOPT_DNS_CACHE_TIMEOUT, 120L); curl_easy_setopt(ch->cp, CURLOPT_MAXREDIRS, 20L); /* prevent infinite redirects */ + /* libcurl's DEFAULT_CONNCACHE_SIZE */ + ch->maxconnects = 5; const char *cainfo = zend_ini_string_literal("openssl.cafile"); if (!(cainfo && cainfo[0] != '\0')) { @@ -1274,6 +1306,8 @@ static void php_curl_copy_fcc_with_option(php_curl *ch, CURLoption option, zend_ void _php_setup_easy_copy_handlers(php_curl *ch, php_curl *source) { + ch->maxconnects = source->maxconnects; + if (!Z_ISUNDEF(source->handlers.write->stream)) { Z_ADDREF(source->handlers.write->stream); } @@ -1561,9 +1595,7 @@ static inline zend_result build_mime_structure_from_hash(php_curl *ch, zval *zpo } if (Z_TYPE_P(current) == IS_ARRAY) { - zval *current_element; - - ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(current), current_element) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(current), zval *current_element) { add_simple_field(mime, string_key, current_element); } ZEND_HASH_FOREACH_END(); @@ -1607,6 +1639,10 @@ PHP_FUNCTION(curl_copy_handle) Z_PARAM_OBJECT_OF_CLASS(zid, curl_ce) ZEND_PARSE_PARAMETERS_END(); + if (!php_curl_check_not_in_exec(Z_CURL_P(zid))) { + RETURN_THROWS(); + } + zend_object *new_object = Z_OBJ_P(zid)->handlers->clone_obj(Z_OBJ_P(zid)); if (EG(exception)) { if (new_object != NULL) { @@ -1859,6 +1895,9 @@ static zend_result _php_curl_setopt(php_curl *ch, zend_long option, zval *zvalue return FAILURE; } error = curl_easy_setopt(ch->cp, option, lval); + if (option == CURLOPT_MAXCONNECTS && error == CURLE_OK) { + ch->maxconnects = (long) lval; + } break; case CURLOPT_SAFE_UPLOAD: if (!zend_is_true(zvalue)) { @@ -2353,6 +2392,10 @@ PHP_FUNCTION(curl_setopt) ch = Z_CURL_P(zid); + if (!php_curl_check_not_in_exec(ch)) { + RETURN_THROWS(); + } + RETURN_BOOL(_php_curl_setopt(ch, options, zvalue, 0) == SUCCESS); } /* }}} */ @@ -2372,6 +2415,10 @@ PHP_FUNCTION(curl_setopt_array) ch = Z_CURL_P(zid); + if (!php_curl_check_not_in_exec(ch)) { + RETURN_THROWS(); + } + ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(arr), option, string_key, entry) { if (UNEXPECTED(string_key)) { zend_argument_value_error(2, "contains an invalid cURL option"); @@ -2388,6 +2435,366 @@ PHP_FUNCTION(curl_setopt_array) } /* }}} */ +/* CurlSocketPollWeakHandle: identity of one socket in the multi handle's + * connection pool, from the first report to CURL_POLL_REMOVE. Nothing is + * owned: the handle only answers the descriptor while libcurl is interested. */ + +typedef struct php_curl_socket_handle_data { + curl_socket_t socket; /* CURL_SOCKET_BAD once removed */ +} php_curl_socket_handle_data; + +static php_socket_t php_curl_socket_handle_get_fd(php_poll_handle_object *handle) +{ + php_curl_socket_handle_data *data = handle->handle_data; + return data ? (php_socket_t)data->socket : (php_socket_t)CURL_SOCKET_BAD; +} + +static int php_curl_socket_handle_is_valid(php_poll_handle_object *handle) +{ + php_curl_socket_handle_data *data = handle->handle_data; + return data && data->socket != CURL_SOCKET_BAD; +} + +static void php_curl_socket_handle_cleanup(php_poll_handle_object *handle) +{ + if (handle->handle_data) { + efree(handle->handle_data); + handle->handle_data = NULL; + } +} + +static php_poll_handle_ops php_curl_socket_handle_ops = { + .get_fd = php_curl_socket_handle_get_fd, + .is_valid = php_curl_socket_handle_is_valid, + .cleanup = php_curl_socket_handle_cleanup, +}; + +static zend_object *php_curl_socket_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_curl_socket_handle_ops); + intern->std.handlers = &php_curl_socket_handle_object_handlers; + return &intern->std; +} + +ZEND_METHOD(CurlSocketPollWeakHandle, __construct) +{ + zend_throw_error(NULL, "Cannot directly construct %pS", Z_OBJCE_P(ZEND_THIS)->name); +} + +ZEND_METHOD(CurlSocketPollWeakHandle, isValid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + RETURN_BOOL(intern->ops->is_valid(intern)); +} + +/* One socket libcurl wants watched, attached to it with curl_multi_assign() */ +typedef struct _php_curl_socket_entry { + curl_socket_t socket; + int what; + zend_object *handle; /* CurlSocketPollWeakHandle, referenced; its registrations are its directions */ + struct _php_curl_socket_entry *next_removed; +} php_curl_socket_entry; + +static php_io_registration *php_curl_socket_registration(php_curl_socket_entry *e, uint32_t event) +{ + php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(e->handle); + for (php_io_registration *reg = h->registrations; reg; reg = reg->next) { + if (reg->event == event) { + return reg; + } + } + return NULL; +} + +static void php_curl_socket_entry_invalidate(php_curl_socket_entry *e) +{ + php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(e->handle); + ((php_curl_socket_handle_data *)h->handle_data)->socket = CURL_SOCKET_BAD; + php_poll_handle_invalidate(e->handle); +} + +static void php_curl_socket_entry_free(php_curl_socket_entry *e) +{ + /* Ending the registrations calls the provider's remove hook, which may throw */ + php_io_unregister_all(&PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(e->handle)->registrations); + OBJ_RELEASE(e->handle); + efree(e); +} + +/* CURLMOPT_SOCKETFUNCTION: bookkeeping only, never a provider call. On + * CURL_POLL_REMOVE the handle is invalidated here, inside the callback, + * because libcurl closes the socket right after. */ +static int php_curl_socket_callback(CURL *easy, curl_socket_t s, int what, void *userp, void *socketp) +{ + php_curl *ch = (php_curl *)userp; + php_curl_socket_entry *e = (php_curl_socket_entry *)socketp; + + if (what == CURL_POLL_REMOVE) { + if (e) { + php_curl_socket_entry_invalidate(e); + zend_hash_index_del(ch->io_sockets, (zend_ulong)s); + e->next_removed = ch->io_removed; + ch->io_removed = e; + } + return 0; + } + + if (!e) { + e = ecalloc(1, sizeof(*e)); + e->socket = s; + + zval handle_zv; + object_init_ex(&handle_zv, php_curl_socket_weak_handle_ce); + php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZV(&handle_zv); + php_curl_socket_handle_data *data = emalloc(sizeof(*data)); + data->socket = s; + h->handle_data = data; + e->handle = Z_OBJ(handle_zv); + + if (!ch->io_sockets) { + ch->io_sockets = emalloc(sizeof(HashTable)); + zend_hash_init(ch->io_sockets, 4, NULL, NULL, 0); + } + zend_hash_index_update_ptr(ch->io_sockets, (zend_ulong)s, e); + curl_multi_assign(ch->multi, s, e); + } + e->what = what; + return 0; +} + +/* CURLMOPT_TIMERFUNCTION: the timeout is relative to this call */ +static int php_curl_timer_callback(CURLM *multi, long timeout_ms, void *userp) +{ + php_curl *ch = (php_curl *)userp; + ch->io_timer = php_io_deadline_from_ms(timeout_ms); + return 0; +} + +static uint32_t php_curl_what_to_events(int what) +{ + return ((what & CURL_POLL_IN) ? PHP_POLL_READ : 0) | ((what & CURL_POLL_OUT) ? PHP_POLL_WRITE : 0); +} + +/* The reconcile step: after libcurl returned, turn the table into provider + * calls. Entries libcurl removed end their registrations, a direction newly + * wanted is registered as Level and one dropped is unregistered. Runs outside + * libcurl so a provider's add() or remove() may throw. */ +static zend_result php_curl_socket_reconcile(php_curl *ch) +{ + /* An exception a curl callback left pending does not stop the transfer */ + zend_object *pending = EG(exception); + + while (ch->io_removed) { + php_curl_socket_entry *e = ch->io_removed; + ch->io_removed = e->next_removed; + php_curl_socket_entry_free(e); + if (EG(exception) != pending) { + return FAILURE; + } + } + + if (ch->io_sockets) { + ZEND_HASH_FOREACH_PTR(ch->io_sockets, php_curl_socket_entry *e) { + uint32_t events = php_curl_what_to_events(e->what); + php_poll_handle_object *h = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(e->handle); + for (uint32_t event = PHP_POLL_READ; event <= PHP_POLL_WRITE; event <<= 1) { + if (events & event) { + /* Idempotent: a provider installed meanwhile sees add() here */ + php_io_register_handle(h, event, PHP_IO_TRIGGER_LEVEL); + } else { + php_io_registration *reg = php_curl_socket_registration(e, event); + if (reg) { + php_io_unregister(reg); + } + } + if (EG(exception) != pending) { + return FAILURE; + } + } + } ZEND_HASH_FOREACH_END(); + } + + return SUCCESS; +} + +/* Drops every socket entry. Ones libcurl never reported REMOVE for, as after + * a bailout in a transfer, are invalidated here: their socket goes with the + * multi handle. */ +static void php_curl_socket_table_free(php_curl *ch) +{ + while (ch->io_removed) { + php_curl_socket_entry *e = ch->io_removed; + ch->io_removed = e->next_removed; + php_curl_socket_entry_free(e); + } + if (ch->io_sockets) { + ZEND_HASH_FOREACH_PTR(ch->io_sockets, php_curl_socket_entry *e) { + php_curl_socket_entry_invalidate(e); + php_curl_socket_entry_free(e); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(ch->io_sockets); + efree(ch->io_sockets); + ch->io_sockets = NULL; + } +} + +/* curl_exec() under a provider, on the multi socket API: every iteration waits with one Any op, + * a Poll member per socket and direction plus a Timer member for libcurl's timeout. */ +static CURLcode php_curl_exec_multi(php_curl *ch) +{ + CURLcode result = CURLE_OK; + + /* The multi handle stays with the easy handle, as curl_easy_perform() + * keeps its private multi, so pooled connections are reused */ + if (!ch->multi) { + ch->multi = curl_multi_init(); + if (!ch->multi) { + return CURLE_OUT_OF_MEMORY; + } + curl_multi_setopt(ch->multi, CURLMOPT_SOCKETFUNCTION, php_curl_socket_callback); + curl_multi_setopt(ch->multi, CURLMOPT_SOCKETDATA, ch); + curl_multi_setopt(ch->multi, CURLMOPT_TIMERFUNCTION, php_curl_timer_callback); + curl_multi_setopt(ch->multi, CURLMOPT_TIMERDATA, ch); + } + CURLM *multi = ch->multi; + + /* curl_easy_perform() does the same with its private multi */ + curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, ch->maxconnects); + + CURLMcode mres = curl_multi_add_handle(multi, ch->cp); + if (mres != CURLM_OK) { + return mres == CURLM_OUT_OF_MEMORY ? CURLE_OUT_OF_MEMORY : CURLE_FAILED_INIT; + } + + int still_running; + curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); + + for (;;) { + /* The transfer may end inside any socket action, waited for or not */ + CURLMsg *msg; + int msgs_in_queue; + bool done = false; + while ((msg = curl_multi_info_read(multi, &msgs_in_queue)) != NULL) { + if (msg->msg == CURLMSG_DONE && msg->easy_handle == ch->cp) { + result = msg->data.result; + done = true; + } + } + if (done || !still_running) { + break; + } + + /* Other code may run while this waits, but not on this handle */ + ch->in_exec = true; + + if (php_curl_socket_reconcile(ch) == FAILURE) { + /* The provider threw: the transfer did not finish */ + ch->in_exec = false; + result = CURLE_ABORTED_BY_CALLBACK; + break; + } + + uint32_t n_sockets = ch->io_sockets ? zend_hash_num_elements(ch->io_sockets) : 0; + uint32_t cap = n_sockets * 2 + 1; + php_io_op *ops = safe_emalloc(cap, sizeof(php_io_op), 0); + php_io_op **members = safe_emalloc(cap, sizeof(php_io_op *), 0); + php_io_op_result *results = safe_emalloc(cap, sizeof(php_io_op_result), 0); + uint32_t n_members = 0; + + if (n_sockets > 0) { + ZEND_HASH_FOREACH_PTR(ch->io_sockets, php_curl_socket_entry *e) { + uint32_t events = php_curl_what_to_events(e->what); + for (uint32_t event = PHP_POLL_READ; event <= PHP_POLL_WRITE; event <<= 1) { + if (events & event) { + php_io_op *op = &ops[n_members]; + php_io_op_poll(op, e->handle, (php_socket_t) e->socket, event, php_io_deadline_infinite()); + op->registration = php_curl_socket_registration(e, event); + members[n_members++] = op; + } + } + } ZEND_HASH_FOREACH_END(); + } + + /* Without sockets and without a timer nothing could wake the loop */ + php_deadline timer = ch->io_timer; + if (php_deadline_is_infinite(&timer) && n_sockets == 0) { + timer = php_io_deadline_from_ms(1000); + } + uint32_t timer_index = UINT32_MAX; + if (!php_deadline_is_infinite(&timer)) { + php_io_op_timer(&ops[n_members], timer); + timer_index = n_members; + members[n_members] = &ops[n_members]; + n_members++; + } + + php_io_op any; + php_io_op_result any_result; + php_io_op_any(&any, members, n_members, results); + zend_result rc = php_io_run(&any, &any_result); + ch->in_exec = false; + + if (rc == FAILURE) { + /* Cancelled or the provider threw: the transfer did not finish */ + result = CURLE_ABORTED_BY_CALLBACK; + efree(ops); + efree(members); + efree(results); + break; + } + + bool timer_fired = any.u.any.n_results == 0; + for (uint32_t i = 0; i < any.u.any.n_results; i++) { + uint32_t index = results[i].index; + if (index == timer_index) { + timer_fired = true; + continue; + } + if (results[i].status != PHP_IO_DONE && results[i].status != PHP_IO_READY) { + continue; + } + curl_socket_t s = (curl_socket_t)members[index]->fd; + /* An earlier action in this iteration may have removed it */ + php_curl_socket_entry *e = ch->io_sockets ? zend_hash_index_find_ptr(ch->io_sockets, (zend_ulong)s) : NULL; + if (!e || e->handle != members[index]->handle) { + continue; + } + uint32_t revents = (uint32_t)results[i].res; + int curl_events = ((revents & PHP_POLL_READ) ? CURL_CSELECT_IN : 0) + | ((revents & PHP_POLL_WRITE) ? CURL_CSELECT_OUT : 0) + | ((revents & (PHP_POLL_ERROR|PHP_POLL_HUP)) ? CURL_CSELECT_ERR : 0); + curl_multi_socket_action(multi, s, curl_events, &still_running); + } + if (timer_fired) { + curl_multi_socket_action(multi, CURL_SOCKET_TIMEOUT, 0, &still_running); + } + + efree(ops); + efree(members); + efree(results); + } + + curl_multi_remove_handle(multi, ch->cp); + /* Sockets libcurl released at the end of the transfer */ + php_curl_socket_reconcile(ch); + php_deadline_init_infinite(&ch->io_timer); + + return result; +} + +/* The transfer curl_exec() waits for owns the handle until it returns */ +bool php_curl_check_not_in_exec(php_curl *ch) +{ + if (ch->in_exec) { + zend_throw_error(NULL, "%s(): Attempt to use cURL handle while curl_exec() is in progress on it", get_active_function_name()); + return false; + } + return true; +} + /* {{{ _php_curl_cleanup_handle(ch) Cleanup an execution phase */ void _php_curl_cleanup_handle(php_curl *ch) @@ -2416,11 +2823,19 @@ PHP_FUNCTION(curl_exec) ch = Z_CURL_P(zid); + if (!php_curl_check_not_in_exec(ch)) { + RETURN_THROWS(); + } + _php_curl_verify_handlers(ch, /* reporterror */ true); _php_curl_cleanup_handle(ch); - error = curl_easy_perform(ch->cp); + if (php_io_hooks_active()) { + error = php_curl_exec_multi(ch); + } else { + error = curl_easy_perform(ch->cp); + } SAVE_CURL_ERROR(ch, error); if (error != CURLE_OK) { @@ -2830,6 +3245,12 @@ static void curl_free_obj(zend_object *object) _php_curl_verify_handlers(ch, /* reporterror */ false); + if (ch->multi) { + /* Sends CURL_POLL_REMOVE for every socket still reported */ + curl_multi_cleanup(ch->multi); + ch->multi = NULL; + } + php_curl_socket_table_free(ch); curl_easy_cleanup(ch->cp); /* cURL destructors should be invoked only by last curl handle */ @@ -3003,6 +3424,10 @@ PHP_FUNCTION(curl_reset) RETURN_THROWS(); } + if (!php_curl_check_not_in_exec(ch)) { + RETURN_THROWS(); + } + curl_easy_reset(ch->cp); _php_curl_reset_handlers(ch); _php_curl_set_default_options(ch); diff --git a/ext/curl/multi.c b/ext/curl/multi.c index 0e329e58f977..5dab5ed6cd29 100644 --- a/ext/curl/multi.c +++ b/ext/curl/multi.c @@ -19,6 +19,7 @@ #endif #include "php.h" +#include "php_io.h" #include "Zend/zend_smart_str.h" #include "curl_private.h" @@ -89,6 +90,10 @@ PHP_FUNCTION(curl_multi_add_handle) mh = Z_CURL_MULTI_P(z_mh); ch = Z_CURL_P(z_ch); + if (!php_curl_check_not_in_exec(ch)) { + RETURN_THROWS(); + } + _php_curl_verify_handlers(ch, /* reporterror */ true); _php_curl_cleanup_handle(ch); @@ -194,6 +199,101 @@ PHP_FUNCTION(curl_multi_get_handles) } } +typedef struct php_curl_multi_fd { + php_socket_t fd; + uint32_t events; +} php_curl_multi_fd; + +/* The descriptors curl_multi_wait() would poll, for an Any op */ +static CURLMcode php_curl_multi_get_fds(CURLM *multi, php_curl_multi_fd **fds_out, uint32_t *n_out) +{ + php_curl_multi_fd *fds = NULL; + uint32_t n = 0; + +#if LIBCURL_VERSION_NUM >= 0x080800 /* Available since 8.8.0 */ + /* Grow on CURLM_OUT_OF_MEMORY: older versions do not report the count needed */ + unsigned int size = 8, count = 0; + struct curl_waitfd *wfds; + CURLMcode error; + for (;;) { + wfds = safe_emalloc(size, sizeof(struct curl_waitfd), 0); + error = curl_multi_waitfds(multi, wfds, size, &count); + if (error != CURLM_OUT_OF_MEMORY || size > UINT_MAX / 2) { + break; + } + efree(wfds); + size *= 2; + } + if (error != CURLM_OK) { + efree(wfds); + return error; + } + if (count > 0) { + fds = safe_emalloc(count, sizeof(*fds), 0); + } + for (unsigned int i = 0; i < count; i++) { + uint32_t events = ((wfds[i].events & (CURL_WAIT_POLLIN | CURL_WAIT_POLLPRI)) ? PHP_POLL_READ : 0) + | ((wfds[i].events & CURL_WAIT_POLLOUT) ? PHP_POLL_WRITE : 0); + fds[n].fd = (php_socket_t) wfds[i].fd; + fds[n].events = events ? events : PHP_POLL_READ; + n++; + } + efree(wfds); +#else + fd_set rfds, wfds, efds; + int maxfd = -1; + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_ZERO(&efds); + CURLMcode error = curl_multi_fdset(multi, &rfds, &wfds, &efds, &maxfd); + if (error != CURLM_OK) { + return error; + } + if (maxfd >= 0) { +# ifdef PHP_WIN32 + /* A Windows fd_set is a list of SOCKETs, not a bitmap indexed by them */ + fd_set *sets[] = { &rfds, &wfds, &efds }; + fds = safe_emalloc((size_t) rfds.fd_count + wfds.fd_count + efds.fd_count, sizeof(*fds), 0); + for (int s = 0; s < 3; s++) { + for (u_int i = 0; i < sets[s]->fd_count; i++) { + php_socket_t sock = (php_socket_t) sets[s]->fd_array[i]; + uint32_t j = 0; + while (j < n && fds[j].fd != sock) { + j++; + } + if (j == n) { + fds[n].fd = sock; + fds[n].events = 0; + n++; + } + fds[j].events |= s == 0 ? PHP_POLL_READ : (s == 1 ? PHP_POLL_WRITE : 0); + } + } + for (uint32_t j = 0; j < n; j++) { + if (!fds[j].events) { + fds[j].events = PHP_POLL_READ; + } + } +# else + fds = safe_emalloc((size_t) maxfd + 1, sizeof(*fds), 0); + for (int fd = 0; fd <= maxfd; fd++) { + uint32_t events = (FD_ISSET(fd, &rfds) ? PHP_POLL_READ : 0) | (FD_ISSET(fd, &wfds) ? PHP_POLL_WRITE : 0); + if (!events && !FD_ISSET(fd, &efds)) { + continue; + } + fds[n].fd = (php_socket_t) fd; + fds[n].events = events ? events : PHP_POLL_READ; + n++; + } +# endif + } +#endif + + *fds_out = fds; + *n_out = n; + return CURLM_OK; +} + /* {{{ Get all the sockets associated with the cURL extension, which can then be "selected" */ PHP_FUNCTION(curl_multi_select) { @@ -216,6 +316,64 @@ PHP_FUNCTION(curl_multi_select) RETURN_THROWS(); } + if (php_io_hooks_active()) { + /* An Any over the multi's descriptors, bounded by the shorter of the + * caller's timeout and libcurl's own. These sockets have no handle: + * a user multi handle installs no socket callback that could keep + * one, so a provider that needs identity completes it Unsupported and + * the core waits itself. */ + php_curl_multi_fd *fds = NULL; + uint32_t n_fds = 0; + error = php_curl_multi_get_fds(mh->multi, &fds, &n_fds); + if (CURLM_OK != error) { + SAVE_CURLM_ERROR(mh, error); + RETURN_LONG(-1); + } + /* curl_multi_wait() does not wait without descriptors either */ + if (n_fds == 0) { + RETURN_LONG(0); + } + long wait_ms = (long) (timeout * 1000.0); + long curl_ms = -1; + if (curl_multi_timeout(mh->multi, &curl_ms) == CURLM_OK && curl_ms >= 0 && curl_ms < wait_ms) { + wait_ms = curl_ms; + } + + php_io_op *ops = safe_emalloc(n_fds + 1, sizeof(php_io_op), 0); + php_io_op **op_ptrs = safe_emalloc(n_fds + 1, sizeof(php_io_op *), 0); + php_io_op_result *results = safe_emalloc(n_fds + 1, sizeof(php_io_op_result), 0); + uint32_t n = 0; + for (; n < n_fds; n++) { + php_io_op_poll(&ops[n], NULL, fds[n].fd, fds[n].events, php_io_deadline_infinite()); + op_ptrs[n] = &ops[n]; + } + if (fds) { + efree(fds); + } + php_io_op_timer(&ops[n], php_io_deadline_from_ms(wait_ms)); + op_ptrs[n] = &ops[n]; + uint32_t timer_index = n++; + + php_io_op any; + php_io_op_result any_result; + php_io_op_any(&any, op_ptrs, n, results); + zend_result rc = php_io_run(&any, &any_result); + if (rc == SUCCESS) { + for (uint32_t i = 0; i < any.u.any.n_results; i++) { + if (results[i].index != timer_index && (results[i].status == PHP_IO_DONE || results[i].status == PHP_IO_READY)) { + numfds++; + } + } + } + efree(results); + efree(op_ptrs); + efree(ops); + if (rc == FAILURE) { + RETURN_LONG(-1); + } + RETURN_LONG(numfds); + } + error = curl_multi_wait(mh->multi, NULL, 0, (int) (timeout * 1000.0), &numfds); if (CURLM_OK != error) { SAVE_CURLM_ERROR(mh, error); diff --git a/ext/curl/tests/curl_exec_io_hook.phpt b/ext/curl/tests/curl_exec_io_hook.phpt new file mode 100644 index 000000000000..182b4a0e82e7 --- /dev/null +++ b/ext/curl/tests/curl_exec_io_hook.phpt @@ -0,0 +1,52 @@ +--TEST-- +curl_exec() integrates with io_hooks scheduler +--EXTENSIONS-- +curl +--FILE-- +go($fn); +} + +$scheduler->spawn(function () { + $server = stream_socket_server('tcp://127.0.0.1:0'); + $addr = stream_socket_get_name($server, false); + + go(function () use ($server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 12\r\n\r\nHello, curl!"); + fclose($conn); + }); + + go(function () use ($addr) { + $ch = curl_init("http://$addr/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 5); + $body = curl_exec($ch); + $errno = curl_errno($ch); + if ($errno === 0) { + echo "OK: $body\n"; + } else { + echo "curl error $errno: " . curl_error($ch) . "\n"; + } + }); +}); +$scheduler->loop(); + +?> +--EXPECT-- +OK: Hello, curl! diff --git a/ext/curl/tests/curl_exec_io_hook_busy.phpt b/ext/curl/tests/curl_exec_io_hook_busy.phpt new file mode 100644 index 000000000000..8f4e5259383d --- /dev/null +++ b/ext/curl/tests/curl_exec_io_hook_busy.phpt @@ -0,0 +1,73 @@ +--TEST-- +curl_exec() under io_hooks: the handle refuses other use while the transfer waits +--EXTENSIONS-- +curl +--FILE-- +spawn(function () use ($server, $ctl_server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + fwrite($ctl_server, "r"); + fread($ctl_server, 1); + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nfirst"); + fclose($conn); +}); + +$scheduler->spawn(function () use ($ch) { + var_dump(curl_exec($ch), curl_errno($ch)); +}); + +$scheduler->spawn(function () use ($ch, $ctl_checker) { + fread($ctl_checker, 1); + $calls = [ + 'curl_exec' => fn() => curl_exec($ch), + 'curl_reset' => fn() => curl_reset($ch), + 'curl_setopt' => fn() => curl_setopt($ch, CURLOPT_POSTFIELDS, ['a' => 'b']), + 'curl_setopt_array' => fn() => curl_setopt_array($ch, [CURLOPT_URL => 'http://example.com/']), + 'curl_copy_handle' => fn() => curl_copy_handle($ch), + 'curl_multi_add_handle' => fn() => curl_multi_add_handle(curl_multi_init(), $ch), + ]; + foreach ($calls as $name => $call) { + try { + $call(); + echo "$name: no error\n"; + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + } + fwrite($ctl_checker, "g"); +}); + +$scheduler->loop(); + +curl_setopt($ch, CURLOPT_URL, "http://$addr/"); +echo "usable after the transfer\n"; +?> +--EXPECT-- +curl_exec(): Attempt to use cURL handle while curl_exec() is in progress on it +curl_reset(): Attempt to use cURL handle while curl_exec() is in progress on it +curl_setopt(): Attempt to use cURL handle while curl_exec() is in progress on it +curl_setopt_array(): Attempt to use cURL handle while curl_exec() is in progress on it +curl_copy_handle(): Attempt to use cURL handle while curl_exec() is in progress on it +curl_multi_add_handle(): Attempt to use cURL handle while curl_exec() is in progress on it +string(5) "first" +int(0) +usable after the transfer diff --git a/ext/curl/tests/curl_exec_io_hook_switch.phpt b/ext/curl/tests/curl_exec_io_hook_switch.phpt new file mode 100644 index 000000000000..7c0213725fad --- /dev/null +++ b/ext/curl/tests/curl_exec_io_hook_switch.phpt @@ -0,0 +1,35 @@ +--TEST-- +curl_exec() on one handle with and without an IO hooks provider +--EXTENSIONS-- +curl +--FILE-- +spawn(function () use ($ch) { + var_dump(curl_exec($ch)); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); + +var_dump(curl_exec($ch)); +var_dump(curl_upkeep($ch)); +?> +--EXPECT-- +string(25) "Hello World! +Hello World!" +string(25) "Hello World! +Hello World!" +string(25) "Hello World! +Hello World!" +bool(true) diff --git a/ext/curl/tests/curl_exec_io_hook_timeout_deadline.phpt b/ext/curl/tests/curl_exec_io_hook_timeout_deadline.phpt new file mode 100644 index 000000000000..061b08faac40 --- /dev/null +++ b/ext/curl/tests/curl_exec_io_hook_timeout_deadline.phpt @@ -0,0 +1,66 @@ +--TEST-- +curl_exec() under io_hooks: libcurl's timeout is a deadline, not re-armed after socket activity +--EXTENSIONS-- +curl +--FILE-- +getOperations() as $member) { + if ($member instanceof Io\Operation\Timer) { + $now = hrtime(true); + $t = $member->getTimeout(); + $this->latestDeadline = max($this->latestDeadline, $now + $t->seconds * 1_000_000_000 + $t->nanoseconds); + } + } + } + return parent::run($op); + } +} + +$scheduler = new TimerScheduler(); +Io\Hooks\set_hooks($scheduler); + +[$ctl_server, $ctl_client] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($server, $ctl_server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + usleep(300000); + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 20\r\n\r\n0123456789"); + fread($ctl_server, 1); + fclose($conn); +}); + +$scheduler->spawn(function () use ($scheduler, $addr, $ctl_client) { + $ch = curl_init("http://$addr/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1000); + $start = hrtime(true); + var_dump(curl_exec($ch), curl_errno($ch)); + $overshoot_ms = ($scheduler->latestDeadline - $start) / 1e6 - 1000; + echo $overshoot_ms < 50 ? "deadline kept\n" : "deadline moved by {$overshoot_ms}ms\n"; + fwrite($ctl_client, "x"); +}); + +$scheduler->loop(); +?> +--EXPECT-- +bool(false) +int(28) +deadline kept diff --git a/ext/curl/tests/curl_exec_io_hook_upload_slow_reader.phpt b/ext/curl/tests/curl_exec_io_hook_upload_slow_reader.phpt new file mode 100644 index 000000000000..fb3ed8c3f7cf --- /dev/null +++ b/ext/curl/tests/curl_exec_io_hook_upload_slow_reader.phpt @@ -0,0 +1,61 @@ +--TEST-- +curl_exec() under IO hooks: a large upload to a slow reader completes +--EXTENSIONS-- +curl +--FILE-- +spawn(function () use ($server, $size) { + $conn = stream_socket_accept($server, 10); + $headers = ''; + while (!str_contains($headers, "\r\n\r\n")) { + $headers .= fread($conn, 8192); + } + [$headers, $body] = explode("\r\n\r\n", $headers, 2); + preg_match('/Content-Length: (\d+)/i', $headers, $m); + $length = (int) $m[1]; + if (str_contains($headers, 'Expect: 100-continue')) { + fwrite($conn, "HTTP/1.1 100 Continue\r\n\r\n"); + } + $received = strlen($body); + // A reader slower than the sender, so the socket's send buffer fills up + while ($received < $length) { + $chunk = fread($conn, 65536); + if ($chunk === false || $chunk === '') { + break; + } + $received += strlen($chunk); + usleep(2000); + } + $reply = "received $received"; + fwrite($conn, "HTTP/1.1 200 OK\r\nContent-Length: " . strlen($reply) . "\r\nConnection: close\r\n\r\n$reply"); + fclose($conn); +}); + +$scheduler->spawn(function () use ($addr, $size) { + $ch = curl_init("http://$addr/upload"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, str_repeat('u', $size)); + curl_setopt($ch, CURLOPT_TIMEOUT, 60); + $body = curl_exec($ch); + if ($body === false) { + echo "curl error ", curl_errno($ch), ": ", curl_error($ch), "\n"; + } else { + echo $body, "\n"; + } +}); + +$scheduler->loop(); +?> +--EXPECT-- +received 4194304 diff --git a/ext/curl/tests/curl_multi_select_io_hook.phpt b/ext/curl/tests/curl_multi_select_io_hook.phpt new file mode 100644 index 000000000000..30a3314dcf56 --- /dev/null +++ b/ext/curl/tests/curl_multi_select_io_hook.phpt @@ -0,0 +1,66 @@ +--TEST-- +curl_multi_select() under IO hooks waits with an Any operation +--EXTENSIONS-- +curl +--FILE-- +anys++; + } + return parent::run($op); + } +} + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + usleep(20000); + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 5\r\n\r\nmulti"); + fclose($conn); +}); + +$scheduler->spawn(function () use ($addr) { + $mh = curl_multi_init(); + $ch = curl_init("http://$addr/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_multi_add_handle($mh, $ch); + $selects = 0; + do { + $status = curl_multi_exec($mh, $active); + if ($active) { + $n = curl_multi_select($mh, 1.0); + var_dump($n >= 0); + $selects++; + } + } while ($active && $status == CURLM_OK); + var_dump(curl_multi_getcontent($ch)); + curl_multi_remove_handle($mh, $ch); + var_dump($selects >= 1); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +var_dump($scheduler->anys >= 1); +?> +--EXPECTF-- +%A +string(5) "multi" +bool(true) +bool(true) diff --git a/ext/curl/tests/curl_multi_select_io_hook_no_fds.phpt b/ext/curl/tests/curl_multi_select_io_hook_no_fds.phpt new file mode 100644 index 000000000000..e45f7cc3080f --- /dev/null +++ b/ext/curl/tests/curl_multi_select_io_hook_no_fds.phpt @@ -0,0 +1,31 @@ +--TEST-- +curl_multi_select() under IO hooks returns at once without descriptors, as curl_multi_wait() does +--EXTENSIONS-- +curl +--FILE-- +ops++; + return parent::run($op); + } +} + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); +$scheduler->spawn(function () { + $mh = curl_multi_init(); + var_dump(curl_multi_select($mh, 10.0)); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +var_dump($scheduler->ops); +?> +--EXPECT-- +int(0) +int(0) diff --git a/ext/curl/tests/curl_socket_poll_weak_handle_io_hook.phpt b/ext/curl/tests/curl_socket_poll_weak_handle_io_hook.phpt new file mode 100644 index 000000000000..1c4d0f68a70b --- /dev/null +++ b/ext/curl/tests/curl_socket_poll_weak_handle_io_hook.phpt @@ -0,0 +1,81 @@ +--TEST-- +CurlSocketPollWeakHandle cannot be constructed or cloned, and is invalid once libcurl drops the socket +--EXTENSIONS-- +curl +--FILE-- +getOperations() : [$op]; + foreach ($ops as $member) { + if ($member->getHandle() instanceof CurlSocketPollWeakHandle) { + $this->handle = $member->getHandle(); + $this->validInRun = $this->handle->isValid(); + } + } + return parent::run($op); + } +} + +try { + new CurlSocketPollWeakHandle(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +$scheduler = new CapturingScheduler(); +Io\Hooks\set_hooks($scheduler); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nhi"); + fclose($conn); +}); + +$scheduler->spawn(function () use ($addr) { + $ch = curl_init("http://$addr/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + var_dump(curl_exec($ch)); +}); + +$scheduler->loop(); +Io\Hooks\set_hooks(null); + +$handle = $scheduler->handle; +var_dump($handle instanceof CurlSocketPollWeakHandle, $scheduler->validInRun, $handle->isValid()); +try { + clone $handle; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + (new Io\Poll\Context())->add($handle, [Io\Poll\Event::Read]); +} catch (Io\Poll\InvalidHandleException $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +Call to private CurlSocketPollWeakHandle::__construct() from global scope +string(2) "hi" +bool(true) +bool(true) +bool(false) +Trying to clone an uncloneable object of class CurlSocketPollWeakHandle +Invalid handle for polling diff --git a/ext/libxml/libxml.c b/ext/libxml/libxml.c index e53746379428..4a435db8c8b0 100644 --- a/ext/libxml/libxml.c +++ b/ext/libxml/libxml.c @@ -509,6 +509,10 @@ static void *php_libxml_streams_IO_open_write_wrapper(const char *filename) static int php_libxml_streams_IO_read(void *context, char *buffer, int len) { + /* libxml retries a failed read, which must not run on an unwinding fiber */ + if (EG(exception)) { + return -1; + } return php_stream_read((php_stream*)context, buffer, len); } diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c index e55083d940cf..a5405e6a5111 100644 --- a/ext/mysqli/mysqli_api.c +++ b/ext/mysqli/mysqli_api.c @@ -275,6 +275,12 @@ PHP_FUNCTION(mysqli_close) MYSQLI_FETCH_RESOURCE_CONN(mysql, mysql_link, MYSQLI_STATUS_INITIALIZED); + /* Another fiber is suspended in a call on it, which the close would free */ + if (mysql->mysql && mysqlnd_conn_is_busy(mysql->mysql->data)) { + zend_throw_error(NULL, "Concurrent access to a MySQL connection"); + RETURN_THROWS(); + } + php_mysqli_close(mysql, MYSQLI_CLOSE_EXPLICIT, ((MYSQLI_RESOURCE *)(Z_MYSQLI_P(mysql_link))->ptr)->status); ((MYSQLI_RESOURCE *)(Z_MYSQLI_P(mysql_link))->ptr)->status = MYSQLI_STATUS_UNKNOWN; @@ -1470,6 +1476,11 @@ PHP_FUNCTION(mysqli_stmt_close) } MYSQLI_FETCH_RESOURCE_STMT(stmt, mysql_stmt, MYSQLI_STATUS_VALID); + if (stmt->stmt->data && mysqlnd_conn_is_busy(stmt->stmt->data->conn)) { + zend_throw_error(NULL, "Concurrent access to a MySQL connection"); + RETURN_THROWS(); + } + mysqli_stmt_close(stmt->stmt, false); stmt->stmt = NULL; php_clear_stmt_bind(stmt); diff --git a/ext/mysqli/mysqli_nonapi.c b/ext/mysqli/mysqli_nonapi.c index 2f4b6006821c..fda473267f2d 100644 --- a/ext/mysqli/mysqli_nonapi.c +++ b/ext/mysqli/mysqli_nonapi.c @@ -140,6 +140,10 @@ void mysqli_common_connect(INTERNAL_FUNCTION_PARAMETERS, bool is_real_connect, b if (mysql->mysql && mysqli_resource && (mysqli_resource->status > MYSQLI_STATUS_INITIALIZED)) { + if (mysqlnd_conn_is_busy(mysql->mysql->data)) { + zend_throw_error(NULL, "Concurrent access to a MySQL connection"); + RETURN_THROWS(); + } /* already connected, we should close the connection */ php_mysqli_close(mysql, MYSQLI_CLOSE_IMPLICIT, mysqli_resource->status); /* the link is not connected anymore, even if the new connection fails */ diff --git a/ext/mysqli/tests/io_hooks_close_busy.phpt b/ext/mysqli/tests/io_hooks_close_busy.phpt new file mode 100644 index 000000000000..17acc42d5950 --- /dev/null +++ b/ext/mysqli/tests/io_hooks_close_busy.phpt @@ -0,0 +1,43 @@ +--TEST-- +IO hooks: closing a connection another fiber is suspended in throws instead of freeing it +--EXTENSIONS-- +mysqli +--SKIPIF-- + +--FILE-- +prepare('SELECT 1'); +$scheduler = new Scheduler(); +Io\Hooks\set_hooks($scheduler); + +$scheduler->spawn(function () use ($db) { + echo "A: query\n"; + $r = $db->query('SELECT SLEEP(0.5) AS s'); + echo "A: ", $r->fetch_assoc()['s'], "\n"; +}); +$scheduler->spawn(function () use ($db, $stmt) { + foreach ([fn () => $db->close(), fn () => $stmt->close(), fn () => $db->real_connect()] as $close) { + try { + $close(); + } catch (Error $e) { + echo "B: ", $e->getMessage(), "\n"; + } + } +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +var_dump($db->close()); +?> +--EXPECT-- +A: query +B: Concurrent access to a MySQL connection +B: Concurrent access to a MySQL connection +B: Concurrent access to a MySQL connection +A: 0 +bool(true) diff --git a/ext/mysqlnd/mysqlnd.h b/ext/mysqlnd/mysqlnd.h index 032438063bea..c0a934e476cb 100644 --- a/ext/mysqlnd/mysqlnd.h +++ b/ext/mysqlnd/mysqlnd.h @@ -97,6 +97,9 @@ PHPAPI MYSQLND * mysqlnd_connection_connect(MYSQLND * conn, PHPAPI void mysqlnd_debug(const char *mode); +/* An operation of another flow is on the connection's stream */ +PHPAPI bool mysqlnd_conn_is_busy(const MYSQLND_CONN_DATA *conn); + /* Query */ #define mysqlnd_fetch_into(result, flags, ret_val) (result)->m.fetch_into((result), (flags), (ret_val) ZEND_FILE_LINE_CC) #define mysqlnd_fetch_row_c(result) (result)->m.fetch_row_c((result)) diff --git a/ext/mysqlnd/mysqlnd_connection.c b/ext/mysqlnd/mysqlnd_connection.c index 517b149c0b4d..7136d517cecf 100644 --- a/ext/mysqlnd/mysqlnd_connection.c +++ b/ext/mysqlnd/mysqlnd_connection.c @@ -27,6 +27,7 @@ #include "mysqlnd_debug.h" #include "mysqlnd_ext_plugin.h" #include "zend_smart_str.h" +#include "main/php_io_hooks.h" extern MYSQLND_CHARSET *mysqlnd_charsets; @@ -2337,3 +2338,10 @@ mysqlnd_connection_init(const size_t client_flags, const bool persistent, MYSQLN DBG_RETURN(ret); } /* }}} */ + +PHPAPI bool mysqlnd_conn_is_busy(const MYSQLND_CONN_DATA *conn) +{ + MYSQLND_VIO *vio = conn ? conn->vio : NULL; + php_stream *stream = vio ? vio->data->m.get_stream(vio) : NULL; + return stream && php_io_stream_busy(stream); +} diff --git a/ext/openssl/openssl.c b/ext/openssl/openssl.c index b05ee7418d0c..c30bda7a4aec 100644 --- a/ext/openssl/openssl.c +++ b/ext/openssl/openssl.c @@ -814,6 +814,7 @@ PHP_MINIT_FUNCTION(openssl) /* register a resource id number with OpenSSL so that we can map SSL -> stream structures in * OpenSSL callbacks */ ssl_stream_data_index = SSL_get_ex_new_index(0, "PHP stream index", NULL, NULL, NULL); + php_openssl_stream_bio_init(); php_stream_xport_register("ssl", php_openssl_ssl_socket_factory); #if OPENSSL_VERSION_NUMBER < 0x40000000L && !defined(OPENSSL_NO_SSL3) @@ -885,6 +886,7 @@ PHP_MINFO_FUNCTION(openssl) PHP_MSHUTDOWN_FUNCTION(openssl) { php_openssl_backend_shutdown(); + php_openssl_stream_bio_shutdown(); php_unregister_url_stream_wrapper("https"); php_unregister_url_stream_wrapper("ftps"); diff --git a/ext/openssl/php_openssl.h b/ext/openssl/php_openssl.h index 588cca0e93d8..1747cf4d717d 100644 --- a/ext/openssl/php_openssl.h +++ b/ext/openssl/php_openssl.h @@ -98,6 +98,8 @@ ZEND_TSRMLS_CACHE_EXTERN(); #endif php_stream_transport_factory_func php_openssl_ssl_socket_factory; +void php_openssl_stream_bio_init(void); +void php_openssl_stream_bio_shutdown(void); void php_openssl_store_errors(void); void php_openssl_errors_set_mark(void); diff --git a/ext/openssl/tests/stream_io_hooks.phpt b/ext/openssl/tests/stream_io_hooks.phpt new file mode 100644 index 000000000000..42a3e3bc87de --- /dev/null +++ b/ext/openssl/tests/stream_io_hooks.phpt @@ -0,0 +1,113 @@ +--TEST-- +TLS streams under IO hooks: the handshake, reads and writes run as operations +--EXTENSIONS-- +openssl +--FILE-- +saveNewCertAsFileWithKey('io-hooks', $certFile); + +/* Records which operation classes the TLS layer submits */ +class CountingScheduler extends Scheduler +{ + public array $seen = []; + + public function run(\Io\Operation $op): \Io\Completion + { + $class = substr(get_class($op), strlen('Io\\Operation\\')); + $this->seen[$class] = ($this->seen[$class] ?? 0) + 1; + return parent::run($op); + } +} + +$scheduler = new CountingScheduler(); +Io\Hooks\set_hooks($scheduler); + +$serverCtx = stream_context_create(['ssl' => ['local_cert' => $certFile]]); +$clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, +]]); + +$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx); +$addr = stream_socket_get_name($server, false); + +/* Server: the handshake happens inside accept, on this fiber */ +$scheduler->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + var_dump($conn !== false); + $data = fread($conn, 100); + echo "server got: $data\n"; + fwrite($conn, "hello from server\n"); + /* Keep the connection quiet so the client read below times out */ + $line = fgets($conn); + echo "server got: ", $line; + fwrite($conn, str_repeat("x", 100000)); + fclose($conn); +}); + +/* Client: connect, handshake, exchange, then a timed-out read and a + * non-blocking read */ +$scheduler->spawn(function () use ($addr, $clientCtx) { + $client = stream_socket_client("tls://$addr", $errno, $errstr, 5, + STREAM_CLIENT_CONNECT, $clientCtx); + var_dump($client !== false); + fwrite($client, "hello from client"); + $data = fread($client, 100); + echo "client got: $data"; + + stream_set_timeout($client, 0, 200000); + $start = hrtime(true); + var_dump(fread($client, 100)); + $meta = stream_get_meta_data($client); + var_dump($meta['timed_out'], (hrtime(true) - $start) / 1e6 >= 150); + + stream_set_blocking($client, false); + var_dump(fread($client, 100)); + var_dump(stream_socket_get_crypto_status($client) === STREAM_CRYPTO_STATUS_WANT_READ); + stream_set_blocking($client, true); + stream_set_timeout($client, 5); + + fwrite($client, "bye\n"); + $total = 0; + while (!feof($client)) { + $chunk = fread($client, 8192); + if ($chunk === false) break; + $total += strlen($chunk); + } + var_dump($total); + fclose($client); +}); + +$scheduler->loop(); + +/* Send is only an operation when the syscall would block, so it depends on + * the queue and the buffers; the others are certain */ +foreach (['Accept', 'Connect', 'Recv'] as $class) { + echo "$class: ", isset($scheduler->seen[$class]) ? "yes" : "no", "\n"; +} +?> +--CLEAN-- + +--EXPECT-- +bool(true) +bool(true) +server got: hello from client +client got: hello from server +bool(false) +bool(true) +bool(true) +string(0) "" +bool(true) +server got: bye +int(100000) +Accept: yes +Connect: yes +Recv: yes diff --git a/ext/openssl/tests/stream_io_hooks_cancel.phpt b/ext/openssl/tests/stream_io_hooks_cancel.phpt new file mode 100644 index 000000000000..cc924f888039 --- /dev/null +++ b/ext/openssl/tests/stream_io_hooks_cancel.phpt @@ -0,0 +1,78 @@ +--TEST-- +TLS streams under IO hooks: a cancelled Recv leaves the connection usable +--EXTENSIONS-- +openssl +--FILE-- +saveNewCertAsFileWithKey('io-hooks-cancel', $certFile); + +class CancellingScheduler extends Scheduler +{ + public bool $cancelNextRecv = false; + + public function run(\Io\Operation $op): \Io\Completion + { + if ($this->cancelNextRecv && $op instanceof Io\Operation\Recv) { + $this->cancelNextRecv = false; + throw new RuntimeException("Recv cancelled"); + } + return parent::run($op); + } +} + +$scheduler = new CancellingScheduler(); +Io\Hooks\set_hooks($scheduler); + +$serverCtx = stream_context_create(['ssl' => ['local_cert' => $certFile]]); +$clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, +]]); + +$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + $line = fgets($conn); + echo "server got: $line"; + fwrite($conn, "hello"); + $line = fgets($conn); + echo "server got: $line"; + fclose($conn); +}); + +$scheduler->spawn(function () use ($scheduler, $addr, $clientCtx) { + $client = stream_socket_client("tls://$addr", $errno, $errstr, 5, + STREAM_CLIENT_CONNECT, $clientCtx); + $scheduler->cancelNextRecv = true; + try { + fread($client, 10); + } catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; + } + var_dump(feof($client)); + fwrite($client, "go\n"); + var_dump(fread($client, 10)); + fwrite($client, "bye\n"); + fclose($client); +}); + +$scheduler->loop(); +?> +--CLEAN-- + +--EXPECT-- +Recv cancelled +bool(false) +server got: go +string(5) "hello" +server got: bye diff --git a/ext/openssl/tests/stream_io_hooks_orphan.phpt b/ext/openssl/tests/stream_io_hooks_orphan.phpt new file mode 100644 index 000000000000..0de90f3fdd74 --- /dev/null +++ b/ext/openssl/tests/stream_io_hooks_orphan.phpt @@ -0,0 +1,93 @@ +--TEST-- +TLS streams under IO hooks: a Recv orphaned on the ring marks the connection dead +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- +saveNewCertAsFileWithKey('io-hooks-orphan', $certFile); + +final class GiveUp implements Io\Hooks\Hooks +{ + public function __construct(private Io\Ring\Engine $ring) {} + public function getCapabilities(): array { return Scheduler::capabilities($this->ring); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op); + throw new RuntimeException("gave up on " . $op::class); + } +} + +$ring = new Io\Ring\Engine(); +$scheduler = new Scheduler($ring); +Io\Hooks\set_hooks($scheduler); + +$serverCtx = stream_context_create(['ssl' => ['local_cert' => $certFile]]); +$clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, +]]); + +$server = stream_socket_server('tls://127.0.0.1:0', $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx); +$addr = stream_socket_get_name($server, false); + +$conn = $client = null; +$scheduler->spawn(function () use ($server, &$conn) { + $conn = stream_socket_accept($server, 5); +}); +$scheduler->spawn(function () use ($addr, $clientCtx, &$client) { + $client = stream_socket_client("tls://$addr", $errno, $errstr, 5, + STREAM_CLIENT_CONNECT, $clientCtx); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); + +// OpenSSL's read buffer is released once drained; data sent after the +// orphan lands in the ring's own buffer, if anywhere +fwrite($conn, "hello"); +var_dump(fread($client, 5)); + +Io\Hooks\set_hooks(new GiveUp($ring)); +try { + fread($client, 100); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +fwrite($conn, str_repeat("x", 20000)); + +$churn = []; +for ($i = 0; $i < 64; $i++) { + $churn[] = str_repeat(chr(65 + $i % 26), 17000); +} +unset($churn); + +// The stream stays frozen until the orphan settled +if ($ring->countPending()) { + $ring->waitCompletions(); +} + +var_dump(fread($client, 100), feof($client)); +fclose($client); +fclose($conn); +?> +--CLEAN-- + +--EXPECT-- +string(5) "hello" +gave up on Io\Operation\Recv +bool(false) +bool(true) diff --git a/ext/openssl/tests/stream_poll_handle_cast.phpt b/ext/openssl/tests/stream_poll_handle_cast.phpt new file mode 100644 index 000000000000..450784e3ddec --- /dev/null +++ b/ext/openssl/tests/stream_poll_handle_cast.phpt @@ -0,0 +1,73 @@ +--TEST-- +A stream poll handle takes the descriptor of a TLS stream without touching its buffers +--EXTENSIONS-- +openssl +--SKIPIF-- + +--FILE-- + ['local_cert' => '%s']]); + $sock = stream_socket_server("tls://127.0.0.1:0", $errno, $errstr, + STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $serverCtx); + phpt_notify_server_start($sock); + + $link = stream_socket_accept($sock); + /* One record of 100 bytes */ + fwrite($link, str_repeat("x", 100)); + phpt_wait(); + fclose($link); +CODE; +$serverCode = sprintf($serverCode, $certFile); + +$clientCode = <<<'CODE' + $clientCtx = stream_context_create(['ssl' => [ + 'verify_peer' => false, + 'verify_peer_name' => false, + ]]); + $sock = stream_socket_client("tls://{{ ADDR }}", $errno, $errstr, 2, STREAM_CLIENT_CONNECT, $clientCtx); + + /* Unbuffered: 10 bytes leave 90 decrypted inside OpenSSL, none in the stream */ + stream_set_read_buffer($sock, 0); + var_dump(strlen(fread($sock, 10))); + + /* Adding the handle casts for the descriptor: the select cast would move the 90 + * bytes into the stream buffer here, the poll cast leaves them where they are */ + $ctx = new Io\Poll\Context(); + $w = $ctx->add(StreamPollWeakHandle::create($sock), [Io\Poll\Event::Read]); + var_dump(stream_get_meta_data($sock)['unread_bytes']); + + /* Bytes OpenSSL holds are not readiness of the socket */ + var_dump(count($ctx->wait(Time\Duration::fromMilliseconds(100)))); + var_dump(strlen(fread($sock, 90))); + + /* The peer's close is */ + phpt_notify(); + $fired = $ctx->wait(Time\Duration::fromSeconds(2)); + var_dump(count($fired), $fired[0] === $w); + var_dump(fread($sock, 10), feof($sock)); +CODE; + +include 'CertificateGenerator.inc'; +(new CertificateGenerator())->saveNewCertAsFileWithKey('stream_poll_handle_cast', $certFile); + +include 'ServerClientTestCase.inc'; +ServerClientTestCase::getInstance()->run($clientCode, $serverCode); +?> +--CLEAN-- + +--EXPECT-- +int(10) +int(0) +int(0) +int(90) +int(1) +bool(true) +string(0) "" +bool(true) diff --git a/ext/openssl/xp_ssl.c b/ext/openssl/xp_ssl.c index 854227318da0..1692a1f641c7 100644 --- a/ext/openssl/xp_ssl.c +++ b/ext/openssl/xp_ssl.c @@ -148,8 +148,6 @@ extern php_stream* php_openssl_get_stream_from_ssl_handle(const SSL *ssl); extern zend_string* php_openssl_x509_fingerprint( X509 *peer, const char *method, bool raw, php_stream *stream); extern int php_openssl_get_ssl_stream_data_index(void); -static struct timeval php_openssl_subtract_timeval(struct timeval a, struct timeval b); -static int php_openssl_compare_timeval(struct timeval a, struct timeval b); static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, size_t count); static const php_stream_ops php_openssl_socket_ops; @@ -227,6 +225,8 @@ typedef struct _php_openssl_netstream_data_t { int is_client; int ssl_active; int last_status; + /* Set around each SSL call on a blocking stream; the BIO waits until it */ + php_deadline *deadline; php_stream_xport_crypt_method_t method; php_openssl_handshake_bucket_t *reneg; php_openssl_sni_cert_t *sni_certs; @@ -250,7 +250,11 @@ typedef struct _php_openssl_netstream_data_t { #endif char *url_name; unsigned state_set:1; - unsigned _spare:31; + /* The last transport op was cancelled */ + unsigned io_cancelled:1; + /* An op was orphaned mid-record: the TLS stream is out of sync */ + unsigned io_dead:1; + unsigned _spare:29; } php_openssl_netstream_data_t; /* it doesn't matter that we do some hash traversal here, since it is done only @@ -279,7 +283,7 @@ static int php_openssl_is_http_stream_talking_to_iis(php_stream *stream) /* {{{ } /* }}} */ -static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool is_init) /* {{{ */ +static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool blocking) /* {{{ */ { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; int err = SSL_get_error(sslsock->ssl_handle, nr_bytes); @@ -298,7 +302,7 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i /* re-negotiation, or perhaps the SSL layer needs more * packets: retry in next iteration */ errno = EAGAIN; - retry = is_init ? true : sslsock->s.is_blocked; + retry = blocking; if (!retry) { sslsock->last_status = err == SSL_ERROR_WANT_READ ? STREAM_CRYPTO_STATUS_WANT_READ : STREAM_CRYPTO_STATUS_WANT_WRITE; @@ -314,10 +318,11 @@ static int php_openssl_handle_ssl_error(php_stream *stream, int nr_bytes, bool i stream->eof = 1; retry = false; } else { - char *estr = php_socket_strerror(php_socket_errno(), NULL, 0); - php_stream_warn(stream, ProtocolError, "SSL: %s", estr); - - efree(estr); + if (!EG(exception)) { + char *estr = php_socket_strerror(php_socket_errno(), NULL, 0); + php_stream_warn(stream, ProtocolError, "SSL: %s", estr); + efree(estr); + } retry = false; } break; @@ -404,6 +409,153 @@ static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) /* {{{ */ } /* }}} */ +/* A zero or negative timeout means no timeout for the TLS layer */ +static struct timeval *php_openssl_timeout(struct timeval *tv) +{ + return (tv->tv_sec > 0 || (tv->tv_sec == 0 && tv->tv_usec > 0)) ? tv : NULL; +} + +/* The transport BIO. Ciphertext moves through the stream's socket: as Recv + * and Send ops up to the deadline set by the caller, or with plain syscalls + * on the non-blocking descriptor when no deadline is set. */ +static BIO_METHOD *php_openssl_stream_bio_method = NULL; + +static int php_openssl_stream_bio_io(BIO *bio, char *buf, int len, bool read) +{ + php_stream *stream = BIO_get_data(bio); + php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t *) stream->abstract; + ssize_t n; + + BIO_clear_retry_flags(bio); + if (buf == NULL || len <= 0 || sslsock->s.socket == SOCK_ERR) { + return -1; + } + if (sslsock->io_dead) { + errno = ECANCELED; +#ifdef PHP_WIN32 + WSASetLastError(PHP_IO_SOCK_ECANCELED); +#endif + return -1; + } + + if (sslsock->deadline != NULL) { + n = read + ? php_io_recv(stream, sslsock->s.socket, buf, len, 0, sslsock->deadline) + : php_io_send(stream, sslsock->s.socket, buf, len, 0, sslsock->deadline); + if (n < 0 && php_socket_errno() == PHP_IO_SOCK_ECANCELED) { + sslsock->io_cancelled = 1; + /* Still frozen: the op outlived the call and owns the socket's data */ + if (stream->flags & PHP_STREAM_FLAG_IN_USE) { + sslsock->io_dead = 1; + } + } + } else { + n = read + ? recv(sslsock->s.socket, buf, len, 0) + : send(sslsock->s.socket, buf, len, 0); + } + + if (n > 0) { + return (int) n; + } + if (n == 0) { +#ifdef BIO_FLAGS_IN_EOF + if (read) { + BIO_set_flags(bio, BIO_FLAGS_IN_EOF); + } +#endif + return 0; + } + + int err = php_socket_errno(); + /* A cancelled op is a retry too: OpenSSL keeps its record state and the + * read or write loop ends the call on io_cancelled */ + if (PHP_IS_TRANSIENT_ERROR(err) || err == PHP_IO_SOCK_EINTR || err == PHP_IO_SOCK_ETIMEDOUT + || err == PHP_IO_SOCK_ECANCELED) { + if (read) { + BIO_set_retry_read(bio); + } else { + BIO_set_retry_write(bio); + } + } + return -1; +} + +static int php_openssl_stream_bio_read(BIO *bio, char *buf, int len) +{ + return php_openssl_stream_bio_io(bio, buf, len, true); +} + +static int php_openssl_stream_bio_write(BIO *bio, const char *buf, int len) +{ + return php_openssl_stream_bio_io(bio, (char *) buf, len, false); +} + +static long php_openssl_stream_bio_ctrl(BIO *bio, int cmd, long num, void *ptr) +{ + switch (cmd) { + case BIO_CTRL_FLUSH: + case BIO_CTRL_DUP: + return 1; +#ifdef BIO_FLAGS_IN_EOF + case BIO_CTRL_EOF: + return (BIO_get_flags(bio) & BIO_FLAGS_IN_EOF) != 0; +#endif + default: + return 0; + } +} + +static int php_openssl_stream_bio_create(BIO *bio) +{ + BIO_set_init(bio, 0); + return 1; +} + +static int php_openssl_stream_bio_destroy(BIO *bio) +{ + if (bio == NULL) { + return 0; + } + BIO_set_data(bio, NULL); + BIO_set_init(bio, 0); + return 1; +} + +void php_openssl_stream_bio_init(void) +{ + BIO_METHOD *m = BIO_meth_new(BIO_get_new_index() | BIO_TYPE_SOURCE_SINK, "PHP stream"); + if (m == NULL) { + return; + } + BIO_meth_set_write(m, php_openssl_stream_bio_write); + BIO_meth_set_read(m, php_openssl_stream_bio_read); + BIO_meth_set_ctrl(m, php_openssl_stream_bio_ctrl); + BIO_meth_set_create(m, php_openssl_stream_bio_create); + BIO_meth_set_destroy(m, php_openssl_stream_bio_destroy); + php_openssl_stream_bio_method = m; +} + +void php_openssl_stream_bio_shutdown(void) +{ + if (php_openssl_stream_bio_method != NULL) { + BIO_meth_free(php_openssl_stream_bio_method); + php_openssl_stream_bio_method = NULL; + } +} + +static zend_result php_openssl_set_stream_bio(php_stream *stream, php_openssl_netstream_data_t *sslsock) +{ + BIO *bio = php_openssl_stream_bio_method ? BIO_new(php_openssl_stream_bio_method) : NULL; + if (bio == NULL) { + return FAILURE; + } + BIO_set_data(bio, stream); + BIO_set_init(bio, 1); + SSL_set_bio(sslsock->ssl_handle, bio, bio); + return SUCCESS; +} + static bool php_openssl_x509_fingerprint_is_equal(php_stream *stream, X509 *peer, const char *method, const zend_string *expected) { bool is_equal = false; @@ -2802,7 +2954,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, SSL_set_ex_data(sslsock->ssl_handle, php_openssl_get_ssl_stream_data_index(), stream); - if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { + if (php_openssl_set_stream_bio(stream, sslsock) == FAILURE) { php_openssl_handle_ssl_error(stream, 0, true); } @@ -2838,7 +2990,7 @@ static zend_result php_openssl_setup_crypto(php_stream *stream, return FAILURE; } - if (!SSL_set_fd(sslsock->ssl_handle, sslsock->s.socket)) { + if (php_openssl_set_stream_bio(stream, sslsock) == FAILURE) { php_openssl_handle_ssl_error(stream, 0, true); } @@ -2900,15 +3052,6 @@ static int php_openssl_capture_peer_certs(php_stream *stream, } /* }}} */ -static zend_result php_openssl_set_blocking(php_openssl_netstream_data_t *sslsock, int block) -{ - zend_result result = php_set_sock_blocking(sslsock->s.socket, block); - if (EXPECTED(SUCCESS == result)) { - sslsock->s.is_blocked = block; - } - return result; -} - #ifdef HAVE_TLS13 /* Send pending client early data (0-RTT), then connect. Returns like SSL_connect(). */ static int php_openssl_handshake_client_early_data(php_openssl_netstream_data_t *sslsock) @@ -2995,8 +3138,8 @@ static int php_openssl_enable_crypto(php_stream *stream, sslsock->last_status = STREAM_CRYPTO_STATUS_NONE; if (cparam->inputs.activate && !sslsock->ssl_active) { - struct timeval start_time, *timeout; - bool blocked = sslsock->s.is_blocked, has_timeout = false; + bool blocked = sslsock->s.is_blocked; + php_deadline deadline; if (!sslsock->state_set) { #ifdef PHP_OPENSSL_TLS_DEBUG @@ -3034,61 +3177,32 @@ static int php_openssl_enable_crypto(php_stream *stream, sslsock->state_set = 1; } - SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS); + SSL_set_mode(sslsock->ssl_handle, SSL_MODE_RELEASE_BUFFERS + | SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - if (SUCCESS == php_openssl_set_blocking(sslsock, 0)) { - /* The following mode are added only if we are able to change socket - * to non blocking mode which is also used for read and write */ - SSL_set_mode(sslsock->ssl_handle, SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); - } - - timeout = sslsock->is_client ? &sslsock->connect_timeout : &sslsock->s.timeout; - has_timeout = !sslsock->s.is_blocked && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec)); - /* gettimeofday is not monotonic; using it here is not strictly correct */ - if (has_timeout) { - gettimeofday(&start_time, NULL); + /* A blocking stream waits in the BIO up to the timeout, a non-blocking + * one returns on WANT_READ or WANT_WRITE */ + php_deadline_init_infinite(&deadline); + if (blocked) { + php_deadline_init(&deadline, php_openssl_timeout( + sslsock->is_client ? &sslsock->connect_timeout : &sslsock->s.timeout)); } + sslsock->deadline = blocked ? &deadline : NULL; do { - struct timeval cur_time, elapsed_time; - ERR_clear_error(); n = php_openssl_do_handshake(stream, sslsock); - - if (has_timeout) { - gettimeofday(&cur_time, NULL); - elapsed_time = php_openssl_subtract_timeval(cur_time, start_time); - - if (php_openssl_compare_timeval( elapsed_time, *timeout) > 0) { - php_openssl_set_blocking(sslsock, blocked); - php_stream_warn(stream, TimeOut, "SSL: Handshake timed out"); - return -1; - } + if (n > 0) { + break; } - - if (n <= 0) { - /* in case of SSL_ERROR_WANT_READ/WRITE, do not retry in non-blocking mode */ - retry = php_openssl_handle_ssl_error(stream, n, blocked); - if (retry) { - /* wait until something interesting happens in the socket. It may be a - * timeout. Also consider the unlikely of possibility of a write block */ - int err = SSL_get_error(sslsock->ssl_handle, n); - struct timeval left_time; - - if (has_timeout) { - left_time = php_openssl_subtract_timeval(*timeout, elapsed_time); - } - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? - (POLLIN|POLLPRI) : POLLOUT, has_timeout ? &left_time : NULL); - } - } else { - retry = 0; + retry = php_openssl_handle_ssl_error(stream, n, blocked); + if (retry && php_deadline_to_timeout_ms(&deadline) == 0) { + sslsock->deadline = NULL; + php_stream_warn(stream, TimeOut, "SSL: Handshake timed out"); + return -1; } } while (retry); - - if (sslsock->s.is_blocked != blocked) { - php_openssl_set_blocking(sslsock, blocked); - } + sslsock->deadline = NULL; if (n == 1) { peer_cert = SSL_get_peer_certificate(sslsock->ssl_handle); @@ -3141,74 +3255,42 @@ static ssize_t php_openssl_sockop_write(php_stream *stream, const char *buf, siz } /* }}} */ -/** - * Factored out common functionality (blocking, timeout, loop management) for read and write. - * Perform IO (read or write) to an SSL socket. If we have a timeout, we switch to non-blocking mode - * for the duration of the operation, using select to do our waits. If we time out, or we have an error - * report that back to PHP - */ +/* Read or write through the TLS layer. A blocking stream waits inside the + * BIO up to its timeout, a non-blocking one returns with the WANT status. */ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, size_t count) /* {{{ */ { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; - /* Only do this if SSL is active. */ + if (sslsock->ssl_active && sslsock->io_dead) { + if (read) { + stream->eof = 1; + } + return -1; + } + if (sslsock->ssl_active) { - /* We have already returned some buffered data. Don't retry and don't - * block. We're just trying to fill the buffer more, but the stream might - * be empty, so we don't want to wait in vain. */ + /* With buffered data already returned, only check for more without waiting */ bool supplemental = stream->has_buffered_data; - int retry = !supplemental; - struct timeval start_time; - struct timeval *timeout = NULL; - bool began_blocked = sslsock->s.is_blocked; - bool has_timeout = false; + bool blocked = sslsock->s.is_blocked && !supplemental; + php_deadline deadline; int nr_bytes = 0; + int retry; /* prevent overflow in openssl */ if (count > INT_MAX) { count = INT_MAX; } - /* never use a timeout with non-blocking sockets */ - if (began_blocked && !supplemental) { - timeout = &sslsock->s.timeout; + php_deadline_init_infinite(&deadline); + if (blocked) { + php_deadline_init(&deadline, php_openssl_timeout(&sslsock->s.timeout)); } + sslsock->deadline = blocked ? &deadline : NULL; - if (timeout || supplemental) { - php_openssl_set_blocking(sslsock, 0); - } - - if (!sslsock->s.is_blocked && timeout && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec))) { - has_timeout = true; - /* gettimeofday is not monotonic; using it here is not strictly correct */ - gettimeofday(&start_time, NULL); - } - - /* Main IO loop. */ do { - struct timeval cur_time, elapsed_time, left_time; - - /* If we have a timeout to check, figure out how much time has elapsed since we started. */ - if (has_timeout) { - gettimeofday(&cur_time, NULL); - - /* Determine how much time we've taken so far. */ - elapsed_time = php_openssl_subtract_timeval(cur_time, start_time); - - /* and return an error if we've taken too long. */ - if (php_openssl_compare_timeval(elapsed_time, *timeout) > 0 ) { - /* If the socket was originally blocking, set it back. */ - if (began_blocked) { - php_openssl_set_blocking(sslsock, 1); - } - sslsock->s.timeout_event = true; - return -1; - } - } - - /* Now, do the IO operation. Don't block if we can't complete... */ ERR_clear_error(); sslsock->last_status = STREAM_CRYPTO_STATUS_NONE; + sslsock->io_cancelled = 0; if (read) { nr_bytes = SSL_read(sslsock->ssl_handle, buf, (int)count); @@ -3217,89 +3299,43 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si php_stream_xport_shutdown(stream, (stream_shutdown_t)SHUT_RDWR); nr_bytes = 0; stream->eof = 1; - break; + break; } } else { nr_bytes = SSL_write(sslsock->ssl_handle, buf, (int)count); } - /* Now, how much time until we time out? */ - if (has_timeout) { - left_time = php_openssl_subtract_timeval( *timeout, elapsed_time ); + if (nr_bytes > 0) { + break; } - /* If we didn't do anything on the last loop (or an error) check to see if we should retry or exit. */ - if (nr_bytes <= 0) { - - /* Get the error code from SSL, and check to see if it's an error or not. */ - int err = SSL_get_error(sslsock->ssl_handle, nr_bytes ); - retry = php_openssl_handle_ssl_error(stream, nr_bytes, false); - - /* If we get this (the above doesn't check) then we'll retry as well. */ - if (errno == EAGAIN && err == SSL_ERROR_WANT_READ && read) { - retry = 1; - } - if (errno == EAGAIN && err == SSL_ERROR_WANT_WRITE && read == 0) { - retry = 1; - } - - /* Also, on reads, we may get this condition on an EOF. We should check properly. */ - if (read) { - stream->eof = (retry == 0 && errno != EAGAIN && !SSL_pending(sslsock->ssl_handle)); - } - - /* Don't loop indefinitely in non-blocking mode if no data is available */ - if (began_blocked == 0 || supplemental) { - break; - } - - /* Now, if we have to wait some time, and we're supposed to be blocking, wait for the socket to become - * available. Now, php_pollfd_for uses select to wait up to our time_left value only... - */ - if (retry) { - if (read) { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ? - (POLLOUT|POLLPRI) : (POLLIN|POLLPRI), has_timeout ? &left_time : NULL); - } else { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? - (POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL); - } - } - } else { - /* Else, if we got bytes back, check for possible errors. */ - int err = SSL_get_error(sslsock->ssl_handle, nr_bytes); - - /* If we didn't get any error, then let's return it to PHP. */ - if (err == SSL_ERROR_NONE) { - break; - } - - /* Otherwise, we need to wait again (up to time_left or we get an error) */ - if (began_blocked) { - if (read) { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_WRITE) ? - (POLLOUT|POLLPRI) : (POLLIN|POLLPRI), has_timeout ? &left_time : NULL); - } else { - php_pollfd_for(sslsock->s.socket, (err == SSL_ERROR_WANT_READ) ? - (POLLIN|POLLPRI) : (POLLOUT|POLLPRI), has_timeout ? &left_time : NULL); - } - } else if (err == SSL_ERROR_WANT_READ) { - sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_READ; - } else if (err == SSL_ERROR_WANT_WRITE) { - sslsock->last_status = STREAM_CRYPTO_STATUS_WANT_WRITE; - } + retry = php_openssl_handle_ssl_error(stream, nr_bytes, blocked); + if (sslsock->io_cancelled) { + /* The wait was cancelled behind a retry: OpenSSL kept its state, the call ends */ + retry = 0; + nr_bytes = -1; + errno = ECANCELED; +#ifdef PHP_WIN32 + WSASetLastError(PHP_IO_SOCK_ECANCELED); +#endif + } + /* A cancelled Recv says nothing about the connection */ + if (read && !sslsock->io_cancelled) { + stream->eof = (retry == 0 && errno != EAGAIN && !SSL_pending(sslsock->ssl_handle)); + } + if (retry && php_deadline_to_timeout_ms(&deadline) == 0) { + sslsock->deadline = NULL; + sslsock->s.timeout_event = true; + return -1; } - - /* Finally, we keep going until we got data, and an SSL_ERROR_NONE, unless we had an error. */ } while (retry); + sslsock->deadline = NULL; - /* Tell PHP if we read / wrote bytes. */ if (nr_bytes > 0) { php_stream_notify_progress_increment(PHP_STREAM_CONTEXT(stream), nr_bytes, 0); } - /* This might be a supplemental read after consuming buffered data. If - * the read returned nothing, ignore status WANT_READ. */ + /* A supplemental read that returned nothing is not a WANT_READ */ if (read && supplemental && nr_bytes <= 0 && @@ -3308,16 +3344,10 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si sslsock->last_status = STREAM_CRYPTO_STATUS_NONE; } - /* And if we were originally supposed to be blocking, let's reset the socket to that. */ - if (began_blocked) { - php_openssl_set_blocking(sslsock, 1); - } - return 0 > nr_bytes ? 0 : nr_bytes; } else { size_t nr_bytes = 0; - /* This block is if we had no timeout... We will just sit and wait forever on the IO operation. */ if (read) { nr_bytes = php_stream_socket_ops.read(stream, buf, count); } else { @@ -3329,39 +3359,9 @@ static ssize_t php_openssl_sockop_io(int read, php_stream *stream, char *buf, si } /* }}} */ -static struct timeval php_openssl_subtract_timeval(struct timeval a, struct timeval b) /* {{{ */ -{ - struct timeval difference; - - difference.tv_sec = a.tv_sec - b.tv_sec; - difference.tv_usec = a.tv_usec - b.tv_usec; - - if (a.tv_usec < b.tv_usec) { - difference.tv_sec -= 1L; - difference.tv_usec += 1000000L; - } - - return difference; -} -/* }}} */ - -static int php_openssl_compare_timeval( struct timeval a, struct timeval b ) -{ - if (a.tv_sec > b.tv_sec || (a.tv_sec == b.tv_sec && a.tv_usec > b.tv_usec) ) { - return 1; - } else if( a.tv_sec == b.tv_sec && a.tv_usec == b.tv_usec ) { - return 0; - } else { - return -1; - } -} - static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ */ { php_openssl_netstream_data_t *sslsock = (php_openssl_netstream_data_t*)stream->abstract; -#ifdef PHP_WIN32 - int n; -#endif unsigned i; if (close_handle) { @@ -3397,13 +3397,16 @@ static int php_openssl_sockop_close(php_stream *stream, int close_handle) /* {{{ * We use a small timeout which should encourage the OS to send the data, * but at the same time avoid hanging indefinitely. * */ - do { - n = php_pollfd_for_ms(sslsock->s.socket, POLLOUT, 500); - } while (n == -1 && php_socket_errno() == EINTR); + /* A plain poll, not an op: fclose() may run outside any fiber a + * suspending provider could park it on */ + php_pollfd_for_ms(sslsock->s.socket, POLLOUT, 500); #endif + php_netstream_restore_blocking(&sslsock->s); closesocket(sslsock->s.socket); sslsock->s.socket = SOCK_ERR; } + } else { + php_netstream_restore_blocking(&sslsock->s); } if (sslsock->sni_certs) { @@ -3505,7 +3508,8 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ } } - php_socket_t clisock = php_network_accept_incoming_ex(sock->s.socket, + php_socket_t clisock = php_network_accept_incoming_stream_ex(stream, + sock->s.socket, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, xparam->want_addr ? &xparam->outputs.addrlen : NULL, @@ -3522,10 +3526,10 @@ static inline int php_openssl_tcp_sockop_accept(php_stream *stream, php_openssl_ memcpy(clisockdata, sock, sizeof(clisockdata->s)); clisockdata->s.socket = clisock; -#ifdef __linux__ - /* O_NONBLOCK is not inherited on Linux */ clisockdata->s.is_blocked = true; -#endif + php_netstream_set_nonblocking(&clisockdata->s); + /* accepted by us, non-blocking already when the ring accepted it */ + clisockdata->s.restore_blocking = true; xparam->outputs.client = php_stream_alloc_rel(stream->ops, clisockdata, NULL, "r+"); if (xparam->outputs.client) { @@ -3673,99 +3677,24 @@ static int php_openssl_sockop_set_option(php_stream *stream, int option, int val !(stream->flags & PHP_STREAM_FLAG_NO_IO) && ((MSG_DONTWAIT != 0) || !sslsock->s.is_blocked) ) || - php_pollfd_for(sslsock->s.socket, PHP_POLLREADABLE|POLLPRI, &tv) > 0 + php_io_poll_tv(stream, sslsock->s.socket, PHP_POLL_READ, &tv) > 0 ) { /* the poll() call was skipped if the socket is non-blocking (or MSG_DONTWAIT is available) and if the timeout is zero */ /* additionally, we don't use this optimization if SSL is active because in that case, we're not using MSG_DONTWAIT */ if (sslsock->ssl_active) { - int retry = 1; - struct timeval start_time; - struct timeval *timeout = NULL; - bool began_blocked = sslsock->s.is_blocked; - bool has_timeout = false; - - /* never use a timeout with non-blocking sockets */ - if (began_blocked) { - timeout = &tv; - } - - if (timeout) { - php_openssl_set_blocking(sslsock, 0); - } - - if (!sslsock->s.is_blocked && timeout && (timeout->tv_sec > 0 || (timeout->tv_sec == 0 && timeout->tv_usec))) { - has_timeout = true; - /* gettimeofday is not monotonic; using it here is not strictly correct */ - gettimeofday(&start_time, NULL); - } - - /* Main IO loop. */ - do { - struct timeval cur_time, elapsed_time, left_time; - - /* If we have a timeout to check, figure out how much time has elapsed since we started. */ - if (has_timeout) { - gettimeofday(&cur_time, NULL); - - /* Determine how much time we've taken so far. */ - elapsed_time = php_openssl_subtract_timeval(cur_time, start_time); - - /* and return an error if we've taken too long. */ - if (php_openssl_compare_timeval(elapsed_time, *timeout) > 0 ) { - /* If the socket was originally blocking, set it back. */ - if (began_blocked) { - php_openssl_set_blocking(sslsock, 1); - } - sslsock->s.timeout_event = true; - return PHP_STREAM_OPTION_RETURN_ERR; - } - } - - int n = SSL_peek(sslsock->ssl_handle, &buf, sizeof(buf)); - /* If we didn't do anything on the last loop (or an error) check to see if we should retry or exit. */ - if (n <= 0) { - /* Now, do the IO operation. Don't block if we can't complete... */ - int err = SSL_get_error(sslsock->ssl_handle, n); - switch (err) { - case SSL_ERROR_SYSCALL: - retry = php_socket_errno() == EAGAIN; - break; - case SSL_ERROR_WANT_READ: - case SSL_ERROR_WANT_WRITE: - retry = 1; - break; - default: - /* any other problem is a fatal error */ - retry = 0; - } - - /* Don't loop indefinitely in non-blocking mode if no data is available */ - if (!began_blocked || !has_timeout) { - alive = retry; - break; - } - - /* Now, if we have to wait some time, and we're supposed to be blocking, wait for the socket to become - * available. Now, php_pollfd_for uses select to wait up to our time_left value only... - */ - if (retry) { - /* Now, how much time until we time out? */ - left_time = php_openssl_subtract_timeval(*timeout, elapsed_time); - if (php_pollfd_for(sslsock->s.socket, PHP_POLLREADABLE|POLLPRI|POLLOUT, has_timeout ? &left_time : NULL) <= 0) { - retry = 0; - alive = 0; - }; - } - } else { - retry = 0; - alive = 1; - } - /* Finally, we keep going until there are any data or there is no time to wait. */ - } while (retry); - - if (began_blocked && !sslsock->s.is_blocked) { - // Set it back to blocking - php_openssl_set_blocking(sslsock, 1); + /* Peek through the TLS layer, waiting up to the timeout on a + * blocking stream; no complete record yet means the peer is there */ + php_deadline deadline; + int n; + + deadline = php_io_deadline_from_timeval(&tv); + sslsock->deadline = sslsock->s.is_blocked ? &deadline : NULL; + ERR_clear_error(); + n = SSL_peek(sslsock->ssl_handle, &buf, sizeof(buf)); + sslsock->deadline = NULL; + if (n <= 0) { + int err = SSL_get_error(sslsock->ssl_handle, n); + alive = err == SSL_ERROR_WANT_READ || err == SSL_ERROR_WANT_WRITE; } } else { #ifdef PHP_WIN32 @@ -3909,6 +3838,15 @@ static int php_openssl_sockop_cast(php_stream *stream, int castas, void **ret) } return SUCCESS; + case PHP_STREAM_AS_FD_FOR_POLL: + /* Bytes OpenSSL holds decrypted are not readiness of the socket: a + * watcher reports the descriptor, and the stream's unread_bytes stay + * what they were */ + if (ret) { + *(php_socket_t *)ret = sslsock->s.socket; + } + return SUCCESS; + case PHP_STREAM_AS_FD: case PHP_STREAM_AS_SOCKETD: if (sslsock->ssl_active) { diff --git a/ext/pcntl/pcntl.c b/ext/pcntl/pcntl.c index 0a40bd0f2dfe..ce2ced239ef0 100644 --- a/ext/pcntl/pcntl.c +++ b/ext/pcntl/pcntl.c @@ -26,6 +26,8 @@ #endif #include "php.h" +#include "php_io.h" +#include "ext/standard/io_poll.h" #include "ext/standard/info.h" #include "ext/standard/php_filestat.h" #include "php_signal.h" @@ -191,6 +193,7 @@ static void pcntl_siginfo_to_zval(int, siginfo_t*, zval*); static void pcntl_signal_dispatch(void); static void pcntl_signal_dispatch_tick_function(int dummy_int, void *dummy_pointer); static void pcntl_interrupt_function(zend_execute_data *execute_data); +static bool pcntl_signal_pending(void); static PHP_GINIT_FUNCTION(pcntl) { @@ -224,6 +227,7 @@ PHP_MINIT_FUNCTION(pcntl) register_pcntl_symbols(module_number); orig_interrupt_function = zend_interrupt_function; zend_interrupt_function = pcntl_interrupt_function; + php_io_signal_pending = pcntl_signal_pending; return SUCCESS; } @@ -271,7 +275,14 @@ PHP_FUNCTION(pcntl_fork) ZEND_PARSE_PARAMETERS_NONE(); + if (php_io_ops_in_flight() > 0) { + /* An operation submitted to a queue would not be in flight in the child */ + zend_throw_error(NULL, "Cannot fork while IO operations are in flight"); + RETURN_THROWS(); + } + id = fork(); + php_io_child_forget(id); if (id == -1) { PCNTL_G(last_error) = errno; switch (errno) { @@ -384,10 +395,12 @@ PHP_FUNCTION(pcntl_waitpid) memset(&rusage, 0, sizeof(struct rusage)); child_id = wait4((pid_t) pid, &status, options, &rusage); } else { - child_id = waitpid((pid_t) pid, &status, options); + php_deadline dl = php_io_deadline_infinite(); + child_id = php_io_waitpid(NULL, (pid_t) pid, &status, options, &dl); } #else - child_id = waitpid((pid_t) pid, &status, options); + php_deadline dl = php_io_deadline_infinite(); + child_id = php_io_waitpid(NULL, (pid_t) pid, &status, options, &dl); #endif if (child_id < 0) { @@ -497,13 +510,13 @@ PHP_FUNCTION(pcntl_wait) memset(&rusage, 0, sizeof(struct rusage)); child_id = wait3(&status, options, &rusage); - } else if (options) { - child_id = wait3(&status, options, NULL); } else { - child_id = wait(&status); + php_deadline dl = php_io_deadline_infinite(); + child_id = php_io_waitpid(NULL, -1, &status, options, &dl); } #else - child_id = wait(&status); + php_deadline dl = php_io_deadline_infinite(); + child_id = php_io_waitpid(NULL, -1, &status, options, &dl); #endif if (child_id < 0) { PCNTL_G(last_error) = errno; @@ -660,9 +673,32 @@ PHP_FUNCTION(pcntl_wstopsig) } /* }}} */ +/* Signals blocked for a SignalHandle are not the new program's business; + * the old mask comes back if the exec fails */ +static void pcntl_exec_set_mask(sigset_t *old_mask) +{ + sigset_t mask; + php_io_poll_signal_child_mask(&mask); +#ifdef ZTS + pthread_sigmask(SIG_SETMASK, &mask, old_mask); +#else + sigprocmask(SIG_SETMASK, &mask, old_mask); +#endif +} + +static void pcntl_exec_restore_mask(const sigset_t *old_mask) +{ +#ifdef ZTS + pthread_sigmask(SIG_SETMASK, old_mask, NULL); +#else + sigprocmask(SIG_SETMASK, old_mask, NULL); +#endif +} + /* {{{ Executes specified program in current process space as defined by exec(2) */ PHP_FUNCTION(pcntl_exec) { + sigset_t old_mask; zval *args = NULL; HashTable *env_vars_ht = NULL; zval *element; @@ -760,8 +796,10 @@ PHP_FUNCTION(pcntl_exec) } ZEND_HASH_FOREACH_END(); *(pair) = NULL; + pcntl_exec_set_mask(&old_mask); if (execve(path, argv, envp) == -1) { PCNTL_G(last_error) = errno; + pcntl_exec_restore_mask(&old_mask); php_error_docref(NULL, E_WARNING, "Error has occurred: (errno %d) %s", errno, strerror(errno)); } @@ -771,8 +809,10 @@ PHP_FUNCTION(pcntl_exec) efree(envp); } else { + pcntl_exec_set_mask(&old_mask); if (execv(path, argv) == -1) { PCNTL_G(last_error) = errno; + pcntl_exec_restore_mask(&old_mask); php_error_docref(NULL, E_WARNING, "Error has occurred: (errno %d) %s", errno, strerror(errno)); } } @@ -1029,7 +1069,8 @@ PHP_FUNCTION(pcntl_sigwaitinfo) errno = 0; siginfo_t siginfo; - int signal_no = sigwaitinfo(&set, &siginfo); + php_deadline dl = php_io_deadline_infinite(); + int signal_no = php_io_sigwait(NULL, &set, &siginfo, &dl); /* sigwaitinfo() never sets errno to EAGAIN according to POSIX */ if (signal_no == -1) { PCNTL_G(last_error) = errno; @@ -1089,10 +1130,11 @@ PHP_FUNCTION(pcntl_sigtimedwait) errno = 0; siginfo_t siginfo; - struct timespec timeout; - timeout.tv_sec = (time_t) tv_sec; - timeout.tv_nsec = tv_nsec; - int signal_no = sigtimedwait(&set, &siginfo, &timeout); + /* Too far to represent is as good as forever */ + zend_hrtime_t ns = (zend_hrtime_t) tv_sec >= ZEND_HRTIME_T_MAX / ZEND_NANO_IN_SEC + ? ZEND_HRTIME_T_MAX : (zend_hrtime_t) tv_sec * ZEND_NANO_IN_SEC + tv_nsec; + php_deadline dl = php_io_deadline_from_ns(ns); + int signal_no = php_io_sigwait(NULL, &set, &siginfo, &dl); if (signal_no == -1) { if (errno != EAGAIN) { PCNTL_G(last_error) = errno; @@ -1586,7 +1628,14 @@ PHP_FUNCTION(pcntl_rfork) } #endif + if (php_io_ops_in_flight() > 0) { + /* An operation submitted to a queue would not be in flight in the child */ + zend_throw_error(NULL, "Cannot fork while IO operations are in flight"); + RETURN_THROWS(); + } + pid = rfork(flags); + php_io_child_forget(pid); if (pid == -1) { PCNTL_G(last_error) = errno; @@ -1626,7 +1675,14 @@ PHP_FUNCTION(pcntl_forkx) RETURN_THROWS(); } + if (php_io_ops_in_flight() > 0) { + /* An operation submitted to a queue would not be in flight in the child */ + zend_throw_error(NULL, "Cannot fork while IO operations are in flight"); + RETURN_THROWS(); + } + pid = forkx(flags); + php_io_child_forget(pid); if (pid == -1) { PCNTL_G(last_error) = errno; @@ -1945,6 +2001,11 @@ PHP_FUNCTION(pcntl_setqos_class) } #endif +static bool pcntl_signal_pending(void) +{ + return PCNTL_G(pending_signals); +} + static void pcntl_interrupt_function(zend_execute_data *execute_data) { pcntl_signal_dispatch(); diff --git a/ext/pcntl/tests/pcntl_sigtimedwait_large_timeout.phpt b/ext/pcntl/tests/pcntl_sigtimedwait_large_timeout.phpt new file mode 100644 index 000000000000..d04696621567 --- /dev/null +++ b/ext/pcntl/tests/pcntl_sigtimedwait_large_timeout.phpt @@ -0,0 +1,18 @@ +--TEST-- +pcntl_sigtimedwait(): a timeout too large for nanoseconds waits instead of wrapping around +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) diff --git a/ext/pcntl/tests/sleep_interrupted_by_signal.phpt b/ext/pcntl/tests/sleep_interrupted_by_signal.phpt new file mode 100644 index 000000000000..4782530050b2 --- /dev/null +++ b/ext/pcntl/tests/sleep_interrupted_by_signal.phpt @@ -0,0 +1,49 @@ +--TEST-- +A signal interrupts sleep(), usleep(), time_nanosleep() and stream_socket_accept() +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + 0 && $r < 10); + +pcntl_alarm(1); +$t = microtime(true); +usleep(10000000); +var_dump(microtime(true) - $t < 9); + +pcntl_alarm(1); +$r = time_nanosleep(10, 0); +var_dump($r['seconds'] < 10, $r['nanoseconds'] >= 0); + +/* The seconds do not overflow into a short sleep */ +pcntl_alarm(1); +$r = time_nanosleep(PHP_INT_MAX, 0); +var_dump($r['seconds'] > PHP_INT_MAX - 10); + +$s = stream_socket_server("tcp://127.0.0.1:0"); +pcntl_alarm(1); +var_dump(stream_socket_accept($s, -1)); +?> +--EXPECTF-- +handler +bool(true) +handler +bool(true) +handler +bool(true) +bool(true) +handler +bool(true) + +Warning: stream_socket_accept(): Accept failed: Interrupted system call in %s on line %d +handler +bool(false) diff --git a/ext/pcntl/tests/stdin_read_interrupted_by_signals.phpt b/ext/pcntl/tests/stdin_read_interrupted_by_signals.phpt new file mode 100644 index 000000000000..ae21c8c5fecd --- /dev/null +++ b/ext/pcntl/tests/stdin_read_interrupted_by_signals.phpt @@ -0,0 +1,37 @@ +--TEST-- +A read on a pipe retries once after EINTR and then gives up +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => ['pipe', 'w']], $pipes); +echo fgets($pipes[1]); +usleep(300000); +proc_terminate($p, SIGUSR1); +usleep(300000); +proc_terminate($p, SIGUSR1); +usleep(300000); +/* Unblocks the child if it still reads */ +@fwrite($pipes[0], "data\n"); +echo stream_get_contents($pipes[1]); +proc_close($p); +?> +--EXPECT-- +ready +signal +signal +bool(false) diff --git a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt index c497855f7347..d0fa618ebe5e 100644 --- a/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt +++ b/ext/reflection/tests/ReflectionExtension_getClassNames_basic.phpt @@ -8,13 +8,39 @@ $standard = new ReflectionExtension('standard'); $classNames = $standard->getClassNames(); sort($classNames); foreach ($classNames as $className) { + /* Only with --with-ior, see ReflectionExtension_getClassNames_ring.phpt */ + if (str_starts_with($className, 'Io\\Ring\\')) { + continue; + } echo $className, PHP_EOL; } ?> --EXPECT-- AssertionError Directory +Io\Completion +Io\CompletionStatus +Io\Hooks\Capability +Io\Hooks\Hooks +Io\InvalidOperationException +Io\InvalidRegistrationException Io\IoException +Io\Operation +Io\OperationQueue +Io\Operation\Accept +Io\Operation\Any +Io\Operation\Connect +Io\Operation\Fsync +Io\Operation\GetAddrInfo +Io\Operation\GetNameInfo +Io\Operation\Poll +Io\Operation\Read +Io\Operation\Recv +Io\Operation\Send +Io\Operation\SigWait +Io\Operation\Timer +Io\Operation\WaitPid +Io\Operation\Write Io\Poll\Backend Io\Poll\BackendUnavailableException Io\Poll\Context @@ -28,8 +54,16 @@ Io\Poll\Handle Io\Poll\HandleAlreadyWatchedException Io\Poll\InactiveWatcherException Io\Poll\InvalidHandleException +Io\Poll\NotifyHandle +Io\Poll\OperationQueue Io\Poll\PollException +Io\Poll\ProcessHandle +Io\Poll\SignalHandle +Io\Poll\TimerHandle +Io\Poll\Trigger Io\Poll\Watcher +Io\Poll\WeakHandle +Io\Registration RoundingMode SortDirection StreamBucket @@ -39,5 +73,6 @@ StreamErrorMode StreamErrorStore StreamException StreamPollHandle +StreamPollWeakHandle __PHP_Incomplete_Class -php_user_filter +php_user_filter \ No newline at end of file diff --git a/ext/reflection/tests/ReflectionExtension_getClassNames_ring.phpt b/ext/reflection/tests/ReflectionExtension_getClassNames_ring.phpt new file mode 100644 index 000000000000..ef78f7b5246d --- /dev/null +++ b/ext/reflection/tests/ReflectionExtension_getClassNames_ring.phpt @@ -0,0 +1,20 @@ +--TEST-- +ReflectionExtension::getClassNames() lists the Io\Ring classes of a build with ior +--SKIPIF-- + +--FILE-- +getClassNames(), fn ($c) => str_starts_with($c, 'Io\\Ring\\')); +sort($classNames); +foreach ($classNames as $className) { + echo $className, PHP_EOL; +} +?> +--EXPECT-- +Io\Ring\Backend +Io\Ring\Engine +Io\Ring\FailedRingOperationException +Io\Ring\RingException diff --git a/ext/sockets/php_sockets.h b/ext/sockets/php_sockets.h index 4d047885dbaa..bb8431c3bd25 100644 --- a/ext/sockets/php_sockets.h +++ b/ext/sockets/php_sockets.h @@ -25,6 +25,7 @@ #ifdef HAVE_SOCKETS #include +#include "php_io_hooks.h" #ifdef PHP_WIN32 # include "windows_common.h" #else @@ -64,12 +65,18 @@ typedef SOCKET PHP_SOCKET; /* Socket class */ +typedef struct _php_socket_poll_handle_data php_socket_poll_handle_data; + typedef struct { PHP_SOCKET bsd_socket; int type; int error; int blocking; + bool nonblocking_fd; /* blocking is emulated */ + bool in_use; /* an operation on it is in flight */ zval zstream; + zend_object *weak_handle; /* SocketPollWeakHandle, referenced until the descriptor closes */ + php_socket_poll_handle_data *strong_handles; /* SocketPollHandle objects, retired with it */ zend_object std; } php_socket; @@ -86,6 +93,14 @@ extern PHP_SOCKETS_API zend_class_entry *socket_ce; } \ } while (0) +/* Not while an operation of another flow is on it */ +#define ENSURE_SOCKET_FREE(php_sock) do { \ + if ((php_sock)->in_use) { \ + zend_throw_error(NULL, "Concurrent access to a socket"); \ + RETURN_THROWS(); \ + } \ +} while (0) + #ifdef PHP_WIN32 struct sockaddr_un { short sun_family; @@ -124,6 +139,31 @@ enum sockopt_return { PHP_SOCKETS_API char *sockets_strerror(int error); PHP_SOCKETS_API bool socket_import_file_descriptor(PHP_SOCKET socket, php_socket *retsock); +#define PHP_SOCKET_EMULATES_BLOCKING(sock) ((sock)->blocking && (sock)->nonblocking_fd) + +/* A call that may block under a provider: the Socket is frozen for it, and its operations carry + * the stream it shares its descriptor with or, without one, its handle */ +typedef struct php_socket_op { + php_stream *stream; + zend_object *handle; + bool active; /* a provider is installed */ +} php_socket_op; + +typedef struct php_socket_waiter { + uint64_t end; + int optname; + bool started; + php_socket_op op; + php_deadline deadline; /* the op's, from the first wait */ +} php_socket_waiter; + +#define PHP_SOCKET_WAITER(optname) { 0, (optname), false, { NULL, NULL, false }, { 0 } } + +/* false with an Error thrown when the Socket is in another operation */ +bool php_socket_op_begin(php_socket *sock, php_socket_op *o); +void php_socket_op_end(php_socket *sock, php_socket_op *o); +bool php_socket_wait_retry(php_socket *sock, php_socket_waiter *w, int events, int flags); + #else #define phpext_sockets_ptr NULL #endif diff --git a/ext/sockets/sendrecvmsg.c b/ext/sockets/sendrecvmsg.c index cc268bd68b0a..0c3907168a6a 100644 --- a/ext/sockets/sendrecvmsg.c +++ b/ext/sockets/sendrecvmsg.c @@ -18,6 +18,7 @@ #endif #include #include "php_sockets.h" +#include "php_network.h" #include "sendrecvmsg.h" #include "conversions.h" #include @@ -202,10 +203,20 @@ PHP_FUNCTION(socket_sendmsg) RETURN_FALSE; } - res = sendmsg(php_sock->bsd_socket, msghdr, (int)flags); + php_socket_waiter w = PHP_SOCKET_WAITER(SO_SNDTIMEO); + if (!php_socket_op_begin(php_sock, &w.op)) { + allocations_dispose(&allocations); + RETURN_THROWS(); + } + do { + res = sendmsg(php_sock->bsd_socket, msghdr, (int)flags); + } while (res == -1 && php_socket_wait_retry(php_sock, &w, POLLOUT, (int)flags)); + php_socket_op_end(php_sock, &w.op); if (res != -1) { RETVAL_LONG((zend_long)res); + } else if (EG(exception)) { + RETVAL_FALSE; } else { PHP_SOCKET_ERROR(php_sock, "Error in sendmsg", errno); RETVAL_FALSE; @@ -243,7 +254,15 @@ PHP_FUNCTION(socket_recvmsg) RETURN_FALSE; } - res = recvmsg(php_sock->bsd_socket, msghdr, (int)flags); + php_socket_waiter w = PHP_SOCKET_WAITER(SO_RCVTIMEO); + if (!php_socket_op_begin(php_sock, &w.op)) { + allocations_dispose(&allocations); + RETURN_THROWS(); + } + do { + res = recvmsg(php_sock->bsd_socket, msghdr, (int)flags); + } while (res == -1 && php_socket_wait_retry(php_sock, &w, PHP_POLLREADABLE, (int)flags)); + php_socket_op_end(php_sock, &w.op); if (res != -1) { zval *zres, tmp; @@ -269,6 +288,8 @@ PHP_FUNCTION(socket_recvmsg) assert(zres == NULL); } RETVAL_LONG((zend_long)res); + } else if (EG(exception)) { + RETVAL_FALSE; } else { SOCKETS_G(last_error) = errno; php_error_docref(NULL, E_WARNING, "Error in recvmsg [%d]: %s", diff --git a/ext/sockets/sockets.c b/ext/sockets/sockets.c index 1d4214ea9bfc..8f819efafe34 100644 --- a/ext/sockets/sockets.c +++ b/ext/sockets/sockets.c @@ -17,6 +17,7 @@ */ #include "zend_exceptions.h" +#include "zend_hrtime.h" #ifdef HAVE_CONFIG_H #include #endif @@ -26,6 +27,7 @@ #include "php_network.h" #include "ext/standard/file.h" #include "ext/standard/info.h" +#include "ext/standard/io_poll.h" #include "php_ini.h" #ifdef PHP_WIN32 # include "windows_common.h" @@ -124,6 +126,10 @@ static PHP_RSHUTDOWN_FUNCTION(sockets); zend_class_entry *socket_ce; static zend_object_handlers socket_object_handlers; +zend_class_entry *socket_poll_handle_ce; +zend_class_entry *socket_poll_weak_handle_ce; +static zend_object_handlers socket_poll_handle_object_handlers; +static zend_object_handlers socket_poll_weak_handle_object_handlers; static zend_object *socket_create_object(zend_class_entry *class_type) { php_socket *intern = zend_object_alloc(sizeof(php_socket), class_type); @@ -135,20 +141,292 @@ static zend_object *socket_create_object(zend_class_entry *class_type) { intern->type = PF_UNSPEC; intern->error = 0; intern->blocking = 1; + intern->nonblocking_fd = false; + intern->in_use = false; ZVAL_UNDEF(&intern->zstream); + intern->weak_handle = NULL; + intern->strong_handles = NULL; return &intern->std; } +/* SocketPollHandle keeps its Socket and is retired with the descriptor; SocketPollWeakHandle is + * the Socket's one handle, created by its operations and registrations or by create(), kept by the + * Socket until the descriptor closes and invalid from then on */ + +struct _php_socket_poll_handle_data { + php_socket *sock; /* referenced */ + php_poll_handle_object *handle; + bool linked; /* on the Socket's list, until the descriptor closes */ + php_socket_poll_handle_data *prev; + php_socket_poll_handle_data *next; +}; + +typedef struct php_socket_poll_weak_handle_data { + php_socket *sock; /* not referenced, NULL once the descriptor closed */ +} php_socket_poll_weak_handle_data; + +static php_socket_t socket_poll_handle_get_fd(php_poll_handle_object *handle) +{ + php_socket_poll_handle_data *data = handle->handle_data; + if (!data || IS_INVALID_SOCKET(data->sock)) { + return SOCK_ERR; + } + return (php_socket_t) data->sock->bsd_socket; +} + +static int socket_poll_handle_is_valid(php_poll_handle_object *handle) +{ + return socket_poll_handle_get_fd(handle) != SOCK_ERR; +} + +static void socket_poll_handle_unlink(php_socket_poll_handle_data *data) +{ + if (!data->linked) { + return; + } + data->linked = false; + if (data->prev) { + data->prev->next = data->next; + } else { + data->sock->strong_handles = data->next; + } + if (data->next) { + data->next->prev = data->prev; + } +} + +static void socket_poll_handle_cleanup(php_poll_handle_object *handle) +{ + php_socket_poll_handle_data *data = handle->handle_data; + if (data) { + socket_poll_handle_unlink(data); + OBJ_RELEASE(&data->sock->std); + efree(data); + handle->handle_data = NULL; + } +} + +static php_poll_handle_ops socket_poll_handle_ops = { + .get_fd = socket_poll_handle_get_fd, + .is_valid = socket_poll_handle_is_valid, + .cleanup = socket_poll_handle_cleanup, +}; + +static zend_object *socket_poll_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &socket_poll_handle_ops); + intern->std.handlers = &socket_poll_handle_object_handlers; + return &intern->std; +} + +static HashTable *socket_poll_handle_get_gc(zend_object *object, zval **table, int *n) +{ + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(object); + php_socket_poll_handle_data *data = intern->handle_data; + zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); + if (data) { + zend_get_gc_buffer_add_obj(gc_buffer, &data->sock->std); + } + zend_get_gc_buffer_use(gc_buffer, table, n); + return NULL; +} + +static php_socket_t socket_poll_weak_handle_get_fd(php_poll_handle_object *handle) +{ + php_socket_poll_weak_handle_data *data = handle->handle_data; + if (!data || !data->sock || IS_INVALID_SOCKET(data->sock)) { + return SOCK_ERR; + } + return (php_socket_t) data->sock->bsd_socket; +} + +static int socket_poll_weak_handle_is_valid(php_poll_handle_object *handle) +{ + return socket_poll_weak_handle_get_fd(handle) != SOCK_ERR; +} + +static void socket_poll_weak_handle_cleanup(php_poll_handle_object *handle) +{ + php_socket_poll_weak_handle_data *data = handle->handle_data; + if (data) { + /* Freed by the object store before the Socket let go of it */ + if (data->sock) { + data->sock->weak_handle = NULL; + } + efree(data); + handle->handle_data = NULL; + } +} + +static php_poll_handle_ops socket_poll_weak_handle_ops = { + .get_fd = socket_poll_weak_handle_get_fd, + .is_valid = socket_poll_weak_handle_is_valid, + .cleanup = socket_poll_weak_handle_cleanup, +}; + +static zend_object *socket_poll_weak_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &socket_poll_weak_handle_ops); + intern->std.handlers = &socket_poll_weak_handle_object_handlers; + return &intern->std; +} + +/* The Socket's handle, created on the first call and kept by the Socket: borrowed */ +static zend_object *php_socket_get_weak_handle(php_socket *sock) +{ + if (!sock->weak_handle) { + zval handle_zv; + object_init_ex(&handle_zv, socket_poll_weak_handle_ce); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(&handle_zv); + php_socket_poll_weak_handle_data *data = emalloc(sizeof(*data)); + data->sock = sock; + intern->handle_data = data; + /* Every Socket is a script's */ + intern->flags |= PHP_POLL_HANDLE_F_EXPOSED; + sock->weak_handle = Z_OBJ(handle_zv); + } + return sock->weak_handle; +} + +/* In an op, or its descriptor still used by an op whose frame is gone */ +static bool php_socket_busy(php_socket *sock) +{ + return sock->in_use || (sock->weak_handle && php_io_handle_busy(sock->weak_handle)); +} + +/* Before the descriptor closes */ +static void php_socket_drain(php_socket *sock) +{ + if (sock->weak_handle) { + php_io_handle_drain(sock->weak_handle); + } +} + +/* While the descriptor is still open: the handles stop reporting, the registrations end with the + * provider's remove() per pair, then the Socket drops its handle */ +static void php_socket_release_io(php_socket *sock) +{ + while (sock->strong_handles) { + php_socket_poll_handle_data *data = sock->strong_handles; + socket_poll_handle_unlink(data); + /* Retiring the watchers may drop the last reference to the handle */ + php_poll_handle_invalidate(&data->handle->std); + } + zend_object *handle_obj = sock->weak_handle; + if (handle_obj) { + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + ((php_socket_poll_weak_handle_data *) handle->handle_data)->sock = NULL; + php_poll_handle_invalidate(handle_obj); + php_io_unregister_all(&handle->registrations); + /* A remove() that closed the Socket again let go of it already */ + if (sock->weak_handle == handle_obj) { + sock->weak_handle = NULL; + OBJ_RELEASE(handle_obj); + } + } +} + +PHP_METHOD(SocketPollHandle, __construct) +{ + zval *zsocket; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(zsocket, socket_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + if (intern->handle_data) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); + } + php_socket *sock = Z_SOCKET_P(zsocket); + ENSURE_SOCKET_VALID(sock); + + php_socket_poll_handle_data *data = emalloc(sizeof(*data)); + data->sock = sock; + data->handle = intern; + data->linked = true; + data->prev = NULL; + data->next = sock->strong_handles; + if (data->next) { + data->next->prev = data; + } + sock->strong_handles = data; + GC_ADDREF(&sock->std); + intern->handle_data = data; +} + +PHP_METHOD(SocketPollHandle, getSocket) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_socket_poll_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + RETURN_OBJ_COPY(&data->sock->std); +} + +PHP_METHOD(SocketPollHandle, isValid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + RETURN_BOOL(intern->ops->is_valid(intern)); +} + +PHP_METHOD(SocketPollWeakHandle, __construct) +{ + zend_throw_error(NULL, "Direct instantiation of %pS is not allowed, use %pS::create instead", + Z_OBJCE_P(ZEND_THIS)->name, Z_OBJCE_P(ZEND_THIS)->name); +} + +PHP_METHOD(SocketPollWeakHandle, create) +{ + zval *zsocket; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(zsocket, socket_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_socket *sock = Z_SOCKET_P(zsocket); + ENSURE_SOCKET_VALID(sock); + RETURN_OBJ_COPY(php_socket_get_weak_handle(sock)); +} + +/* Not while the Socket is in an operation */ +PHP_METHOD(SocketPollWeakHandle, getSocket) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_socket_poll_weak_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + if (!data || !data->sock || php_socket_busy(data->sock)) { + RETURN_NULL(); + } + RETURN_OBJ_COPY(&data->sock->std); +} + +PHP_METHOD(SocketPollWeakHandle, isValid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + RETURN_BOOL(intern->ops->is_valid(intern)); +} + static zend_function *socket_get_constructor(zend_object *object) { zend_throw_error(NULL, "Cannot directly construct Socket, use socket_create() instead"); return NULL; } +static void php_socket_release_io(php_socket *sock); + static void socket_free_obj(zend_object *object) { php_socket *socket = socket_from_obj(object); + php_socket_drain(socket); + php_socket_release_io(socket); if (Z_ISUNDEF(socket->zstream)) { if (!IS_INVALID_SOCKET(socket)) { close(socket->bsd_socket); @@ -212,8 +490,15 @@ static void address_info_free_obj(zend_object *object) /* Module registration */ +/* The poll handle interfaces are ext/standard's, so it starts first */ +static const zend_module_dep sockets_deps[] = { + ZEND_MOD_REQUIRED("standard") + ZEND_MOD_END +}; + zend_module_entry sockets_module_entry = { - STANDARD_MODULE_HEADER, + STANDARD_MODULE_HEADER_EX, NULL, + sockets_deps, "sockets", ext_functions, PHP_MINIT(sockets), @@ -237,6 +522,311 @@ zend_module_entry sockets_module_entry = { ZEND_GET_MODULE(sockets) #endif +#ifdef PHP_WIN32 +# define PHP_SOCKET_WAIT_TIMEOUT WSAETIMEDOUT +#else +# define PHP_SOCKET_WAIT_TIMEOUT EAGAIN +#endif + +/* Waits for a Socket that emulates blocking mode, bounded by its SO_RCVTIMEO + * or SO_SNDTIMEO like the blocking call. false with errno set on failure. */ +static bool php_socket_wait(php_socket *sock, php_socket_waiter *w, int events) +{ + if (!w->started) { + w->started = true; + w->end = 0; +#ifdef PHP_WIN32 + DWORD ms = 0; + int optlen = sizeof(ms); + if (getsockopt(sock->bsd_socket, SOL_SOCKET, w->optname, (char *) &ms, &optlen) == 0 && ms > 0) { + w->end = zend_hrtime() + (uint64_t) ms * 1000000; + } +#else + struct timeval tv; + socklen_t optlen = sizeof(tv); + if (getsockopt(sock->bsd_socket, SOL_SOCKET, w->optname, &tv, &optlen) == 0 + && (tv.tv_sec > 0 || tv.tv_usec > 0)) { + w->end = zend_hrtime() + (uint64_t) tv.tv_sec * ZEND_NANO_IN_SEC + (uint64_t) tv.tv_usec * 1000; + } +#endif + } + + for (;;) { + int timeout = -1; + if (w->end) { + uint64_t now = zend_hrtime(); + uint64_t ms = now < w->end ? (w->end - now + 999999) / 1000000 : 0; + timeout = ms > INT_MAX ? INT_MAX : (int) ms; + } + int n = php_pollfd_for_ms(sock->bsd_socket, events, timeout); + if (n > 0) { + return true; + } + if (n == 0) { + set_errno(PHP_SOCKET_WAIT_TIMEOUT); + return false; + } + /* a timed call fails on a signal, otherwise only when PHP has work for it */ + if (errno != EINTR || w->end || atomic_load(&EG(vm_interrupt))) { + return false; + } + } +} + +/* Under a provider a blocking point is an operation that may suspend: the descriptor is made + * non-blocking once, so no syscall blocks and the Socket emulates its mode from then on, the Socket + * is frozen for the call, and the op carries the stream the Socket shares its descriptor with or + * the Socket's own handle. Without a provider the call proceeds as before. */ +bool php_socket_op_begin(php_socket *sock, php_socket_op *o) +{ + o->stream = NULL; + o->handle = NULL; + o->active = php_io_hooks_active(); + if (php_socket_busy(sock)) { + o->active = false; + zend_throw_error(NULL, "Concurrent access to a socket"); + set_errno(ECANCELED); + return false; + } + if (!o->active) { + return true; + } + if (sock->blocking && !sock->nonblocking_fd + && php_set_sock_blocking(sock->bsd_socket, 0) == SUCCESS) { + sock->nonblocking_fd = true; + } + if (!Z_ISUNDEF(sock->zstream)) { + o->stream = zend_fetch_resource2_ex(&sock->zstream, NULL, php_file_le_stream(), + php_file_le_pstream()); + } + if (!o->stream) { + o->handle = php_socket_get_weak_handle(sock); + } + sock->in_use = true; + return true; +} + +void php_socket_op_end(php_socket *sock, php_socket_op *o) +{ + if (o->active) { + sock->in_use = false; + } +} + +/* The op's deadline from the Socket's mode: the blocking call's SO_RCVTIMEO or SO_SNDTIMEO */ +static php_deadline php_socket_op_deadline(php_socket *sock, int optname, int flags) +{ + php_deadline dl; + if (!sock->blocking +#ifdef MSG_DONTWAIT + || (flags & MSG_DONTWAIT) +#endif + ) { + php_deadline_init_nonblock(&dl); + return dl; + } + struct timeval tv = { 0, 0 }; +#ifdef PHP_WIN32 + DWORD ms = 0; + int optlen = sizeof(ms); + if (getsockopt(sock->bsd_socket, SOL_SOCKET, optname, (char *) &ms, &optlen) == 0 && ms > 0) { + tv.tv_sec = ms / 1000; + tv.tv_usec = (ms % 1000) * 1000; + } +#else + socklen_t optlen = sizeof(tv); + if (getsockopt(sock->bsd_socket, SOL_SOCKET, optname, &tv, &optlen) != 0) { + tv.tv_sec = tv.tv_usec = 0; + } +#endif + if (tv.tv_sec > 0 || tv.tv_usec > 0) { + php_deadline_init(&dl, &tv); + } else { + php_deadline_init_infinite(&dl); + } + return dl; +} + +/* An op that timed out fails like the blocking call */ +static zend_always_inline void php_socket_op_errno(ssize_t n) +{ + if (n < 0 && errno == PHP_IO_SOCK_ETIMEDOUT) { + set_errno(PHP_SOCKET_WAIT_TIMEOUT); + } +} + +bool php_socket_wait_retry(php_socket *sock, php_socket_waiter *w, int events, int flags) +{ + if (!PHP_SOCKET_EMULATES_BLOCKING(sock)) { + return false; + } + if (!PHP_IS_TRANSIENT_ERROR(errno)) { + return false; + } +#ifdef MSG_DONTWAIT + if (flags & MSG_DONTWAIT) { + return false; + } +#endif + if (w->op.active) { + /* A readiness op after the drain; the deadline is the call's, not the wait's */ + if (!w->started) { + w->started = true; + w->deadline = php_socket_op_deadline(sock, w->optname, flags); + } + int n = php_io_poll_ex(w->op.stream, w->op.handle, sock->bsd_socket, + (events & POLLIN) ? PHP_POLL_READ : PHP_POLL_WRITE, &w->deadline, PHP_IO_OP_F_AFTER_DRAIN); + if (n == 0) { + set_errno(PHP_SOCKET_WAIT_TIMEOUT); + } + return n > 0; + } + return php_socket_wait(sock, w, events); +} + +static ssize_t php_socket_recv_once(php_socket *sock, php_socket_op *o, char *buf, size_t len, + int flags) +{ + if (!o->active) { + return recv(sock->bsd_socket, buf, len, flags); + } + php_deadline dl = php_socket_op_deadline(sock, SO_RCVTIMEO, flags); + ssize_t n = php_io_recv_ex(o->stream, o->handle, sock->bsd_socket, buf, len, flags, &dl); + php_socket_op_errno(n); + return n; +} + +static ssize_t php_socket_send_once(php_socket *sock, php_socket_op *o, const char *buf, size_t len, + int flags) +{ + if (!o->active) { + return send(sock->bsd_socket, buf, len, flags); + } + php_deadline dl = php_socket_op_deadline(sock, SO_SNDTIMEO, flags); + ssize_t n = php_io_send_ex(o->stream, o->handle, sock->bsd_socket, buf, len, flags, &dl); + php_socket_op_errno(n); + return n; +} + +static ssize_t php_socket_recv(php_socket *sock, char *buf, size_t len, int flags) +{ + php_socket_waiter w = PHP_SOCKET_WAITER(SO_RCVTIMEO); + size_t got = 0; + ssize_t n; +#ifdef PHP_WIN32 + /* Winsock refuses MSG_WAITALL on a non-blocking socket (WSAEOPNOTSUPP); + * the loop below provides it for a Socket that emulates blocking mode */ + int call_flags = sock->nonblocking_fd ? flags & ~MSG_WAITALL : flags; +#else + int call_flags = flags; +#endif + + if (!php_socket_op_begin(sock, &w.op)) { + return -1; + } + for (;;) { + n = php_socket_recv_once(sock, &w.op, buf + got, len - got, call_flags); + if (n > 0) { + got += n; + if (got < len && (flags & MSG_WAITALL) && !(flags & MSG_PEEK) && PHP_SOCKET_EMULATES_BLOCKING(sock)) { + continue; + } + break; + } + if (n == 0 || w.op.active || !php_socket_wait_retry(sock, &w, PHP_POLLREADABLE, flags)) { + break; + } + } + php_socket_op_end(sock, &w.op); + return got > 0 ? (ssize_t) got : n; +} + +/* Only for a Socket that emulates blocking mode: sends everything on a + * stream socket like the blocking call */ +static ssize_t php_socket_send_all(php_socket *sock, php_socket_op *o, const char *buf, size_t len, + int flags) +{ + php_socket_waiter w = PHP_SOCKET_WAITER(SO_SNDTIMEO); + size_t sent = 0; + ssize_t n; + + w.op = *o; + for (;;) { + n = php_socket_send_once(sock, o, buf + sent, len - sent, flags); + if (n > 0) { + sent += n; + if (sent == len) { + break; + } +#ifdef MSG_DONTWAIT + if (flags & MSG_DONTWAIT) { + break; + } +#endif + continue; + } + if (n == 0 || o->active || !php_socket_wait_retry(sock, &w, POLLOUT, flags)) { + break; + } + } + return sent > 0 ? (ssize_t) sent : n; +} + +static int php_socket_connect(php_socket *sock, const struct sockaddr *addr, socklen_t addrlen) +{ + php_socket_op o; + if (!php_socket_op_begin(sock, &o)) { + return -1; + } + if (o.active) { + php_deadline dl = php_socket_op_deadline(sock, SO_SNDTIMEO, 0); + int ret = php_io_connect_ex(o.stream, o.handle, sock->bsd_socket, addr, addrlen, &dl); + php_socket_op_end(sock, &o); + php_socket_op_errno(ret); + if (ret != 0 && errno == PHP_SOCKET_WAIT_TIMEOUT) { + set_errno(EINPROGRESS); + } + return ret; + } + int ret = connect(sock->bsd_socket, addr, addrlen); + if (ret == 0 || !PHP_SOCKET_EMULATES_BLOCKING(sock)) { + return ret; + } +#ifdef PHP_WIN32 + if (errno != WSAEWOULDBLOCK) { + return ret; + } + /* a failed connect is reported in the except set */ + int events = POLLOUT | POLLPRI; +#else + if (errno != EINPROGRESS) { + return ret; + } + int events = POLLOUT; +#endif + + php_socket_waiter w = PHP_SOCKET_WAITER(SO_SNDTIMEO); + if (!php_socket_wait(sock, &w, events)) { +#ifndef PHP_WIN32 + if (errno == EAGAIN) { + errno = EINPROGRESS; + } +#endif + return -1; + } + + int err = 0; + socklen_t errlen = sizeof(err); + if (getsockopt(sock->bsd_socket, SOL_SOCKET, SO_ERROR, (char *) &err, &errlen) != 0) { + return -1; + } + if (err) { + set_errno(err); + return -1; + } + return 0; +} + static bool php_open_listen_sock(php_socket *sock, unsigned short port, int backlog) /* {{{ */ { struct sockaddr_in la = {0}; @@ -273,26 +863,71 @@ static bool php_open_listen_sock(php_socket *sock, unsigned short port, int back static bool php_accept_connect(php_socket *in_sock, php_socket *out_sock, struct sockaddr *la, socklen_t *la_len) /* {{{ */ { + php_socket_op o; + if (!php_socket_op_begin(in_sock, &o)) { + return false; + } + if (o.active) { + php_deadline dl = php_socket_op_deadline(in_sock, SO_RCVTIMEO, 0); + out_sock->bsd_socket = php_io_accept_ex(o.stream, o.handle, in_sock->bsd_socket, la, la_len, &dl); + php_socket_op_end(in_sock, &o); + if (IS_INVALID_SOCKET(out_sock)) { + if (EG(exception)) { + return false; + } + php_socket_op_errno(-1); + PHP_SOCKET_ERROR(out_sock, "unable to accept incoming connection", errno); + return false; + } +#ifndef PHP_WIN32 + int mode = fcntl(out_sock->bsd_socket, F_GETFD); + if (mode >= 0 && !(mode & FD_CLOEXEC)) { + fcntl(out_sock->bsd_socket, F_SETFD, mode | FD_CLOEXEC); + } +#endif + if (!in_sock->blocking) { + php_set_sock_blocking(out_sock->bsd_socket, 0); + } + out_sock->error = 0; + out_sock->blocking = 1; + out_sock->type = la->sa_family; + return true; + } #if defined(HAVE_ACCEPT4) int flags = SOCK_CLOEXEC; if (!in_sock->blocking) { flags |= SOCK_NONBLOCK; } - out_sock->bsd_socket = accept4(in_sock->bsd_socket, la, la_len, flags); + php_socket_waiter w = PHP_SOCKET_WAITER(SO_RCVTIMEO); + socklen_t la_size = *la_len; + do { + *la_len = la_size; + out_sock->bsd_socket = accept4(in_sock->bsd_socket, la, la_len, flags); + } while (IS_INVALID_SOCKET(out_sock) && php_socket_wait_retry(in_sock, &w, PHP_POLLREADABLE, 0)); if (IS_INVALID_SOCKET(out_sock)) { PHP_SOCKET_ERROR(out_sock, "unable to accept incoming connection", errno); return false; } #else - out_sock->bsd_socket = accept(in_sock->bsd_socket, la, la_len); + php_socket_waiter w = PHP_SOCKET_WAITER(SO_RCVTIMEO); + socklen_t la_size = *la_len; + do { + *la_len = la_size; + out_sock->bsd_socket = accept(in_sock->bsd_socket, la, la_len); + } while (IS_INVALID_SOCKET(out_sock) && php_socket_wait_retry(in_sock, &w, PHP_POLLREADABLE, 0)); if (IS_INVALID_SOCKET(out_sock)) { PHP_SOCKET_ERROR(out_sock, "unable to accept incoming connection", errno); return false; } + /* the accepted socket may inherit the mode the stream gave the listening one */ + if (in_sock->nonblocking_fd) { + php_set_sock_blocking(out_sock->bsd_socket, in_sock->blocking); + } + #if !defined(PHP_WIN32) /** * for fewer and fewer platforms not supporting accept4 syscall we use fcntl instead, @@ -334,12 +969,16 @@ static int php_read(php_socket *sock, void *buf, size_t maxlen, int flags) char *t = (char *) buf; #ifndef PHP_WIN32 - m = fcntl(sock->bsd_socket, F_GETFL); - if (m < 0) { - return m; + if (sock->nonblocking_fd) { + nonblock = !sock->blocking; + } else { + m = fcntl(sock->bsd_socket, F_GETFL); + if (m < 0) { + return m; + } + nonblock = (m & O_NONBLOCK); + m = 0; } - nonblock = (m & O_NONBLOCK); - m = 0; #else nonblock = !sock->blocking; #endif @@ -366,7 +1005,7 @@ static int php_read(php_socket *sock, void *buf, size_t maxlen, int flags) } if (n < maxlen) { - m = recv(sock->bsd_socket, (void *) t, 1, flags); + m = php_socket_recv(sock, t, 1, flags); } if (errno != 0 && errno != ESPIPE && errno != EAGAIN) { @@ -500,6 +1139,25 @@ static PHP_MINIT_FUNCTION(sockets) address_info_object_handlers.clone_obj = NULL; address_info_object_handlers.compare = zend_objects_not_comparable; + socket_poll_handle_ce = register_class_SocketPollHandle(php_io_poll_handle_class_entry); + socket_poll_handle_ce->create_object = socket_poll_handle_create_object; + memcpy(&socket_poll_handle_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + socket_poll_handle_object_handlers.offset = offsetof(php_poll_handle_object, std); + socket_poll_handle_object_handlers.free_obj = php_poll_handle_object_free; + socket_poll_handle_object_handlers.get_gc = socket_poll_handle_get_gc; + socket_poll_handle_object_handlers.clone_obj = NULL; + socket_poll_handle_ce->default_object_handlers = &socket_poll_handle_object_handlers; + + socket_poll_weak_handle_ce = register_class_SocketPollWeakHandle( + php_io_poll_weak_handle_class_entry); + socket_poll_weak_handle_ce->create_object = socket_poll_weak_handle_create_object; + memcpy(&socket_poll_weak_handle_object_handlers, &std_object_handlers, + sizeof(zend_object_handlers)); + socket_poll_weak_handle_object_handlers.offset = offsetof(php_poll_handle_object, std); + socket_poll_weak_handle_object_handlers.free_obj = php_poll_handle_object_free; + socket_poll_weak_handle_object_handlers.clone_obj = NULL; + socket_poll_weak_handle_ce->default_object_handlers = &socket_poll_weak_handle_object_handlers; + register_sockets_symbols(module_number); php_socket_sendrecvmsg_init(INIT_FUNC_ARGS_PASSTHRU); @@ -610,6 +1268,268 @@ static void php_sock_array_from_fd_set(zval *sock_array, fd_set *fds) /* {{{ */ } /* }}} */ +/* Under a provider socket_select() is what stream_select() is: a zero-timeout poll() over the + * sets first, then one Any op of a Poll member per Socket and a Timer member for the timeout, with + * every Socket of the sets frozen for the wait, and the stream a Socket shares its descriptor + * with frozen and carried like the Socket's own operations carry it. */ + +typedef struct php_socket_select_member { + php_socket *sock; + php_socket_t fd; + uint32_t events; +} php_socket_select_member; + +typedef struct php_socket_select_frozen { + php_socket **socks; + php_stream **streams; + uint32_t n_socks, cap_socks, n_streams, cap_streams; +} php_socket_select_frozen; + +static bool php_socket_select_freeze(HashTable *sock_array, php_socket_select_frozen *fz) +{ + ZEND_HASH_FOREACH_VAL(sock_array, zval *element) { + ZVAL_DEREF(element); + php_socket *sock = Z_SOCKET_P(element); + bool seen = false; + for (uint32_t i = 0; i < fz->n_socks; i++) { + if (fz->socks[i] == sock) { + seen = true; + break; + } + } + if (seen) { + continue; + } + if (php_socket_busy(sock)) { + zend_throw_error(NULL, "Concurrent access to a socket"); + return false; + } + php_stream *stream = NULL; + if (!Z_ISUNDEF(sock->zstream)) { + stream = zend_fetch_resource2_ex(&sock->zstream, NULL, php_file_le_stream(), + php_file_le_pstream()); + } + if (stream) { + if (stream->flags & PHP_STREAM_FLAG_IN_USE) { + zend_throw_error(NULL, "Concurrent access to a stream"); + return false; + } + if (fz->n_streams == fz->cap_streams) { + fz->cap_streams = fz->cap_streams ? fz->cap_streams * 2 : 8; + fz->streams = safe_erealloc(fz->streams, fz->cap_streams, sizeof(*fz->streams), 0); + } + stream->flags |= PHP_STREAM_FLAG_IN_USE; + GC_ADDREF(stream->res); + fz->streams[fz->n_streams++] = stream; + } + if (fz->n_socks == fz->cap_socks) { + fz->cap_socks = fz->cap_socks ? fz->cap_socks * 2 : 8; + fz->socks = safe_erealloc(fz->socks, fz->cap_socks, sizeof(*fz->socks), 0); + } + sock->in_use = true; + fz->socks[fz->n_socks++] = sock; + } ZEND_HASH_FOREACH_END(); + return true; +} + +static void php_socket_select_unfreeze(php_socket_select_frozen *fz) +{ + for (uint32_t i = 0; i < fz->n_socks; i++) { + fz->socks[i]->in_use = false; + } + for (uint32_t i = 0; i < fz->n_streams; i++) { + zend_resource *res = fz->streams[i]->res; + fz->streams[i]->flags &= ~PHP_STREAM_FLAG_IN_USE; + zend_list_delete(res); + } + if (fz->socks) { + efree(fz->socks); + } + if (fz->streams) { + efree(fz->streams); + } +} + +static void php_socket_select_collect(HashTable *sock_array, uint32_t events, + php_socket_select_member **members, uint32_t *n, uint32_t *cap) +{ + ZEND_HASH_FOREACH_VAL(sock_array, zval *element) { + ZVAL_DEREF(element); + php_socket *sock = Z_SOCKET_P(element); + php_socket_t fd = (php_socket_t) sock->bsd_socket; + bool merged = false; + for (uint32_t i = 0; i < *n; i++) { + if ((*members)[i].fd == fd) { + (*members)[i].events |= events; + merged = true; + break; + } + } + if (!merged) { + if (*n == *cap) { + *cap = *cap ? *cap * 2 : 8; + *members = safe_erealloc(*members, *cap, sizeof(**members), 0); + } + (*members)[*n].sock = sock; + (*members)[*n].fd = fd; + (*members)[*n].events = events; + (*n)++; + } + } ZEND_HASH_FOREACH_END(); +} + +/* Returns the number of ready descriptors, 0 on timeout, -1 with errno */ +static int php_socket_select_any(zval *r_array, zval *w_array, zval *e_array, struct timeval *tv, + fd_set *rfds, fd_set *wfds, fd_set *efds) +{ + php_socket_select_member *members = NULL; + uint32_t n = 0, cap = 0; + bool priority = php_poll_backend_supports_priority(PHP_POLL_BACKEND_AUTO); + php_socket_select_frozen fz = { NULL, NULL, 0, 0, 0, 0 }; + + if ((r_array && !php_socket_select_freeze(Z_ARRVAL_P(r_array), &fz)) + || (w_array && !php_socket_select_freeze(Z_ARRVAL_P(w_array), &fz)) + || (e_array && !php_socket_select_freeze(Z_ARRVAL_P(e_array), &fz))) { + php_socket_select_unfreeze(&fz); + set_errno(ECANCELED); + return -1; + } + + if (r_array) { + php_socket_select_collect(Z_ARRVAL_P(r_array), PHP_POLL_READ, &members, &n, &cap); + } + if (w_array) { + php_socket_select_collect(Z_ARRVAL_P(w_array), PHP_POLL_WRITE, &members, &n, &cap); + } + if (e_array) { + php_socket_select_collect(Z_ARRVAL_P(e_array), priority ? PHP_POLL_PRI : 0, &members, &n, &cap); + } + + bool checked = false; + if (n > 0) { + php_pollfd stack[16]; + php_pollfd *fds = n > sizeof(stack) / sizeof(stack[0]) ? safe_emalloc(n, sizeof(*fds), 0) : stack; + for (uint32_t i = 0; i < n; i++) { + fds[i].fd = members[i].fd; + fds[i].events = ((members[i].events & PHP_POLL_READ) ? POLLIN : 0) + | ((members[i].events & PHP_POLL_WRITE) ? POLLOUT : 0) + | ((members[i].events & PHP_POLL_PRI) ? POLLPRI : 0); + fds[i].revents = 0; + } + int ready = php_poll2(fds, n, 0); + int found = 0; + if (ready > 0) { + for (uint32_t i = 0; i < n; i++) { + short revents = fds[i].revents; + if (revents & POLLNVAL) { + found = 0; + break; + } + if ((revents & (POLLIN | POLLHUP | POLLERR)) && (members[i].events & PHP_POLL_READ)) { + PHP_SAFE_FD_SET(members[i].fd, rfds); + found++; + } + if ((revents & (POLLOUT | POLLERR)) && (members[i].events & PHP_POLL_WRITE)) { + PHP_SAFE_FD_SET(members[i].fd, wfds); + found++; + } + if ((revents & POLLPRI) && (members[i].events & PHP_POLL_PRI)) { + PHP_SAFE_FD_SET(members[i].fd, efds); + found++; + } + } + } + if (fds != stack) { + efree(fds); + } + if (found > 0 || (ready == 0 && tv && tv->tv_sec == 0 && tv->tv_usec == 0)) { + efree(members); + php_socket_select_unfreeze(&fz); + return found; + } + checked = ready == 0; + } + + php_io_op *ops = safe_emalloc(n + 1, sizeof(php_io_op), 0); + php_io_op **op_ptrs = safe_emalloc(n + 1, sizeof(php_io_op *), 0); + php_io_op_result *results = safe_emalloc(n + 1, sizeof(php_io_op_result), 0); + uint32_t n_members = 0; + + for (uint32_t i = 0; i < n; i++) { + if (members[i].events == 0) { + continue; + } + php_socket *sock = members[i].sock; + php_stream *stream = NULL; + if (!Z_ISUNDEF(sock->zstream)) { + stream = zend_fetch_resource2_ex(&sock->zstream, NULL, php_file_le_stream(), + php_file_le_pstream()); + } + php_io_op_poll(&ops[n_members], stream ? NULL : php_socket_get_weak_handle(sock), members[i].fd, + members[i].events, php_io_deadline_infinite()); + ops[n_members].stream = stream; + if (checked) { + ops[n_members].flags |= PHP_IO_OP_F_CHECKED; + } + op_ptrs[n_members] = &ops[n_members]; + n_members++; + } + uint32_t timer_index = UINT32_MAX; + if (tv) { + php_io_op_timer(&ops[n_members], php_io_deadline_from_timeval(tv)); + op_ptrs[n_members] = &ops[n_members]; + timer_index = n_members; + n_members++; + } + + int ret = 0; + if (n_members == 0) { + set_errno(EINVAL); + ret = -1; + } else { + php_io_op any; + php_io_op_result any_result; + php_io_op_any(&any, op_ptrs, n_members, results); + if (php_io_run(&any, &any_result) == FAILURE) { + set_errno(ECANCELED); + ret = -1; + } else if (any_result.status == PHP_IO_INTERRUPTED) { + set_errno(EINTR); + ret = -1; + } else { + for (uint32_t i = 0; i < any.u.any.n_results; i++) { + uint32_t index = results[i].index; + if (index == timer_index || (results[i].status != PHP_IO_DONE && results[i].status != PHP_IO_READY)) { + continue; + } + php_socket_t fd = ops[index].fd; + uint32_t revents = (uint32_t) results[i].res; + if ((revents & (PHP_POLL_READ | PHP_POLL_HUP | PHP_POLL_ERROR)) && (ops[index].u.poll.events & PHP_POLL_READ)) { + PHP_SAFE_FD_SET(fd, rfds); + ret++; + } + if ((revents & (PHP_POLL_WRITE | PHP_POLL_ERROR)) && (ops[index].u.poll.events & PHP_POLL_WRITE)) { + PHP_SAFE_FD_SET(fd, wfds); + ret++; + } + if ((revents & PHP_POLL_PRI) && (ops[index].u.poll.events & PHP_POLL_PRI)) { + PHP_SAFE_FD_SET(fd, efds); + ret++; + } + } + } + } + + efree(results); + efree(op_ptrs); + efree(ops); + if (members) { + efree(members); + } + php_socket_select_unfreeze(&fz); + return ret; +} + /* {{{ Runs the select() system call on the sets mentioned with a timeout specified by tv_sec and tv_usec */ PHP_FUNCTION(socket_select) { @@ -686,7 +1606,17 @@ PHP_FUNCTION(socket_select) tv_p = &tv; } - retval = select(max_fd+1, &rfds, &wfds, &efds, tv_p); + if (php_io_hooks_active()) { + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_ZERO(&efds); + retval = php_socket_select_any(r_array, w_array, e_array, tv_p, &rfds, &wfds, &efds); + if (retval == -1 && EG(exception)) { + RETURN_THROWS(); + } + } else { + retval = select(max_fd+1, &rfds, &wfds, &efds, tv_p); + } if (retval == -1) { SOCKETS_G(last_error) = errno; @@ -752,6 +1682,9 @@ PHP_FUNCTION(socket_accept) if (!php_accept_connect(php_sock, new_sock, (struct sockaddr*)&sa, &php_sa_len)) { zval_ptr_dtor(return_value); + if (EG(exception)) { + RETURN_THROWS(); + } RETURN_FALSE; } } @@ -783,6 +1716,12 @@ PHP_FUNCTION(socket_set_nonblock) } } + /* A descriptor switched for a provider keeps its mode: the change is logical */ + if (php_sock->nonblocking_fd && Z_ISUNDEF(php_sock->zstream)) { + php_sock->blocking = 0; + RETURN_TRUE; + } + if (php_set_sock_blocking(php_sock->bsd_socket, 0) == SUCCESS) { php_sock->blocking = 0; RETURN_TRUE; @@ -821,6 +1760,11 @@ PHP_FUNCTION(socket_set_block) } } + if (php_sock->nonblocking_fd && Z_ISUNDEF(php_sock->zstream)) { + php_sock->blocking = 1; + RETURN_TRUE; + } + if (php_set_sock_blocking(php_sock->bsd_socket, 1) == SUCCESS) { php_sock->blocking = 1; RETURN_TRUE; @@ -866,7 +1810,10 @@ PHP_FUNCTION(socket_close) php_socket *socket = Z_SOCKET_P(arg1); ENSURE_SOCKET_VALID(socket); + ENSURE_SOCKET_FREE(socket); + php_socket_drain(socket); + php_socket_release_io(socket); if (!Z_ISUNDEF(socket->zstream)) { php_stream *stream = NULL; php_stream_from_zval_no_verify(stream, &socket->zstream); @@ -918,13 +1865,27 @@ PHP_FUNCTION(socket_write) length = str_len; } + php_socket_op o; + if (!php_socket_op_begin(php_sock, &o)) { + RETURN_THROWS(); + } + if (PHP_SOCKET_EMULATES_BLOCKING(php_sock)) { + retval = php_socket_send_all(php_sock, &o, str, MIN(length, str_len), 0); + } else if (o.active) { + retval = php_socket_send_once(php_sock, &o, str, MIN(length, str_len), 0); + } else { #ifndef PHP_WIN32 - retval = write(php_sock->bsd_socket, str, MIN(length, str_len)); + retval = write(php_sock->bsd_socket, str, MIN(length, str_len)); #else - retval = send(php_sock->bsd_socket, str, min(length, str_len), 0); + retval = send(php_sock->bsd_socket, str, min(length, str_len), 0); #endif + } + php_socket_op_end(php_sock, &o); if (retval < 0) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "unable to write to socket", errno); RETURN_FALSE; } @@ -962,10 +1923,14 @@ PHP_FUNCTION(socket_read) if (type == PHP_NORMAL_READ) { retval = php_read(php_sock, ZSTR_VAL(tmpbuf), length, 0); } else { - retval = recv(php_sock->bsd_socket, ZSTR_VAL(tmpbuf), length, 0); + retval = php_socket_recv(php_sock, ZSTR_VAL(tmpbuf), length, 0); } if (retval == -1) { + if (EG(exception)) { + zend_string_efree(tmpbuf); + RETURN_THROWS(); + } /* if the socket is in non-blocking mode and there's no data to read, don't output any error, as this is a normal situation, and not an error */ if (PHP_IS_TRANSIENT_ERROR(errno)) { @@ -1244,7 +2209,7 @@ PHP_FUNCTION(socket_connect) RETURN_FALSE; } - retval = connect(php_sock->bsd_socket, (struct sockaddr *)&sin6, sizeof(struct sockaddr_in6)); + retval = php_socket_connect(php_sock, (struct sockaddr *)&sin6, sizeof(struct sockaddr_in6)); break; } #endif @@ -1263,7 +2228,7 @@ PHP_FUNCTION(socket_connect) RETURN_FALSE; } - retval = connect(php_sock->bsd_socket, (struct sockaddr *)&sin, sizeof(struct sockaddr_in)); + retval = php_socket_connect(php_sock, (struct sockaddr *)&sin, sizeof(struct sockaddr_in)); break; } @@ -1277,7 +2242,7 @@ PHP_FUNCTION(socket_connect) s_un.sun_family = AF_UNIX; memcpy(&s_un.sun_path, ZSTR_VAL(addr), ZSTR_LEN(addr)); - retval = connect(php_sock->bsd_socket, (struct sockaddr *) &s_un, + retval = php_socket_connect(php_sock, (struct sockaddr *) &s_un, (socklen_t)(offsetof(struct sockaddr_un, sun_path) + ZSTR_LEN(addr))); break; } @@ -1288,6 +2253,9 @@ PHP_FUNCTION(socket_connect) } if (retval != 0) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "unable to connect", errno); RETURN_FALSE; } @@ -1445,7 +2413,7 @@ PHP_FUNCTION(socket_recv) recv_buf = zend_string_alloc(len, 0); - if ((retval = recv(php_sock->bsd_socket, ZSTR_VAL(recv_buf), len, flags)) < 1) { + if ((retval = php_socket_recv(php_sock, ZSTR_VAL(recv_buf), len, flags)) < 1) { zend_string_efree(recv_buf); ZEND_TRY_ASSIGN_REF_NULL(buf); } else { @@ -1455,6 +2423,9 @@ PHP_FUNCTION(socket_recv) } if (retval == -1) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "Unable to read from socket", errno); RETURN_FALSE; } @@ -1487,9 +2458,23 @@ PHP_FUNCTION(socket_send) RETURN_THROWS(); } - retval = send(php_sock->bsd_socket, buf, (buf_len < (size_t)len ? buf_len : (size_t)len), flags); + php_socket_op o; + if (!php_socket_op_begin(php_sock, &o)) { + RETURN_THROWS(); + } + if (PHP_SOCKET_EMULATES_BLOCKING(php_sock)) { + retval = php_socket_send_all(php_sock, &o, buf, (buf_len < (size_t)len ? buf_len : (size_t)len), flags); + } else if (o.active) { + retval = php_socket_send_once(php_sock, &o, buf, (buf_len < (size_t)len ? buf_len : (size_t)len), flags); + } else { + retval = send(php_sock->bsd_socket, buf, (buf_len < (size_t)len ? buf_len : (size_t)len), flags); + } + php_socket_op_end(php_sock, &o); if (retval == (size_t)-1) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "Unable to write to socket", errno); RETURN_FALSE; } @@ -1517,6 +2502,7 @@ PHP_FUNCTION(socket_recvfrom) zend_long length, flags; const char *address; zend_string *recv_buf; + php_socket_waiter w = PHP_SOCKET_WAITER(SO_RCVTIMEO); ZEND_PARSE_PARAMETERS_START(5, 6) Z_PARAM_OBJECT_OF_CLASS(zsocket, socket_ce) @@ -1579,9 +2565,18 @@ PHP_FUNCTION(socket_recvfrom) memset(&s_un, 0, slen); s_un.sun_family = AF_UNIX; - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&s_un, (socklen_t *)&slen); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&s_un, (socklen_t *)&slen); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, PHP_POLLREADABLE, flags)); + php_socket_op_end(php_sock, &w.op); if (retval < 0) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "Unable to recvfrom", errno); zend_string_efree(recv_buf); RETURN_FALSE; @@ -1607,9 +2602,18 @@ PHP_FUNCTION(socket_recvfrom) RETURN_THROWS(); } - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sin, (socklen_t *)&slen); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sin, (socklen_t *)&slen); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, PHP_POLLREADABLE, flags)); + php_socket_op_end(php_sock, &w.op); if (retval < 0) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "Unable to recvfrom", errno); zend_string_efree(recv_buf); RETURN_FALSE; @@ -1638,9 +2642,18 @@ PHP_FUNCTION(socket_recvfrom) RETURN_THROWS(); } - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sin6, (socklen_t *)&slen); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sin6, (socklen_t *)&slen); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, PHP_POLLREADABLE, flags)); + php_socket_op_end(php_sock, &w.op); if (retval < 0) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "unable to recvfrom", errno); zend_string_efree(recv_buf); RETURN_FALSE; @@ -1662,9 +2675,18 @@ PHP_FUNCTION(socket_recvfrom) slen = sizeof(sll); memset(&sll, 0, slen); - retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sll, (socklen_t *)&slen); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = recvfrom(php_sock->bsd_socket, ZSTR_VAL(recv_buf), length, flags, (struct sockaddr *)&sll, (socklen_t *)&slen); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, PHP_POLLREADABLE, flags)); + php_socket_op_end(php_sock, &w.op); if (retval < 0) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "unable to recvfrom", errno); zend_string_efree(recv_buf); RETURN_FALSE; @@ -1712,6 +2734,7 @@ PHP_FUNCTION(socket_sendto) int retval; size_t buf_len; zend_long len, flags, port = 0; + php_socket_waiter w = PHP_SOCKET_WAITER(SO_SNDTIMEO); bool port_is_null = 1; char *buf; zend_string *addr; @@ -1757,7 +2780,13 @@ PHP_FUNCTION(socket_sendto) s_un.sun_family = AF_UNIX; memcpy(s_un.sun_path, ZSTR_VAL(addr), ZSTR_LEN(addr) + 1); - retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &s_un, SUN_LEN(&s_un)); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &s_un, SUN_LEN(&s_un)); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, POLLOUT, flags)); + php_socket_op_end(php_sock, &w.op); break; case AF_INET: @@ -1774,7 +2803,13 @@ PHP_FUNCTION(socket_sendto) RETURN_FALSE; } - retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &sin, sizeof(sin)); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &sin, sizeof(sin)); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, POLLOUT, flags)); + php_socket_op_end(php_sock, &w.op); break; #ifdef HAVE_IPV6 case AF_INET6: @@ -1791,7 +2826,13 @@ PHP_FUNCTION(socket_sendto) RETURN_FALSE; } - retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &sin6, sizeof(sin6)); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *) &sin6, sizeof(sin6)); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, POLLOUT, flags)); + php_socket_op_end(php_sock, &w.op); break; #endif #ifdef AF_PACKET @@ -1805,7 +2846,13 @@ PHP_FUNCTION(socket_sendto) sll.sll_family = AF_PACKET; sll.sll_ifindex = (int)port; - retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *)&sll, sizeof(sll)); + if (!php_socket_op_begin(php_sock, &w.op)) { + RETURN_THROWS(); + } + do { + retval = sendto(php_sock->bsd_socket, buf, ((size_t)len > buf_len) ? buf_len : (size_t)len, flags, (struct sockaddr *)&sll, sizeof(sll)); + } while (retval == -1 && php_socket_wait_retry(php_sock, &w, POLLOUT, flags)); + php_socket_op_end(php_sock, &w.op); break; #endif default: @@ -1814,6 +2861,9 @@ PHP_FUNCTION(socket_sendto) } if (retval == -1) { + if (EG(exception)) { + RETURN_THROWS(); + } PHP_SOCKET_ERROR(php_sock, "Unable to write to socket", errno); RETURN_FALSE; } @@ -2519,6 +3569,7 @@ PHP_FUNCTION(socket_shutdown) php_sock = Z_SOCKET_P(arg1); ENSURE_SOCKET_VALID(php_sock); + ENSURE_SOCKET_FREE(php_sock); if (how_shutdown < SHUT_RD || how_shutdown > SHUT_RDWR) { zend_argument_value_error(2, "must be one of SHUT_RD, SHUT_WR or SHUT_RDWR"); @@ -2675,16 +3726,22 @@ PHP_FUNCTION(socket_import_stream) RETURN_FALSE; } + /* socket streams keep the descriptor non-blocking and report their own mode */ + zval meta; + array_init(&meta); + if (php_stream_set_option(stream, PHP_STREAM_OPTION_META_DATA_API, 0, &meta) == PHP_STREAM_OPTION_RETURN_OK) { + zval *blocked = zend_hash_str_find(Z_ARRVAL(meta), "blocked", sizeof("blocked") - 1); #ifdef PHP_WIN32 - /* on windows, check if the stream is a socket stream and read its - * private data; otherwise assume it's in non-blocking mode */ - if (php_stream_is(stream, PHP_STREAM_IS_SOCKET)) { - retsock->blocking = - ((php_netstream_data_t *)stream->abstract)->is_blocked; - } else { - retsock->blocking = 1; - } + retsock->blocking = blocked ? zend_is_true(blocked) : 1; + retsock->nonblocking_fd = blocked != NULL; +#else + if (blocked && !retsock->blocking) { + retsock->blocking = zend_is_true(blocked); + retsock->nonblocking_fd = true; + } #endif + } + zval_ptr_dtor(&meta); /* hold a zval reference to the stream (holding a php_stream* directly could * also be done, but this makes socket_export_stream a bit simpler) */ @@ -2783,6 +3840,16 @@ PHP_FUNCTION(socket_export_stream) stream_data->is_blocked = socket->blocking; stream_data->timeout.tv_sec = FG(default_socket_timeout); stream_data->timeout.tv_usec = 0; + stream_data->restore_blocking = false; + php_netstream_set_nonblocking(stream_data); +#ifdef PHP_WIN32 + stream_data->restore_blocking = socket->blocking; +#endif + socket->nonblocking_fd = true; + /* The stream is the registrant from now on */ + if (socket->weak_handle) { + php_io_unregister_all(&PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(socket->weak_handle)->registrations); + } php_stream_to_zval(stream, &socket->zstream); diff --git a/ext/sockets/sockets.stub.php b/ext/sockets/sockets.stub.php index 681d5ba6d0ff..d007197c96fd 100644 --- a/ext/sockets/sockets.stub.php +++ b/ext/sockets/sockets.stub.php @@ -2220,6 +2220,38 @@ final class AddressInfo { } +/** + * A handle that keeps its Socket, for polling it from userland. + * @strict-properties + * @not-serializable + */ +final class SocketPollHandle implements Io\Poll\Handle +{ + public function __construct(Socket $socket) {} + + public function getSocket(): Socket {} + + public function isValid(): bool {} +} + +/** + * The Socket's one handle, created by its operations and registrations or by + * create() and kept by the Socket while it is open. + * @strict-properties + * @not-serializable + */ +final class SocketPollWeakHandle implements Io\Poll\WeakHandle +{ + private function __construct() {} + + public static function create(Socket $socket): static {} + + /** The socket, or null: closed or in an operation. */ + public function getSocket(): ?Socket {} + + public function isValid(): bool {} +} + function socket_select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, int $microseconds = 0): int|false {} function socket_create_listen(int $port, int $backlog = SOMAXCONN): Socket|false {} diff --git a/ext/sockets/sockets_arginfo.h b/ext/sockets/sockets_arginfo.h index 3f62bb3607fe..f30fb8e863b8 100644 --- a/ext/sockets/sockets_arginfo.h +++ b/ext/sockets/sockets_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit sockets.stub.php instead. - * Stub hash: aceee39bed5332f7f26d5d768976c4d5ab96ab3c */ + * Stub hash: 343d6854a3102e1f6803f1c656c76bbf172768d0 */ #include "zend_constants.h" @@ -214,6 +214,26 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_socket_wsaprotocol_info_release, ZEND_END_ARG_INFO() #endif +#define arginfo_class_SocketPollHandle___construct arginfo_socket_export_stream + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_SocketPollHandle_getSocket, 0, 0, Socket, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_SocketPollHandle_isValid, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_SocketPollWeakHandle___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_SocketPollWeakHandle_create, 0, 1, IS_STATIC, 0) + ZEND_ARG_OBJ_INFO(0, socket, Socket, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_SocketPollWeakHandle_getSocket, 0, 0, Socket, 1) +ZEND_END_ARG_INFO() + +#define arginfo_class_SocketPollWeakHandle_isValid arginfo_class_SocketPollHandle_isValid + ZEND_FUNCTION(socket_select); ZEND_FUNCTION(socket_create_listen); ZEND_FUNCTION(socket_accept); @@ -260,6 +280,13 @@ ZEND_FUNCTION(socket_wsaprotocol_info_export); ZEND_FUNCTION(socket_wsaprotocol_info_import); ZEND_FUNCTION(socket_wsaprotocol_info_release); #endif +ZEND_METHOD(SocketPollHandle, __construct); +ZEND_METHOD(SocketPollHandle, getSocket); +ZEND_METHOD(SocketPollHandle, isValid); +ZEND_METHOD(SocketPollWeakHandle, __construct); +ZEND_METHOD(SocketPollWeakHandle, create); +ZEND_METHOD(SocketPollWeakHandle, getSocket); +ZEND_METHOD(SocketPollWeakHandle, isValid); static const zend_function_entry ext_functions[] = { ZEND_FE(socket_select, arginfo_socket_select) @@ -313,6 +340,21 @@ static const zend_function_entry ext_functions[] = { ZEND_FE_END }; +static const zend_function_entry class_SocketPollHandle_methods[] = { + ZEND_ME(SocketPollHandle, __construct, arginfo_class_SocketPollHandle___construct, ZEND_ACC_PUBLIC) + ZEND_ME(SocketPollHandle, getSocket, arginfo_class_SocketPollHandle_getSocket, ZEND_ACC_PUBLIC) + ZEND_ME(SocketPollHandle, isValid, arginfo_class_SocketPollHandle_isValid, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_SocketPollWeakHandle_methods[] = { + ZEND_ME(SocketPollWeakHandle, __construct, arginfo_class_SocketPollWeakHandle___construct, ZEND_ACC_PRIVATE) + ZEND_ME(SocketPollWeakHandle, create, arginfo_class_SocketPollWeakHandle_create, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(SocketPollWeakHandle, getSocket, arginfo_class_SocketPollWeakHandle_getSocket, ZEND_ACC_PUBLIC) + ZEND_ME(SocketPollWeakHandle, isValid, arginfo_class_SocketPollWeakHandle_isValid, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + static void register_sockets_symbols(int module_number) { REGISTER_LONG_CONSTANT("AF_UNIX", AF_UNIX, CONST_PERSISTENT); @@ -1135,3 +1177,25 @@ static zend_class_entry *register_class_AddressInfo(void) return class_entry; } + +static zend_class_entry *register_class_SocketPollHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "SocketPollHandle", class_SocketPollHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + +static zend_class_entry *register_class_SocketPollWeakHandle(zend_class_entry *class_entry_Io_Poll_WeakHandle) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "SocketPollWeakHandle", class_SocketPollWeakHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_WeakHandle); + + return class_entry; +} diff --git a/ext/sockets/tests/socket_export_stream_blocking.phpt b/ext/sockets/tests/socket_export_stream_blocking.phpt new file mode 100644 index 000000000000..4a17989a85f3 --- /dev/null +++ b/ext/sockets/tests/socket_export_stream_blocking.phpt @@ -0,0 +1,74 @@ +--TEST-- +socket_export_stream(): the Socket stays blocking on the descriptor the stream made non-blocking +--EXTENSIONS-- +sockets +pcntl +posix +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(true) +read: +string(5) "world" +write: +int(1048576) +string(7) "1048576" +signal: +handler +bool(false) +bool(true) +non-blocking: +bool(false) +bool(false) +bool(true) diff --git a/ext/sockets/tests/socket_export_stream_timeout.phpt b/ext/sockets/tests/socket_export_stream_timeout.phpt new file mode 100644 index 000000000000..173c87c475e0 --- /dev/null +++ b/ext/sockets/tests/socket_export_stream_timeout.phpt @@ -0,0 +1,22 @@ +--TEST-- +socket_export_stream(): the stream's read timeout applies to a blocking socket +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- + +--EXPECT-- +bool(false) +bool(true) +string(4) "data" diff --git a/ext/sockets/tests/socket_import_stream_blocking.phpt b/ext/sockets/tests/socket_import_stream_blocking.phpt new file mode 100644 index 000000000000..0315d6f296a2 --- /dev/null +++ b/ext/sockets/tests/socket_import_stream_blocking.phpt @@ -0,0 +1,75 @@ +--TEST-- +socket_import_stream(): a blocking Socket waits on the non-blocking descriptor of the stream +--EXTENSIONS-- +sockets +pcntl +posix +--SKIPIF-- + +--FILE-- + 0, 'usec' => 100000]); +var_dump(@socket_read($s, 10), socket_last_error($s) === SOCKET_EAGAIN); +socket_set_option($s, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 0, 'usec' => 0]); + +echo "blocking:\n"; +$pid = pcntl_fork(); +if ($pid == 0) { + usleep(100000); + fwrite($b, "hello\n"); + usleep(100000); + fwrite($b, "abc"); + usleep(100000); + fwrite($b, "def"); + posix_kill(posix_getpid(), SIGKILL); +} +var_dump(socket_read($s, 10)); +var_dump(socket_recv($s, $buf, 6, MSG_WAITALL), $buf); +pcntl_waitpid($pid, $status); + +echo "accept:\n"; +$srv = stream_socket_server("tcp://127.0.0.1:0"); +$ls = socket_import_stream($srv); +socket_set_block($ls); +$addr = stream_socket_get_name($srv, false); +$pid = pcntl_fork(); +if ($pid == 0) { + usleep(100000); + $c = stream_socket_client("tcp://$addr"); + fwrite($c, "client"); + fclose($c); + posix_kill(posix_getpid(), SIGKILL); +} +$conn = socket_accept($ls); +var_dump($conn instanceof Socket, socket_read($conn, 10)); +pcntl_waitpid($pid, $status); +?> +--EXPECT-- +non-blocking: +bool(false) +bool(true) +bool(false) +bool(true) +SO_RCVTIMEO: +bool(false) +bool(true) +blocking: +string(6) "hello +" +int(6) +string(6) "abcdef" +accept: +bool(true) +string(6) "client" diff --git a/ext/sockets/tests/socket_io_hooks_concurrent.phpt b/ext/sockets/tests/socket_io_hooks_concurrent.phpt new file mode 100644 index 000000000000..eec766cba4c9 --- /dev/null +++ b/ext/sockets/tests/socket_io_hooks_concurrent.phpt @@ -0,0 +1,51 @@ +--TEST-- +IO hooks: a Socket in an operation refuses another operation, a close and a select from run() +--EXTENSIONS-- +sockets +--FILE-- +getHandle()->getSocket()); + foreach (['socket_read' => fn () => socket_read($this->socket, 10), + 'socket_write' => fn () => socket_write($this->socket, "x"), + 'socket_close' => fn () => socket_close($this->socket), + 'socket_shutdown' => fn () => socket_shutdown($this->socket), + 'socket_select' => function () { $r = [$this->socket]; $w = $e = null; return socket_select($r, $w, $e, 0); }] as $name => $fn) { + try { + $fn(); + echo "$name: no error\n"; + } catch (Error $e) { + echo "$name: ", $e->getMessage(), "\n"; + } + } + return $op->complete(Io\CompletionStatus::Timeout); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +Io\Hooks\set_hooks(new ConcurrentHook($a)); +socket_set_option($a, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 1, 'usec' => 0]); +/* The timed out read fails like the blocking call: EAGAIN, on Windows the Winsock timeout code with its warning */ +var_dump(@socket_read($a, 10), socket_last_error($a) === (PHP_OS_FAMILY === 'Windows' ? SOCKET_ETIMEDOUT : SOCKET_EAGAIN)); +// Open again after the operation +var_dump(SocketPollWeakHandle::create($a)->getSocket() === $a); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +bool(true) +NULL +socket_read: Concurrent access to a socket +socket_write: Concurrent access to a socket +socket_close: Concurrent access to a socket +socket_shutdown: Concurrent access to a socket +socket_select: Concurrent access to a socket +bool(false) +bool(true) +bool(true) diff --git a/ext/sockets/tests/socket_io_hooks_dgram.phpt b/ext/sockets/tests/socket_io_hooks_dgram.phpt new file mode 100644 index 000000000000..a5f0467e78f9 --- /dev/null +++ b/ext/sockets/tests/socket_io_hooks_dgram.phpt @@ -0,0 +1,66 @@ +--TEST-- +IO hooks: datagram waits are readiness operations bounded by the socket timeouts +--EXTENSIONS-- +sockets +--FILE-- +ops[] = substr($op::class, 13) . ' ' . get_class($op->getHandle()) + . ' ' . implode(',', array_map(fn ($e) => $e->name, $op->getEvents())); + } + return parent::run($op); + } +} + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +$srv = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); +socket_bind($srv, '127.0.0.1', 0); +socket_getsockname($srv, $addr, $port); + +/* The wait ends like the blocking call: EAGAIN, on Windows the Winsock timeout and would-block codes */ +$timedOut = PHP_OS_FAMILY === 'Windows' ? SOCKET_ETIMEDOUT : SOCKET_EAGAIN; +$wouldBlock = PHP_OS_FAMILY === 'Windows' ? SOCKET_EWOULDBLOCK : SOCKET_EAGAIN; +$scheduler->spawn(function () use ($srv, $timedOut, $wouldBlock) { + var_dump(socket_recvfrom($srv, $buf, 100, 0, $from, $fromPort), $buf); + // Nothing more arrives: the wait ends with the receive timeout, as the blocking call does + socket_set_option($srv, SOL_SOCKET, SO_RCVTIMEO, ['sec' => 0, 'usec' => 50000]); + $start = hrtime(true); + var_dump(@socket_recvfrom($srv, $buf, 100, 0, $from, $fromPort), socket_last_error($srv) === $timedOut); + var_dump((hrtime(true) - $start) / 1e6 >= 40); + // A non-blocking Socket does not wait + socket_set_nonblock($srv); + var_dump(@socket_recvfrom($srv, $buf, 100, 0, $from, $fromPort), socket_last_error($srv) === $wouldBlock); +}); +$scheduler->spawn(function () use ($addr, $port) { + usleep(30000); + $c = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP); + var_dump(socket_sendto($c, "ping", 4, 0, $addr, $port)); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +print_r($scheduler->ops); +?> +--EXPECT-- +int(4) +int(4) +string(4) "ping" +bool(false) +bool(true) +bool(true) +bool(false) +bool(true) +Array +( + [0] => Poll SocketPollWeakHandle Read + [1] => Poll SocketPollWeakHandle Read +) diff --git a/ext/sockets/tests/socket_io_hooks_import.phpt b/ext/sockets/tests/socket_io_hooks_import.phpt new file mode 100644 index 000000000000..17e3dae7b061 --- /dev/null +++ b/ext/sockets/tests/socket_io_hooks_import.phpt @@ -0,0 +1,95 @@ +--TEST-- +IO hooks: a Socket sharing a stream's descriptor operates on the stream, with the stream's handle +--EXTENSIONS-- +sockets +--SKIPIF-- +getSupportedHookCapabilities() : $queue->getHookCapabilities(); +if (!in_array(Io\Hooks\Capability::EdgeRegistrations, $caps, true) && !in_array(Io\Hooks\Capability::LevelRegistrations, $caps, true)) { + die('skip the queue takes no registrations'); +} +?> +--FILE-- +getHandle() === $this->target) { + $reg = $op->getRegistration(); + $this->ops[] = substr($op::class, 13) . ($reg ? ' reg ' . get_class($reg->getHandle()) : ''); + } + return parent::run($op); + } + + public function add(\Io\Registration $r): void + { + if ($r->getHandle() === $this->target) { + $this->adds[] = $r->getEvent()->name . ' ' . get_class($r->getHandle()); + } + parent::add($r); + } +} + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +$s = socket_import_stream($a); +// Registrations on both queues, direct ops where the queue performs them +$scheduler = new Tracing(null, [Io\Hooks\Capability::EdgeRegistrations, Io\Hooks\Capability::LevelRegistrations, + Io\Hooks\Capability::DirectData, Io\Hooks\Capability::DirectAccept]); +$scheduler->target = StreamPollWeakHandle::create($a); +Io\Hooks\set_hooks($scheduler); + +$scheduler->spawn(function () use ($s, $a) { + var_dump(socket_read($s, 10)); + var_dump(fread($a, 10)); +}); +$scheduler->spawn(function () use ($b, $s, $a) { + usleep(20000); + fwrite($b, "hello"); + usleep(20000); + fwrite($b, "world"); +}); +$scheduler->loop(); + +// The Socket's read and the stream's read are the same kind of op on the stream's one pair +var_dump($scheduler->ops, $scheduler->adds); + +// A select over an imported Socket waits on the stream too +$scheduler->spawn(function () use ($s) { + $r = [$s]; $w = $e = null; + var_dump(socket_select($r, $w, $e, 1), $r === [$s], socket_read($s, 10)); +}); +$scheduler->spawn(function () use ($b) { + usleep(20000); + fwrite($b, "more"); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +string(5) "hello" +string(5) "world" +array(2) { + [0]=> + string(29) "Recv reg StreamPollWeakHandle" + [1]=> + string(29) "Recv reg StreamPollWeakHandle" +} +array(1) { + [0]=> + string(25) "Read StreamPollWeakHandle" +} +int(1) +bool(true) +string(4) "more" diff --git a/ext/sockets/tests/socket_io_hooks_ops.phpt b/ext/sockets/tests/socket_io_hooks_ops.phpt new file mode 100644 index 000000000000..f22c34b30888 --- /dev/null +++ b/ext/sockets/tests/socket_io_hooks_ops.phpt @@ -0,0 +1,129 @@ +--TEST-- +IO hooks: socket functions issue operations that carry the Socket's handle and register its pairs +--EXTENSIONS-- +sockets +--SKIPIF-- +getSupportedHookCapabilities() : $queue->getHookCapabilities(); +if (!in_array(Io\Hooks\Capability::EdgeRegistrations, $caps, true) && !in_array(Io\Hooks\Capability::LevelRegistrations, $caps, true)) { + die('skip the queue takes no registrations'); +} +?> +--FILE-- +getHandle(); + $reg = $op->getRegistration(); + $this->ops[substr($op::class, 13)] = true; + // The Socket's one handle, which hands nothing out while the op runs; a wait on a + // registered pair carries the pair's record with the same handle + $this->handlesOk = $this->handlesOk && $h instanceof SocketPollWeakHandle && $h->isValid() + && $h->getSocket() === null && $op->getHandle() === $h + && ($reg === null || $reg->getHandle() === $h); + } + return parent::run($op); + } + + public function add(\Io\Registration $r): void + { + $this->adds[] = $r; + $this->pairs[] = $r->getEvent()->name . ' ' . get_class($r->getHandle()) . ' ' . $r->getTrigger()->name; + parent::add($r); + } + + public function remove(\Io\Registration $r): void + { + $this->removes[] = $r; + parent::remove($r); + } +} + +// Registrations on both queues, direct ops where the queue performs them +$scheduler = new Tracing(null, [Io\Hooks\Capability::EdgeRegistrations, Io\Hooks\Capability::LevelRegistrations, + Io\Hooks\Capability::DirectData, Io\Hooks\Capability::DirectAccept]); +Io\Hooks\set_hooks($scheduler); + +$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); +socket_bind($server, '127.0.0.1', 0); +socket_listen($server); +socket_getsockname($server, $addr, $port); +$serverHandle = SocketPollWeakHandle::create($server); + +$scheduler->spawn(function () use ($server, $serverHandle, $scheduler) { + $conn = socket_accept($server); + var_dump($conn instanceof Socket); + var_dump(socket_read($conn, 10)); + var_dump(socket_recv($conn, $buf, 3, MSG_WAITALL), $buf); + var_dump(socket_write($conn, "reply")); + // The listener's handle is the object its accept carried, open again after the op + var_dump($serverHandle === SocketPollWeakHandle::create($server), $serverHandle->getSocket() === $server); + socket_close($conn); +}); +$client = []; +$scheduler->spawn(function () use ($addr, $port, &$client) { + $c = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + $client[] = socket_connect($c, $addr, $port); + usleep(20000); + $client[] = socket_send($c, "hello", 5, 0); + usleep(20000); + $client[] = socket_write($c, "abc"); + $client[] = socket_read($c, 10); + socket_close($c); +}); +$scheduler->loop(); +var_dump($client); + +var_dump(isset($scheduler->ops['Accept']), isset($scheduler->ops['Recv']), $scheduler->handlesOk); + +// The listener's registration goes with its close; its handle answers invalid +socket_close($server); +var_dump($serverHandle->isValid(), $serverHandle->getSocket()); + +// Every pair was registered on a Socket handle, as Edge, and removed before its close +var_dump(count($scheduler->adds) >= 2, count($scheduler->adds) === count($scheduler->removes)); +var_dump(array_all($scheduler->removes, fn ($r) => in_array($r, $scheduler->adds, true))); +var_dump(array_unique(array_map(fn ($p) => substr($p, strpos($p, ' ') + 1), $scheduler->pairs)) === ['SocketPollWeakHandle Edge']); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +bool(true) +string(5) "hello" +int(3) +string(3) "abc" +int(5) +bool(true) +bool(true) +array(4) { + [0]=> + bool(true) + [1]=> + int(5) + [2]=> + int(3) + [3]=> + string(5) "reply" +} +bool(true) +bool(true) +bool(true) +bool(false) +NULL +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/sockets/tests/socket_io_hooks_select.phpt b/ext/sockets/tests/socket_io_hooks_select.phpt new file mode 100644 index 000000000000..839811dc5bc5 --- /dev/null +++ b/ext/sockets/tests/socket_io_hooks_select.phpt @@ -0,0 +1,82 @@ +--TEST-- +IO hooks: socket_select() polls first and waits with one Any operation over the Sockets +--EXTENSIONS-- +sockets +--FILE-- +getOperations(); + $polls = array_filter($members, fn ($m) => $m instanceof \Io\Operation\Poll); + $this->anys[] = count($polls) . '+' . (count($members) - count($polls)) + . ' ' . implode(',', array_unique(array_map(fn ($m) => get_class($m->getHandle()), $polls))); + } + return parent::run($op); + } +} + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +socket_create_pair(PHP_OS_FAMILY === 'Windows' ? AF_INET : AF_UNIX, SOCK_STREAM, 0, $p1); +socket_create_pair(PHP_OS_FAMILY === 'Windows' ? AF_INET : AF_UNIX, SOCK_STREAM, 0, $p2); +[$a, $b] = $p1; +[$c, $d] = $p2; + +$scheduler->spawn(function () use ($a, $c) { + // Nothing readable: the select times out through the Timer member + $r = [$a, $c]; $w = null; $e = null; + $start = hrtime(true); + var_dump(socket_select($r, $w, $e, 0, 50000), $r); + var_dump((hrtime(true) - $start) / 1e6 >= 40); + + // Both readable by now, and the writable side: answered by the poll before the Any + usleep(150000); + $r = [$a, $c]; $w = [$a]; $e = null; + var_dump(socket_select($r, $w, $e, 1), count($r), $w === [$a]); + var_dump(socket_read($a, 10), socket_read($c, 10)); + + // A blocking select woken by the other fiber + socket_write($a, "go"); + $r = [$a]; $w = null; $e = null; + var_dump(socket_select($r, $w, $e, null), $r === [$a], socket_read($a, 10)); +}); +$scheduler->spawn(function () use ($b, $d) { + usleep(70000); + socket_write($b, "one"); + socket_write($d, "two"); + socket_read($b, 2); + // Let the select block before the wakeup: a direct queue sends without a fiber switch + usleep(20000); + socket_write($b, "three"); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +var_dump($scheduler->anys); +?> +--EXPECT-- +int(0) +array(0) { +} +bool(true) +int(3) +int(2) +bool(true) +string(3) "one" +string(3) "two" +int(1) +bool(true) +string(5) "three" +array(2) { + [0]=> + string(24) "2+1 SocketPollWeakHandle" + [1]=> + string(24) "1+0 SocketPollWeakHandle" +} diff --git a/ext/sockets/tests/socket_poll_handle.phpt b/ext/sockets/tests/socket_poll_handle.phpt new file mode 100644 index 000000000000..d9759abc7419 --- /dev/null +++ b/ext/sockets/tests/socket_poll_handle.phpt @@ -0,0 +1,78 @@ +--TEST-- +SocketPollHandle and SocketPollWeakHandle: polling a Socket, retired when it closes +--EXTENSIONS-- +sockets +--FILE-- +getSocket() === $a, $h->isValid()); + +$ctx = new Io\Poll\Context(); +$ctx->onWatcherRemoved(function ($w) { echo "removed: ", get_class($w->getHandle()), "\n"; }); +$w = $ctx->add($h, [Io\Poll\Event::Read]); +socket_write($b, "x"); +$fired = $ctx->wait(Time\Duration::fromSeconds(1)); +var_dump(count($fired), $fired[0] === $w, $fired[0]->hasTriggered(Io\Poll\Event::Read)); + +// Closing the Socket retires the watcher while the descriptor is open; the handle keeps the Socket +socket_close($a); +var_dump($w->isActive(), $h->isValid(), $h->getSocket() === $a); +var_dump($ctx->wait(Time\Duration::fromSeconds(0))); + +// The weak handle is the Socket's one object and reports the close +$wk = SocketPollWeakHandle::create($b); +var_dump($wk === SocketPollWeakHandle::create($b), $wk->getSocket() === $b, $wk->isValid()); +$w2 = $ctx->add($wk, [Io\Poll\Event::Read]); +socket_close($b); +var_dump($w2->isActive(), $wk->isValid(), $wk->getSocket()); +var_dump($ctx->wait(Time\Duration::fromSeconds(0))); + +try { + new SocketPollWeakHandle(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + clone $wk; +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + new SocketPollHandle($a); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +try { + SocketPollWeakHandle::create($b); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +bool(true) +bool(true) +int(1) +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) +removed: SocketPollHandle +array(0) { +} +bool(true) +bool(true) +bool(true) +bool(false) +bool(false) +NULL +removed: SocketPollWeakHandle +array(0) { +} +Call to private SocketPollWeakHandle::__construct() from global scope +Trying to clone an uncloneable object of class SocketPollWeakHandle +SocketPollHandle::__construct(): Argument #1 ($socket) has already been closed +SocketPollWeakHandle::create(): Argument #1 ($socket) has already been closed diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 1665feac37df..5f1b6572d612 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -111,6 +111,8 @@ PHPAPI php_basic_globals basic_globals; #include "streamsfuncs.h" #include "zend_frameless_function.h" #include "basic_functions_arginfo.h" +#include "io_hooks.h" +#include "php_io.h" #if __has_feature(memory_sanitizer) # include @@ -338,6 +340,8 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */ BASIC_MINIT_SUBMODULE(stream_errors) BASIC_MINIT_SUBMODULE(user_streams) + BASIC_MINIT_SUBMODULE(io_hooks) + BASIC_MINIT_SUBMODULE(io_ring) php_register_url_stream_wrapper("php", &php_stream_php_wrapper); php_register_url_stream_wrapper("file", &php_plain_files_wrapper); @@ -424,6 +428,17 @@ PHP_RINIT_FUNCTION(basic) /* {{{ */ /* Default to global filters only */ FG(stream_filters) = NULL; + FG(io_hooks) = NULL; + FG(io_queue) = NULL; + FG(io_queue_pid) = 0; + FG(io_hooks_generation) = 0; + FG(io_registrations) = NULL; + FG(io_ops_in_flight) = 0; + FG(io_orphans) = NULL; + FG(io_addrinfo) = NULL; + FG(io_hooks_locked) = 0; + FG(io_shut_down) = false; + return SUCCESS; } /* }}} */ @@ -483,6 +498,9 @@ PHP_RSHUTDOWN_FUNCTION(basic) /* {{{ */ BG(page_uid) = -1; BG(page_gid) = -1; + + php_io_hooks_request_shutdown(); + return SUCCESS; } /* }}} */ @@ -1118,6 +1136,23 @@ PHP_FUNCTION(flush) } /* }}} */ +/* Sleeps for ns nanoseconds; FAILURE when cancelled. *elapsed is the time + * slept when a signal interrupted the sleep, 0 when it completed. */ +static zend_result php_sleep_ns(zend_hrtime_t ns, zend_hrtime_t *elapsed) +{ + zend_hrtime_t start = zend_hrtime(); + bool interrupted; + + *elapsed = 0; + if (php_io_sleep(php_io_deadline_from_ns(ns), &interrupted) == FAILURE) { + return FAILURE; + } + if (interrupted) { + *elapsed = MAX(zend_hrtime() - start, 1); + } + return SUCCESS; +} + /* {{{ Delay for a given number of seconds */ PHP_FUNCTION(sleep) { @@ -1137,7 +1172,16 @@ PHP_FUNCTION(sleep) RETURN_THROWS(); } - RETURN_LONG(php_sleep((unsigned int)num)); + zend_hrtime_t ns = (zend_hrtime_t) num * ZEND_NANO_IN_SEC, elapsed; + if (php_sleep_ns(ns, &elapsed) == FAILURE) { + RETURN_THROWS(); + } + if (elapsed == 0 || elapsed >= ns) { + RETURN_LONG(0); + } + + /* The seconds left, rounded like sleep(3) */ + RETURN_LONG((zend_long) ((ns - elapsed + ZEND_NANO_IN_SEC / 2) / ZEND_NANO_IN_SEC)); } /* }}} */ @@ -1155,9 +1199,10 @@ PHP_FUNCTION(usleep) RETURN_THROWS(); } -#ifdef HAVE_USLEEP - usleep((unsigned int)num); -#endif + zend_hrtime_t elapsed; + if (php_sleep_ns((zend_hrtime_t) num * 1000, &elapsed) == FAILURE) { + RETURN_THROWS(); + } } /* }}} */ @@ -1166,7 +1211,6 @@ PHP_FUNCTION(usleep) PHP_FUNCTION(time_nanosleep) { zend_long tv_sec, tv_nsec; - struct timespec php_req, php_rem; ZEND_PARSE_PARAMETERS_START(2, 2) Z_PARAM_LONG(tv_sec) @@ -1181,23 +1225,33 @@ PHP_FUNCTION(time_nanosleep) zend_argument_value_error(2, "must be greater than or equal to 0"); RETURN_THROWS(); } - - php_req.tv_sec = (time_t) tv_sec; - php_req.tv_nsec = (long)tv_nsec; - if (!nanosleep(&php_req, &php_rem)) { - RETURN_TRUE; - } else if (errno == EINTR) { - array_init(return_value); - MSAN_UNPOISON(php_rem); - add_assoc_long_ex(return_value, "seconds", sizeof("seconds")-1, php_rem.tv_sec); - add_assoc_long_ex(return_value, "nanoseconds", sizeof("nanoseconds")-1, php_rem.tv_nsec); - return; - } else if (errno == EINVAL) { + if (tv_nsec > 999999999) { zend_value_error("Nanoseconds was not in the range 0 to 999 999 999 or seconds was negative"); RETURN_THROWS(); } - RETURN_FALSE; + zend_hrtime_t ns = (zend_ulong) tv_sec >= (ZEND_HRTIME_T_MAX - tv_nsec) / ZEND_NANO_IN_SEC + ? ZEND_HRTIME_T_MAX : (zend_hrtime_t) tv_sec * ZEND_NANO_IN_SEC + tv_nsec; + zend_hrtime_t elapsed; + if (php_sleep_ns(ns, &elapsed) == FAILURE) { + RETURN_THROWS(); + } + if (elapsed == 0) { + RETURN_TRUE; + } + + zend_long rem_sec = tv_sec - (zend_long) (elapsed / ZEND_NANO_IN_SEC); + zend_long rem_nsec = tv_nsec - (zend_long) (elapsed % ZEND_NANO_IN_SEC); + if (rem_nsec < 0) { + rem_nsec += ZEND_NANO_IN_SEC; + rem_sec--; + } + if (rem_sec < 0) { + rem_sec = rem_nsec = 0; + } + array_init(return_value); + add_assoc_long_ex(return_value, "seconds", sizeof("seconds")-1, rem_sec); + add_assoc_long_ex(return_value, "nanoseconds", sizeof("nanoseconds")-1, rem_nsec); } /* }}} */ @@ -1206,7 +1260,6 @@ PHP_FUNCTION(time_sleep_until) { double target_secs; struct timeval tm; - struct timespec php_req, php_rem; uint64_t current_ns, target_ns, diff_ns; const uint64_t ns_per_sec = 1000000000; const double top_target_sec = (double)(UINT64_MAX / ns_per_sec); @@ -1232,17 +1285,14 @@ PHP_FUNCTION(time_sleep_until) } diff_ns = target_ns - current_ns; - php_req.tv_sec = (time_t) (diff_ns / ns_per_sec); - php_req.tv_nsec = (long) (diff_ns % ns_per_sec); - while (nanosleep(&php_req, &php_rem)) { - if (errno == EINTR) { - php_req.tv_sec = php_rem.tv_sec; - php_req.tv_nsec = php_rem.tv_nsec; - } else { - RETURN_FALSE; + php_deadline dl = php_io_deadline_from_ns((zend_hrtime_t) diff_ns); + bool interrupted; + do { + if (php_io_sleep(dl, &interrupted) == FAILURE) { + RETURN_THROWS(); } - } + } while (interrupted); RETURN_TRUE; } diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h index 4d5a4c02aec0..f4dde13d0a6f 100644 --- a/ext/standard/basic_functions.h +++ b/ext/standard/basic_functions.h @@ -43,6 +43,8 @@ PHP_MINFO_FUNCTION(basic); ZEND_API void php_get_highlight_struct(zend_syntax_highlighter_ini *syntax_highlighter_ini); PHP_MINIT_FUNCTION(poll); +PHP_MINIT_FUNCTION(io_hooks); +PHP_MINIT_FUNCTION(io_ring); PHP_MINIT_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(user_filters); PHP_RSHUTDOWN_FUNCTION(browscap); diff --git a/ext/standard/config.m4 b/ext/standard/config.m4 index 7edfcf1da1f5..8315c732b178 100644 --- a/ext/standard/config.m4 +++ b/ext/standard/config.m4 @@ -418,7 +418,9 @@ PHP_NEW_EXTENSION([standard], m4_normalize([ image.c incomplete_class.c info.c + io_hooks.c io_poll.c + io_ring.c iptc.c levenshtein.c libavifinfo/avifinfo.c diff --git a/ext/standard/config.w32 b/ext/standard/config.w32 index 56096097620d..c153c08cb1c0 100644 --- a/ext/standard/config.w32 +++ b/ext/standard/config.w32 @@ -28,7 +28,7 @@ EXTENSION("standard", "array.c base64.c basic_functions.c browscap.c \ crypt_sha512.c php_crypt_r.c " + (TARGET_ARCH != 'arm64'? " crc32_x86.c" : "") + " \ datetime.c dir.c dl.c dns.c dns_win32.c exec.c \ file.c filestat.c formatted_print.c fsock.c head.c html.c image.c \ - info.c io_poll.c iptc.c link.c mail.c math.c md5.c metaphone.c microtime.c \ + info.c io_hooks.c io_poll.c io_ring.c iptc.c link.c mail.c math.c md5.c metaphone.c microtime.c \ net.c pack.c pageinfo.c quot_print.c soundex.c \ string.c scanf.c syslog.c type.c uniqid.c url.c var.c \ versioning.c assert.c strnatcmp.c levenshtein.c incomplete_class.c \ diff --git a/ext/standard/dns.c b/ext/standard/dns.c index b3fcc1d236af..14851982db13 100644 --- a/ext/standard/dns.c +++ b/ext/standard/dns.c @@ -17,6 +17,7 @@ /* {{{ includes */ #include "php.h" #include "php_network.h" +#include "php_io.h" #ifdef HAVE_SYS_SOCKET_H #include @@ -176,17 +177,20 @@ static zend_string *php_gethostbyaddr(char *ip) memset(&sa4, 0, sizeof(struct sockaddr_in)); memset(&sa6, 0, sizeof(struct sockaddr_in6)); + php_deadline deadline; + php_deadline_init_infinite(&deadline); + if (inet_pton(AF_INET6, ip, &sa6.sin6_addr)) { sa6.sin6_family = AF_INET6; - if (getnameinfo((struct sockaddr *)&sa6, sizeof(sa6), out, sizeof(out), NULL, 0, NI_NAMEREQD) != 0) { + if (php_io_getnameinfo((struct sockaddr *)&sa6, sizeof(sa6), NI_NAMEREQD, out, sizeof(out), NULL, 0, &deadline) != 0) { return zend_string_init(ip, strlen(ip), 0); } return zend_string_init(out, strlen(out), 0); } else if (inet_pton(AF_INET, ip, &sa4.sin_addr)) { sa4.sin_family = AF_INET; - if (getnameinfo((struct sockaddr *)&sa4, sizeof(sa4), out, sizeof(out), NULL, 0, NI_NAMEREQD) != 0) { + if (php_io_getnameinfo((struct sockaddr *)&sa4, sizeof(sa4), NI_NAMEREQD, out, sizeof(out), NULL, 0, &deadline) != 0) { return zend_string_init(ip, strlen(ip), 0); } return zend_string_init(out, strlen(out), 0); @@ -259,23 +263,25 @@ PHP_FUNCTION(gethostbynamel) RETURN_FALSE; } - hp = php_network_gethostbyname(hostname); - if (!hp) { + /* A GetAddrInfo op for IPv4 addresses, the same op the streams use */ + struct addrinfo hints, *res, *ai; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + php_deadline deadline; + php_deadline_init_infinite(&deadline); + if (php_io_getaddrinfo(hostname, NULL, &hints, &res, &deadline) != 0 || res == NULL) { RETURN_FALSE; } array_init(return_value); - for (i = 0;; i++) { - /* On macos h_addr_list entries may be misaligned. */ + for (ai = res; ai; ai = ai->ai_next) { const char *ipaddr; - struct in_addr *h_addr_entry; /* Don't call this h_addr, it's a macro! */ - memcpy(&h_addr_entry, &hp->h_addr_list[i], sizeof(struct in_addr *)); - if (!h_addr_entry) { - return; + if (ai->ai_family != AF_INET) { + continue; } - - in = *h_addr_entry; + in = ((struct sockaddr_in *) ai->ai_addr)->sin_addr; if (!(ipaddr = inet_ntop(AF_INET, &in, addr4, INET_ADDRSTRLEN))) { /* unlikely regarding (too) long hostname and protocols but checking still */ php_error_docref(NULL, E_WARNING, "Host name to ip failed %s", hostname); @@ -284,30 +290,37 @@ PHP_FUNCTION(gethostbynamel) add_next_index_string(return_value, ipaddr); } } + php_io_freeaddrinfo(res); + (void) hp; + (void) i; } /* }}} */ /* {{{ php_gethostbyname */ static zend_string *php_gethostbyname(char *name) { - struct hostent *hp; - struct in_addr *h_addr_0; /* Don't call this h_addr, it's a macro! */ + struct addrinfo hints, *res, *ai; struct in_addr in; char addr4[INET_ADDRSTRLEN]; const char *address; - hp = php_network_gethostbyname(name); - if (!hp) { + /* A GetAddrInfo op for the first IPv4 address, the same op the streams use */ + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + php_deadline deadline; + php_deadline_init_infinite(&deadline); + if (php_io_getaddrinfo(name, NULL, &hints, &res, &deadline) != 0 || res == NULL) { return zend_string_init(name, strlen(name), 0); } - - /* On macos h_addr_list entries may be misaligned. */ - memcpy(&h_addr_0, &hp->h_addr_list[0], sizeof(struct in_addr *)); - if (!h_addr_0) { + for (ai = res; ai && ai->ai_family != AF_INET; ai = ai->ai_next) { + } + if (!ai) { + php_io_freeaddrinfo(res); return zend_string_init(name, strlen(name), 0); } - - memcpy(&in.s_addr, h_addr_0, sizeof(in.s_addr)); + in = ((struct sockaddr_in *) ai->ai_addr)->sin_addr; + php_io_freeaddrinfo(res); if (!(address = inet_ntop(AF_INET, &in, addr4, INET_ADDRSTRLEN))) { return NULL; diff --git a/ext/standard/file.c b/ext/standard/file.c index daefcd77e725..528e0a0fe50f 100644 --- a/ext/standard/file.c +++ b/ext/standard/file.c @@ -804,7 +804,9 @@ PHP_FUNCTION(popen) { char *command, *mode; size_t command_len, mode_len; +#ifdef PHP_WIN32 FILE *fp; +#endif php_stream *stream; char *posix_mode; @@ -834,6 +836,10 @@ PHP_FUNCTION(popen) RETURN_THROWS(); } +#ifndef PHP_WIN32 + php_stream_error_operation_begin(); + stream = php_stream_popen(command, mode); +#else fp = VCWD_POPEN(command, posix_mode); if (!fp) { php_error_docref(NULL, E_WARNING, "%s", strerror(errno)); @@ -843,6 +849,7 @@ PHP_FUNCTION(popen) php_stream_error_operation_begin(); stream = php_stream_fopen_from_pipe(fp, mode); +#endif if (stream == NULL) { php_error_docref(NULL, E_WARNING, "%s", strerror(errno)); diff --git a/ext/standard/file.h b/ext/standard/file.h index 9ba5f5b8b93d..469d2ef0671b 100644 --- a/ext/standard/file.h +++ b/ext/standard/file.h @@ -102,6 +102,16 @@ typedef struct { HashTable *wrapper_logged_errors; /* key: wrapper address; value: linked list of error entries */ php_stream_error_state stream_error_state; int pclose_wait; + struct _php_io_hooks *io_hooks; /* NULL without a provider */ + uint32_t io_hooks_generation; /* bumped per installed provider */ + struct _php_io_queue *io_queue; /* core queue, created lazily */ + pid_t io_queue_pid; + struct _php_io_registration *io_registrations; + uint32_t io_ops_in_flight; + HashTable *io_orphans; /* stream -> queue keeping its ops */ + HashTable *io_addrinfo; + uint32_t io_hooks_locked; + bool io_shut_down; #ifdef HAVE_GETHOSTBYNAME_R struct hostent tmp_host_info; char *tmp_host_buf; @@ -117,5 +127,4 @@ extern PHPAPI int file_globals_id; extern PHPAPI php_file_globals file_globals; #endif - #endif /* FILE_H */ diff --git a/ext/standard/flock_compat.c b/ext/standard/flock_compat.c index 6967c4358901..9116de5b6dc1 100644 --- a/ext/standard/flock_compat.c +++ b/ext/standard/flock_compat.c @@ -103,33 +103,63 @@ PHPAPI int php_flock(int fd, int operation) HANDLE hdl = (HANDLE) _get_osfhandle(fd); DWORD low = 0xFFFFFFFF, high = 0xFFFFFFFF; OVERLAPPED offset = {0, 0, {{0}}, NULL}; + HANDLE event; + DWORD flags; + DWORD done; DWORD err; + BOOL ok; if (INVALID_HANDLE_VALUE == hdl) { _set_errno(EBADF); return -1; /* error in file descriptor */ } - /* bug for bug compatible with Unix */ - UnlockFileEx(hdl, 0, low, high, &offset); + switch (operation & ~LOCK_NB) { /* translate to LockFileEx() op */ case LOCK_EX: /* exclusive */ - if (LockFileEx(hdl, LOCKFILE_EXCLUSIVE_LOCK + - ((operation & LOCK_NB) ? LOCKFILE_FAIL_IMMEDIATELY : 0), - 0, low, high, &offset)) - return 0; + flags = LOCKFILE_EXCLUSIVE_LOCK; break; case LOCK_SH: /* shared */ - if (LockFileEx(hdl, ((operation & LOCK_NB) ? LOCKFILE_FAIL_IMMEDIATELY : 0), - 0, low, high, &offset)) - return 0; + flags = 0; break; case LOCK_UN: /* unlock */ - return 0; /* always succeeds */ - default: /* default */ + flags = 0; break; + default: /* default */ + _set_errno(EINVAL); + return -1; + } + if (operation & LOCK_NB) { + flags |= LOCKFILE_FAIL_IMMEDIATELY; + } + + /* A handle opened for overlapped I/O completes a lock request + * asynchronously: wait for it on an event. The low bit keeps the + * completion off an I/O completion port the handle is bound to. */ + event = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!event) { + _set_errno(ENOMEM); + return -1; + } + offset.hEvent = (HANDLE) ((ULONG_PTR) event | 1); + + /* bug for bug compatible with Unix */ + UnlockFileEx(hdl, 0, low, high, &offset); + if ((operation & ~LOCK_NB) == LOCK_UN) { + CloseHandle(event); + return 0; /* always succeeds */ + } + + ok = LockFileEx(hdl, flags, 0, low, high, &offset); + err = ok ? ERROR_SUCCESS : GetLastError(); + if (!ok && err == ERROR_IO_PENDING) { + ok = GetOverlappedResult(hdl, &offset, &done, TRUE); + err = ok ? ERROR_SUCCESS : GetLastError(); + } + CloseHandle(event); + if (ok) { + return 0; } - err = GetLastError(); if (ERROR_LOCK_VIOLATION == err || ERROR_SHARING_VIOLATION == err) { _set_errno(EWOULDBLOCK); } else { diff --git a/ext/standard/io_hooks.c b/ext/standard/io_hooks.c new file mode 100644 index 000000000000..f60a105c23c7 --- /dev/null +++ b/ext/standard/io_hooks.c @@ -0,0 +1,1680 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +/* The userland bridge of the IO hooks: Io\Operation wrappers over the C + * ops, Io\Completion, Io\Poll\OperationQueue over the C poll queue, and the + * Io\Hooks\Hooks adapter that set_hooks() installs as the C provider. */ + +#include "php.h" +#include "zend_enum.h" +#include "zend_exceptions.h" +#include "zend_interfaces.h" +#include "ext/standard/file.h" +#include "ext/standard/io_poll.h" +#include "ext/standard/io_hooks.h" +#include "ext/date/php_time.h" +#include "io_hooks_arginfo.h" +#include +#include "io_hooks_decl.h" +#include "io_poll_decl.h" + +#include +#ifndef PHP_WIN32 +# include +# include +#endif + +static zend_class_entry *php_io_completion_status_ce; +static zend_class_entry *php_io_operation_ce; +static zend_class_entry *php_io_operation_poll_ce; +static zend_class_entry *php_io_operation_timer_ce; +static zend_class_entry *php_io_operation_any_ce; +static zend_class_entry *php_io_operation_read_ce; +static zend_class_entry *php_io_operation_write_ce; +static zend_class_entry *php_io_operation_recv_ce; +static zend_class_entry *php_io_operation_send_ce; +static zend_class_entry *php_io_operation_accept_ce; +static zend_class_entry *php_io_operation_connect_ce; +static zend_class_entry *php_io_operation_fsync_ce; +static zend_class_entry *php_io_operation_waitpid_ce; +static zend_class_entry *php_io_operation_sigwait_ce; +static zend_class_entry *php_io_operation_getaddrinfo_ce; +static zend_class_entry *php_io_operation_getnameinfo_ce; +static zend_class_entry *php_io_completion_ce; +static zend_class_entry *php_io_invalid_operation_exception_ce; +static zend_class_entry *php_io_registration_ce; +static zend_class_entry *php_io_invalid_registration_exception_ce; +static zend_class_entry *php_io_poll_trigger_ce; +PHPAPI zend_class_entry *php_io_operation_queue_ce; +static zend_class_entry *php_io_poll_operation_queue_ce; +static zend_class_entry *php_io_hooks_ce; +static zend_class_entry *php_io_hooks_capability_ce; +static zend_class_entry *php_io_poll_context_ce; + +static zend_object_handlers php_io_operation_handlers; +static zend_object_handlers php_io_registration_handlers; +static zend_object_handlers php_io_completion_handlers; +PHPAPI zend_object_handlers php_io_opqueue_handlers; + +typedef struct php_io_operation_obj { + php_io_op *op; /* NULL once ended */ + zend_object *lazy_handle; /* created by getHandle() */ + zend_object std; +} php_io_operation_obj; + +typedef struct php_io_registration_obj { + php_io_registration *reg; /* NULL once ended */ + zend_object std; +} php_io_registration_obj; + +typedef struct php_io_completion_obj { + zend_object *operation; + php_io_status status; + int64_t res; + int error; + uint32_t events; + bool produced; /* the result carries real data */ + zval data; + zval completions; /* Any only */ + zend_object std; +} php_io_completion_obj; + +/* One submission: what comes back as the completion's operation and data */ +struct _php_io_opqueue_sub { + zend_object *operation; + zval data; + php_io_opqueue_obj *owner; + php_io_opqueue_sub *prev; + php_io_opqueue_sub *next; +}; + +#define PHP_IO_OPERATION_FROM_ZOBJ(o) ZEND_CONTAINER_OF(o, php_io_operation_obj, std) +#define PHP_IO_REGISTRATION_FROM_ZOBJ(o) ZEND_CONTAINER_OF(o, php_io_registration_obj, std) +#define PHP_IO_COMPLETION_FROM_ZOBJ(o) ZEND_CONTAINER_OF(o, php_io_completion_obj, std) + +/* Completion status enum */ + +/* The enum cases are declared in the order of php_io_status */ +static_assert(ZEND_ENUM_Io_CompletionStatus_Unsupported - ZEND_ENUM_Io_CompletionStatus_Done == PHP_IO_UNSUPPORTED - PHP_IO_DONE, + "Io\\CompletionStatus must mirror php_io_status"); + +static zend_object *php_io_status_case(php_io_status status) +{ + ZEND_ASSERT(status <= PHP_IO_UNSUPPORTED); + return zend_enum_get_case_by_id(php_io_completion_status_ce, ZEND_ENUM_Io_CompletionStatus_Done + status); +} + +static php_io_status php_io_status_from_case(zend_object *case_obj) +{ + return (php_io_status) (zend_enum_fetch_case_id(case_obj) - ZEND_ENUM_Io_CompletionStatus_Done); +} + +/* Operation objects */ + +static zend_object *php_io_operation_create_object(zend_class_entry *ce) +{ + php_io_operation_obj *intern = zend_object_alloc(sizeof(php_io_operation_obj), ce); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->op = NULL; + intern->lazy_handle = NULL; + return &intern->std; +} + +static void php_io_operation_free_object(zend_object *obj) +{ + php_io_operation_obj *intern = PHP_IO_OPERATION_FROM_ZOBJ(obj); + if (intern->lazy_handle) { + OBJ_RELEASE(intern->lazy_handle); + } + zend_object_std_dtor(&intern->std); +} + +static zend_class_entry *php_io_operation_ce_for(php_io_op_type type) +{ + switch (type) { + case PHP_IO_OP_POLL: return php_io_operation_poll_ce; + case PHP_IO_OP_TIMER: return php_io_operation_timer_ce; + case PHP_IO_OP_ANY: return php_io_operation_any_ce; + case PHP_IO_OP_READ: return php_io_operation_read_ce; + case PHP_IO_OP_WRITE: return php_io_operation_write_ce; + case PHP_IO_OP_RECV: return php_io_operation_recv_ce; + case PHP_IO_OP_SEND: return php_io_operation_send_ce; + case PHP_IO_OP_ACCEPT: return php_io_operation_accept_ce; + case PHP_IO_OP_CONNECT: return php_io_operation_connect_ce; + case PHP_IO_OP_FSYNC: return php_io_operation_fsync_ce; + case PHP_IO_OP_WAITPID: return php_io_operation_waitpid_ce; + case PHP_IO_OP_SIGWAIT: return php_io_operation_sigwait_ce; + case PHP_IO_OP_GETADDRINFO: return php_io_operation_getaddrinfo_ce; + case PHP_IO_OP_GETNAMEINFO: return php_io_operation_getnameinfo_ce; + default: return php_io_operation_ce; + } +} + +PHPAPI zend_object *php_io_operation_get_zobj(php_io_op *op) +{ + if (!op->zobj) { + /* Through the handler directly: the base class is abstract and the + * constructors are private, the core is the only creator */ + zend_object *zobj = php_io_operation_create_object(php_io_operation_ce_for(op->type)); + PHP_IO_OPERATION_FROM_ZOBJ(zobj)->op = op; + op->zobj = zobj; + } + return op->zobj; +} + +static void php_io_opqueue_sub_unlink(php_io_opqueue_obj *q, php_io_opqueue_sub *sub); +static void php_io_opqueue_sub_free(php_io_opqueue_sub *sub); +static void php_io_opqueue_completion_to_zval(zval *rv, php_io_queue_completion *c); + +static void php_io_operation_detach(zend_object *zobj) +{ + php_io_operation_obj *intern = PHP_IO_OPERATION_FROM_ZOBJ(zobj); + php_io_op *op = intern->op; + intern->op = NULL; + /* Ended without a delivery (cancelled, orphaned): the submission + * record of a queue of ours goes with it, or it would keep the + * operation and its data alive for the life of the queue */ + if (op && op->provider_data) { + php_io_opqueue_sub *sub = op->provider_data; + op->provider_data = NULL; + php_io_opqueue_sub_unlink(sub->owner, sub); + php_io_opqueue_sub_free(sub); + } + /* What a signal handle consumed for this wait goes back with the op */ + if (op && op->type == PHP_IO_OP_SIGWAIT && op->u.sigwait.taken == 0) { + zend_object *handle = intern->lazy_handle ? intern->lazy_handle : op->handle; + if (handle) { + op->u.sigwait.taken = php_io_poll_signal_handle_take(handle, op->u.sigwait.set, op->u.sigwait.info); + } + } +} + +static php_io_op *php_io_operation_fetch(zval *zv) +{ + php_io_operation_obj *intern = PHP_IO_OPERATION_FROM_ZOBJ(Z_OBJ_P(zv)); + if (!intern->op) { + zend_throw_exception(php_io_invalid_operation_exception_ce, "The operation has ended", 0); + } + return intern->op; +} + +static uint32_t php_io_op_events(php_io_op *op) +{ + return op->type == PHP_IO_OP_POLL ? op->u.poll.events : op->ready_events; +} + +/* Registration objects */ + +static zend_object *php_io_registration_create_object(zend_class_entry *ce) +{ + php_io_registration_obj *intern = zend_object_alloc(sizeof(php_io_registration_obj), ce); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->reg = NULL; + return &intern->std; +} + +/* The same object for the record's life: created at the first userland add() or + * getRegistration(), released by the core when the record ends */ +static zend_object *php_io_registration_get_zobj(php_io_registration *reg) +{ + if (!reg->zobj) { + zend_object *zobj = php_io_registration_create_object(php_io_registration_ce); + PHP_IO_REGISTRATION_FROM_ZOBJ(zobj)->reg = reg; + reg->zobj = zobj; + } + return reg->zobj; +} + +static void php_io_registration_detach(zend_object *zobj) +{ + PHP_IO_REGISTRATION_FROM_ZOBJ(zobj)->reg = NULL; +} + +static php_io_registration *php_io_registration_fetch(zval *zv) +{ + php_io_registration_obj *intern = PHP_IO_REGISTRATION_FROM_ZOBJ(Z_OBJ_P(zv)); + if (!intern->reg) { + zend_throw_exception(php_io_invalid_registration_exception_ce, "The registration has ended", 0); + } + return intern->reg; +} + +PHP_METHOD(Io_Registration, __construct) +{ + zend_throw_error(NULL, "Registrations are created by the engine"); +} + +PHP_METHOD(Io_Registration, getHandle) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_registration *reg = php_io_registration_fetch(ZEND_THIS); + if (!reg) { + RETURN_THROWS(); + } + RETURN_OBJ_COPY(&php_io_registration_get_handle(reg)->std); +} + +PHP_METHOD(Io_Registration, getEvent) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_registration *reg = php_io_registration_fetch(ZEND_THIS); + if (!reg) { + RETURN_THROWS(); + } + RETURN_OBJ_COPY(zend_enum_get_case_by_id(php_io_poll_event_class_entry, + reg->event == PHP_POLL_READ ? ZEND_ENUM_Io_Poll_Event_Read : ZEND_ENUM_Io_Poll_Event_Write)); +} + +PHP_METHOD(Io_Registration, getTrigger) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_registration *reg = php_io_registration_fetch(ZEND_THIS); + if (!reg) { + RETURN_THROWS(); + } + RETURN_OBJ_COPY(zend_enum_get_case_by_id(php_io_poll_trigger_ce, + reg->trigger == PHP_IO_TRIGGER_EDGE ? ZEND_ENUM_Io_Poll_Trigger_Edge : ZEND_ENUM_Io_Poll_Trigger_Level)); +} + +PHP_METHOD(Io_Registration, isValid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_BOOL(PHP_IO_REGISTRATION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->reg != NULL); +} + +/* Completion objects */ + +static zend_object *php_io_completion_create_object(zend_class_entry *ce) +{ + php_io_completion_obj *intern = zend_object_alloc(sizeof(php_io_completion_obj), ce); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->operation = NULL; + intern->produced = false; + ZVAL_NULL(&intern->data); + ZVAL_UNDEF(&intern->completions); + return &intern->std; +} + +static void php_io_completion_free_object(zend_object *obj) +{ + php_io_completion_obj *intern = PHP_IO_COMPLETION_FROM_ZOBJ(obj); + if (intern->operation) { + OBJ_RELEASE(intern->operation); + } + zval_ptr_dtor(&intern->data); + zval_ptr_dtor(&intern->completions); + zend_object_std_dtor(&intern->std); +} + +static HashTable *php_io_completion_get_gc(zend_object *obj, zval **table, int *n) +{ + php_io_completion_obj *intern = PHP_IO_COMPLETION_FROM_ZOBJ(obj); + zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); + if (intern->operation) { + zend_get_gc_buffer_add_obj(gc_buffer, intern->operation); + } + zend_get_gc_buffer_add_zval(gc_buffer, &intern->data); + zend_get_gc_buffer_add_zval(gc_buffer, &intern->completions); + zend_get_gc_buffer_use(gc_buffer, table, n); + return NULL; +} + +/* operation: reference added here; data and completions: copied, may be NULL */ +static php_io_completion_obj *php_io_completion_create(zval *rv, php_io_op *op, zend_object *operation, + php_io_status status, int64_t res, int error, zval *data, zval *completions) +{ + object_init_ex(rv, php_io_completion_ce); + php_io_completion_obj *c = PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(rv)); + + GC_ADDREF(operation); + c->operation = operation; + c->status = status; + c->res = res; + c->error = error; + c->events = (op && (op->type == PHP_IO_OP_POLL || status == PHP_IO_READY)) ? (uint32_t) res : 0; + if (data) { + ZVAL_COPY(&c->data, data); + } + if (completions) { + ZVAL_COPY(&c->completions, completions); + } + return c; +} + +/* Io\Operation */ + +PHP_METHOD(Io_Operation, __construct) +{ + zend_throw_error(NULL, "Operations are created by the engine"); +} + +PHP_METHOD(Io_Operation, getHandle) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + zend_object *handle = php_io_op_get_handle(op); + if (handle) { + RETURN_OBJ_COPY(handle); + } + + /* The generic handles are created on demand, so a provider on a + * Context can watch the op like any other handle */ + php_io_operation_obj *intern = PHP_IO_OPERATION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + if (!intern->lazy_handle) { + zval handle_zv; + switch (op->type) { + case PHP_IO_OP_TIMER: { + zend_hrtime_t remaining = php_deadline_is_infinite(&op->deadline) + ? ZEND_HRTIME_T_MAX / 2 : php_io_deadline_remaining(&op->deadline, zend_hrtime()); + php_io_poll_timer_handle_create(&handle_zv, remaining, false); + break; + } + case PHP_IO_OP_WAITPID: + if (op->u.waitpid.pid <= 0) { + RETURN_NULL(); + } + php_io_poll_process_handle_create(&handle_zv, op->u.waitpid.pid); + break; + case PHP_IO_OP_SIGWAIT: + php_io_poll_signal_handle_create(&handle_zv, op->u.sigwait.set); + break; + default: + RETURN_NULL(); + } + intern->lazy_handle = Z_OBJ(handle_zv); + } + RETURN_OBJ_COPY(intern->lazy_handle); +} + +PHP_METHOD(Io_Operation, getRegistration) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + if (!op->registration) { + RETURN_NULL(); + } + RETURN_OBJ_COPY(php_io_registration_get_zobj(op->registration)); +} + +PHP_METHOD(Io_Operation, getEvents) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + php_io_poll_events_to_event_enums(php_io_op_events(op), return_value); +} + +PHP_METHOD(Io_Operation, getTimeout) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + if (php_deadline_is_infinite(&op->deadline)) { + RETURN_NULL(); + } + + zend_hrtime_t remaining = php_io_deadline_remaining(&op->deadline, zend_hrtime()); + zval ns; + ZVAL_LONG(&ns, (zend_long) MIN(remaining, (zend_hrtime_t) ZEND_LONG_MAX)); + zend_call_method_with_1_params(NULL, php_date_ce_time_duration, NULL, "fromnanoseconds", return_value, &ns); +} + +PHP_METHOD(Io_Operation, isValid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + RETURN_BOOL(PHP_IO_OPERATION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->op != NULL); +} + +PHP_METHOD(Io_Operation, complete) +{ + zend_object *status_obj; + zend_long res = 0, error = 0; + + ZEND_PARSE_PARAMETERS_START(1, 3) + Z_PARAM_OBJ_OF_CLASS(status_obj, php_io_completion_status_ce) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(res) + Z_PARAM_LONG(error) + ZEND_PARSE_PARAMETERS_END(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), + php_io_status_from_case(status_obj), res, (int) error, NULL, NULL); +} + +PHP_METHOD(Io_Operation, completeReady) +{ + zval *events_zv; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(events_zv) + ZEND_PARSE_PARAMETERS_END(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + + if (zend_hash_num_elements(Z_ARRVAL_P(events_zv)) == 0) { + zend_argument_value_error(1, "must not be empty"); + RETURN_THROWS(); + } + uint32_t events = php_io_poll_event_enums_to_events(events_zv); + if (!events) { + zend_argument_type_error(1, "must be a list of Io\\Poll\\Event enums"); + RETURN_THROWS(); + } + + php_io_status status = (op->type == PHP_IO_OP_POLL || op->type == PHP_IO_OP_TIMER) + ? PHP_IO_DONE : PHP_IO_READY; + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), status, events, 0, NULL, NULL); +} + +/* Data operations */ + +static php_io_op *php_io_operation_fetch_type(zval *zv, php_io_op_type type) +{ + php_io_op *op = php_io_operation_fetch(zv); + if (op && op->type != type) { + zend_throw_error(NULL, "Operation of an unexpected type"); + return NULL; + } + return op; +} + +PHP_METHOD(Io_Operation_Read, getLength) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_READ); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.io.len); +} + +PHP_METHOD(Io_Operation_Read, getOffset) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_READ); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.io.offset); +} + +PHP_METHOD(Io_Operation_Write, getLength) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_WRITE); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.io.len); +} + +PHP_METHOD(Io_Operation_Write, getOffset) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_WRITE); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.io.offset); +} + +PHP_METHOD(Io_Operation_Recv, getLength) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_RECV); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.io.len); +} + +PHP_METHOD(Io_Operation_Recv, getFlags) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_RECV); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG(op->u.io.flags); +} + +PHP_METHOD(Io_Operation_Send, getLength) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_SEND); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.io.len); +} + +PHP_METHOD(Io_Operation_Send, getFlags) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_SEND); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG(op->u.io.flags); +} + +PHP_METHOD(Io_Operation_Fsync, isDataOnly) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_FSYNC); + if (!op) { + RETURN_THROWS(); + } + RETURN_BOOL(op->u.fsync.data_only); +} + +PHP_METHOD(Io_Operation_WaitPid, getPid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_WAITPID); + if (!op) { + RETURN_THROWS(); + } + RETURN_LONG((zend_long) op->u.waitpid.pid); +} + +PHP_METHOD(Io_Operation_SigWait, getSignals) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_SIGWAIT); + if (!op) { + RETURN_THROWS(); + } + array_init(return_value); + for (int signo = 1; signo < PHP_NSIG; signo++) { + if (php_sigismember(op->u.sigwait.set, signo) == 1) { + add_next_index_long(return_value, signo); + } + } +} + +PHP_METHOD(Io_Operation_Connect, getAddress) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_CONNECT); + if (!op) { + RETURN_THROWS(); + } + zend_string *textaddr = NULL; + php_network_populate_name_from_sockaddr((struct sockaddr *) op->u.connect.addr, op->u.connect.addrlen, + &textaddr, NULL, NULL); + if (!textaddr) { + RETURN_EMPTY_STRING(); + } + RETURN_STR(textaddr); +} + +/* DNS operations */ + +PHP_METHOD(Io_Operation_GetAddrInfo, getHost) +{ + ZEND_PARSE_PARAMETERS_NONE(); + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_GETADDRINFO); + if (!op) { + RETURN_THROWS(); + } + RETURN_STRING(op->u.getaddrinfo.node ? op->u.getaddrinfo.node : ""); +} + +PHP_METHOD(Io_Operation_GetAddrInfo, getService) +{ + ZEND_PARSE_PARAMETERS_NONE(); + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_GETADDRINFO); + if (!op) { + RETURN_THROWS(); + } + if (!op->u.getaddrinfo.service) { + RETURN_NULL(); + } + RETURN_STRING(op->u.getaddrinfo.service); +} + +/* The list is one malloc() block per entry with the address behind the + * addrinfo; freeaddrinfo() would free it on glibc but not on macOS, whose + * C library frees ai_addr on its own, so it is registered for + * php_io_freeaddrinfo() instead */ +PHP_METHOD(Io_Operation_GetAddrInfo, completeWithAddresses) +{ + zval *addresses; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(addresses) + ZEND_PARSE_PARAMETERS_END(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_GETADDRINFO); + if (!op) { + RETURN_THROWS(); + } + + const struct addrinfo *hints = op->u.getaddrinfo.hints; + struct addrinfo *head = NULL, **tail = &head; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(addresses), zval *entry) { + if (Z_TYPE_P(entry) != IS_STRING) { + zend_argument_type_error(1, "must be a list of IP address strings"); + goto fail; + } + if (zend_str_has_nul_byte(Z_STR_P(entry))) { + zend_argument_value_error(1, "must not contain any null bytes"); + goto fail; + } + struct sockaddr_storage ss; + socklen_t len; + memset(&ss, 0, sizeof(ss)); + if (inet_pton(AF_INET, Z_STRVAL_P(entry), &((struct sockaddr_in *) &ss)->sin_addr) == 1) { + ss.ss_family = AF_INET; + len = sizeof(struct sockaddr_in); +#ifdef HAVE_IPV6 + } else if (inet_pton(AF_INET6, Z_STRVAL_P(entry), &((struct sockaddr_in6 *) &ss)->sin6_addr) == 1) { + ss.ss_family = AF_INET6; + len = sizeof(struct sockaddr_in6); +#endif + } else { + zend_argument_value_error(1, "must contain valid IP addresses, \"%s\" given", Z_STRVAL_P(entry)); + goto fail; + } + if (hints && hints->ai_family != AF_UNSPEC && hints->ai_family != ss.ss_family) { + /* Not asked for: skip it */ + continue; + } + struct addrinfo *ai = malloc(sizeof(struct addrinfo) + len); + if (!ai) { + zend_throw_error(NULL, "Out of memory"); + goto fail; + } + memset(ai, 0, sizeof(*ai)); + ai->ai_family = ss.ss_family; + ai->ai_socktype = hints ? hints->ai_socktype : SOCK_STREAM; + ai->ai_protocol = hints ? hints->ai_protocol : 0; + ai->ai_addrlen = len; + ai->ai_addr = (struct sockaddr *) (ai + 1); + memcpy(ai->ai_addr, &ss, len); + *tail = ai; + tail = &ai->ai_next; + } ZEND_HASH_FOREACH_END(); + + if (!head) { + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), PHP_IO_DONE, -1, EAI_NONAME, NULL, NULL); + return; + } + php_io_addrinfo_register(head); + *op->u.getaddrinfo.res = head; + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), PHP_IO_DONE, 0, 0, NULL, NULL)->produced = true; + return; + +fail: + php_io_addrinfo_free_list(head); + RETURN_THROWS(); +} + +PHP_METHOD(Io_Operation_GetNameInfo, getAddress) +{ + ZEND_PARSE_PARAMETERS_NONE(); + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_GETNAMEINFO); + if (!op) { + RETURN_THROWS(); + } + zend_string *textaddr = NULL; + php_network_populate_name_from_sockaddr((struct sockaddr *) op->u.getnameinfo.addr, op->u.getnameinfo.addrlen, + &textaddr, NULL, NULL); + if (!textaddr) { + RETURN_EMPTY_STRING(); + } + RETURN_STR(textaddr); +} + +PHP_METHOD(Io_Operation_GetNameInfo, completeWithName) +{ + zend_string *host, *service = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_PATH_STR(host) + Z_PARAM_OPTIONAL + Z_PARAM_PATH_STR_OR_NULL(service) + ZEND_PARSE_PARAMETERS_END(); + + php_io_op *op = php_io_operation_fetch_type(ZEND_THIS, PHP_IO_OP_GETNAMEINFO); + if (!op) { + RETURN_THROWS(); + } + if (op->u.getnameinfo.host && op->u.getnameinfo.hostlen) { + if (ZSTR_LEN(host) >= op->u.getnameinfo.hostlen) { + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), PHP_IO_DONE, -1, EAI_OVERFLOW, NULL, NULL); + return; + } + memcpy(op->u.getnameinfo.host, ZSTR_VAL(host), ZSTR_LEN(host) + 1); + } + if (op->u.getnameinfo.service && op->u.getnameinfo.servicelen) { + size_t len = service ? ZSTR_LEN(service) : 0; + if (len >= op->u.getnameinfo.servicelen) { + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), PHP_IO_DONE, -1, EAI_OVERFLOW, NULL, NULL); + return; + } + if (service) { + memcpy(op->u.getnameinfo.service, ZSTR_VAL(service), len + 1); + } else { + op->u.getnameinfo.service[0] = '\0'; + } + } + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), PHP_IO_DONE, 0, 0, NULL, NULL)->produced = true; +} + +/* Io\Operation\Any */ + +PHP_METHOD(Io_Operation_Any, getOperations) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + + array_init_size(return_value, op->u.any.n); + for (uint32_t i = 0; i < op->u.any.n; i++) { + zval member; + ZVAL_OBJ_COPY(&member, php_io_operation_get_zobj(op->u.any.ops[i])); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &member); + } +} + +/* The index of the member an operation object wraps, or -1 */ +static int32_t php_io_any_member_index(php_io_op *any, zend_object *operation) +{ + for (uint32_t i = 0; i < any->u.any.n; i++) { + if (any->u.any.ops[i]->zobj == operation) { + return (int32_t) i; + } + } + return -1; +} + +PHP_METHOD(Io_Operation_Any, completeWith) +{ + zval *completions; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY(completions) + ZEND_PARSE_PARAMETERS_END(); + + php_io_op *op = php_io_operation_fetch(ZEND_THIS); + if (!op) { + RETURN_THROWS(); + } + + if (zend_hash_num_elements(Z_ARRVAL_P(completions)) == 0) { + zend_argument_value_error(1, "must not be empty"); + RETURN_THROWS(); + } + + bool *seen = ecalloc(op->u.any.n, sizeof(bool)); + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(completions), zval *entry) { + if (Z_TYPE_P(entry) != IS_OBJECT || !instanceof_function(Z_OBJCE_P(entry), php_io_completion_ce)) { + efree(seen); + zend_argument_type_error(1, "must be a list of Io\\Completion objects"); + RETURN_THROWS(); + } + php_io_completion_obj *c = PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(entry)); + int32_t index = php_io_any_member_index(op, c->operation); + if (index < 0) { + efree(seen); + zend_argument_value_error(1, "must only contain completions of the members of this operation"); + RETURN_THROWS(); + } + if (seen[index]) { + efree(seen); + zend_argument_value_error(1, "must not contain two completions of the same member"); + RETURN_THROWS(); + } + seen[index] = true; + } ZEND_HASH_FOREACH_END(); + efree(seen); + + php_io_completion_create(return_value, op, Z_OBJ_P(ZEND_THIS), PHP_IO_DONE, 0, 0, NULL, completions); +} + +/* Io\Completion */ + +PHP_METHOD(Io_Completion, __construct) +{ + zend_throw_error(NULL, "Completions are created from operations"); +} + +PHP_METHOD(Io_Completion, getOperation) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_OBJ_COPY(PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->operation); +} + +PHP_METHOD(Io_Completion, getStatus) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_OBJ_COPY(php_io_status_case(PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->status)); +} + +PHP_METHOD(Io_Completion, getResult) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_LONG((zend_long) PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->res); +} + +PHP_METHOD(Io_Completion, getEvents) +{ + ZEND_PARSE_PARAMETERS_NONE(); + php_io_poll_events_to_event_enums(PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->events, return_value); +} + +PHP_METHOD(Io_Completion, getError) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_LONG(PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->error); +} + +PHP_METHOD(Io_Completion, getData) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_COPY(&PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS))->data); +} + +PHP_METHOD(Io_Completion, getCompletions) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_completion_obj *c = PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + if (Z_TYPE(c->completions) == IS_ARRAY) { + RETURN_COPY(&c->completions); + } + RETURN_EMPTY_ARRAY(); +} + +/* Io\Poll\OperationQueue */ + +PHPAPI zend_object *php_io_opqueue_create_object(zend_class_entry *ce) +{ + php_io_opqueue_obj *intern = zend_object_alloc(sizeof(php_io_opqueue_obj), ce); + zend_object_std_init(&intern->std, ce); + object_properties_init(&intern->std, ce); + intern->queue = NULL; + intern->subs = NULL; + return &intern->std; +} + +static void php_io_opqueue_sub_unlink(php_io_opqueue_obj *q, php_io_opqueue_sub *sub) +{ + if (sub->prev) { + sub->prev->next = sub->next; + } else { + q->subs = sub->next; + } + if (sub->next) { + sub->next->prev = sub->prev; + } +} + +static void php_io_opqueue_sub_free(php_io_opqueue_sub *sub) +{ + OBJ_RELEASE(sub->operation); + zval_ptr_dtor(&sub->data); + efree(sub); +} + +static void php_io_poll_operation_queue_free_object(zend_object *obj) +{ + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(obj); + + if (intern->queue) { + /* Cancels and withdraws everything still submitted */ + intern->queue->ops->destroy(intern->queue); + intern->queue = NULL; + } + while (intern->subs) { + php_io_opqueue_sub *sub = intern->subs; + /* An op still on a suspended frame must not find the record later */ + php_io_op *op = PHP_IO_OPERATION_FROM_ZOBJ(sub->operation)->op; + if (op && op->provider_data == sub) { + op->provider_data = NULL; + } + php_io_opqueue_sub_unlink(intern, sub); + php_io_opqueue_sub_free(sub); + } + zend_object_std_dtor(&intern->std); +} + +static HashTable *php_io_poll_operation_queue_get_gc(zend_object *obj, zval **table, int *n) +{ + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(obj); + zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); + for (php_io_opqueue_sub *sub = intern->subs; sub; sub = sub->next) { + zend_get_gc_buffer_add_obj(gc_buffer, sub->operation); + zend_get_gc_buffer_add_zval(gc_buffer, &sub->data); + } + zend_get_gc_buffer_use(gc_buffer, table, n); + return NULL; +} + +PHP_METHOD(Io_Poll_OperationQueue, __construct) +{ + zval *context = NULL; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_OBJECT_OF_CLASS_OR_NULL(context, php_io_poll_context_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + + if (intern->queue) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); + } + if (context) { + zend_throw_error(NULL, "Sharing an Io\\Poll\\Context is not supported yet"); + RETURN_THROWS(); + } + + intern->queue = php_io_queue_create_poll(PHP_POLL_BACKEND_AUTO); + if (!intern->queue) { + zend_throw_exception(php_io_exception_class_entry, "Failed to create the poll queue", 0); + RETURN_THROWS(); + } +} + +PHP_METHOD(Io_Poll_OperationQueue, getContext) +{ + ZEND_PARSE_PARAMETERS_NONE(); + zend_throw_error(NULL, "Io\\Poll\\OperationQueue::getContext() is not supported yet"); +} + +PHP_METHOD(Io_Poll_OperationQueue, submit) +{ + zval *op_zv, *data = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_OBJECT_OF_CLASS(op_zv, php_io_operation_ce) + Z_PARAM_OPTIONAL + Z_PARAM_ZVAL(data) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + php_io_op *op = php_io_operation_fetch(op_zv); + if (!op) { + RETURN_THROWS(); + } + if (op->queue) { + zend_throw_error(NULL, "The operation is already submitted"); + RETURN_THROWS(); + } + + php_io_opqueue_sub *sub = emalloc(sizeof(*sub)); + sub->operation = Z_OBJ_P(op_zv); + GC_ADDREF(sub->operation); + if (data) { + ZVAL_COPY(&sub->data, data); + } else { + ZVAL_NULL(&sub->data); + } + sub->owner = intern; + sub->prev = NULL; + sub->next = intern->subs; + if (intern->subs) { + intern->subs->prev = sub; + } + intern->subs = sub; + op->provider_data = sub; + + if (intern->queue->ops->submit(intern->queue, op, sub) == FAILURE) { + op->provider_data = NULL; + php_io_opqueue_sub_unlink(intern, sub); + php_io_opqueue_sub_free(sub); + zend_throw_exception_ex(php_io_exception_class_entry, errno, + "Failed to submit the operation: %s", strerror(errno)); + RETURN_THROWS(); + } + + /* Completed at submit: the caller gets it here and never from waitCompletions() */ + php_io_queue_completion c; + if (intern->queue->ops->take_inline(intern->queue, op, &c)) { + php_io_opqueue_completion_to_zval(return_value, &c); + php_io_opqueue_sub_unlink(intern, sub); + php_io_opqueue_sub_free(sub); + return; + } + RETURN_NULL(); +} + +PHP_METHOD(Io_Poll_OperationQueue, cancel) +{ + zval *op_zv; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(op_zv, php_io_operation_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + php_io_op *op = php_io_operation_fetch(op_zv); + if (!op) { + RETURN_THROWS(); + } + if (op->queue != intern->queue) { + zend_throw_error(NULL, "The operation is not submitted to this queue"); + RETURN_THROWS(); + } + + php_io_opqueue_sub *sub = op->provider_data; + if (intern->queue->ops->cancel(intern->queue, op) == FAILURE) { + zend_throw_exception_ex(php_io_exception_class_entry, errno, + "Failed to cancel the operation: %s", strerror(errno)); + RETURN_THROWS(); + } + if (sub) { + op->provider_data = NULL; + php_io_opqueue_sub_unlink(intern, sub); + php_io_opqueue_sub_free(sub); + } +} + +PHP_METHOD(Io_Poll_OperationQueue, add) +{ + zval *reg_zv; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(reg_zv, php_io_registration_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + php_io_registration *reg = php_io_registration_fetch(reg_zv); + if (!reg) { + RETURN_THROWS(); + } + if (intern->queue->ops->add(intern->queue, reg) == FAILURE) { + zend_throw_exception_ex(php_io_exception_class_entry, errno, + "Failed to add the registration: %s", strerror(errno)); + RETURN_THROWS(); + } +} + +PHP_METHOD(Io_Poll_OperationQueue, remove) +{ + zval *reg_zv; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJECT_OF_CLASS(reg_zv, php_io_registration_ce) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + php_io_registration *reg = php_io_registration_fetch(reg_zv); + if (!reg) { + RETURN_THROWS(); + } + intern->queue->ops->remove(intern->queue, reg); +} + +/* Build the Completion for one delivered completion, members of an Any included */ +static void php_io_opqueue_completion_to_zval(zval *rv, php_io_queue_completion *c) +{ + php_io_op *op = c->op; + php_io_opqueue_sub *sub = c->data; + zval members; + + ZVAL_UNDEF(&members); + if (op->type == PHP_IO_OP_ANY) { + array_init_size(&members, op->u.any.n_results); + for (uint32_t i = 0; i < op->u.any.n_results; i++) { + php_io_op_result *r = &op->u.any.results[i]; + php_io_op *member = op->u.any.ops[r->index]; + zval member_zv; + php_io_completion_create(&member_zv, member, php_io_operation_get_zobj(member), + r->status, r->res, r->error, NULL, NULL)->produced = true; + zend_hash_next_index_insert_new(Z_ARRVAL(members), &member_zv); + } + } + + op->provider_data = NULL; + php_io_completion_create(rv, op, sub->operation, c->result.status, c->result.res, + c->result.error, &sub->data, Z_TYPE(members) == IS_ARRAY ? &members : NULL)->produced = true; + zval_ptr_dtor(&members); +} + +PHP_METHOD(Io_Poll_OperationQueue, waitCompletions) +{ + php_date_time_duration *timeout = NULL; + zend_long max = 0; + bool max_is_null = true; + + ZEND_PARSE_PARAMETERS_START(0, 2) + Z_PARAM_OPTIONAL + Z_PARAM_DATE_TIME_DURATION_OR_NULL(timeout) + Z_PARAM_LONG_OR_NULL(max, max_is_null) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + + /* A zero duration is one reap that never blocks: the non-blocking deadline */ + php_deadline dl; + if (timeout) { + if (timeout->duration.negative) { + zend_argument_value_error(1, "must not be negative"); + RETURN_THROWS(); + } + if (timeout->duration.seconds == 0 && timeout->duration.nanoseconds == 0) { + php_deadline_init_nonblock(&dl); + } else if ((zend_hrtime_t) timeout->duration.seconds >= ZEND_HRTIME_T_MAX / ZEND_NANO_IN_SEC) { + php_deadline_init_infinite(&dl); + } else { + dl = php_io_deadline_from_ns((zend_hrtime_t) timeout->duration.seconds * ZEND_NANO_IN_SEC + + (zend_hrtime_t) timeout->duration.nanoseconds); + } + } + + if (max_is_null) { + max = 64; + } else if (max <= 0) { + zend_argument_value_error(2, "must be greater than 0"); + RETURN_THROWS(); + } else if (max > 4096) { + max = 4096; + } + + php_io_queue_completion *completions = safe_emalloc((size_t) max, sizeof(*completions), 0); + int n = intern->queue->ops->wait(intern->queue, completions, (uint32_t) max, timeout ? &dl : NULL); + if (n < 0) { + int err = errno; + efree(completions); + if (err == EINTR) { + /* A signal handler runs before the caller waits again */ + RETURN_EMPTY_ARRAY(); + } + if (err == EDEADLK) { + zend_throw_exception(php_io_exception_class_entry, + "No operation can complete: nothing pending has a descriptor or a deadline", err); + } else { + zend_throw_exception_ex(php_io_exception_class_entry, err, + "Failed to wait for completions: %s", strerror(err)); + } + RETURN_THROWS(); + } + + array_init_size(return_value, n); + for (int i = 0; i < n; i++) { + php_io_opqueue_sub *sub = completions[i].data; + zval completion; + php_io_opqueue_completion_to_zval(&completion, &completions[i]); + zend_hash_next_index_insert_new(Z_ARRVAL_P(return_value), &completion); + php_io_opqueue_sub_unlink(intern, sub); + php_io_opqueue_sub_free(sub); + } + efree(completions); +} + +PHP_METHOD(Io_Poll_OperationQueue, countPending) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + RETURN_LONG(intern->queue->ops->count_pending(intern->queue)); +} + +static const struct { + uint32_t flag; + zend_long case_id; +} php_io_hooks_capabilities[] = { + { PHP_IO_HOOKS_F_FILES, ZEND_ENUM_Io_Hooks_Capability_Files }, + { PHP_IO_HOOKS_F_DIRECT_DATA, ZEND_ENUM_Io_Hooks_Capability_DirectData }, + { PHP_IO_HOOKS_F_DIRECT_ACCEPT, ZEND_ENUM_Io_Hooks_Capability_DirectAccept }, + { PHP_IO_HOOKS_F_EDGE_REGISTRATIONS, ZEND_ENUM_Io_Hooks_Capability_EdgeRegistrations }, + { PHP_IO_HOOKS_F_LEVEL_REGISTRATIONS, ZEND_ENUM_Io_Hooks_Capability_LevelRegistrations }, +}; + +PHPAPI void php_io_hook_flags_to_capabilities(uint32_t flags, zval *rv) +{ + array_init(rv); + for (size_t i = 0; i < sizeof(php_io_hooks_capabilities) / sizeof(php_io_hooks_capabilities[0]); i++) { + if (flags & php_io_hooks_capabilities[i].flag) { + zval c; + ZVAL_OBJ_COPY(&c, zend_enum_get_case_by_id(php_io_hooks_capability_ce, php_io_hooks_capabilities[i].case_id)); + zend_hash_next_index_insert_new(Z_ARRVAL_P(rv), &c); + } + } +} + +PHP_METHOD(Io_Poll_OperationQueue, getHookCapabilities) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + php_io_hook_flags_to_capabilities(intern->queue->ops->hook_flags(intern->queue), return_value); +} + +/* The userland provider adapter, installed as the C provider by set_hooks() */ + +typedef struct php_io_hooks_php { + php_io_hooks hooks; + zend_object *obj; + zend_fcall_info_cache run_fcc; + zend_fcall_info_cache add_fcc; + zend_fcall_info_cache remove_fcc; +} php_io_hooks_php; + +#define PHP_IO_HOOKS_PHP(h) ZEND_CONTAINER_OF(h, php_io_hooks_php, hooks) + +static void php_io_hooks_method_fcc(zend_object *obj, const char *name, zend_fcall_info_cache *fcc) +{ + zend_string *name_str = zend_string_init(name, strlen(name), false); + zend_function *fn = obj->handlers->get_method(&obj, name_str, NULL); + zend_string_release(name_str); + ZEND_ASSERT(fn != NULL); + + *fcc = (zend_fcall_info_cache) { + .function_handler = fn, + .object = obj, + .called_scope = obj->ce, + }; + zend_fcc_addref(fcc); +} + +/* The provider may be replaced from inside run(): the call keeps its own + * reference, and nothing of the provider is read after it */ +static void php_io_hooks_php_call(zend_fcall_info_cache *fcc, zval *retval, zend_object *arg_obj) +{ + zend_fcall_info_cache held = *fcc; + zval arg; + + GC_ADDREF(held.object); + ZVAL_OBJ_COPY(&arg, arg_obj); + zend_call_known_fcc(&held, retval, 1, &arg, NULL); + zval_ptr_dtor(&arg); + OBJ_RELEASE(held.object); +} + +/* A Done that hands data to the caller: bytes in its buffer, a descriptor, + * an exited child, a taken signal, a resolved name */ +static bool php_io_op_result_is_data(php_io_op *op, const php_io_completion_obj *c) +{ + if (c->status != PHP_IO_DONE || c->error) { + return false; + } + switch (op->type) { + case PHP_IO_OP_READ: + case PHP_IO_OP_RECV: + return c->res != 0; + case PHP_IO_OP_ACCEPT: + case PHP_IO_OP_WAITPID: + case PHP_IO_OP_SIGWAIT: + case PHP_IO_OP_GETADDRINFO: + case PHP_IO_OP_GETNAMEINFO: + return true; + default: + return false; + } +} + +static zend_result php_io_hooks_php_run(php_io_hooks *hooks, php_io_op *op, php_io_op_result *result) +{ + zend_object *zobj = php_io_operation_get_zobj(op); + zval retval; + + ZVAL_UNDEF(&retval); + php_io_hooks_php_call(&PHP_IO_HOOKS_PHP(hooks)->run_fcc, &retval, zobj); + + /* A completion from the queue survives an exception: the op's output is already delivered */ + if (EG(exception) && (Z_TYPE(retval) != IS_OBJECT + || !instanceof_function(Z_OBJCE(retval), php_io_completion_ce) + || PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ(retval))->operation != zobj + || !PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ(retval))->produced)) { + zval_ptr_dtor(&retval); + return FAILURE; + } + if (Z_TYPE(retval) != IS_OBJECT || !instanceof_function(Z_OBJCE(retval), php_io_completion_ce)) { + zval_ptr_dtor(&retval); + zend_throw_error(NULL, "Io\\Hooks\\Hooks::run() must return an Io\\Completion"); + return FAILURE; + } + + php_io_completion_obj *c = PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ(retval)); + if (c->operation != zobj) { + zval_ptr_dtor(&retval); + zend_throw_error(NULL, "Io\\Hooks\\Hooks::run() returned the completion of another operation"); + return FAILURE; + } + if (op->queue && !c->produced) { + zval_ptr_dtor(&retval); + zend_throw_error(NULL, "Io\\Hooks\\Hooks::run() must cancel the operation or wait for its completion from the queue it was submitted to"); + return FAILURE; + } + if (!c->produced && php_io_op_result_is_data(op, c)) { + zval_ptr_dtor(&retval); + if (op->type == PHP_IO_OP_GETADDRINFO || op->type == PHP_IO_OP_GETNAMEINFO) { + /* No name without completeWithAddresses() or completeWithName() */ + result->status = PHP_IO_DONE; + result->index = 0; + result->res = -1; + result->error = EAI_FAIL; + return SUCCESS; + } + zend_throw_error(NULL, "Io\\Hooks\\Hooks::run() cannot complete %s with a result it did not produce, " + "only a queue can", ZSTR_VAL(zobj->ce->name)); + return FAILURE; + } + + result->status = c->status; + result->index = 0; + result->res = c->res; + result->error = c->error; + + if (op->type == PHP_IO_OP_ANY) { + uint32_t n = 0; + if (Z_TYPE(c->completions) == IS_ARRAY) { + ZEND_HASH_FOREACH_VAL(Z_ARRVAL(c->completions), zval *entry) { + php_io_completion_obj *m = PHP_IO_COMPLETION_FROM_ZOBJ(Z_OBJ_P(entry)); + int32_t index = php_io_any_member_index(op, m->operation); + if (index < 0 || n >= op->u.any.n) { + continue; + } + op->u.any.results[n].status = m->status; + op->u.any.results[n].index = (uint32_t) index; + op->u.any.results[n].res = m->res; + op->u.any.results[n].error = m->error; + n++; + } ZEND_HASH_FOREACH_END(); + } + op->u.any.n_results = n; + } + + zval_ptr_dtor(&retval); + return SUCCESS; +} + +static void php_io_hooks_php_add(php_io_hooks *hooks, php_io_registration *reg) +{ + php_io_hooks_php_call(&PHP_IO_HOOKS_PHP(hooks)->add_fcc, NULL, php_io_registration_get_zobj(reg)); +} + +static void php_io_hooks_php_remove(php_io_hooks *hooks, php_io_registration *reg) +{ + php_io_hooks_php_call(&PHP_IO_HOOKS_PHP(hooks)->remove_fcc, NULL, php_io_registration_get_zobj(reg)); +} + +static void php_io_hooks_php_dtor(php_io_hooks *hooks) +{ + php_io_hooks_php *php_hooks = PHP_IO_HOOKS_PHP(hooks); + zend_fcc_dtor(&php_hooks->run_fcc); + zend_fcc_dtor(&php_hooks->add_fcc); + zend_fcc_dtor(&php_hooks->remove_fcc); + OBJ_RELEASE(php_hooks->obj); + efree(php_hooks); +} + +static const php_io_hooks_ops php_io_hooks_php_ops = { + .run = php_io_hooks_php_run, + .add = php_io_hooks_php_add, + .remove = php_io_hooks_php_remove, + .dtor = php_io_hooks_php_dtor, +}; + +/* The installed userland provider object, NULL for none or a C provider */ +static zend_object *php_io_hooks_php_current(void) +{ + php_io_hooks *hooks = php_io_hooks_current(); + if (!hooks || hooks->ops != &php_io_hooks_php_ops) { + return NULL; + } + return PHP_IO_HOOKS_PHP(hooks)->obj; +} + +static uint32_t php_io_hooks_capabilities_to_flags(zval *capabilities) +{ + uint32_t flags = 0; + ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(capabilities), zval *entry) { + if (Z_TYPE_P(entry) != IS_OBJECT || Z_OBJCE_P(entry) != php_io_hooks_capability_ce) { + zend_throw_error(NULL, "Io\\Hooks\\Hooks::getCapabilities() must return a list of Io\\Hooks\\Capability"); + return 0; + } + zend_long case_id = zend_enum_fetch_case_id(Z_OBJ_P(entry)); + for (size_t i = 0; i < sizeof(php_io_hooks_capabilities) / sizeof(php_io_hooks_capabilities[0]); i++) { + if (php_io_hooks_capabilities[i].case_id == case_id) { + flags |= php_io_hooks_capabilities[i].flag; + } + } + } ZEND_HASH_FOREACH_END(); + + return flags; +} + +PHP_FUNCTION(Io_Hooks_set_hooks) +{ + zend_object *hooks_obj = NULL; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_OBJ_OF_CLASS_OR_NULL(hooks_obj, php_io_hooks_ce) + ZEND_PARSE_PARAMETERS_END(); + + if (FG(io_hooks_locked)) { + zend_throw_error(NULL, "Io\\Hooks\\set_hooks() cannot be called from getCapabilities(), add(), remove() or a provider destructor"); + RETURN_THROWS(); + } + zend_object *previous = php_io_hooks_php_current(); + if (php_io_hooks_active() && !previous) { + zend_throw_error(NULL, "IO hooks are owned by an internal provider"); + RETURN_THROWS(); + } + + php_io_hooks_php *php_hooks = NULL; + + if (hooks_obj) { + zval capabilities; + zend_fcall_info_cache caps_fcc; + php_io_hooks_method_fcc(hooks_obj, "getCapabilities", &caps_fcc); + ZVAL_UNDEF(&capabilities); + php_io_hooks_lock(); + zend_call_known_fcc(&caps_fcc, &capabilities, 0, NULL, NULL); + php_io_hooks_unlock(); + zend_fcc_dtor(&caps_fcc); + if (EG(exception)) { + zval_ptr_dtor(&capabilities); + RETURN_THROWS(); + } + if (Z_TYPE(capabilities) != IS_ARRAY) { + zval_ptr_dtor(&capabilities); + zend_throw_error(NULL, "Io\\Hooks\\Hooks::getCapabilities() must return an array"); + RETURN_THROWS(); + } + uint32_t flags = php_io_hooks_capabilities_to_flags(&capabilities); + zval_ptr_dtor(&capabilities); + if (EG(exception)) { + RETURN_THROWS(); + } + + php_hooks = emalloc(sizeof(*php_hooks)); + php_hooks->hooks.ops = &php_io_hooks_php_ops; + php_hooks->hooks.flags = flags; + php_hooks->obj = hooks_obj; + GC_ADDREF(hooks_obj); + php_io_hooks_method_fcc(hooks_obj, "run", &php_hooks->run_fcc); + php_io_hooks_method_fcc(hooks_obj, "add", &php_hooks->add_fcc); + php_io_hooks_method_fcc(hooks_obj, "remove", &php_hooks->remove_fcc); + } + + /* The previous provider is returned, so it survives its dtor */ + if (previous) { + GC_ADDREF(previous); + } + php_io_hooks_register(NULL); + + if (php_hooks) { + zend_result rc = php_io_hooks_register(&php_hooks->hooks); + ZEND_ASSERT(rc == SUCCESS); + } + + if (previous) { + RETURN_OBJ(previous); + } + RETURN_NULL(); +} + +PHP_FUNCTION(Io_Hooks_get_hooks) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + zend_object *current = php_io_hooks_php_current(); + if (!current) { + RETURN_NULL(); + } + RETURN_OBJ_COPY(current); +} + +PHP_FUNCTION(Io_Hooks_is_active) +{ + ZEND_PARSE_PARAMETERS_NONE(); + RETURN_BOOL(php_io_hooks_active()); +} + +static zend_class_entry *php_io_operation_init_subclass(zend_class_entry *ce) +{ + ce->create_object = php_io_operation_create_object; + ce->default_object_handlers = &php_io_operation_handlers; + return ce; +} + +PHP_MINIT_FUNCTION(io_hooks) +{ + php_io_completion_status_ce = register_class_Io_CompletionStatus(); + + php_io_operation_ce = register_class_Io_Operation(); + php_io_operation_ce->create_object = php_io_operation_create_object; + memcpy(&php_io_operation_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + php_io_operation_handlers.offset = offsetof(php_io_operation_obj, std); + php_io_operation_handlers.free_obj = php_io_operation_free_object; + php_io_operation_handlers.clone_obj = NULL; + php_io_operation_ce->default_object_handlers = &php_io_operation_handlers; + + php_io_operation_poll_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Poll(php_io_operation_ce)); + php_io_operation_timer_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Timer(php_io_operation_ce)); + php_io_operation_any_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Any(php_io_operation_ce)); + php_io_operation_read_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Read(php_io_operation_ce)); + php_io_operation_write_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Write(php_io_operation_ce)); + php_io_operation_recv_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Recv(php_io_operation_ce)); + php_io_operation_send_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Send(php_io_operation_ce)); + php_io_operation_accept_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Accept(php_io_operation_ce)); + php_io_operation_connect_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Connect(php_io_operation_ce)); + php_io_operation_fsync_ce = php_io_operation_init_subclass( + register_class_Io_Operation_Fsync(php_io_operation_ce)); + php_io_operation_getaddrinfo_ce = php_io_operation_init_subclass( + register_class_Io_Operation_GetAddrInfo(php_io_operation_ce)); + php_io_operation_getnameinfo_ce = php_io_operation_init_subclass( + register_class_Io_Operation_GetNameInfo(php_io_operation_ce)); + php_io_operation_waitpid_ce = php_io_operation_init_subclass( + register_class_Io_Operation_WaitPid(php_io_operation_ce)); + php_io_operation_sigwait_ce = php_io_operation_init_subclass( + register_class_Io_Operation_SigWait(php_io_operation_ce)); + /* Every operation type is ours: userland may not add one */ + php_io_operation_ce->ce_flags |= ZEND_ACC_FINAL; + + php_io_completion_ce = register_class_Io_Completion(); + php_io_completion_ce->create_object = php_io_completion_create_object; + memcpy(&php_io_completion_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + php_io_completion_handlers.offset = offsetof(php_io_completion_obj, std); + php_io_completion_handlers.free_obj = php_io_completion_free_object; + php_io_completion_handlers.get_gc = php_io_completion_get_gc; + php_io_completion_handlers.clone_obj = NULL; + php_io_completion_ce->default_object_handlers = &php_io_completion_handlers; + + php_io_invalid_operation_exception_ce + = register_class_Io_InvalidOperationException(php_io_exception_class_entry); + + php_io_registration_ce = register_class_Io_Registration(); + php_io_registration_ce->create_object = php_io_registration_create_object; + memcpy(&php_io_registration_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + php_io_registration_handlers.offset = offsetof(php_io_registration_obj, std); + php_io_registration_handlers.clone_obj = NULL; + php_io_registration_ce->default_object_handlers = &php_io_registration_handlers; + php_io_invalid_registration_exception_ce + = register_class_Io_InvalidRegistrationException(php_io_exception_class_entry); + php_io_poll_trigger_ce = register_class_Io_Poll_Trigger(); + + php_io_operation_queue_ce = register_class_Io_OperationQueue(); + + php_io_poll_operation_queue_ce = register_class_Io_Poll_OperationQueue(php_io_operation_queue_ce); + php_io_poll_operation_queue_ce->create_object = php_io_opqueue_create_object; + memcpy(&php_io_opqueue_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + php_io_opqueue_handlers.offset = offsetof(php_io_opqueue_obj, std); + php_io_opqueue_handlers.free_obj = php_io_poll_operation_queue_free_object; + php_io_opqueue_handlers.get_gc = php_io_poll_operation_queue_get_gc; + php_io_opqueue_handlers.clone_obj = NULL; + php_io_poll_operation_queue_ce->default_object_handlers = &php_io_opqueue_handlers; + + php_io_poll_context_ce = zend_hash_str_find_ptr(CG(class_table), ZEND_STRL("io\\poll\\context")); + ZEND_ASSERT(php_io_poll_context_ce != NULL); + + php_io_hooks_ce = register_class_Io_Hooks_Hooks(); + php_io_hooks_capability_ce = register_class_Io_Hooks_Capability(); + + zend_register_functions(NULL, ext_functions, NULL, type); + + php_io_op_zobj_detach = php_io_operation_detach; + php_io_registration_zobj_detach = php_io_registration_detach; + + return SUCCESS; +} diff --git a/ext/standard/io_hooks.h b/ext/standard/io_hooks.h new file mode 100644 index 000000000000..3111a938472b --- /dev/null +++ b/ext/standard/io_hooks.h @@ -0,0 +1,42 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_STANDARD_IO_HOOKS_H +#define PHP_STANDARD_IO_HOOKS_H + +#include "main/php_io.h" + +BEGIN_EXTERN_C() + +/* Borrowed: no reference is added */ +PHPAPI zend_object *php_io_operation_get_zobj(php_io_op *op); + +typedef struct _php_io_opqueue_sub php_io_opqueue_sub; + +typedef struct php_io_opqueue_obj { + php_io_queue *queue; + php_io_opqueue_sub *subs; + zend_object std; +} php_io_opqueue_obj; + +#define PHP_IO_OPQUEUE_FROM_ZOBJ(o) ZEND_CONTAINER_OF(o, php_io_opqueue_obj, std) + +PHPAPI extern zend_class_entry *php_io_operation_queue_ce; +PHPAPI extern zend_object_handlers php_io_opqueue_handlers; +PHPAPI zend_object *php_io_opqueue_create_object(zend_class_entry *ce); +PHPAPI void php_io_hook_flags_to_capabilities(uint32_t flags, zval *rv); + +PHP_MINIT_FUNCTION(io_hooks); + +END_EXTERN_C() + +#endif /* PHP_STANDARD_IO_HOOKS_H */ diff --git a/ext/standard/io_hooks.stub.php b/ext/standard/io_hooks.stub.php new file mode 100644 index 000000000000..49744a77358f --- /dev/null +++ b/ext/standard/io_hooks.stub.php @@ -0,0 +1,325 @@ + the events that let the op proceed; empty when there is no handle */ + public function getEvents(): array {} + + /** The remaining time, computed at call time; null when there is no deadline. */ + public function getTimeout(): ?\Time\Duration {} + + /** False once the operation ended. */ + public function isValid(): bool {} + + /** For providers that complete an operation themselves. */ + public function complete(CompletionStatus $status, int $result = 0, int $error = 0): Completion {} + + /** + * Readiness observed for the operation's handle: Done for a Poll or + * Timer operation, Ready for every other type. + * @param list $events + */ + public function completeReady(array $events): Completion {} + } + + /** + * @strict-properties + * @not-serializable + */ + final class Completion + { + private function __construct() {} + + public function getOperation(): Operation {} + + public function getStatus(): CompletionStatus {} + + /** Bytes, a descriptor, or a Poll event mask as int. */ + public function getResult(): int {} + + /** @return list for Poll operations and Ready completions */ + public function getEvents(): array {} + + public function getError(): int {} + + /** The $data passed to OperationQueue::submit(). */ + public function getData(): mixed {} + + /** @return list members of an Any that had completed; empty otherwise */ + public function getCompletions(): array {} + } + + class InvalidOperationException extends IoException {} + + /** + * A registered (handle, event) pair: waits on it repeat until it is removed. The core + * creates it and hands the same object to Hooks::add(), Hooks::remove() and every wait's + * Operation::getRegistration(). Valid from add() until remove() returned or the provider + * was replaced. + * @strict-properties + * @not-serializable + */ + final class Registration + { + private function __construct() {} + + /** The pair's handle, created on the first call. */ + public function getHandle(): Poll\WeakHandle {} + + /** Read or Write */ + public function getEvent(): Poll\Event {} + + public function getTrigger(): Poll\Trigger {} + + /** False once the registration ended. */ + public function isValid(): bool {} + } + + class InvalidRegistrationException extends IoException {} + + interface OperationQueue + { + /** + * The completion of an operation the queue completed at submit (a wait answered + * from recorded readiness, an accept from a buffered connection), which + * waitCompletions() then never delivers; null while the operation is in flight. + */ + public function submit(Operation $op, mixed $data = null): ?Completion; + + public function cancel(Operation $op): void; + + /** Waits on the pair will repeat until remove(); a trigger the queue cannot keep is served one-shot. */ + public function add(Registration $registration): void; + + /** Before the descriptor closes; no wait on the pair is in flight. */ + public function remove(Registration $registration): void; + + /** @return list */ + public function waitCompletions(?\Time\Duration $timeout = null, ?int $max = null): array; + + /** Submitted and not yet completed. */ + public function countPending(): int; + + /** @return list */ + public function getHookCapabilities(): array; + } +} + +namespace Io\Operation { + + final class Poll extends \Io\Operation {} + + final class Timer extends \Io\Operation {} + + final class Read extends \Io\Operation + { + public function getLength(): int {} + + /** -1 for the current position */ + public function getOffset(): int {} + } + + final class Write extends \Io\Operation + { + public function getLength(): int {} + + public function getOffset(): int {} + } + + final class Recv extends \Io\Operation + { + public function getLength(): int {} + + /** MSG_* */ + public function getFlags(): int {} + } + + final class Send extends \Io\Operation + { + public function getLength(): int {} + + public function getFlags(): int {} + } + + final class Accept extends \Io\Operation {} + + final class Connect extends \Io\Operation + { + /** Textual, as stream_socket_get_name() */ + public function getAddress(): string {} + } + + final class Fsync extends \Io\Operation + { + public function isDataOnly(): bool {} + } + + /** A wait for a child; the handle is a Poll\ProcessHandle for a pid, null for any child. */ + final class WaitPid extends \Io\Operation + { + /** The pid, or -1 for any child */ + public function getPid(): int {} + } + + /** A wait for one of a set of signals; the handle is a Poll\SignalHandle over the set. */ + final class SigWait extends \Io\Operation + { + /** @return list */ + public function getSignals(): array {} + } + + /** A name lookup; no handle. */ + final class GetAddrInfo extends \Io\Operation + { + public function getHost(): string {} + + public function getService(): ?string {} + + /** + * Complete with resolved addresses from a userland resolver; the + * core builds the address list. @param list $addresses IPs + */ + public function completeWithAddresses(array $addresses): \Io\Completion {} + } + + /** A reverse lookup; no handle. */ + final class GetNameInfo extends \Io\Operation + { + /** Textual, as stream_socket_get_name() */ + public function getAddress(): string {} + + public function completeWithName(string $host, ?string $service = null): \Io\Completion {} + } + + /** Wait on several Poll and Timer operations at once. */ + final class Any extends \Io\Operation + { + /** @return list<\Io\Operation> */ + public function getOperations(): array {} + + /** + * For providers that complete members themselves. Each entry was + * made from a member with completeReady() or complete(). + * @param list<\Io\Completion> $completions + */ + public function completeWith(array $completions): \Io\Completion {} + } +} + +namespace Io\Poll { + + /** What the waits on a registered pair tolerate. */ + enum Trigger { + /** Every readiness wait follows a drain: recorded readiness may answer it. */ + case Edge; + /** Readiness must be current at arm time. */ + case Level; + } + + /** + * @strict-properties + * @not-serializable + */ + final class OperationQueue implements \Io\OperationQueue + { + public function __construct(?Context $context = null) {} + + /** The loop may still add its own watchers to it. */ + public function getContext(): Context {} + + public function submit(\Io\Operation $op, mixed $data = null): ?\Io\Completion {} + + public function cancel(\Io\Operation $op): void {} + + public function add(\Io\Registration $registration): void {} + + public function remove(\Io\Registration $registration): void {} + + /** @return list<\Io\Completion> */ + public function waitCompletions(?\Time\Duration $timeout = null, ?int $max = null): array {} + + public function countPending(): int {} + + /** @return list<\Io\Hooks\Capability> */ + public function getHookCapabilities(): array {} + } +} + +namespace Io\Hooks { + + interface Hooks + { + /** @return list */ + public function getCapabilities(): array; + + /** + * Execute one operation the core handed over and return its + * completion, or throw to cancel it. + */ + public function run(\Io\Operation $op): \Io\Completion; + + /** + * Waits on the pair will repeat until remove(). Called only with the capability for + * the pair's trigger, at most once per pair. + */ + public function add(\Io\Registration $registration): void; + + /** The pair is done, before its descriptor closes; no wait on it is in flight. */ + public function remove(\Io\Registration $registration): void; + } + + enum Capability { + /** Regular file ops and Fsync reach the provider, which performs them itself. */ + case Files; + /** Read, Write, Recv, Send and Connect are submitted before the core's syscall-first attempt. */ + case DirectData; + /** Accept is submitted before accept(). */ + case DirectAccept; + /** add() and remove() with Trigger::Edge. */ + case EdgeRegistrations; + /** add() and remove() with Trigger::Level. */ + case LevelRegistrations; + } + + /** Installs the provider, returns the previous userland one. Throws if a C provider is active. */ + function set_hooks(?Hooks $hooks): ?Hooks {} + + /** The installed userland provider; null when none is installed or a C provider owns the request. */ + function get_hooks(): ?Hooks {} + + /** True when any provider is installed, C or userland. */ + function is_active(): bool {} +} diff --git a/ext/standard/io_hooks_arginfo.h b/ext/standard/io_hooks_arginfo.h new file mode 100644 index 000000000000..f735893a6890 --- /dev/null +++ b/ext/standard/io_hooks_arginfo.h @@ -0,0 +1,633 @@ +/* This is a generated file, edit io_hooks.stub.php instead. + * Stub hash: 50d263a9ea9ac42721b53d6348ea137e40f5776f + * Has decl header: yes */ + +#include "zend_enum.h" + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Io_Hooks_set_hooks, 0, 1, Io\\Hooks\\Hooks, 1) + ZEND_ARG_OBJ_INFO(0, hooks, Io\\Hooks\\Hooks, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_Io_Hooks_get_hooks, 0, 0, Io\\Hooks\\Hooks, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_Io_Hooks_is_active, 0, 0, _IS_BOOL, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Operation___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_getHandle, 0, 0, Io\\Poll\\Handle, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_getRegistration, 0, 0, Io\\Registration, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Operation_getEvents, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_getTimeout, 0, 0, Time\\Duration, 1) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Operation_isValid arginfo_Io_Hooks_is_active + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_complete, 0, 1, Io\\Completion, 0) + ZEND_ARG_OBJ_INFO(0, status, Io\\CompletionStatus, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, result, IS_LONG, 0, "0") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, error, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_completeReady, 0, 1, Io\\Completion, 0) + ZEND_ARG_TYPE_INFO(0, events, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Completion___construct arginfo_class_Io_Operation___construct + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Completion_getOperation, 0, 0, Io\\Operation, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Completion_getStatus, 0, 0, Io\\CompletionStatus, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Completion_getResult, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Completion_getEvents arginfo_class_Io_Operation_getEvents + +#define arginfo_class_Io_Completion_getError arginfo_class_Io_Completion_getResult + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Completion_getData, 0, 0, IS_MIXED, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Completion_getCompletions arginfo_class_Io_Operation_getEvents + +#define arginfo_class_Io_Registration___construct arginfo_class_Io_Operation___construct + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Registration_getHandle, 0, 0, Io\\Poll\\WeakHandle, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Registration_getEvent, 0, 0, Io\\Poll\\Event, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Registration_getTrigger, 0, 0, Io\\Poll\\Trigger, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Registration_isValid arginfo_Io_Hooks_is_active + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_OperationQueue_submit, 0, 1, Io\\Completion, 1) + ZEND_ARG_OBJ_INFO(0, op, Io\\Operation, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_OperationQueue_cancel, 0, 1, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, op, Io\\Operation, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_OperationQueue_add, 0, 1, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, registration, Io\\Registration, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_OperationQueue_remove arginfo_class_Io_OperationQueue_add + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_OperationQueue_waitCompletions, 0, 0, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, timeout, Time\\Duration, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_OperationQueue_countPending arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_OperationQueue_getHookCapabilities arginfo_class_Io_Operation_getEvents + +#define arginfo_class_Io_Operation_Read_getLength arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Read_getOffset arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Write_getLength arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Write_getOffset arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Recv_getLength arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Recv_getFlags arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Send_getLength arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_Send_getFlags arginfo_class_Io_Completion_getResult + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Operation_Connect_getAddress, 0, 0, IS_STRING, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Operation_Fsync_isDataOnly arginfo_Io_Hooks_is_active + +#define arginfo_class_Io_Operation_WaitPid_getPid arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Operation_SigWait_getSignals arginfo_class_Io_Operation_getEvents + +#define arginfo_class_Io_Operation_GetAddrInfo_getHost arginfo_class_Io_Operation_Connect_getAddress + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Operation_GetAddrInfo_getService, 0, 0, IS_STRING, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_GetAddrInfo_completeWithAddresses, 0, 1, Io\\Completion, 0) + ZEND_ARG_TYPE_INFO(0, addresses, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Operation_GetNameInfo_getAddress arginfo_class_Io_Operation_Connect_getAddress + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_GetNameInfo_completeWithName, 0, 1, Io\\Completion, 0) + ZEND_ARG_TYPE_INFO(0, host, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, service, IS_STRING, 1, "null") +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Operation_Any_getOperations arginfo_class_Io_Operation_getEvents + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Operation_Any_completeWith, 0, 1, Io\\Completion, 0) + ZEND_ARG_TYPE_INFO(0, completions, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_OperationQueue___construct, 0, 0, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, context, Io\\Poll\\Context, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Poll_OperationQueue_getContext, 0, 0, Io\\Poll\\Context, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Poll_OperationQueue_submit arginfo_class_Io_OperationQueue_submit + +#define arginfo_class_Io_Poll_OperationQueue_cancel arginfo_class_Io_OperationQueue_cancel + +#define arginfo_class_Io_Poll_OperationQueue_add arginfo_class_Io_OperationQueue_add + +#define arginfo_class_Io_Poll_OperationQueue_remove arginfo_class_Io_OperationQueue_add + +#define arginfo_class_Io_Poll_OperationQueue_waitCompletions arginfo_class_Io_OperationQueue_waitCompletions + +#define arginfo_class_Io_Poll_OperationQueue_countPending arginfo_class_Io_Completion_getResult + +#define arginfo_class_Io_Poll_OperationQueue_getHookCapabilities arginfo_class_Io_Operation_getEvents + +#define arginfo_class_Io_Hooks_Hooks_getCapabilities arginfo_class_Io_Operation_getEvents + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Hooks_Hooks_run, 0, 1, Io\\Completion, 0) + ZEND_ARG_OBJ_INFO(0, op, Io\\Operation, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Hooks_Hooks_add arginfo_class_Io_OperationQueue_add + +#define arginfo_class_Io_Hooks_Hooks_remove arginfo_class_Io_OperationQueue_add + +ZEND_FUNCTION(Io_Hooks_set_hooks); +ZEND_FUNCTION(Io_Hooks_get_hooks); +ZEND_FUNCTION(Io_Hooks_is_active); +ZEND_METHOD(Io_Operation, __construct); +ZEND_METHOD(Io_Operation, getHandle); +ZEND_METHOD(Io_Operation, getRegistration); +ZEND_METHOD(Io_Operation, getEvents); +ZEND_METHOD(Io_Operation, getTimeout); +ZEND_METHOD(Io_Operation, isValid); +ZEND_METHOD(Io_Operation, complete); +ZEND_METHOD(Io_Operation, completeReady); +ZEND_METHOD(Io_Completion, __construct); +ZEND_METHOD(Io_Completion, getOperation); +ZEND_METHOD(Io_Completion, getStatus); +ZEND_METHOD(Io_Completion, getResult); +ZEND_METHOD(Io_Completion, getEvents); +ZEND_METHOD(Io_Completion, getError); +ZEND_METHOD(Io_Completion, getData); +ZEND_METHOD(Io_Completion, getCompletions); +ZEND_METHOD(Io_Registration, __construct); +ZEND_METHOD(Io_Registration, getHandle); +ZEND_METHOD(Io_Registration, getEvent); +ZEND_METHOD(Io_Registration, getTrigger); +ZEND_METHOD(Io_Registration, isValid); +ZEND_METHOD(Io_Operation_Read, getLength); +ZEND_METHOD(Io_Operation_Read, getOffset); +ZEND_METHOD(Io_Operation_Write, getLength); +ZEND_METHOD(Io_Operation_Write, getOffset); +ZEND_METHOD(Io_Operation_Recv, getLength); +ZEND_METHOD(Io_Operation_Recv, getFlags); +ZEND_METHOD(Io_Operation_Send, getLength); +ZEND_METHOD(Io_Operation_Send, getFlags); +ZEND_METHOD(Io_Operation_Connect, getAddress); +ZEND_METHOD(Io_Operation_Fsync, isDataOnly); +ZEND_METHOD(Io_Operation_WaitPid, getPid); +ZEND_METHOD(Io_Operation_SigWait, getSignals); +ZEND_METHOD(Io_Operation_GetAddrInfo, getHost); +ZEND_METHOD(Io_Operation_GetAddrInfo, getService); +ZEND_METHOD(Io_Operation_GetAddrInfo, completeWithAddresses); +ZEND_METHOD(Io_Operation_GetNameInfo, getAddress); +ZEND_METHOD(Io_Operation_GetNameInfo, completeWithName); +ZEND_METHOD(Io_Operation_Any, getOperations); +ZEND_METHOD(Io_Operation_Any, completeWith); +ZEND_METHOD(Io_Poll_OperationQueue, __construct); +ZEND_METHOD(Io_Poll_OperationQueue, getContext); +ZEND_METHOD(Io_Poll_OperationQueue, submit); +ZEND_METHOD(Io_Poll_OperationQueue, cancel); +ZEND_METHOD(Io_Poll_OperationQueue, add); +ZEND_METHOD(Io_Poll_OperationQueue, remove); +ZEND_METHOD(Io_Poll_OperationQueue, waitCompletions); +ZEND_METHOD(Io_Poll_OperationQueue, countPending); +ZEND_METHOD(Io_Poll_OperationQueue, getHookCapabilities); + +static const zend_function_entry ext_functions[] = { + ZEND_RAW_FENTRY(ZEND_NS_NAME("Io\\Hooks", "set_hooks"), zif_Io_Hooks_set_hooks, arginfo_Io_Hooks_set_hooks, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("Io\\Hooks", "get_hooks"), zif_Io_Hooks_get_hooks, arginfo_Io_Hooks_get_hooks, 0, NULL, NULL) + ZEND_RAW_FENTRY(ZEND_NS_NAME("Io\\Hooks", "is_active"), zif_Io_Hooks_is_active, arginfo_Io_Hooks_is_active, 0, NULL, NULL) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_methods[] = { + ZEND_ME(Io_Operation, __construct, arginfo_class_Io_Operation___construct, ZEND_ACC_PRIVATE) + ZEND_ME(Io_Operation, getHandle, arginfo_class_Io_Operation_getHandle, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation, getRegistration, arginfo_class_Io_Operation_getRegistration, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation, getEvents, arginfo_class_Io_Operation_getEvents, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation, getTimeout, arginfo_class_Io_Operation_getTimeout, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation, isValid, arginfo_class_Io_Operation_isValid, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation, complete, arginfo_class_Io_Operation_complete, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation, completeReady, arginfo_class_Io_Operation_completeReady, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Completion_methods[] = { + ZEND_ME(Io_Completion, __construct, arginfo_class_Io_Completion___construct, ZEND_ACC_PRIVATE) + ZEND_ME(Io_Completion, getOperation, arginfo_class_Io_Completion_getOperation, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Completion, getStatus, arginfo_class_Io_Completion_getStatus, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Completion, getResult, arginfo_class_Io_Completion_getResult, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Completion, getEvents, arginfo_class_Io_Completion_getEvents, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Completion, getError, arginfo_class_Io_Completion_getError, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Completion, getData, arginfo_class_Io_Completion_getData, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Completion, getCompletions, arginfo_class_Io_Completion_getCompletions, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Registration_methods[] = { + ZEND_ME(Io_Registration, __construct, arginfo_class_Io_Registration___construct, ZEND_ACC_PRIVATE) + ZEND_ME(Io_Registration, getHandle, arginfo_class_Io_Registration_getHandle, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Registration, getEvent, arginfo_class_Io_Registration_getEvent, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Registration, getTrigger, arginfo_class_Io_Registration_getTrigger, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Registration, isValid, arginfo_class_Io_Registration_isValid, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_OperationQueue_methods[] = { + ZEND_RAW_FENTRY("submit", NULL, arginfo_class_Io_OperationQueue_submit, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("cancel", NULL, arginfo_class_Io_OperationQueue_cancel, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("add", NULL, arginfo_class_Io_OperationQueue_add, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("remove", NULL, arginfo_class_Io_OperationQueue_remove, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("waitCompletions", NULL, arginfo_class_Io_OperationQueue_waitCompletions, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("countPending", NULL, arginfo_class_Io_OperationQueue_countPending, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("getHookCapabilities", NULL, arginfo_class_Io_OperationQueue_getHookCapabilities, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Read_methods[] = { + ZEND_ME(Io_Operation_Read, getLength, arginfo_class_Io_Operation_Read_getLength, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_Read, getOffset, arginfo_class_Io_Operation_Read_getOffset, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Write_methods[] = { + ZEND_ME(Io_Operation_Write, getLength, arginfo_class_Io_Operation_Write_getLength, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_Write, getOffset, arginfo_class_Io_Operation_Write_getOffset, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Recv_methods[] = { + ZEND_ME(Io_Operation_Recv, getLength, arginfo_class_Io_Operation_Recv_getLength, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_Recv, getFlags, arginfo_class_Io_Operation_Recv_getFlags, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Send_methods[] = { + ZEND_ME(Io_Operation_Send, getLength, arginfo_class_Io_Operation_Send_getLength, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_Send, getFlags, arginfo_class_Io_Operation_Send_getFlags, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Connect_methods[] = { + ZEND_ME(Io_Operation_Connect, getAddress, arginfo_class_Io_Operation_Connect_getAddress, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Fsync_methods[] = { + ZEND_ME(Io_Operation_Fsync, isDataOnly, arginfo_class_Io_Operation_Fsync_isDataOnly, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_WaitPid_methods[] = { + ZEND_ME(Io_Operation_WaitPid, getPid, arginfo_class_Io_Operation_WaitPid_getPid, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_SigWait_methods[] = { + ZEND_ME(Io_Operation_SigWait, getSignals, arginfo_class_Io_Operation_SigWait_getSignals, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_GetAddrInfo_methods[] = { + ZEND_ME(Io_Operation_GetAddrInfo, getHost, arginfo_class_Io_Operation_GetAddrInfo_getHost, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_GetAddrInfo, getService, arginfo_class_Io_Operation_GetAddrInfo_getService, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_GetAddrInfo, completeWithAddresses, arginfo_class_Io_Operation_GetAddrInfo_completeWithAddresses, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_GetNameInfo_methods[] = { + ZEND_ME(Io_Operation_GetNameInfo, getAddress, arginfo_class_Io_Operation_GetNameInfo_getAddress, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_GetNameInfo, completeWithName, arginfo_class_Io_Operation_GetNameInfo_completeWithName, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Operation_Any_methods[] = { + ZEND_ME(Io_Operation_Any, getOperations, arginfo_class_Io_Operation_Any_getOperations, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Operation_Any, completeWith, arginfo_class_Io_Operation_Any_completeWith, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Poll_OperationQueue_methods[] = { + ZEND_ME(Io_Poll_OperationQueue, __construct, arginfo_class_Io_Poll_OperationQueue___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, getContext, arginfo_class_Io_Poll_OperationQueue_getContext, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, submit, arginfo_class_Io_Poll_OperationQueue_submit, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, cancel, arginfo_class_Io_Poll_OperationQueue_cancel, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, add, arginfo_class_Io_Poll_OperationQueue_add, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, remove, arginfo_class_Io_Poll_OperationQueue_remove, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, waitCompletions, arginfo_class_Io_Poll_OperationQueue_waitCompletions, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, countPending, arginfo_class_Io_Poll_OperationQueue_countPending, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_OperationQueue, getHookCapabilities, arginfo_class_Io_Poll_OperationQueue_getHookCapabilities, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Hooks_Hooks_methods[] = { + ZEND_RAW_FENTRY("getCapabilities", NULL, arginfo_class_Io_Hooks_Hooks_getCapabilities, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("run", NULL, arginfo_class_Io_Hooks_Hooks_run, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("add", NULL, arginfo_class_Io_Hooks_Hooks_add, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_RAW_FENTRY("remove", NULL, arginfo_class_Io_Hooks_Hooks_remove, ZEND_ACC_PUBLIC|ZEND_ACC_ABSTRACT, NULL, NULL) + ZEND_FE_END +}; + +static zend_class_entry *register_class_Io_CompletionStatus(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("Io\\CompletionStatus", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Done", NULL); + + zend_enum_add_case_cstr(class_entry, "Ready", NULL); + + zend_enum_add_case_cstr(class_entry, "Timeout", NULL); + + zend_enum_add_case_cstr(class_entry, "Interrupted", NULL); + + zend_enum_add_case_cstr(class_entry, "Cancelled", NULL); + + zend_enum_add_case_cstr(class_entry, "Unsupported", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io", "Operation", class_Io_Operation_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_ABSTRACT|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Completion(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io", "Completion", class_Io_Completion_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + + return class_entry; +} + +static zend_class_entry *register_class_Io_InvalidOperationException(zend_class_entry *class_entry_Io_IoException) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io", "InvalidOperationException", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_IoException, 0); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Registration(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io", "Registration", class_Io_Registration_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + + return class_entry; +} + +static zend_class_entry *register_class_Io_InvalidRegistrationException(zend_class_entry *class_entry_Io_IoException) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io", "InvalidRegistrationException", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_IoException, 0); + + return class_entry; +} + +static zend_class_entry *register_class_Io_OperationQueue(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io", "OperationQueue", class_Io_OperationQueue_methods); + class_entry = zend_register_internal_interface(&ce); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Poll(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Poll", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Timer(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Timer", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Read(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Read", class_Io_Operation_Read_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Write(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Write", class_Io_Operation_Write_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Recv(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Recv", class_Io_Operation_Recv_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Send(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Send", class_Io_Operation_Send_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Accept(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Accept", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Connect(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Connect", class_Io_Operation_Connect_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Fsync(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Fsync", class_Io_Operation_Fsync_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_WaitPid(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "WaitPid", class_Io_Operation_WaitPid_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_SigWait(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "SigWait", class_Io_Operation_SigWait_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_GetAddrInfo(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "GetAddrInfo", class_Io_Operation_GetAddrInfo_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_GetNameInfo(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "GetNameInfo", class_Io_Operation_GetNameInfo_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Operation_Any(zend_class_entry *class_entry_Io_Operation) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Operation", "Any", class_Io_Operation_Any_methods); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Operation, ZEND_ACC_FINAL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Poll_Trigger(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("Io\\Poll\\Trigger", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Edge", NULL); + + zend_enum_add_case_cstr(class_entry, "Level", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Poll_OperationQueue(zend_class_entry *class_entry_Io_OperationQueue) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Poll", "OperationQueue", class_Io_Poll_OperationQueue_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_OperationQueue); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Hooks_Hooks(void) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Hooks", "Hooks", class_Io_Hooks_Hooks_methods); + class_entry = zend_register_internal_interface(&ce); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Hooks_Capability(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("Io\\Hooks\\Capability", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "Files", NULL); + + zend_enum_add_case_cstr(class_entry, "DirectData", NULL); + + zend_enum_add_case_cstr(class_entry, "DirectAccept", NULL); + + zend_enum_add_case_cstr(class_entry, "EdgeRegistrations", NULL); + + zend_enum_add_case_cstr(class_entry, "LevelRegistrations", NULL); + + return class_entry; +} diff --git a/ext/standard/io_hooks_decl.h b/ext/standard/io_hooks_decl.h new file mode 100644 index 000000000000..6dd679024f76 --- /dev/null +++ b/ext/standard/io_hooks_decl.h @@ -0,0 +1,29 @@ +/* This is a generated file, edit io_hooks.stub.php instead. + * Stub hash: 50d263a9ea9ac42721b53d6348ea137e40f5776f */ + +#ifndef ZEND_IO_HOOKS_DECL_50d263a9ea9ac42721b53d6348ea137e40f5776f_H +#define ZEND_IO_HOOKS_DECL_50d263a9ea9ac42721b53d6348ea137e40f5776f_H + +typedef enum zend_enum_Io_CompletionStatus { + ZEND_ENUM_Io_CompletionStatus_Done = 1, + ZEND_ENUM_Io_CompletionStatus_Ready = 2, + ZEND_ENUM_Io_CompletionStatus_Timeout = 3, + ZEND_ENUM_Io_CompletionStatus_Interrupted = 4, + ZEND_ENUM_Io_CompletionStatus_Cancelled = 5, + ZEND_ENUM_Io_CompletionStatus_Unsupported = 6, +} zend_enum_Io_CompletionStatus; + +typedef enum zend_enum_Io_Poll_Trigger { + ZEND_ENUM_Io_Poll_Trigger_Edge = 1, + ZEND_ENUM_Io_Poll_Trigger_Level = 2, +} zend_enum_Io_Poll_Trigger; + +typedef enum zend_enum_Io_Hooks_Capability { + ZEND_ENUM_Io_Hooks_Capability_Files = 1, + ZEND_ENUM_Io_Hooks_Capability_DirectData = 2, + ZEND_ENUM_Io_Hooks_Capability_DirectAccept = 3, + ZEND_ENUM_Io_Hooks_Capability_EdgeRegistrations = 4, + ZEND_ENUM_Io_Hooks_Capability_LevelRegistrations = 5, +} zend_enum_Io_Hooks_Capability; + +#endif /* ZEND_IO_HOOKS_DECL_50d263a9ea9ac42721b53d6348ea137e40f5776f_H */ diff --git a/ext/standard/io_poll.c b/ext/standard/io_poll.c index dd0b27ae0067..1eac92cd8361 100644 --- a/ext/standard/io_poll.c +++ b/ext/standard/io_poll.c @@ -15,20 +15,43 @@ #include "php.h" #include "zend_enum.h" #include "zend_exceptions.h" +#include "SAPI.h" #include "php_network.h" #include "php_poll.h" #include "io_poll.h" #include "io_poll_arginfo.h" #include "io_poll_decl.h" #include "ext/date/php_time.h" +#include "zend_interfaces.h" + +#include "php_io.h" +#include "ext/standard/proc_open.h" + +#include +#include +#ifdef PHP_WIN32 +# include "win32/sockets.h" +# include "win32/winutil.h" +#endif +#ifndef PHP_WIN32 +# include +#endif +#ifdef __linux__ +# include +#endif /* Class entries */ static zend_class_entry *php_io_poll_backend_class_entry; -static zend_class_entry *php_io_poll_event_class_entry; +zend_class_entry *php_io_poll_event_class_entry; static zend_class_entry *php_io_poll_context_class_entry; static zend_class_entry *php_io_poll_watcher_class_entry; -static zend_class_entry *php_io_poll_handle_class_entry; -static zend_class_entry *php_io_exception_class_entry; +PHPAPI zend_class_entry *php_io_poll_handle_class_entry; +PHPAPI zend_class_entry *php_io_poll_weak_handle_class_entry; +static zend_class_entry *php_io_poll_timer_handle_class_entry; +static zend_class_entry *php_io_poll_notify_handle_class_entry; +static zend_class_entry *php_io_poll_process_handle_class_entry; +static zend_class_entry *php_io_poll_signal_handle_class_entry; +PHPAPI zend_class_entry *php_io_exception_class_entry; static zend_class_entry *php_io_poll_exception_class_entry; static zend_class_entry *php_io_poll_failed_backend_unavailable_class_entry; static zend_class_entry *php_io_poll_failed_operation_class_entry; @@ -39,7 +62,7 @@ static zend_class_entry *php_io_poll_failed_wait_class_entry; static zend_class_entry *php_io_poll_inactive_watcher_class_entry; static zend_class_entry *php_io_poll_handle_already_watched_class_entry; static zend_class_entry *php_io_poll_invalid_handle_class_entry; -static zend_class_entry *php_stream_poll_handle_class_entry; +PHPAPI zend_class_entry *php_stream_poll_handle_class_entry; /* Object handlers */ static zend_object_handlers php_io_poll_context_object_handlers; @@ -48,6 +71,8 @@ static zend_object_handlers php_io_poll_handle_object_handlers; typedef struct php_io_poll_context_object php_io_poll_context_object; +PHPAPI void php_io_poll_handle_remove_from_all_contexts(zend_object *handle_obj); + /* Watcher object structure */ typedef struct php_io_poll_watcher_object { php_poll_handle_object *handle; @@ -59,6 +84,7 @@ typedef struct php_io_poll_watcher_object { php_io_poll_context_object *context; /* Back reference to Context object */ php_socket_t fd; /* Registered fd, SOCK_ERR when inactive */ php_stream *stream; /* Watched stream, NULL when not registered in its watcher list */ + php_poll_timer *timer; /* TimerHandle watchers only */ zend_object std; } php_io_poll_watcher_object; @@ -66,6 +92,9 @@ typedef struct php_io_poll_watcher_object { struct php_io_poll_context_object { php_poll_ctx *ctx; HashTable *watchers; /* Maps fd -> watcher object */ + HashTable *timer_watchers; /* Maps watcher pointer key -> watcher object */ + HashTable *removed; /* Watchers awaiting onWatcherRemoved() */ + zend_fcall_info_cache on_watcher_removed_fcc; zend_object std; }; @@ -89,13 +118,18 @@ static inline void php_io_poll_throw_failed_operation( zend_throw_exception(ce, message, (zend_long) error); } +PHPAPI void php_io_poll_throw_failed_wait(const char *message, php_poll_error error) +{ + php_io_poll_throw_failed_operation(php_io_poll_failed_wait_class_entry, message, error); +} + /* Event enum to bit mask mapping */ static uint32_t php_io_poll_event_enum_to_bit(zend_object *event_enum) { return 1 << (zend_enum_fetch_case_id(event_enum) - 1); } -static uint32_t php_io_poll_event_enums_to_events(zval *event_enums) +PHPAPI uint32_t php_io_poll_event_enums_to_events(zval *event_enums) { HashTable *ht; uint32_t events = 0; @@ -118,7 +152,7 @@ static uint32_t php_io_poll_event_enums_to_events(zval *event_enums) return events; } -static zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums) +PHPAPI zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums) { zval enum_case; @@ -152,92 +186,1223 @@ static zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *even ZVAL_OBJ_COPY(&enum_case, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_EdgeTriggered)); add_next_index_zval(event_enums, &enum_case); } + if (events & PHP_POLL_PRI) { + ZVAL_OBJ_COPY(&enum_case, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Priority)); + add_next_index_zval(event_enums, &enum_case); + } + if (events & PHP_POLL_TIMER) { + ZVAL_OBJ_COPY(&enum_case, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Timer)); + add_next_index_zval(event_enums, &enum_case); + } + if (events & PHP_POLL_NOTIFY) { + ZVAL_OBJ_COPY(&enum_case, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Notify)); + add_next_index_zval(event_enums, &enum_case); + } + if (events & PHP_POLL_SIGNAL) { + ZVAL_OBJ_COPY(&enum_case, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Signal)); + add_next_index_zval(event_enums, &enum_case); + } + if (events & PHP_POLL_PROCESS) { + ZVAL_OBJ_COPY(&enum_case, zend_enum_get_case_by_id(php_io_poll_event_class_entry, ZEND_ENUM_Io_Poll_Event_Process)); + add_next_index_zval(event_enums, &enum_case); + } + + return SUCCESS; +} + +/* Backend enum name to backend type mapping */ +static php_poll_backend_type php_io_poll_backend_enum_to_type(zend_object *backend_enum) +{ + return zend_enum_fetch_case_id(backend_enum) - 2; +} + +static const char *php_io_poll_backend_type_to_name(php_poll_backend_type type) +{ + switch (type) { + case PHP_POLL_BACKEND_POLL: + return "Poll"; + case PHP_POLL_BACKEND_EPOLL: + return "Epoll"; + case PHP_POLL_BACKEND_KQUEUE: + return "Kqueue"; + case PHP_POLL_BACKEND_EVENTPORT: + return "EventPorts"; + case PHP_POLL_BACKEND_WSAPOLL: + return "WSAPoll"; + case PHP_POLL_BACKEND_AUTO: + default: + return "Auto"; + } +} + +/* Stream Poll Handle Implementation */ + +/* The stream is resolved from the resource on every use: fclose() frees the stream + * while the resource, which the handle holds a reference to, stays as a closed one. */ +static php_stream *php_stream_poll_handle_get_stream(php_poll_handle_object *handle) +{ + php_stream_poll_handle_data *data = handle->handle_data; + + if (!data) { + return NULL; + } + + return zend_fetch_resource2(data->res, NULL, php_file_le_stream(), php_file_le_pstream()); +} + +static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle) +{ + php_stream *stream = php_stream_poll_handle_get_stream(handle); + php_socket_t fd; + + if (!stream) { + return SOCK_ERR; + } + + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_POLL | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, 1) + != SUCCESS + || fd == -1) { + return SOCK_ERR; + } + + return fd; +} + +static int php_stream_poll_handle_is_valid(php_poll_handle_object *handle) +{ + php_stream *stream = php_stream_poll_handle_get_stream(handle); + return stream && !php_stream_eof(stream); +} + +static void php_stream_poll_handle_cleanup(php_poll_handle_object *handle) +{ + php_stream_poll_handle_data *data = handle->handle_data; + if (data) { + if (data->res) { + zend_list_delete(data->res); + } + efree(data); + handle->handle_data = NULL; + } +} + +static php_poll_handle_ops php_stream_poll_handle_ops = { + .get_fd = php_stream_poll_handle_get_fd, + .is_valid = php_stream_poll_handle_is_valid, + .cleanup = php_stream_poll_handle_cleanup +}; + +PHPAPI void php_stream_poll_handle_from_stream(zval *dest, php_stream *stream) +{ + object_init_ex(dest, php_stream_poll_handle_class_entry); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(dest); + php_stream_poll_handle_data *data = emalloc(sizeof(php_stream_poll_handle_data)); + data->res = stream->res; + intern->handle_data = data; + + GC_ADDREF(stream->res); +} + +/* StreamPollWeakHandle: like StreamPollHandle but does not hold a refcount on the stream. + * The stream holds the one reference the core needs and releases it in php_stream_free(), + * after the stream pointer is zeroed and the handle is removed from every Context + * (php_stream_poll_weak_handle_notify); userland references keep it as an invalid handle. */ + +static zend_class_entry *php_stream_poll_weak_handle_class_entry; + +typedef struct php_stream_poll_weak_handle_data { + php_stream *stream; /* Not referenced, NULL once closed */ +} php_stream_poll_weak_handle_data; + +static php_socket_t php_stream_poll_weak_handle_get_fd(php_poll_handle_object *handle) +{ + php_stream_poll_weak_handle_data *data = handle->handle_data; + if (!data || !data->stream) { + return SOCK_ERR; + } + php_socket_t fd; + if (php_stream_cast(data->stream, PHP_STREAM_AS_FD_FOR_POLL | PHP_STREAM_CAST_INTERNAL, + (void *) &fd, 1) != SUCCESS || fd == -1) { + return SOCK_ERR; + } + return fd; +} + +/* No liveness check: that is IO, and the handle is asked from inside the stream's operations */ +static int php_stream_poll_weak_handle_is_valid(php_poll_handle_object *handle) +{ + php_stream_poll_weak_handle_data *data = handle->handle_data; + return data && data->stream && !data->stream->eof; +} + +static void php_stream_poll_weak_handle_cleanup(php_poll_handle_object *handle) +{ + php_stream_poll_weak_handle_data *data = handle->handle_data; + if (data) { + if (data->stream) { + data->stream->weak_poll_handle = NULL; + } + efree(data); + handle->handle_data = NULL; + } +} + +/* Called from php_stream_free() while the fd is still open */ +PHPAPI void php_stream_poll_weak_handle_notify(zend_object *handle_obj) +{ + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + php_stream_poll_weak_handle_data *data = handle->handle_data; + data->stream = NULL; + if (handle->watching) { + php_io_poll_handle_remove_from_all_contexts(handle_obj); + } +} + +static php_poll_handle_ops php_stream_poll_weak_handle_ops = { + .get_fd = php_stream_poll_weak_handle_get_fd, + .is_valid = php_stream_poll_weak_handle_is_valid, + .cleanup = php_stream_poll_weak_handle_cleanup, +}; + +static zend_object *php_stream_poll_weak_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_stream_poll_weak_handle_ops); + intern->std.handlers = &php_io_poll_handle_object_handlers; + return &intern->std; +} + +PHPAPI zend_object *php_stream_get_poll_handle(php_stream *stream, bool expose) +{ + php_poll_handle_object *intern; + + if (stream->weak_poll_handle) { + intern = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(stream->weak_poll_handle); + } else { + zval handle_zv; + object_init_ex(&handle_zv, php_stream_poll_weak_handle_class_entry); + intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(&handle_zv); + + php_stream_poll_weak_handle_data *data = emalloc(sizeof(php_stream_poll_weak_handle_data)); + data->stream = stream; + intern->handle_data = data; + + /* The reference is the stream's */ + stream->weak_poll_handle = Z_OBJ(handle_zv); + } + if (expose) { + intern->flags |= PHP_POLL_HANDLE_F_EXPOSED; + } + return &intern->std; +} + +PHP_METHOD(StreamPollWeakHandle, __construct) +{ + zend_throw_error(NULL, "Direct instantiation of %pS is not allowed, use %pS::create instead", + Z_OBJCE_P(ZEND_THIS)->name, Z_OBJCE_P(ZEND_THIS)->name); +} + +PHP_METHOD(StreamPollWeakHandle, create) +{ + zval *stream_zv; + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_RESOURCE(stream_zv) + ZEND_PARSE_PARAMETERS_END(); + + php_stream *stream = (php_stream *) zend_fetch_resource2( + Z_RES_P(stream_zv), "stream", php_file_le_stream(), php_file_le_pstream()); + if (!stream) { + RETURN_THROWS(); + } + + RETURN_OBJ_COPY(php_stream_get_poll_handle(stream, true)); +} + +/* Not for an internal stream (never handed to a script) or a frozen one (in an operation) */ +PHP_METHOD(StreamPollWeakHandle, getStream) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + php_stream_poll_weak_handle_data *data = intern->handle_data; + + if (!data || !data->stream || !(intern->flags & PHP_POLL_HANDLE_F_EXPOSED) + || (data->stream->flags & PHP_STREAM_FLAG_IN_USE)) { + RETURN_NULL(); + } + + GC_ADDREF(data->stream->res); + php_stream_to_zval(data->stream, return_value); +} + +PHP_METHOD(StreamPollWeakHandle, isExposed) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + RETURN_BOOL(intern->flags & PHP_POLL_HANDLE_F_EXPOSED); +} + +PHP_METHOD(StreamPollWeakHandle, isValid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + RETURN_BOOL(intern->ops->is_valid(intern)); +} + +/* TimerHandle: a deadline in the context, no descriptor anywhere */ + +typedef struct php_io_poll_timer_handle_data { + zend_hrtime_t timeout; + bool periodic; +} php_io_poll_timer_handle_data; + +static php_socket_t php_io_poll_timer_handle_get_fd(php_poll_handle_object *handle) +{ + return SOCK_ERR; +} + +static int php_io_poll_timer_handle_is_valid(php_poll_handle_object *handle) +{ + return handle->handle_data != NULL; +} + +static void php_io_poll_timer_handle_cleanup(php_poll_handle_object *handle) +{ + if (handle->handle_data) { + efree(handle->handle_data); + handle->handle_data = NULL; + } +} + +static php_poll_handle_ops php_io_poll_timer_handle_ops = { + .get_fd = php_io_poll_timer_handle_get_fd, + .is_valid = php_io_poll_timer_handle_is_valid, + .cleanup = php_io_poll_timer_handle_cleanup, +}; + +static zend_object *php_io_poll_timer_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_io_poll_timer_handle_ops); + intern->std.handlers = &php_io_poll_handle_object_handlers; + return &intern->std; +} + +static void php_io_poll_timer_handle_init(php_poll_handle_object *handle, zend_hrtime_t timeout, bool periodic) +{ + php_io_poll_timer_handle_data *data = emalloc(sizeof(*data)); + data->timeout = timeout; + data->periodic = periodic; + handle->handle_data = data; +} + +PHPAPI void php_io_poll_timer_handle_create(zval *dest, zend_hrtime_t timeout_ns, bool periodic) +{ + object_init_ex(dest, php_io_poll_timer_handle_class_entry); + php_io_poll_timer_handle_init(PHP_POLL_HANDLE_OBJ_FROM_ZV(dest), timeout_ns, periodic); +} + +static bool php_io_poll_duration_to_ns(php_date_time_duration *d, uint32_t arg_num, zend_hrtime_t *ns) +{ + if (d->duration.negative) { + zend_argument_value_error(arg_num, "must not be negative"); + return false; + } + if (d->duration.seconds >= ZEND_HRTIME_T_MAX / ZEND_NANO_IN_SEC) { + zend_argument_value_error(arg_num, "is too large"); + return false; + } + *ns = (zend_hrtime_t) d->duration.seconds * ZEND_NANO_IN_SEC + d->duration.nanoseconds; + return true; +} + +static void php_io_poll_ns_to_duration(zval *rv, zend_hrtime_t ns) +{ + zval arg; + ZVAL_LONG(&arg, (zend_long) MIN(ns, (zend_hrtime_t) ZEND_LONG_MAX)); + zend_call_method_with_1_params(NULL, php_date_ce_time_duration, NULL, "fromnanoseconds", rv, &arg); +} + +PHP_METHOD(Io_Poll_TimerHandle, __construct) +{ + php_date_time_duration *timeout; + bool periodic = false; + + ZEND_PARSE_PARAMETERS_START(1, 2) + Z_PARAM_DATE_TIME_DURATION(timeout) + Z_PARAM_OPTIONAL + Z_PARAM_BOOL(periodic) + ZEND_PARSE_PARAMETERS_END(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + if (intern->handle_data) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); + } + + zend_hrtime_t ns; + if (!php_io_poll_duration_to_ns(timeout, 1, &ns)) { + RETURN_THROWS(); + } + if (periodic && ns == 0) { + zend_argument_value_error(1, "must not be zero for a periodic timer"); + RETURN_THROWS(); + } + php_io_poll_timer_handle_init(intern, ns, periodic); +} + +PHP_METHOD(Io_Poll_TimerHandle, getTimeout) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_poll_timer_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + php_io_poll_ns_to_duration(return_value, data->timeout); +} + +PHP_METHOD(Io_Poll_TimerHandle, isPeriodic) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_poll_timer_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + RETURN_BOOL(data->periodic); +} + + +/* Handles that stand for something other than descriptor readiness report + * one event of their own and are watched as READ on their descriptor */ +static const char *php_io_poll_virtual_event_name(uint32_t event) +{ + switch (event) { + case PHP_POLL_NOTIFY: return "Event::Notify for a NotifyHandle"; + case PHP_POLL_SIGNAL: return "Event::Signal for a SignalHandle"; + case PHP_POLL_PROCESS: return "Event::Process for a ProcessHandle"; + default: return ""; + } +} + +#define PHP_IO_POLL_VIRTUAL_EVENTS (PHP_POLL_TIMER | PHP_POLL_NOTIFY | PHP_POLL_SIGNAL | PHP_POLL_PROCESS) + +/* Maps the requested events of a handle to what the backend watches; a + * ValueError on the given argument when they do not fit the handle */ +static zend_result php_io_poll_handle_backend_events(php_poll_handle_object *handle, + uint32_t events, uint32_t arg_num, uint32_t *backend_events) +{ + uint32_t virtual = handle->ops->event; + if (virtual) { + if ((events & ~(virtual | PHP_POLL_ONESHOT)) || !(events & virtual)) { + zend_argument_value_error(arg_num, "must be %s", php_io_poll_virtual_event_name(virtual)); + return FAILURE; + } + *backend_events = (events & ~virtual) | PHP_POLL_READ; + } else if (events & PHP_IO_POLL_VIRTUAL_EVENTS) { + zend_argument_value_error(arg_num, + "must not contain Event::Timer, Event::Notify, Event::Signal or Event::Process for this handle"); + return FAILURE; + } else { + *backend_events = events; + } + return SUCCESS; +} + +/* The descriptor of a virtual handle fired: its event only when the + * source had something to take */ +static uint32_t php_io_poll_handle_fired(php_poll_handle_object *handle, uint32_t revents) +{ + uint32_t virtual = handle->ops->event; + if (!virtual) { + return revents; + } + bool took = (revents & PHP_POLL_READ) && (!handle->ops->fired || handle->ops->fired(handle)); + return (revents & (PHP_POLL_ERROR | PHP_POLL_HUP)) | (took ? virtual : 0); +} + +/* NotifyHandle: an eventfd, or a pipe where there is none. Level: readable + * from notify() until clear(). */ + +#ifdef PHP_WIN32 +/* A SOCKET of a loopback pair; INVALID_SOCKET is -1 as a signed value */ +typedef intptr_t php_io_poll_notify_fd; +# define PHP_IO_POLL_NOTIFY_CLOSE(fd) closesocket((php_socket_t) (fd)) +#else +typedef int php_io_poll_notify_fd; +# define PHP_IO_POLL_NOTIFY_CLOSE(fd) close(fd) +#endif + +typedef struct php_io_poll_notify_handle_data { + php_io_poll_notify_fd read_fd; + php_io_poll_notify_fd write_fd; /* -1 when external */ + bool owned; + /* External descriptors only */ + void (*clear)(void *arg); + void *clear_arg; + zend_object *owner; +} php_io_poll_notify_handle_data; + +static php_socket_t php_io_poll_notify_handle_get_fd(php_poll_handle_object *handle) +{ + php_io_poll_notify_handle_data *data = handle->handle_data; + return data ? (php_socket_t) data->read_fd : SOCK_ERR; +} + +static int php_io_poll_notify_handle_is_valid(php_poll_handle_object *handle) +{ + php_io_poll_notify_handle_data *data = handle->handle_data; + return data && data->read_fd >= 0; +} + +static void php_io_poll_notify_handle_cleanup(php_poll_handle_object *handle) +{ + php_io_poll_notify_handle_data *data = handle->handle_data; + if (data) { + if (data->owned) { + if (data->write_fd >= 0 && data->write_fd != data->read_fd) { + PHP_IO_POLL_NOTIFY_CLOSE(data->write_fd); + } + if (data->read_fd >= 0) { + PHP_IO_POLL_NOTIFY_CLOSE(data->read_fd); + } + } + if (data->owner) { + OBJ_RELEASE(data->owner); + } + efree(data); + handle->handle_data = NULL; + } +} + +static php_poll_handle_ops php_io_poll_notify_handle_ops = { + .get_fd = php_io_poll_notify_handle_get_fd, + .is_valid = php_io_poll_notify_handle_is_valid, + .cleanup = php_io_poll_notify_handle_cleanup, + .event = PHP_POLL_NOTIFY, +}; + +static zend_object *php_io_poll_notify_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_io_poll_notify_handle_ops); + intern->std.handlers = &php_io_poll_handle_object_handlers; + return &intern->std; +} + +static zend_result php_io_poll_notify_handle_open(php_io_poll_notify_handle_data *data) +{ +#ifdef __linux__ + int fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fd < 0) { + return FAILURE; + } + data->read_fd = data->write_fd = fd; + return SUCCESS; +#elif defined(PHP_WIN32) + SOCKET socks[2]; + if (socketpair(AF_INET, SOCK_STREAM, 0, socks) != 0) { + return FAILURE; + } + u_long nonblock = 1; + ioctlsocket(socks[0], FIONBIO, &nonblock); + ioctlsocket(socks[1], FIONBIO, &nonblock); + data->read_fd = (php_io_poll_notify_fd) socks[0]; + data->write_fd = (php_io_poll_notify_fd) socks[1]; + return SUCCESS; +#else + int fds[2]; + if (pipe(fds) != 0) { + return FAILURE; + } + for (int i = 0; i < 2; i++) { + fcntl(fds[i], F_SETFL, fcntl(fds[i], F_GETFL) | O_NONBLOCK); + fcntl(fds[i], F_SETFD, FD_CLOEXEC); + } + data->read_fd = fds[0]; + data->write_fd = fds[1]; + return SUCCESS; +#endif +} + +PHPAPI void php_poll_notify(zend_object *handle_obj) +{ + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + php_io_poll_notify_handle_data *data = handle->handle_data; + if (!data || data->write_fd < 0) { + return; + } +#ifdef __linux__ + uint64_t one = 1; + ssize_t n = write(data->write_fd, &one, sizeof(one)); +#elif defined(PHP_WIN32) + char one = 1; + int n = send((php_socket_t) data->write_fd, &one, sizeof(one), 0); +#else + char one = 1; + ssize_t n = write(data->write_fd, &one, sizeof(one)); +#endif + /* A full counter or pipe is still readable, so nothing is lost */ + (void) n; +} + + +/* ProcessHandle: the platform's process source (php_poll_process_source_open) + * where there is one. When it fires the exit status is read and recorded; + * the child stays waitable for whoever collects it. */ + +typedef struct php_io_poll_process_handle_data { + pid_t pid; + int fd; + bool has_status; + bool exited; /* with a status, or collected elsewhere */ + int status; +#ifdef PHP_WIN32 + HANDLE process; /* the process object: keeps the pid valid and answers the exit code */ +#endif +} php_io_poll_process_handle_data; + +#ifdef PHP_WIN32 +/* The process object answers at any time whether the process exited, and its exit code */ +static void php_io_poll_process_handle_probe(php_io_poll_process_handle_data *data) +{ + if (data->exited || !data->process) { + return; + } + if (WaitForSingleObject(data->process, 0) == WAIT_OBJECT_0) { + DWORD code = 0; + if (GetExitCodeProcess(data->process, &code)) { + data->status = (int) code; + data->has_status = true; + } + data->exited = true; + } +} +#endif + +static php_socket_t php_io_poll_process_handle_get_fd(php_poll_handle_object *handle) +{ + php_io_poll_process_handle_data *data = handle->handle_data; + return data ? (php_socket_t) data->fd : SOCK_ERR; +} + +static int php_io_poll_process_handle_is_valid(php_poll_handle_object *handle) +{ + return handle->handle_data != NULL; +} + +static void php_io_poll_process_handle_cleanup(php_poll_handle_object *handle) +{ + php_io_poll_process_handle_data *data = handle->handle_data; + if (data) { +#ifdef PHP_WIN32 + if (data->process) { + CloseHandle(data->process); + } +#endif + if (data->fd >= 0) { + close(data->fd); + } + efree(data); + handle->handle_data = NULL; + } +} + +/* The exit is state: every context watching the handle reports it once */ +static bool php_io_poll_process_handle_fired(php_poll_handle_object *handle) +{ + php_io_poll_process_handle_data *data = handle->handle_data; + if (!data) { + return false; + } +#ifdef PHP_WIN32 + php_io_poll_process_handle_probe(data); +#else + if (!data->exited) { + int status; + pid_t pid = php_poll_process_exit_probe(data->pid, &status); + if (pid == data->pid) { + data->has_status = true; + data->exited = true; + data->status = status; + } else if (pid == -1 && errno == ECHILD) { + /* Not our child, or collected by someone else: gone all the same */ + data->exited = true; + } + } +#endif + return data->exited; +} + +static php_poll_handle_ops php_io_poll_process_handle_ops = { + .get_fd = php_io_poll_process_handle_get_fd, + .is_valid = php_io_poll_process_handle_is_valid, + .cleanup = php_io_poll_process_handle_cleanup, + .event = PHP_POLL_PROCESS, + .fired = php_io_poll_process_handle_fired, +}; + +/* A process handle whose process is gone: its descriptor stays readable */ +static bool php_io_poll_handle_exhausted(php_poll_handle_object *handle) +{ + if (handle->ops != &php_io_poll_process_handle_ops) { + return false; + } + php_io_poll_process_handle_data *data = handle->handle_data; + return data && data->exited; +} + +static zend_object *php_io_poll_process_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_io_poll_process_handle_ops); + intern->std.handlers = &php_io_poll_handle_object_handlers; + return &intern->std; +} + +static zend_result php_io_poll_process_handle_init(php_poll_handle_object *handle, pid_t pid, uint32_t arg_num) +{ + int fd = -1; +#ifdef PHP_WIN32 + HANDLE process = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid); + if (!process) { + if (arg_num) { + char *msg = php_win32_error_to_msg(GetLastError()); + zend_argument_value_error(arg_num, "must be the id of a running process: %s", msg); + php_win32_error_msg_free(msg); + } + return FAILURE; + } +#else + fd = php_poll_process_source_open(pid); + if (fd < 0 && errno != ENOSYS) { + if (arg_num) { + zend_argument_value_error(arg_num, "must be the id of a running process: %s", strerror(errno)); + } + return FAILURE; + } +#endif + php_io_poll_process_handle_data *data = ecalloc(1, sizeof(*data)); + data->pid = pid; + data->fd = fd; +#ifdef PHP_WIN32 + data->process = process; +#endif + handle->handle_data = data; + return SUCCESS; +} + +PHPAPI void php_io_poll_process_handle_create(zval *dest, pid_t pid) +{ + object_init_ex(dest, php_io_poll_process_handle_class_entry); + if (php_io_poll_process_handle_init(PHP_POLL_HANDLE_OBJ_FROM_ZV(dest), pid, 0) == FAILURE) { + /* Not watchable, but still identity for the provider */ + php_io_poll_process_handle_data *data = ecalloc(1, sizeof(*data)); + data->pid = pid; + data->fd = -1; + PHP_POLL_HANDLE_OBJ_FROM_ZV(dest)->handle_data = data; + } +} + +PHP_METHOD(Io_Poll_ProcessHandle, __construct) +{ + zend_long pid; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(pid) + ZEND_PARSE_PARAMETERS_END(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + if (intern->handle_data) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); + } + if (pid <= 0) { + zend_argument_value_error(1, "must be greater than 0"); + RETURN_THROWS(); + } + if (pid > INT_MAX) { + zend_argument_value_error(1, "must be less than or equal to %d", INT_MAX); + RETURN_THROWS(); + } + if (php_io_poll_process_handle_init(intern, (pid_t) pid, 1) == FAILURE) { + RETURN_THROWS(); + } +} + +PHP_METHOD(Io_Poll_ProcessHandle, fromProcess) +{ + zval *zproc; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_RESOURCE(zproc) + ZEND_PARSE_PARAMETERS_END(); + + php_process_id_t pid; + if (!php_proc_open_get_pid(zproc, &pid)) { + RETURN_THROWS(); + } + object_init_ex(return_value, php_io_poll_process_handle_class_entry); + if (php_io_poll_process_handle_init(PHP_POLL_HANDLE_OBJ_FROM_ZV(return_value), (pid_t) pid, 1) == FAILURE) { + /* The engine releases the result of a throwing call */ + RETURN_THROWS(); + } +} + +PHP_METHOD(Io_Poll_ProcessHandle, getPid) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_poll_process_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + RETURN_LONG((zend_long) data->pid); +} + +PHP_METHOD(Io_Poll_ProcessHandle, getStatus) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_poll_process_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; +#ifdef PHP_WIN32 + php_io_poll_process_handle_probe(data); +#endif + if (!data->has_status) { + RETURN_NULL(); + } + RETURN_LONG(data->status); +} + +/* SignalHandle: the platform's signal source (php_poll_signal_source_open) + * over the set where there is one. The signals are blocked while a context + * watches the handle; when the source fires every pending delivery is taken + * and its info recorded until an op or getDelivered() takes it. */ + +typedef struct php_io_poll_signal_handle_data { + php_sigset_t set; + int fd; + uint32_t registrations; /* contexts watching the handle */ + php_siginfo_t *infos; + uint32_t n_infos; + uint32_t cap_infos; +} php_io_poll_signal_handle_data; + +#ifndef PHP_WIN32 +# ifdef ZTS +# define php_io_poll_sigmask pthread_sigmask +# else +# define php_io_poll_sigmask sigprocmask +# endif + +/* Watched handles per signal, and the signals the handles blocked themselves: + * a signal is unblocked again when the last watched handle for it is removed, + * and never when the process had it blocked before any handle */ +ZEND_TLS uint32_t php_io_poll_signal_handle_count[NSIG]; +ZEND_TLS sigset_t php_io_poll_signals_blocked_by_handles; + +PHPAPI void php_io_poll_signal_child_mask(sigset_t *mask) +{ + php_io_poll_sigmask(SIG_BLOCK, NULL, mask); + for (int signo = 1; signo < NSIG; signo++) { + if (sigismember(&php_io_poll_signals_blocked_by_handles, signo) == 1) { + sigdelset(mask, signo); + } + } +} +#endif + +static php_socket_t php_io_poll_signal_handle_get_fd(php_poll_handle_object *handle) +{ + php_io_poll_signal_handle_data *data = handle->handle_data; + return data ? (php_socket_t) data->fd : SOCK_ERR; +} + +static int php_io_poll_signal_handle_is_valid(php_poll_handle_object *handle) +{ + return handle->handle_data != NULL; +} + +static void php_io_poll_signal_handle_record(php_io_poll_signal_handle_data *data, const php_siginfo_t *info) +{ + if (data->n_infos == data->cap_infos) { + data->cap_infos = data->cap_infos ? data->cap_infos * 2 : 4; + data->infos = safe_erealloc(data->infos, data->cap_infos, sizeof(*data->infos), 0); + } + data->infos[data->n_infos++] = *info; +} + +static void php_io_poll_signal_handle_block(php_io_poll_signal_handle_data *data) +{ +#ifndef PHP_WIN32 + /* Block what is not blocked yet, so the signals queue for the source */ + sigset_t old; + if (php_io_poll_sigmask(SIG_BLOCK, &data->set, &old) != 0) { + return; + } + for (int signo = 1; signo < NSIG; signo++) { + if (sigismember(&data->set, signo) != 1) { + continue; + } + if (php_io_poll_signal_handle_count[signo]++ == 0 && sigismember(&old, signo) == 0) { + sigaddset(&php_io_poll_signals_blocked_by_handles, signo); + } + } +#endif +} + +/* What arrived while the handle was watched is taken into its record before + * the unblock, so nothing is delivered with its default action later */ +static void php_io_poll_signal_handle_unblock(php_io_poll_signal_handle_data *data) +{ +#ifndef PHP_WIN32 + sigset_t unblock; + bool any = false; + sigemptyset(&unblock); + for (int signo = 1; signo < NSIG; signo++) { + if (sigismember(&data->set, signo) != 1 || php_io_poll_signal_handle_count[signo] == 0) { + continue; + } + if (--php_io_poll_signal_handle_count[signo] == 0 + && sigismember(&php_io_poll_signals_blocked_by_handles, signo) == 1) { + sigdelset(&php_io_poll_signals_blocked_by_handles, signo); + sigaddset(&unblock, signo); + any = true; + } + } + if (any) { + siginfo_t info; + while (php_poll_signal_take_pending(&unblock, &info) > 0) { + php_io_poll_signal_handle_record(data, &info); + } + php_io_poll_sigmask(SIG_UNBLOCK, &unblock, NULL); + } +#endif +} + +static void php_io_poll_signal_handle_added(php_poll_handle_object *handle) +{ + php_io_poll_signal_handle_data *data = handle->handle_data; + if (data && data->registrations++ == 0) { + php_io_poll_signal_handle_block(data); + } +} + +static void php_io_poll_signal_handle_removed(php_poll_handle_object *handle) +{ + php_io_poll_signal_handle_data *data = handle->handle_data; + if (data && data->registrations > 0 && --data->registrations == 0) { + php_io_poll_signal_handle_unblock(data); + } +} + +static void php_io_poll_signal_handle_cleanup(php_poll_handle_object *handle) +{ + php_io_poll_signal_handle_data *data = handle->handle_data; + if (data) { + /* Freed by the cycle collector before a context that still watches it */ + if (data->registrations > 0) { + data->registrations = 0; + php_io_poll_signal_handle_unblock(data); + } + if (data->fd >= 0) { + close(data->fd); + } + if (data->infos) { + efree(data->infos); + } + efree(data); + handle->handle_data = NULL; + } +} + +static bool php_io_poll_signal_handle_fired(php_poll_handle_object *handle) +{ + php_io_poll_signal_handle_data *data = handle->handle_data; + bool took = false; + if (!data || data->fd < 0) { + return false; + } +#ifndef PHP_WIN32 + siginfo_t info; + while (php_poll_signal_source_take(data->fd, &data->set, &info) > 0) { + php_io_poll_signal_handle_record(data, &info); + took = true; + } +#endif + return took; +} + +static php_poll_handle_ops php_io_poll_signal_handle_ops = { + .get_fd = php_io_poll_signal_handle_get_fd, + .is_valid = php_io_poll_signal_handle_is_valid, + .cleanup = php_io_poll_signal_handle_cleanup, + .event = PHP_POLL_SIGNAL, + .fired = php_io_poll_signal_handle_fired, + .added = php_io_poll_signal_handle_added, + .removed = php_io_poll_signal_handle_removed, +}; + +static zend_object *php_io_poll_signal_handle_create_object(zend_class_entry *ce) +{ + php_poll_handle_object *intern = php_poll_handle_object_create( + sizeof(php_poll_handle_object), ce, &php_io_poll_signal_handle_ops); + intern->std.handlers = &php_io_poll_handle_object_handlers; + return &intern->std; +} + +static void php_io_poll_signal_handle_init(php_poll_handle_object *handle, const php_sigset_t *set) +{ + php_io_poll_signal_handle_data *data = ecalloc(1, sizeof(*data)); + data->set = *set; + data->fd = -1; +#ifndef PHP_WIN32 + /* The source sees only blocked signals, so it is inert until a context + * watches the handle. No source (ENOSYS, or the platform could not open + * one): the handle is still identity for a provider, and Context::add() + * refuses it */ + data->fd = php_poll_signal_source_open(set); +#endif + handle->handle_data = data; +} + +PHPAPI void php_io_poll_signal_handle_create(zval *dest, const php_sigset_t *set) +{ + object_init_ex(dest, php_io_poll_signal_handle_class_entry); + php_io_poll_signal_handle_init(PHP_POLL_HANDLE_OBJ_FROM_ZV(dest), set); +} + +PHPAPI int php_io_poll_signal_handle_take(zend_object *handle_obj, const php_sigset_t *set, php_siginfo_t *info) +{ + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + if (handle->ops != &php_io_poll_signal_handle_ops) { + return 0; + } + php_io_poll_signal_handle_data *data = handle->handle_data; + if (!data) { + return 0; + } + for (uint32_t i = 0; i < data->n_infos; i++) { + int signo = data->infos[i].si_signo; + if (php_sigismember(set, signo) == 1) { + if (info) { + *info = data->infos[i]; + } + memmove(&data->infos[i], &data->infos[i + 1], (data->n_infos - i - 1) * sizeof(*data->infos)); + data->n_infos--; + return signo; + } + } + return 0; +} - return SUCCESS; +/* SIGKILL and SIGSTOP cannot be blocked, and blocking a signal raised by a + * fault is undefined behaviour */ +static bool php_io_poll_signal_unwatchable(zend_long signo) +{ + switch (signo) { +#ifdef SIGKILL + case SIGKILL: +#endif +#ifdef SIGSTOP + case SIGSTOP: +#endif +#ifdef SIGBUS + case SIGBUS: +#endif + case SIGSEGV: + case SIGFPE: + case SIGILL: + return true; + default: + return false; + } } -/* Backend enum name to backend type mapping */ -static php_poll_backend_type php_io_poll_backend_enum_to_type(zend_object *backend_enum) +/* max_execution_time runs on this signal; blocking it disables the timeout */ +static bool php_io_poll_signal_is_engine_timeout(zend_long signo) { - return zend_enum_fetch_case_id(backend_enum) - 2; +#if defined(ZEND_MAX_EXECUTION_TIMERS) + return signo == SIGRTMIN; +#elif defined(PHP_WIN32) + return false; +#elif defined(__CYGWIN__) || defined(__PASE__) || (defined(__aarch64__) && defined(__APPLE__)) + return signo == SIGALRM; +#else + return signo == SIGPROF; +#endif } -static const char *php_io_poll_backend_type_to_name(php_poll_backend_type type) +PHP_METHOD(Io_Poll_SignalHandle, __construct) { - switch (type) { - case PHP_POLL_BACKEND_POLL: - return "Poll"; - case PHP_POLL_BACKEND_EPOLL: - return "Epoll"; - case PHP_POLL_BACKEND_KQUEUE: - return "Kqueue"; - case PHP_POLL_BACKEND_EVENTPORT: - return "EventPorts"; - case PHP_POLL_BACKEND_WSAPOLL: - return "WSAPoll"; - case PHP_POLL_BACKEND_AUTO: - default: - return "Auto"; + HashTable *signals; + + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_ARRAY_HT(signals) + ZEND_PARSE_PARAMETERS_END(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + if (intern->handle_data) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); + } +#ifdef ZTS + /* The mask is per thread: in a multi-threaded SAPI another thread would + * take the signal with its default action and the handle would never + * see it. The CLI runs one PHP thread. */ + if (strcmp(sapi_module.name, "cli") != 0) { + zend_throw_exception(php_io_poll_exception_class_entry, + "Io\\Poll\\SignalHandle is only available in the CLI in thread-safe builds", 0); + RETURN_THROWS(); + } +#endif + if (zend_hash_num_elements(signals) == 0) { + zend_argument_must_not_be_empty_error(1); + RETURN_THROWS(); } -} -/* Stream Poll Handle Implementation */ + php_sigset_t set; + php_sigemptyset(&set); + ZEND_HASH_FOREACH_VAL(signals, zval *entry) { + ZVAL_DEREF(entry); + if (Z_TYPE_P(entry) != IS_LONG) { + zend_argument_type_error(1, "signals must be of type int, %s given", zend_zval_value_name(entry)); + RETURN_THROWS(); + } + zend_long signo = Z_LVAL_P(entry); + if (signo < 1 || signo >= PHP_NSIG) { + zend_argument_value_error(1, "signals must be between 1 and %d", PHP_NSIG - 1); + RETURN_THROWS(); + } + if (php_io_poll_signal_unwatchable(signo)) { + zend_argument_value_error(1, "must not contain signal " ZEND_LONG_FMT ", which cannot be blocked", signo); + RETURN_THROWS(); + } + if (php_io_poll_signal_is_engine_timeout(signo)) { + zend_argument_value_error(1, "must not contain signal " ZEND_LONG_FMT ", which the execution timeout uses", signo); + RETURN_THROWS(); + } + if (php_sigaddset(&set, (int) signo) != 0) { + zend_argument_value_error(1, "must not contain signal " ZEND_LONG_FMT ", which is reserved", signo); + RETURN_THROWS(); + } + } ZEND_HASH_FOREACH_END(); -/* The stream is resolved from the resource on every use: fclose() frees the stream - * while the resource, which the handle holds a reference to, stays as a closed one. */ -static php_stream *php_stream_poll_handle_get_stream(php_poll_handle_object *handle) + php_io_poll_signal_handle_init(intern, &set); +} + +PHP_METHOD(Io_Poll_SignalHandle, getSignals) { - php_stream_poll_handle_data *data = handle->handle_data; + ZEND_PARSE_PARAMETERS_NONE(); - if (!data) { - return NULL; + php_io_poll_signal_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + array_init(return_value); + for (int signo = 1; signo < PHP_NSIG; signo++) { + if (php_sigismember(&data->set, signo) == 1) { + add_next_index_long(return_value, signo); + } } - - return zend_fetch_resource2(data->res, NULL, php_file_le_stream(), php_file_le_pstream()); } -static php_socket_t php_stream_poll_handle_get_fd(php_poll_handle_object *handle) +PHP_METHOD(Io_Poll_SignalHandle, getDelivered) { - php_stream *stream = php_stream_poll_handle_get_stream(handle); - php_socket_t fd; + ZEND_PARSE_PARAMETERS_NONE(); - if (!stream) { - return SOCK_ERR; + php_io_poll_signal_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + array_init(return_value); + for (uint32_t i = 0; i < data->n_infos; i++) { + add_next_index_long(return_value, data->infos[i].si_signo); } + data->n_infos = 0; +} - if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, - (void *) &fd, 1) - != SUCCESS - || fd == -1) { - return SOCK_ERR; +PHP_METHOD(Io_Poll_NotifyHandle, __construct) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + if (intern->handle_data) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); } - return fd; + php_io_poll_notify_handle_data *data = ecalloc(1, sizeof(*data)); + data->read_fd = data->write_fd = -1; + data->owned = true; + if (php_io_poll_notify_handle_open(data) != SUCCESS) { + efree(data); + zend_throw_exception_ex(php_io_poll_failed_context_init_class_entry, PHP_POLL_ERR_SYSTEM, + "Failed to create the notification descriptor: %s", strerror(errno)); + RETURN_THROWS(); + } + intern->handle_data = data; } -static int php_stream_poll_handle_is_valid(php_poll_handle_object *handle) +PHPAPI void php_io_poll_notify_handle_create_external(zval *dest, php_socket_t fd, + void (*clear)(void *arg), void *arg, zend_object *owner) { - php_stream *stream = php_stream_poll_handle_get_stream(handle); - return stream && !php_stream_eof(stream); + object_init_ex(dest, php_io_poll_notify_handle_class_entry); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(dest); + php_io_poll_notify_handle_data *data = ecalloc(1, sizeof(*data)); + data->read_fd = (php_io_poll_notify_fd) fd; + data->write_fd = -1; + data->owned = false; + data->clear = clear; + data->clear_arg = arg; + data->owner = owner; + if (owner) { + GC_ADDREF(owner); + } + intern->handle_data = data; } -static void php_stream_poll_handle_cleanup(php_poll_handle_object *handle) +/* Handles hold no zvals; an external NotifyHandle holds its owner */ +static HashTable *php_io_poll_handle_get_gc(zend_object *obj, zval **table, int *n) { - php_stream_poll_handle_data *data = handle->handle_data; - if (data) { - if (data->res) { - zend_list_delete(data->res); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(obj); + zend_get_gc_buffer *gc_buffer = zend_get_gc_buffer_create(); + if (intern->ops == &php_io_poll_notify_handle_ops && intern->handle_data) { + php_io_poll_notify_handle_data *data = intern->handle_data; + if (data->owner) { + zend_get_gc_buffer_add_obj(gc_buffer, data->owner); } - efree(data); - handle->handle_data = NULL; } + zend_get_gc_buffer_use(gc_buffer, table, n); + return NULL; +} + +PHP_METHOD(Io_Poll_NotifyHandle, notify) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); + php_io_poll_notify_handle_data *data = intern->handle_data; + if (!data->owned) { + zend_throw_error(NULL, "This Io\\Poll\\NotifyHandle is raised by its owner and cannot be notified"); + RETURN_THROWS(); + } + php_poll_notify(&intern->std); } -static php_poll_handle_ops php_stream_poll_handle_ops = { - .get_fd = php_stream_poll_handle_get_fd, - .is_valid = php_stream_poll_handle_is_valid, - .cleanup = php_stream_poll_handle_cleanup -}; +PHP_METHOD(Io_Poll_NotifyHandle, clear) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_poll_notify_handle_data *data = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS)->handle_data; + if (!data->owned) { + data->clear(data->clear_arg); + return; + } +#ifdef __linux__ + uint64_t count; + ssize_t n = read(data->read_fd, &count, sizeof(count)); + (void) n; +#elif defined(PHP_WIN32) + char buf[64]; + while (recv((php_socket_t) data->read_fd, buf, sizeof(buf), 0) > 0) { + } +#else + char buf[64]; + while (read(data->read_fd, buf, sizeof(buf)) > 0) { + } +#endif +} /* Handle interface internal only */ static int php_stream_poll_handle_implement_interface(zend_class_entry *interface, zend_class_entry *implementor) @@ -274,6 +1439,7 @@ static zend_object *php_io_poll_watcher_create_object(zend_class_entry *ce) intern->context = NULL; intern->fd = SOCK_ERR; intern->stream = NULL; + intern->timer = NULL; ZVAL_NULL(&intern->data); return &intern->std; @@ -288,6 +1454,9 @@ static zend_object *php_io_poll_context_create_object(zend_class_entry *ce) intern->ctx = NULL; intern->watchers = NULL; + intern->timer_watchers = NULL; + intern->removed = NULL; + intern->on_watcher_removed_fcc = empty_fcall_info_cache; return &intern->std; } @@ -337,15 +1506,99 @@ static void php_io_poll_stream_unwatch(php_io_poll_watcher_object *watcher) } } -static void php_io_poll_context_retire_watcher( +static void php_io_poll_handle_unwatch( php_io_poll_context_object *context, php_io_poll_watcher_object *watcher) +{ + php_poll_handle_object *handle = watcher->handle; + if (handle && handle->watching + && zend_hash_index_del(handle->watching, php_io_poll_compute_ptr_key(context)) == SUCCESS + && handle->ops->removed) { + handle->ops->removed(handle); + } +} + +/* The single removal path: drops the backend registration, both reverse + * lookups and the context's reference. notify queues a removal the caller + * did not initiate for onWatcherRemoved(); no user code runs here. */ +static void php_io_poll_context_retire_watcher( + php_io_poll_context_object *context, php_io_poll_watcher_object *watcher, bool notify) { php_socket_t fd = watcher->fd; + bool is_timer = watcher->timer != NULL; - php_poll_remove(context->ctx, (int) fd); + if (is_timer) { + php_poll_timer_remove(context->ctx, watcher->timer); + watcher->timer = NULL; + } else { + php_poll_remove(context->ctx, (int) fd); + } php_io_poll_stream_unwatch(watcher); + php_io_poll_handle_unwatch(context, watcher); php_io_poll_watcher_deactivate(watcher); - zend_hash_index_del(context->watchers, (zend_ulong) fd); + + /* Keep the watcher alive across the registry removal and the callback */ + GC_ADDREF(&watcher->std); + if (is_timer) { + zend_hash_index_del(context->timer_watchers, php_io_poll_compute_ptr_key(watcher)); + } else { + zend_hash_index_del(context->watchers, (zend_ulong) fd); + } + + if (notify && ZEND_FCC_INITIALIZED(context->on_watcher_removed_fcc)) { + if (!context->removed) { + context->removed = emalloc(sizeof(HashTable)); + zend_hash_init(context->removed, 4, NULL, ZVAL_PTR_DTOR, 0); + } + zval watcher_zv; + ZVAL_OBJ_COPY(&watcher_zv, &watcher->std); + zend_hash_next_index_insert_new(context->removed, &watcher_zv); + } + + OBJ_RELEASE(&watcher->std); +} + +/* Runs the queued onWatcherRemoved() calls, stopping at the first exception + * with the rest still queued */ +static zend_result php_io_poll_context_deliver_removed(php_io_poll_context_object *context) +{ + while (context->removed && zend_hash_num_elements(context->removed)) { + zend_ulong idx = 0; + zval *first = NULL, watcher_zv; + ZEND_HASH_FOREACH_NUM_KEY_VAL(context->removed, idx, first) { + break; + } ZEND_HASH_FOREACH_END(); + ZVAL_COPY(&watcher_zv, first); + zend_hash_index_del(context->removed, idx); + if (zend_hash_num_elements(context->removed) == 0) { + zend_hash_clean(context->removed); + } + + if (ZEND_FCC_INITIALIZED(context->on_watcher_removed_fcc)) { + zend_call_known_fcc(&context->on_watcher_removed_fcc, NULL, 1, &watcher_zv, NULL); + } + zval_ptr_dtor(&watcher_zv); + if (EG(exception)) { + return FAILURE; + } + } + return SUCCESS; +} + +/* Retires every watcher of the list; they are kept alive until the loop is + * done so that releasing one runs no user code in the middle of it */ +static void php_io_poll_retire_watchers(php_io_poll_watcher_object **list, uint32_t n) +{ + for (uint32_t i = 0; i < n; i++) { + php_io_poll_watcher_object *watcher = list[i]; + watcher->closed = true; + if (watcher->active && watcher->context) { + php_io_poll_context_retire_watcher(watcher->context, watcher, true); + } + } + for (uint32_t i = 0; i < n; i++) { + OBJ_RELEASE(&list[i]->std); + } + efree(list); } /* Called from php_stream_free() while the fd is still open */ @@ -354,17 +1607,19 @@ PHPAPI void php_io_poll_stream_notify_close(php_stream *stream) HashTable *watchers = stream->poll_watchers; stream->poll_watchers = NULL; + uint32_t n = 0; + php_io_poll_watcher_object **list = safe_emalloc(zend_hash_num_elements(watchers), sizeof(*list), 0); ZEND_HASH_FOREACH_VAL(watchers, zval *zv) { php_io_poll_watcher_object *watcher = Z_PTR_P(zv); watcher->stream = NULL; - watcher->closed = true; - if (watcher->context) { - php_io_poll_context_retire_watcher(watcher->context, watcher); - } + GC_ADDREF(&watcher->std); + list[n++] = watcher; } ZEND_HASH_FOREACH_END(); zend_hash_destroy(watchers); pefree(watchers, stream->is_persistent); + + php_io_poll_retire_watchers(list, n); } /* Object Destruction Functions */ @@ -376,8 +1631,15 @@ static void php_io_poll_watcher_free_object(zend_object *obj) php_io_poll_stream_unwatch(intern); zval_ptr_dtor(&intern->data); + /* Freed by the cycle collector before its context */ + if (intern->active && intern->context) { + php_io_poll_handle_unwatch(intern->context, intern); + } + if (intern->handle) { OBJ_RELEASE(&intern->handle->std); + /* A context freed later in the same collection must not follow it */ + intern->handle = NULL; } zend_object_std_dtor(&intern->std); @@ -391,6 +1653,17 @@ static void php_io_poll_context_free_object(zend_object *obj) ZEND_HASH_FOREACH_VAL(intern->watchers, zval *zv) { php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv)); php_io_poll_stream_unwatch(watcher); + php_io_poll_handle_unwatch(intern, watcher); + php_io_poll_watcher_deactivate(watcher); + } ZEND_HASH_FOREACH_END(); + } + + if (intern->timer_watchers) { + ZEND_HASH_FOREACH_VAL(intern->timer_watchers, zval *zv) { + php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZOBJ(Z_OBJ_P(zv)); + php_poll_timer_remove(intern->ctx, watcher->timer); + watcher->timer = NULL; + php_io_poll_handle_unwatch(intern, watcher); php_io_poll_watcher_deactivate(watcher); } ZEND_HASH_FOREACH_END(); } @@ -403,6 +1676,19 @@ static void php_io_poll_context_free_object(zend_object *obj) zend_hash_destroy(intern->watchers); efree(intern->watchers); } + if (intern->timer_watchers) { + zend_hash_destroy(intern->timer_watchers); + efree(intern->timer_watchers); + } + + if (intern->removed) { + zend_hash_destroy(intern->removed); + efree(intern->removed); + } + + if (ZEND_FCC_INITIALIZED(intern->on_watcher_removed_fcc)) { + zend_fcc_dtor(&intern->on_watcher_removed_fcc); + } zend_object_std_dtor(&intern->std); } @@ -431,6 +1717,25 @@ static HashTable *php_io_poll_context_get_gc(zend_object *obj, zval **table, int zend_get_gc_buffer_add_zval(gc_buffer, zv); } ZEND_HASH_FOREACH_END(); } + if (intern->timer_watchers) { + ZEND_HASH_FOREACH_VAL(intern->timer_watchers, zval *zv) { + zend_get_gc_buffer_add_zval(gc_buffer, zv); + } ZEND_HASH_FOREACH_END(); + } + if (intern->removed) { + ZEND_HASH_FOREACH_VAL(intern->removed, zval *zv) { + zend_get_gc_buffer_add_zval(gc_buffer, zv); + } ZEND_HASH_FOREACH_END(); + } + + if (ZEND_FCC_INITIALIZED(intern->on_watcher_removed_fcc)) { + if (intern->on_watcher_removed_fcc.object) { + zend_get_gc_buffer_add_obj(gc_buffer, intern->on_watcher_removed_fcc.object); + } + if (intern->on_watcher_removed_fcc.closure) { + zend_get_gc_buffer_add_obj(gc_buffer, intern->on_watcher_removed_fcc.closure); + } + } zend_get_gc_buffer_use(gc_buffer, table, n); return NULL; @@ -447,12 +1752,31 @@ static zend_result php_io_poll_watcher_modify_events( return FAILURE; } - /* Re-add if the backend dropped a fired one-shot registration */ php_poll_ctx *poll_ctx = watcher->context->ctx; + + if (watcher->timer) { + php_io_poll_timer_handle_data *td = watcher->handle->handle_data; + if ((events & ~(PHP_POLL_TIMER | PHP_POLL_ONESHOT)) || !(events & PHP_POLL_TIMER)) { + zend_argument_value_error(1, "must be Event::Timer for a TimerHandle"); + return FAILURE; + } + /* Re-arm from now, with the handle's timeout */ + php_poll_timer_modify(poll_ctx, watcher->timer, zend_hrtime() + td->timeout, + td->periodic ? td->timeout : 0, watcher); + watcher->watched_events = events; + return SUCCESS; + } + + uint32_t backend_events; + if (php_io_poll_handle_backend_events(watcher->handle, events, 1, &backend_events) == FAILURE) { + return FAILURE; + } + + /* Re-add if the backend dropped a fired one-shot registration */ int fd = (int) watcher->fd; - if (php_poll_modify(poll_ctx, fd, events, watcher) != SUCCESS + if (php_poll_modify(poll_ctx, fd, backend_events, watcher) != SUCCESS && (php_poll_get_error(poll_ctx) != PHP_POLL_ERR_NOTFOUND - || php_poll_add(poll_ctx, fd, events, watcher) != SUCCESS)) { + || php_poll_add(poll_ctx, fd, backend_events, watcher) != SUCCESS)) { php_poll_error err = php_poll_get_error(poll_ctx); php_io_poll_throw_failed_operation(php_io_poll_failed_watcher_mod_class_entry, "Failed to modify watcher in polling system", err); @@ -522,6 +1846,30 @@ PHP_METHOD(Io_Poll_Backend, supportsEdgeTriggering) RETURN_BOOL(php_poll_backend_supports_edge_triggering(type)); } +PHP_METHOD(Io_Poll_Backend, supportsPriority) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_backend_type type = php_io_poll_backend_enum_to_type(Z_OBJ_P(ZEND_THIS)); + RETURN_BOOL(php_poll_backend_supports_priority(type)); +} + +PHP_METHOD(Io_Poll_Backend, supportsProcessHandles) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_backend_type type = php_io_poll_backend_enum_to_type(Z_OBJ_P(ZEND_THIS)); + RETURN_BOOL(php_poll_backend_supports_process_handles(type)); +} + +PHP_METHOD(Io_Poll_Backend, supportsSignalHandles) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_poll_backend_type type = php_io_poll_backend_enum_to_type(Z_OBJ_P(ZEND_THIS)); + RETURN_BOOL(php_poll_backend_supports_signal_handles(type)); +} + PHP_METHOD(StreamPollHandle, __construct) { php_stream *stream; @@ -530,10 +1878,10 @@ PHP_METHOD(StreamPollHandle, __construct) PHP_Z_PARAM_STREAM(stream) ZEND_PARSE_PARAMETERS_END(); - php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis()); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); if (intern->handle_data) { - zend_throw_error(NULL, "StreamPollHandle object is already constructed"); + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); RETURN_THROWS(); } @@ -550,7 +1898,7 @@ PHP_METHOD(StreamPollHandle, getStream) { ZEND_PARSE_PARAMETERS_NONE(); - php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis()); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); php_stream_poll_handle_data *data = intern->handle_data; if (!data || !data->res) { @@ -565,7 +1913,7 @@ PHP_METHOD(StreamPollHandle, isValid) { ZEND_PARSE_PARAMETERS_NONE(); - php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(getThis()); + php_poll_handle_object *intern = PHP_POLL_HANDLE_OBJ_FROM_ZV(ZEND_THIS); RETURN_BOOL(intern->ops->is_valid(intern)); } @@ -578,7 +1926,7 @@ PHP_METHOD(Io_Poll_Watcher, getHandle) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); if (!intern->handle) { RETURN_NULL(); } @@ -590,7 +1938,7 @@ PHP_METHOD(Io_Poll_Watcher, getWatchedEvents) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); php_io_poll_events_to_event_enums(intern->watched_events, return_value); } @@ -598,7 +1946,7 @@ PHP_METHOD(Io_Poll_Watcher, getTriggeredEvents) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); php_io_poll_events_to_event_enums(intern->triggered_events, return_value); } @@ -606,7 +1954,7 @@ PHP_METHOD(Io_Poll_Watcher, getData) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); ZVAL_COPY(return_value, &intern->data); } @@ -620,7 +1968,7 @@ PHP_METHOD(Io_Poll_Watcher, hasTriggered) uint32_t event_bit = php_io_poll_event_enum_to_bit(Z_OBJ_P(event_enum)); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); RETURN_BOOL((intern->triggered_events & event_bit) != 0); } @@ -628,7 +1976,7 @@ PHP_METHOD(Io_Poll_Watcher, isActive) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); RETURN_BOOL(intern->active); } @@ -649,7 +1997,7 @@ PHP_METHOD(Io_Poll_Watcher, modify) RETURN_THROWS(); } - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); /* Modify events first */ if (php_io_poll_watcher_modify_events(intern, events) != SUCCESS) { @@ -678,7 +2026,7 @@ PHP_METHOD(Io_Poll_Watcher, modifyEvents) RETURN_THROWS(); } - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); if (php_io_poll_watcher_modify_events(intern, events) != SUCCESS) { RETURN_THROWS(); @@ -693,7 +2041,7 @@ PHP_METHOD(Io_Poll_Watcher, modifyData) Z_PARAM_ZVAL(data) ZEND_PARSE_PARAMETERS_END(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); if (php_io_poll_watcher_modify_data(intern, data) != SUCCESS) { RETURN_THROWS(); @@ -704,7 +2052,7 @@ PHP_METHOD(Io_Poll_Watcher, remove) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(getThis()); + php_io_poll_watcher_object *intern = PHP_POLL_WATCHER_OBJ_FROM_ZV(ZEND_THIS); if (!intern->active || !intern->context) { /* Closing the stream already removed it, so this is just the expected cleanup */ @@ -716,7 +2064,7 @@ PHP_METHOD(Io_Poll_Watcher, remove) RETURN_THROWS(); } - php_io_poll_context_retire_watcher(intern->context, intern); + php_io_poll_context_retire_watcher(intern->context, intern, false); } PHP_METHOD(Io_Poll_Context, __construct) @@ -728,10 +2076,10 @@ PHP_METHOD(Io_Poll_Context, __construct) Z_PARAM_OBJECT_OF_CLASS(backend_obj, php_io_poll_backend_class_entry) ZEND_PARSE_PARAMETERS_END(); - php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis()); + php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(ZEND_THIS); if (intern->ctx) { - zend_throw_error(NULL, "Io\\Poll\\Context object is already constructed"); + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); RETURN_THROWS(); } @@ -760,6 +2108,8 @@ PHP_METHOD(Io_Poll_Context, __construct) intern->watchers = emalloc(sizeof(HashTable)); zend_hash_init(intern->watchers, 8, NULL, ZVAL_PTR_DTOR, 0); + intern->timer_watchers = emalloc(sizeof(HashTable)); + zend_hash_init(intern->timer_watchers, 4, NULL, ZVAL_PTR_DTOR, 0); } PHP_METHOD(Io_Poll_Context, add) @@ -775,7 +2125,7 @@ PHP_METHOD(Io_Poll_Context, add) Z_PARAM_ZVAL(data) ZEND_PARSE_PARAMETERS_END(); - php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis()); + php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(ZEND_THIS); php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZV(handle_obj); events = php_io_poll_event_enums_to_events(event_enums); @@ -784,8 +2134,53 @@ PHP_METHOD(Io_Poll_Context, add) RETURN_THROWS(); } + if (handle->ops == &php_io_poll_timer_handle_ops) { + php_io_poll_timer_handle_data *td = handle->handle_data; + if ((events & ~(PHP_POLL_TIMER | PHP_POLL_ONESHOT)) || !(events & PHP_POLL_TIMER)) { + zend_argument_value_error(2, "must be Event::Timer for a TimerHandle"); + RETURN_THROWS(); + } + if (!td) { + zend_throw_exception( + php_io_poll_invalid_handle_class_entry, "Invalid handle for polling", 0); + RETURN_THROWS(); + } + + object_init_ex(return_value, php_io_poll_watcher_class_entry); + php_io_poll_watcher_object *watcher = PHP_POLL_WATCHER_OBJ_FROM_ZV(return_value); + watcher->handle = handle; + watcher->watched_events = events; + GC_ADDREF(&handle->std); + if (data) { + ZVAL_COPY(&watcher->data, data); + } + + watcher->timer = php_poll_timer_add(intern->ctx, zend_hrtime() + td->timeout, + td->periodic ? td->timeout : 0, watcher); + + zval watcher_zv; + ZVAL_OBJ(&watcher_zv, &watcher->std); + GC_ADDREF(&watcher->std); + zend_hash_index_add_new(intern->timer_watchers, php_io_poll_compute_ptr_key(watcher), &watcher_zv); + + watcher->active = true; + watcher->context = intern; + return; + } + + uint32_t backend_events; + if (php_io_poll_handle_backend_events(handle, events, 2, &backend_events) == FAILURE) { + RETURN_THROWS(); + } + /* Get file descriptor */ php_socket_t fd = php_poll_handle_get_fd(handle); + if (fd == SOCK_ERR && handle->ops->event && handle->handle_data) { + /* A process or signal handle without a source on this platform */ + php_io_poll_throw_failed_operation(php_io_poll_failed_handle_add_class_entry, + "This backend has no source for the handle", PHP_POLL_ERR_NOSUPPORT); + RETURN_THROWS(); + } if (fd == SOCK_ERR) { zend_throw_exception( php_io_poll_invalid_handle_class_entry, "Invalid handle for polling", 0); @@ -800,7 +2195,7 @@ PHP_METHOD(Io_Poll_Context, add) php_io_poll_handle_already_watched_class_entry, "Handle already added", 0); RETURN_THROWS(); } - php_io_poll_context_retire_watcher(intern, existing); + php_io_poll_context_retire_watcher(intern, existing, true); } /* Create watcher object */ @@ -820,7 +2215,7 @@ PHP_METHOD(Io_Poll_Context, add) } /* Add to poll context */ - if (php_poll_add(intern->ctx, (int) fd, events, watcher) != SUCCESS) { + if (php_poll_add(intern->ctx, (int) fd, backend_events, watcher) != SUCCESS) { php_poll_error err = php_poll_get_error(intern->ctx); if (err == PHP_POLL_ERR_EXISTS) { zend_throw_exception( @@ -842,6 +2237,18 @@ PHP_METHOD(Io_Poll_Context, add) watcher->context = intern; watcher->fd = fd; + /* Reverse lookup so that invalidating the handle finds every context */ + if (!handle->watching) { + handle->watching = emalloc(sizeof(HashTable)); + zend_hash_init(handle->watching, 4, NULL, NULL, 0); + } + zval watcher_ptr_zv; + ZVAL_PTR(&watcher_ptr_zv, watcher); + zend_hash_index_update(handle->watching, php_io_poll_compute_ptr_key(intern), &watcher_ptr_zv); + if (handle->ops->added) { + handle->ops->added(handle); + } + if (handle->ops == &php_stream_poll_handle_ops) { php_stream *stream = php_stream_poll_handle_get_stream(handle); if (stream) { @@ -862,7 +2269,7 @@ PHP_METHOD(Io_Poll_Context, wait) Z_PARAM_LONG_OR_NULL(max_events, max_events_is_null) ZEND_PARSE_PARAMETERS_END(); - php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis()); + php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(ZEND_THIS); /* Build timespec from php_date_time_duration, or NULL for indefinite */ struct timespec timeout_ts; @@ -881,6 +2288,8 @@ PHP_METHOD(Io_Poll_Context, wait) if (max_events <= 0) { max_events = 64; } + /* Timers are reported in the same array */ + max_events += php_poll_timer_count(intern->ctx); } else if (UNEXPECTED(max_events <= 0)) { zend_argument_value_error(2, "must be greater than 0"); RETURN_THROWS(); @@ -889,12 +2298,20 @@ PHP_METHOD(Io_Poll_Context, wait) RETURN_THROWS(); } + if (php_io_poll_context_deliver_removed(intern) == FAILURE) { + RETURN_THROWS(); + } + php_poll_event *events = safe_emalloc((size_t) max_events, sizeof(*events), 0); int num_events = php_poll_wait(intern->ctx, events, (int) max_events, timeout ? &timeout_ts : NULL); if (num_events < 0) { php_poll_error err = php_poll_get_error(intern->ctx); efree(events); + if (err == PHP_POLL_ERR_INTERRUPTED) { + /* A signal handler runs before the caller waits again */ + RETURN_EMPTY_ARRAY(); + } php_io_poll_throw_failed_operation( php_io_poll_failed_wait_class_entry, "Poll wait failed", err); RETURN_THROWS(); @@ -905,7 +2322,15 @@ PHP_METHOD(Io_Poll_Context, wait) for (int i = 0; i < num_events; i++) { php_io_poll_watcher_object *watcher = (php_io_poll_watcher_object *) events[i].data; if (watcher) { - watcher->triggered_events = events[i].revents; + uint32_t triggered = php_io_poll_handle_fired(watcher->handle, events[i].revents); + if (!watcher->timer && php_io_poll_handle_exhausted(watcher->handle)) { + /* Reported once; the watcher stays until it is removed */ + php_poll_remove(intern->ctx, (int) watcher->fd); + } + if (!triggered) { + continue; + } + watcher->triggered_events = triggered; zval watcher_zv; ZVAL_OBJ(&watcher_zv, &watcher->std); @@ -922,13 +2347,71 @@ PHP_METHOD(Io_Poll_Context, getBackend) { ZEND_PARSE_PARAMETERS_NONE(); - php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(getThis()); + php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(ZEND_THIS); php_poll_backend_type backend_type = php_poll_get_backend_type(intern->ctx); const char *backend_name = php_io_poll_backend_type_to_name(backend_type); RETURN_OBJ_COPY(zend_enum_get_case_cstr(php_io_poll_backend_class_entry, backend_name)); } +PHP_METHOD(Io_Poll_Context, onWatcherRemoved) +{ + zend_fcall_info fci = empty_fcall_info; + zend_fcall_info_cache fcc = empty_fcall_info_cache; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_FUNC_OR_NULL(fci, fcc) + ZEND_PARSE_PARAMETERS_END(); + + php_io_poll_context_object *intern = PHP_POLL_CONTEXT_OBJ_FROM_ZV(ZEND_THIS); + + if (ZEND_FCC_INITIALIZED(intern->on_watcher_removed_fcc)) { + zend_fcc_dtor(&intern->on_watcher_removed_fcc); + } + + if (ZEND_FCI_INITIALIZED(fci)) { + intern->on_watcher_removed_fcc = fcc; + zend_fcc_addref(&intern->on_watcher_removed_fcc); + } +} + +PHPAPI void php_poll_handle_invalidate(zend_object *handle_obj) +{ + php_io_poll_handle_remove_from_all_contexts(handle_obj); +} + +/* Invalidation: the handle's resource is going away, so every watcher on it + * is retired from its context while the fd is still open. */ +PHPAPI void php_io_poll_handle_remove_from_all_contexts(zend_object *handle_obj) +{ + php_poll_handle_object *handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle_obj); + if (!handle->watching) { + return; + } + + HashTable *watching = handle->watching; + handle->watching = NULL; + + uint32_t n = 0; + php_io_poll_watcher_object **list = safe_emalloc(zend_hash_num_elements(watching), sizeof(*list), 0); + ZEND_HASH_FOREACH_VAL(watching, zval *zv) { + php_io_poll_watcher_object *watcher = (php_io_poll_watcher_object *) Z_PTR_P(zv); + GC_ADDREF(&watcher->std); + list[n++] = watcher; + } ZEND_HASH_FOREACH_END(); + + zend_hash_destroy(watching); + efree(watching); + + php_io_poll_retire_watchers(list, n); + if (handle->ops->removed) { + for (uint32_t i = 0; i < n; i++) { + handle->ops->removed(handle); + } + } +} + /* Initialize the stream poll classes - add to PHP_MINIT_FUNCTION */ PHP_MINIT_FUNCTION(poll) { @@ -950,9 +2433,37 @@ PHP_MINIT_FUNCTION(poll) memcpy(&php_io_poll_handle_object_handlers, &std_object_handlers, sizeof(zend_object_handlers)); php_io_poll_handle_object_handlers.offset = offsetof(php_poll_handle_object, std); php_io_poll_handle_object_handlers.free_obj = php_poll_handle_object_free; + php_io_poll_handle_object_handlers.get_gc = php_io_poll_handle_get_gc; php_io_poll_handle_object_handlers.clone_obj = NULL; php_stream_poll_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + /* Register WeakHandle interface */ + php_io_poll_weak_handle_class_entry = register_class_Io_Poll_WeakHandle(php_io_poll_handle_class_entry); + php_io_poll_weak_handle_class_entry->interface_gets_implemented = php_stream_poll_handle_implement_interface; + + /* Register StreamPollWeakHandle class */ + php_stream_poll_weak_handle_class_entry + = register_class_StreamPollWeakHandle(php_io_poll_weak_handle_class_entry); + php_stream_poll_weak_handle_class_entry->create_object = php_stream_poll_weak_handle_create_object; + php_stream_poll_weak_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + + /* Register the generic handles */ + php_io_poll_timer_handle_class_entry = register_class_Io_Poll_TimerHandle(php_io_poll_handle_class_entry); + php_io_poll_timer_handle_class_entry->create_object = php_io_poll_timer_handle_create_object; + php_io_poll_timer_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + + php_io_poll_notify_handle_class_entry = register_class_Io_Poll_NotifyHandle(php_io_poll_handle_class_entry); + php_io_poll_notify_handle_class_entry->create_object = php_io_poll_notify_handle_create_object; + php_io_poll_notify_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + + php_io_poll_process_handle_class_entry = register_class_Io_Poll_ProcessHandle(php_io_poll_handle_class_entry); + php_io_poll_process_handle_class_entry->create_object = php_io_poll_process_handle_create_object; + php_io_poll_process_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + + php_io_poll_signal_handle_class_entry = register_class_Io_Poll_SignalHandle(php_io_poll_handle_class_entry); + php_io_poll_signal_handle_class_entry->create_object = php_io_poll_signal_handle_create_object; + php_io_poll_signal_handle_class_entry->default_object_handlers = &php_io_poll_handle_object_handlers; + /* Register Watcher class */ php_io_poll_watcher_class_entry = register_class_Io_Poll_Watcher(); php_io_poll_watcher_class_entry->create_object = php_io_poll_watcher_create_object; diff --git a/ext/standard/io_poll.h b/ext/standard/io_poll.h index 1b94355f27a5..59cbbd4019a6 100644 --- a/ext/standard/io_poll.h +++ b/ext/standard/io_poll.h @@ -16,11 +16,50 @@ #define PHP_IO_POLL_H #include "php_streams.h" +#include "main/php_poll.h" BEGIN_EXTERN_C() PHPAPI void php_io_poll_stream_notify_close(php_stream *stream); +PHPAPI extern zend_class_entry *php_io_exception_class_entry; +PHPAPI extern zend_class_entry *php_io_poll_event_class_entry; +PHPAPI extern zend_class_entry *php_io_poll_handle_class_entry; +PHPAPI extern zend_class_entry *php_io_poll_weak_handle_class_entry; +PHPAPI extern zend_class_entry *php_stream_poll_handle_class_entry; + +PHPAPI zend_result php_io_poll_events_to_event_enums(uint32_t events, zval *event_enums); +PHPAPI void php_io_poll_throw_failed_wait(const char *message, php_poll_error error); +PHPAPI uint32_t php_io_poll_event_enums_to_events(zval *event_enums); + +PHPAPI void php_stream_poll_handle_from_stream(zval *dest, php_stream *stream); + +PHPAPI void php_io_poll_timer_handle_create(zval *dest, zend_hrtime_t timeout_ns, bool periodic); +PHPAPI void php_io_poll_process_handle_create(zval *dest, pid_t pid); +PHPAPI void php_io_poll_signal_handle_create(zval *dest, const php_sigset_t *set); +/* Returns 0 when no signal in the set was recorded */ +PHPAPI int php_io_poll_signal_handle_take(zend_object *handle, const php_sigset_t *set, + php_siginfo_t *info); + +/* Thread safe and async-signal safe */ +PHPAPI void php_poll_notify(zend_object *handle); + +/* A NotifyHandle over a descriptor owned and cleared by owner; notify() is unavailable on it */ +PHPAPI void php_io_poll_notify_handle_create_external(zval *dest, php_socket_t fd, + void (*clear)(void *arg), void *arg, zend_object *owner); + +#ifndef PHP_WIN32 +/* The current mask without the signals blocked by live SignalHandle objects */ +PHPAPI void php_io_poll_signal_child_mask(sigset_t *mask); +#endif +/* The stream's StreamPollWeakHandle, created on the first call and kept by the stream: borrowed. + * expose lets getStream() hand the stream out, and stays set once it was. */ +PHPAPI zend_object *php_stream_get_poll_handle(php_stream *stream, bool expose); +PHPAPI void php_stream_poll_weak_handle_notify(zend_object *handle_obj); +PHPAPI void php_io_poll_handle_remove_from_all_contexts(zend_object *handle_obj); +/* Retires every watcher on the handle while its descriptor is still open */ +PHPAPI void php_poll_handle_invalidate(zend_object *handle_obj); + END_EXTERN_C() #endif /* PHP_IO_POLL_H */ diff --git a/ext/standard/io_poll.stub.php b/ext/standard/io_poll.stub.php index e3bcbf11c871..2fda882674cf 100644 --- a/ext/standard/io_poll.stub.php +++ b/ext/standard/io_poll.stub.php @@ -27,6 +27,15 @@ static public function getAvailableBackends(): array {} public function isAvailable(): bool {} public function supportsEdgeTriggering(): bool {} + + /** Whether Event::Priority is reported; kqueue and WSAPoll cannot. */ + public function supportsPriority(): bool {} + + /** Whether a ProcessHandle can be added: a pidfd on Linux, EVFILT_PROC on kqueue. */ + public function supportsProcessHandles(): bool {} + + /** Whether a SignalHandle can be added: a signalfd on Linux, EVFILT_SIGNAL on kqueue. */ + public function supportsSignalHandles(): bool {} } // Keep in sync with main/php_poll.h! @@ -38,12 +47,102 @@ enum Event { case ReadHangUp; case OneShot; case EdgeTriggered; + /** Priority data (POLLPRI), backend dependent */ + case Priority; + /** A TimerHandle fired */ + case Timer; + /** A NotifyHandle is raised and not yet cleared */ + case Notify; + /** A SignalHandle delivered a signal */ + case Signal; + /** A ProcessHandle: the child exited and its status is in the handle */ + case Process; } interface Handle { } + /** + * A handle that holds its resource weakly: the resource can go away while + * the handle is referenced, and the handle then reports invalid. + */ + interface WeakHandle extends Handle + { + } + + /** + * A deadline in the context, one-shot or periodic. Watched with + * Event::Timer; a fired one-shot timer is re-armed by modifyEvents(). + * @strict-properties + * @not-serializable + */ + final class TimerHandle implements Handle + { + public function __construct(\Time\Duration $timeout, bool $periodic = false) {} + + public function getTimeout(): \Time\Duration {} + + public function isPeriodic(): bool {} + } + + /** + * Signals as events. The signals are blocked in the process signal mask + * for the life of the handle, so they queue instead of running a + * handler; a delivery is consumed and recorded when the context reports + * Event::Signal. + * @strict-properties + * @not-serializable + */ + final class SignalHandle implements Handle + { + /** @param list $signals */ + public function __construct(array $signals) {} + + /** @return list */ + public function getSignals(): array {} + + /** @return list signals delivered since the previous call, in order */ + public function getDelivered(): array {} + } + + /** + * A process by pid. Each context reports Event::Process once when the + * process exited, and getStatus() has its wait status then. The handle + * never collects the child: proc_close() or pcntl_waitpid() still does + * and returns at once. The status stays null for a process that is not + * our child or was collected elsewhere. + * @strict-properties + * @not-serializable + */ + final class ProcessHandle implements Handle + { + public function __construct(int $pid) {} + + /** @param resource $process a proc_open() resource */ + public static function fromProcess($process): static {} + + public function getPid(): int {} + + /** The wait status once the child exited, null before. */ + public function getStatus(): ?int {} + } + + /** + * A readiness source the program raises itself. notify() makes it ready + * and it stays ready until clear() consumed every pending notification. + * @strict-properties + * @not-serializable + */ + final class NotifyHandle implements Handle + { + public function __construct() {} + + public function notify(): void {} + + public function clear(): void {} + } + /** * @strict-properties * @not-serializable @@ -89,6 +188,13 @@ public function add(Handle $handle, array $events, mixed $data = null): Watcher public function wait(?\Time\Duration $timeout = null, ?int $maxEvents = null): array {} public function getBackend(): Backend {} + + /** + * Called with each watcher the context lost without Watcher::remove(), + * because its stream was closed or its handle invalidated. The calls + * are made by the next wait(), before it polls. + */ + public function onWatcherRemoved(?callable $callback = null): void {} } class PollException extends \Io\IoException {} @@ -150,6 +256,34 @@ class InvalidHandleException extends PollException {} } namespace { + /** + * One per stream, created by the core for its operations and registrations + * or by create(); the stream keeps it while it is open. + * @strict-properties + * @not-serializable + */ + final class StreamPollWeakHandle implements Io\Poll\WeakHandle + { + private function __construct() {} + + /** @param resource $stream */ + public static function create($stream): static {} + + /** + * The stream, or null: closed, not exposed, or in an operation. + * @return resource|null + */ + public function getStream(): mixed {} + + /** + * Whether getStream() may hand the stream out: created through create(), + * or by the core for a stream a script holds + */ + public function isExposed(): bool {} + + public function isValid(): bool {} + } + /** * @strict-properties * @not-serializable diff --git a/ext/standard/io_poll_arginfo.h b/ext/standard/io_poll_arginfo.h index 925f483ddc88..4225031ff8ec 100644 --- a/ext/standard/io_poll_arginfo.h +++ b/ext/standard/io_poll_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit io_poll.stub.php instead. - * Stub hash: 2f52b00fd6dfc62291e0dd288ffd68547b29bdaa + * Stub hash: d208d642abc91d7c7f0f47462c1660bf65464a77 * Has decl header: yes */ #include "zend_enum.h" @@ -12,9 +12,54 @@ ZEND_END_ARG_INFO() #define arginfo_class_Io_Poll_Backend_supportsEdgeTriggering arginfo_class_Io_Poll_Backend_isAvailable -ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_Watcher___construct, 0, 0, 0) +#define arginfo_class_Io_Poll_Backend_supportsPriority arginfo_class_Io_Poll_Backend_isAvailable + +#define arginfo_class_Io_Poll_Backend_supportsProcessHandles arginfo_class_Io_Poll_Backend_isAvailable + +#define arginfo_class_Io_Poll_Backend_supportsSignalHandles arginfo_class_Io_Poll_Backend_isAvailable + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_TimerHandle___construct, 0, 0, 1) + ZEND_ARG_OBJ_INFO(0, timeout, Time\\Duration, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, periodic, _IS_BOOL, 0, "false") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Poll_TimerHandle_getTimeout, 0, 0, Time\\Duration, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Poll_TimerHandle_isPeriodic arginfo_class_Io_Poll_Backend_isAvailable + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_SignalHandle___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, signals, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Poll_SignalHandle_getSignals arginfo_class_Io_Poll_Backend_getAvailableBackends + +#define arginfo_class_Io_Poll_SignalHandle_getDelivered arginfo_class_Io_Poll_Backend_getAvailableBackends + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_ProcessHandle___construct, 0, 0, 1) + ZEND_ARG_TYPE_INFO(0, pid, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_ProcessHandle_fromProcess, 0, 1, IS_STATIC, 0) + ZEND_ARG_INFO(0, process) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_ProcessHandle_getPid, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_ProcessHandle_getStatus, 0, 0, IS_LONG, 1) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_NotifyHandle___construct, 0, 0, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_NotifyHandle_notify, 0, 0, IS_VOID, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Poll_NotifyHandle_clear arginfo_class_Io_Poll_NotifyHandle_notify + +#define arginfo_class_Io_Poll_Watcher___construct arginfo_class_Io_Poll_NotifyHandle___construct + ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Poll_Watcher_getHandle, 0, 0, Io\\Poll\\Handle, 0) ZEND_END_ARG_INFO() @@ -44,8 +89,7 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_Watcher_modifyData ZEND_ARG_TYPE_INFO(0, data, IS_MIXED, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_Watcher_remove, 0, 0, IS_VOID, 0) -ZEND_END_ARG_INFO() +#define arginfo_class_Io_Poll_Watcher_remove arginfo_class_Io_Poll_NotifyHandle_notify ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Poll_Context___construct, 0, 0, 0) ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, backend, Io\\Poll\\Backend, 0, "Io\\Poll\\Backend::Auto") @@ -65,17 +109,49 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Poll_Context_getBackend, 0, 0, Io\\Poll\\Backend, 0) ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Poll_Context_onWatcherRemoved, 0, 0, IS_VOID, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, callback, IS_CALLABLE, 1, "null") +ZEND_END_ARG_INFO() + +#define arginfo_class_StreamPollWeakHandle___construct arginfo_class_Io_Poll_NotifyHandle___construct + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_StreamPollWeakHandle_create, 0, 1, IS_STATIC, 0) + ZEND_ARG_INFO(0, stream) +ZEND_END_ARG_INFO() + +#define arginfo_class_StreamPollWeakHandle_getStream arginfo_class_Io_Poll_Watcher_getData + +#define arginfo_class_StreamPollWeakHandle_isExposed arginfo_class_Io_Poll_Backend_isAvailable + +#define arginfo_class_StreamPollWeakHandle_isValid arginfo_class_Io_Poll_Backend_isAvailable + ZEND_BEGIN_ARG_INFO_EX(arginfo_class_StreamPollHandle___construct, 0, 0, 1) ZEND_ARG_INFO(0, stream) ZEND_END_ARG_INFO() -#define arginfo_class_StreamPollHandle_getStream arginfo_class_Io_Poll_Watcher___construct +#define arginfo_class_StreamPollHandle_getStream arginfo_class_Io_Poll_NotifyHandle___construct #define arginfo_class_StreamPollHandle_isValid arginfo_class_Io_Poll_Backend_isAvailable ZEND_METHOD(Io_Poll_Backend, getAvailableBackends); ZEND_METHOD(Io_Poll_Backend, isAvailable); ZEND_METHOD(Io_Poll_Backend, supportsEdgeTriggering); +ZEND_METHOD(Io_Poll_Backend, supportsPriority); +ZEND_METHOD(Io_Poll_Backend, supportsProcessHandles); +ZEND_METHOD(Io_Poll_Backend, supportsSignalHandles); +ZEND_METHOD(Io_Poll_TimerHandle, __construct); +ZEND_METHOD(Io_Poll_TimerHandle, getTimeout); +ZEND_METHOD(Io_Poll_TimerHandle, isPeriodic); +ZEND_METHOD(Io_Poll_SignalHandle, __construct); +ZEND_METHOD(Io_Poll_SignalHandle, getSignals); +ZEND_METHOD(Io_Poll_SignalHandle, getDelivered); +ZEND_METHOD(Io_Poll_ProcessHandle, __construct); +ZEND_METHOD(Io_Poll_ProcessHandle, fromProcess); +ZEND_METHOD(Io_Poll_ProcessHandle, getPid); +ZEND_METHOD(Io_Poll_ProcessHandle, getStatus); +ZEND_METHOD(Io_Poll_NotifyHandle, __construct); +ZEND_METHOD(Io_Poll_NotifyHandle, notify); +ZEND_METHOD(Io_Poll_NotifyHandle, clear); ZEND_METHOD(Io_Poll_Watcher, __construct); ZEND_METHOD(Io_Poll_Watcher, getHandle); ZEND_METHOD(Io_Poll_Watcher, getWatchedEvents); @@ -91,6 +167,12 @@ ZEND_METHOD(Io_Poll_Context, __construct); ZEND_METHOD(Io_Poll_Context, add); ZEND_METHOD(Io_Poll_Context, wait); ZEND_METHOD(Io_Poll_Context, getBackend); +ZEND_METHOD(Io_Poll_Context, onWatcherRemoved); +ZEND_METHOD(StreamPollWeakHandle, __construct); +ZEND_METHOD(StreamPollWeakHandle, create); +ZEND_METHOD(StreamPollWeakHandle, getStream); +ZEND_METHOD(StreamPollWeakHandle, isExposed); +ZEND_METHOD(StreamPollWeakHandle, isValid); ZEND_METHOD(StreamPollHandle, __construct); ZEND_METHOD(StreamPollHandle, getStream); ZEND_METHOD(StreamPollHandle, isValid); @@ -99,6 +181,38 @@ static const zend_function_entry class_Io_Poll_Backend_methods[] = { ZEND_ME(Io_Poll_Backend, getAvailableBackends, arginfo_class_Io_Poll_Backend_getAvailableBackends, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) ZEND_ME(Io_Poll_Backend, isAvailable, arginfo_class_Io_Poll_Backend_isAvailable, ZEND_ACC_PUBLIC) ZEND_ME(Io_Poll_Backend, supportsEdgeTriggering, arginfo_class_Io_Poll_Backend_supportsEdgeTriggering, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_Backend, supportsPriority, arginfo_class_Io_Poll_Backend_supportsPriority, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_Backend, supportsProcessHandles, arginfo_class_Io_Poll_Backend_supportsProcessHandles, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_Backend, supportsSignalHandles, arginfo_class_Io_Poll_Backend_supportsSignalHandles, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Poll_TimerHandle_methods[] = { + ZEND_ME(Io_Poll_TimerHandle, __construct, arginfo_class_Io_Poll_TimerHandle___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_TimerHandle, getTimeout, arginfo_class_Io_Poll_TimerHandle_getTimeout, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_TimerHandle, isPeriodic, arginfo_class_Io_Poll_TimerHandle_isPeriodic, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Poll_SignalHandle_methods[] = { + ZEND_ME(Io_Poll_SignalHandle, __construct, arginfo_class_Io_Poll_SignalHandle___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_SignalHandle, getSignals, arginfo_class_Io_Poll_SignalHandle_getSignals, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_SignalHandle, getDelivered, arginfo_class_Io_Poll_SignalHandle_getDelivered, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Poll_ProcessHandle_methods[] = { + ZEND_ME(Io_Poll_ProcessHandle, __construct, arginfo_class_Io_Poll_ProcessHandle___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_ProcessHandle, fromProcess, arginfo_class_Io_Poll_ProcessHandle_fromProcess, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(Io_Poll_ProcessHandle, getPid, arginfo_class_Io_Poll_ProcessHandle_getPid, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_ProcessHandle, getStatus, arginfo_class_Io_Poll_ProcessHandle_getStatus, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_Io_Poll_NotifyHandle_methods[] = { + ZEND_ME(Io_Poll_NotifyHandle, __construct, arginfo_class_Io_Poll_NotifyHandle___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_NotifyHandle, notify, arginfo_class_Io_Poll_NotifyHandle_notify, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_NotifyHandle, clear, arginfo_class_Io_Poll_NotifyHandle_clear, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -122,6 +236,16 @@ static const zend_function_entry class_Io_Poll_Context_methods[] = { ZEND_ME(Io_Poll_Context, add, arginfo_class_Io_Poll_Context_add, ZEND_ACC_PUBLIC) ZEND_ME(Io_Poll_Context, wait, arginfo_class_Io_Poll_Context_wait, ZEND_ACC_PUBLIC) ZEND_ME(Io_Poll_Context, getBackend, arginfo_class_Io_Poll_Context_getBackend, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Poll_Context, onWatcherRemoved, arginfo_class_Io_Poll_Context_onWatcherRemoved, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static const zend_function_entry class_StreamPollWeakHandle_methods[] = { + ZEND_ME(StreamPollWeakHandle, __construct, arginfo_class_StreamPollWeakHandle___construct, ZEND_ACC_PRIVATE) + ZEND_ME(StreamPollWeakHandle, create, arginfo_class_StreamPollWeakHandle_create, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC) + ZEND_ME(StreamPollWeakHandle, getStream, arginfo_class_StreamPollWeakHandle_getStream, ZEND_ACC_PUBLIC) + ZEND_ME(StreamPollWeakHandle, isExposed, arginfo_class_StreamPollWeakHandle_isExposed, ZEND_ACC_PUBLIC) + ZEND_ME(StreamPollWeakHandle, isValid, arginfo_class_StreamPollWeakHandle_isValid, ZEND_ACC_PUBLIC) ZEND_FE_END }; @@ -179,6 +303,16 @@ static zend_class_entry *register_class_Io_Poll_Event(void) zend_enum_add_case_cstr(class_entry, "EdgeTriggered", NULL); + zend_enum_add_case_cstr(class_entry, "Priority", NULL); + + zend_enum_add_case_cstr(class_entry, "Timer", NULL); + + zend_enum_add_case_cstr(class_entry, "Notify", NULL); + + zend_enum_add_case_cstr(class_entry, "Signal", NULL); + + zend_enum_add_case_cstr(class_entry, "Process", NULL); + return class_entry; } @@ -192,6 +326,61 @@ static zend_class_entry *register_class_Io_Poll_Handle(void) return class_entry; } +static zend_class_entry *register_class_Io_Poll_WeakHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Poll", "WeakHandle", NULL); + class_entry = zend_register_internal_interface(&ce); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Poll_TimerHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Poll", "TimerHandle", class_Io_Poll_TimerHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Poll_SignalHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Poll", "SignalHandle", class_Io_Poll_SignalHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Poll_ProcessHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Poll", "ProcessHandle", class_Io_Poll_ProcessHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Poll_NotifyHandle(zend_class_entry *class_entry_Io_Poll_Handle) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Poll", "NotifyHandle", class_Io_Poll_NotifyHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_Handle); + + return class_entry; +} + static zend_class_entry *register_class_Io_Poll_Watcher(void) { zend_class_entry ce, *class_entry; @@ -384,6 +573,17 @@ static zend_class_entry *register_class_Io_Poll_InvalidHandleException(zend_clas return class_entry; } +static zend_class_entry *register_class_StreamPollWeakHandle(zend_class_entry *class_entry_Io_Poll_WeakHandle) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "StreamPollWeakHandle", class_StreamPollWeakHandle_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_Poll_WeakHandle); + + return class_entry; +} + static zend_class_entry *register_class_StreamPollHandle(zend_class_entry *class_entry_Io_Poll_Handle) { zend_class_entry ce, *class_entry; diff --git a/ext/standard/io_poll_decl.h b/ext/standard/io_poll_decl.h index de49880392a7..398f7ffd993c 100644 --- a/ext/standard/io_poll_decl.h +++ b/ext/standard/io_poll_decl.h @@ -1,8 +1,8 @@ /* This is a generated file, edit io_poll.stub.php instead. - * Stub hash: 2f52b00fd6dfc62291e0dd288ffd68547b29bdaa */ + * Stub hash: d208d642abc91d7c7f0f47462c1660bf65464a77 */ -#ifndef ZEND_IO_POLL_DECL_2f52b00fd6dfc62291e0dd288ffd68547b29bdaa_H -#define ZEND_IO_POLL_DECL_2f52b00fd6dfc62291e0dd288ffd68547b29bdaa_H +#ifndef ZEND_IO_POLL_DECL_d208d642abc91d7c7f0f47462c1660bf65464a77_H +#define ZEND_IO_POLL_DECL_d208d642abc91d7c7f0f47462c1660bf65464a77_H typedef enum zend_enum_Io_Poll_Backend { ZEND_ENUM_Io_Poll_Backend_Auto = 1, @@ -21,6 +21,11 @@ typedef enum zend_enum_Io_Poll_Event { ZEND_ENUM_Io_Poll_Event_ReadHangUp = 5, ZEND_ENUM_Io_Poll_Event_OneShot = 6, ZEND_ENUM_Io_Poll_Event_EdgeTriggered = 7, + ZEND_ENUM_Io_Poll_Event_Priority = 8, + ZEND_ENUM_Io_Poll_Event_Timer = 9, + ZEND_ENUM_Io_Poll_Event_Notify = 10, + ZEND_ENUM_Io_Poll_Event_Signal = 11, + ZEND_ENUM_Io_Poll_Event_Process = 12, } zend_enum_Io_Poll_Event; -#endif /* ZEND_IO_POLL_DECL_2f52b00fd6dfc62291e0dd288ffd68547b29bdaa_H */ +#endif /* ZEND_IO_POLL_DECL_d208d642abc91d7c7f0f47462c1660bf65464a77_H */ diff --git a/ext/standard/io_ring.c b/ext/standard/io_ring.c new file mode 100644 index 000000000000..c265b5bb703f --- /dev/null +++ b/ext/standard/io_ring.c @@ -0,0 +1,204 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +/* Io\Ring\Engine: the Ring as an Io\OperationQueue, over the C ring queue + * of main/io. The queue methods are the shared implementation of + * ext/standard/io_hooks.c; this file adds what only a ring has. */ + +#include "php.h" +#include "zend_enum.h" +#include "zend_exceptions.h" +#include "ext/standard/file.h" +#include "ext/standard/io_poll.h" +#include "ext/standard/io_hooks.h" +#include "php_io.h" + +#include + +#ifdef HAVE_IOR + +#include "io_ring_arginfo.h" +#include "io_ring_decl.h" + +static zend_class_entry *php_io_ring_backend_ce; +static zend_class_entry *php_io_ring_engine_ce; +static zend_class_entry *php_io_ring_exception_ce; +static zend_class_entry *php_io_ring_failed_operation_exception_ce; + +static php_io_ring *php_io_ring_engine_ring(php_io_opqueue_obj *intern) +{ + return php_io_queue_ring(intern->queue); +} + +ZEND_METHOD(Io_Poll_OperationQueue, submit); +ZEND_METHOD(Io_Poll_OperationQueue, cancel); +ZEND_METHOD(Io_Poll_OperationQueue, add); +ZEND_METHOD(Io_Poll_OperationQueue, remove); +ZEND_METHOD(Io_Poll_OperationQueue, waitCompletions); +ZEND_METHOD(Io_Poll_OperationQueue, countPending); +ZEND_METHOD(Io_Poll_OperationQueue, getHookCapabilities); + +/* A forked child cannot use the ring it inherited */ +static zend_result php_io_ring_engine_check(zval *zv) +{ + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(zv)); + if (intern->queue && php_io_ring_inherited(php_io_ring_engine_ring(intern))) { + zend_throw_exception(php_io_ring_exception_ce, "The ring was created in another process", 0); + return FAILURE; + } + return SUCCESS; +} + +static void php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAMETERS, zif_handler handler) +{ + if (php_io_ring_engine_check(ZEND_THIS) == FAILURE) { + RETURN_THROWS(); + } + handler(INTERNAL_FUNCTION_PARAM_PASSTHRU); +} + +PHP_METHOD(Io_Ring_Engine, submit) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_MN(Io_Poll_OperationQueue_submit)); +} + +PHP_METHOD(Io_Ring_Engine, cancel) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_MN(Io_Poll_OperationQueue_cancel)); +} + +PHP_METHOD(Io_Ring_Engine, add) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_MN(Io_Poll_OperationQueue_add)); +} + +PHP_METHOD(Io_Ring_Engine, remove) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_MN(Io_Poll_OperationQueue_remove)); +} + +PHP_METHOD(Io_Ring_Engine, waitCompletions) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, + ZEND_MN(Io_Poll_OperationQueue_waitCompletions)); +} + +PHP_METHOD(Io_Ring_Engine, countPending) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, + ZEND_MN(Io_Poll_OperationQueue_countPending)); +} + +PHP_METHOD(Io_Ring_Engine, getHookCapabilities) +{ + php_io_ring_engine_forward(INTERNAL_FUNCTION_PARAM_PASSTHRU, + ZEND_MN(Io_Poll_OperationQueue_getHookCapabilities)); +} + +PHP_METHOD(Io_Ring_Engine, getSupportedHookCapabilities) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + if (php_io_ring_engine_check(ZEND_THIS) == FAILURE) { + RETURN_THROWS(); + } + php_io_hook_flags_to_capabilities( + php_io_ring_supported_hook_flags(php_io_ring_engine_ring(intern)), return_value); +} + +PHP_METHOD(Io_Ring_Engine, __construct) +{ + zend_long entries = 0; + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(entries) + ZEND_PARSE_PARAMETERS_END(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + + if (intern->queue) { + zend_throw_error(NULL, "%pS object is already constructed", Z_OBJCE_P(ZEND_THIS)->name); + RETURN_THROWS(); + } + if (entries < 0 || entries > UINT32_MAX) { + zend_argument_value_error(1, "must be between 0 and %u", UINT32_MAX); + RETURN_THROWS(); + } + + intern->queue = php_io_queue_create_ring((uint32_t) entries); + if (!intern->queue) { + zend_throw_exception_ex(php_io_ring_exception_ce, errno, + "Failed to create the ring: %s", strerror(errno)); + RETURN_THROWS(); + } +} + +PHP_METHOD(Io_Ring_Engine, getBackend) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + if (php_io_ring_engine_check(ZEND_THIS) == FAILURE) { + RETURN_THROWS(); + } + + zend_long id; + switch (php_io_ring_get_backend_type(php_io_ring_engine_ring(intern))) { + case PHP_IO_RING_BACKEND_IO_URING: id = ZEND_ENUM_Io_Ring_Backend_IoUring; break; + case PHP_IO_RING_BACKEND_IOCP: id = ZEND_ENUM_Io_Ring_Backend_Iocp; break; + default: id = ZEND_ENUM_Io_Ring_Backend_Threads; break; + } + RETURN_OBJ_COPY(zend_enum_get_case_by_id(php_io_ring_backend_ce, id)); +} + +static void php_io_ring_engine_notify_clear(void *arg) +{ + php_io_ring_notify_clear(arg); +} + +PHP_METHOD(Io_Ring_Engine, getHandle) +{ + ZEND_PARSE_PARAMETERS_NONE(); + + php_io_opqueue_obj *intern = PHP_IO_OPQUEUE_FROM_ZOBJ(Z_OBJ_P(ZEND_THIS)); + if (php_io_ring_engine_check(ZEND_THIS) == FAILURE) { + RETURN_THROWS(); + } + + php_io_ring *ring = php_io_ring_engine_ring(intern); + php_socket_t fd = php_io_ring_notify_fd(ring); + if (fd == SOCK_ERR) { + zend_throw_exception(php_io_ring_exception_ce, "The ring has no notification descriptor", 0); + RETURN_THROWS(); + } + php_io_poll_notify_handle_create_external(return_value, fd, php_io_ring_engine_notify_clear, ring, Z_OBJ_P(ZEND_THIS)); +} + +#endif /* HAVE_IOR */ + +PHP_MINIT_FUNCTION(io_ring) +{ +#ifdef HAVE_IOR + php_io_ring_backend_ce = register_class_Io_Ring_Backend(); + + php_io_ring_engine_ce = register_class_Io_Ring_Engine(php_io_operation_queue_ce); + php_io_ring_engine_ce->create_object = php_io_opqueue_create_object; + php_io_ring_engine_ce->default_object_handlers = &php_io_opqueue_handlers; + + php_io_ring_exception_ce = register_class_Io_Ring_RingException(php_io_exception_class_entry); + php_io_ring_failed_operation_exception_ce + = register_class_Io_Ring_FailedRingOperationException(php_io_ring_exception_ce); +#endif + return SUCCESS; +} diff --git a/ext/standard/io_ring.stub.php b/ext/standard/io_ring.stub.php new file mode 100644 index 000000000000..5a06ea8533fa --- /dev/null +++ b/ext/standard/io_ring.stub.php @@ -0,0 +1,71 @@ + + */ + public function waitCompletions(?\Time\Duration $timeout = null, ?int $max = null): array {} + + public function countPending(): int {} + + /** + * Capabilities worth reporting by default: EdgeRegistrations and DirectAccept, which the + * ring serves from a multishot accept; see getSupportedHookCapabilities() for the rest. + * @return list<\Io\Hooks\Capability> + */ + public function getHookCapabilities(): array {} + + /** + * Capabilities this backend can serve, for a provider that opts in. + * @return list<\Io\Hooks\Capability> + */ + public function getSupportedHookCapabilities(): array {} + } + + class RingException extends \Io\IoException {} + + /** Submit, cancel and wait failures, the errno in the code */ + class FailedRingOperationException extends RingException {} +} diff --git a/ext/standard/io_ring_arginfo.h b/ext/standard/io_ring_arginfo.h new file mode 100644 index 000000000000..997c8e593550 --- /dev/null +++ b/ext/standard/io_ring_arginfo.h @@ -0,0 +1,114 @@ +/* This is a generated file, edit io_ring.stub.php instead. + * Stub hash: 4a811c2ae4a406fa77283f03f8859e113b4afb0f + * Has decl header: yes */ + +#include "zend_enum.h" + +ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Io_Ring_Engine___construct, 0, 0, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, entries, IS_LONG, 0, "0") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Ring_Engine_getBackend, 0, 0, Io\\Ring\\Backend, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Ring_Engine_getHandle, 0, 0, Io\\Poll\\\116otifyHandle, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_OBJ_INFO_EX(arginfo_class_Io_Ring_Engine_submit, 0, 1, Io\\Completion, 1) + ZEND_ARG_OBJ_INFO(0, op, Io\\Operation, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, data, IS_MIXED, 0, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Ring_Engine_cancel, 0, 1, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, op, Io\\Operation, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Ring_Engine_add, 0, 1, IS_VOID, 0) + ZEND_ARG_OBJ_INFO(0, registration, Io\\Registration, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Ring_Engine_remove arginfo_class_Io_Ring_Engine_add + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Ring_Engine_waitCompletions, 0, 0, IS_ARRAY, 0) + ZEND_ARG_OBJ_INFO_WITH_DEFAULT_VALUE(0, timeout, Time\\Duration, 1, "null") + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, max, IS_LONG, 1, "null") +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Ring_Engine_countPending, 0, 0, IS_LONG, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_Io_Ring_Engine_getHookCapabilities, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +#define arginfo_class_Io_Ring_Engine_getSupportedHookCapabilities arginfo_class_Io_Ring_Engine_getHookCapabilities + +ZEND_METHOD(Io_Ring_Engine, __construct); +ZEND_METHOD(Io_Ring_Engine, getBackend); +ZEND_METHOD(Io_Ring_Engine, getHandle); +ZEND_METHOD(Io_Ring_Engine, submit); +ZEND_METHOD(Io_Ring_Engine, cancel); +ZEND_METHOD(Io_Ring_Engine, add); +ZEND_METHOD(Io_Ring_Engine, remove); +ZEND_METHOD(Io_Ring_Engine, waitCompletions); +ZEND_METHOD(Io_Ring_Engine, countPending); +ZEND_METHOD(Io_Ring_Engine, getHookCapabilities); +ZEND_METHOD(Io_Ring_Engine, getSupportedHookCapabilities); + +static const zend_function_entry class_Io_Ring_Engine_methods[] = { + ZEND_ME(Io_Ring_Engine, __construct, arginfo_class_Io_Ring_Engine___construct, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, getBackend, arginfo_class_Io_Ring_Engine_getBackend, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, getHandle, arginfo_class_Io_Ring_Engine_getHandle, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, submit, arginfo_class_Io_Ring_Engine_submit, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, cancel, arginfo_class_Io_Ring_Engine_cancel, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, add, arginfo_class_Io_Ring_Engine_add, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, remove, arginfo_class_Io_Ring_Engine_remove, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, waitCompletions, arginfo_class_Io_Ring_Engine_waitCompletions, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, countPending, arginfo_class_Io_Ring_Engine_countPending, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, getHookCapabilities, arginfo_class_Io_Ring_Engine_getHookCapabilities, ZEND_ACC_PUBLIC) + ZEND_ME(Io_Ring_Engine, getSupportedHookCapabilities, arginfo_class_Io_Ring_Engine_getSupportedHookCapabilities, ZEND_ACC_PUBLIC) + ZEND_FE_END +}; + +static zend_class_entry *register_class_Io_Ring_Backend(void) +{ + zend_class_entry *class_entry = zend_register_internal_enum("Io\\Ring\\Backend", IS_UNDEF, NULL); + + zend_enum_add_case_cstr(class_entry, "IoUring", NULL); + + zend_enum_add_case_cstr(class_entry, "Iocp", NULL); + + zend_enum_add_case_cstr(class_entry, "Threads", NULL); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Ring_Engine(zend_class_entry *class_entry_Io_OperationQueue) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Ring", "Engine", class_Io_Ring_Engine_methods); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES|ZEND_ACC_NOT_SERIALIZABLE); + zend_class_implements(class_entry, 1, class_entry_Io_OperationQueue); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Ring_RingException(zend_class_entry *class_entry_Io_IoException) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Ring", "RingException", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_IoException, 0); + + return class_entry; +} + +static zend_class_entry *register_class_Io_Ring_FailedRingOperationException(zend_class_entry *class_entry_Io_Ring_RingException) +{ + zend_class_entry ce, *class_entry; + + INIT_NS_CLASS_ENTRY(ce, "Io\\Ring", "FailedRingOperationException", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, class_entry_Io_Ring_RingException, 0); + + return class_entry; +} diff --git a/ext/standard/io_ring_decl.h b/ext/standard/io_ring_decl.h new file mode 100644 index 000000000000..e277dabec9ba --- /dev/null +++ b/ext/standard/io_ring_decl.h @@ -0,0 +1,13 @@ +/* This is a generated file, edit io_ring.stub.php instead. + * Stub hash: 4a811c2ae4a406fa77283f03f8859e113b4afb0f */ + +#ifndef ZEND_IO_RING_DECL_4a811c2ae4a406fa77283f03f8859e113b4afb0f_H +#define ZEND_IO_RING_DECL_4a811c2ae4a406fa77283f03f8859e113b4afb0f_H + +typedef enum zend_enum_Io_Ring_Backend { + ZEND_ENUM_Io_Ring_Backend_IoUring = 1, + ZEND_ENUM_Io_Ring_Backend_Iocp = 2, + ZEND_ENUM_Io_Ring_Backend_Threads = 3, +} zend_enum_Io_Ring_Backend; + +#endif /* ZEND_IO_RING_DECL_4a811c2ae4a406fa77283f03f8859e113b4afb0f_H */ diff --git a/ext/standard/mail.c b/ext/standard/mail.c index 6d85d06d1f6d..0b77601e3236 100644 --- a/ext/standard/mail.c +++ b/ext/standard/mail.c @@ -45,6 +45,10 @@ #ifdef PHP_WIN32 # include "win32/sendmail.h" +#else +# include +# include +# include "ext/standard/io_poll.h" #endif #define SKIP_LONG_HEADER_SEP(str, pos) \ @@ -432,6 +436,61 @@ static int php_mail_detect_multiple_crlf(const char *hdr) { /* {{{ php_mail */ +#ifndef PHP_WIN32 +/* popen(cmd, "w") without the signals that SignalHandle objects blocked */ +static FILE *php_mail_popen(const char *cmd, pid_t *pid) +{ + int fds[2]; + if (pipe(fds) != 0) { + return NULL; + } + fcntl(fds[1], F_SETFD, FD_CLOEXEC); + + posix_spawn_file_actions_t actions; + posix_spawnattr_t attr; + sigset_t mask; + posix_spawn_file_actions_init(&actions); + if (fds[0] != STDIN_FILENO) { + posix_spawn_file_actions_adddup2(&actions, fds[0], STDIN_FILENO); + posix_spawn_file_actions_addclose(&actions, fds[0]); + } + posix_spawnattr_init(&attr); + php_io_poll_signal_child_mask(&mask); + posix_spawnattr_setsigmask(&attr, &mask); + posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK); + int r = posix_spawn(pid, "/bin/sh", &actions, &attr, + (char * const[]) {"sh", "-c", (char *) cmd, NULL}, environ); + posix_spawnattr_destroy(&attr); + posix_spawn_file_actions_destroy(&actions); + close(fds[0]); + if (r != 0) { + close(fds[1]); + errno = r; + return NULL; + } + + FILE *fp = fdopen(fds[1], "w"); + if (!fp) { + close(fds[1]); + waitpid(*pid, NULL, 0); + return NULL; + } + errno = 0; + return fp; +} + +static int php_mail_pclose(FILE *fp, pid_t pid) +{ + int status; + pid_t r; + fclose(fp); + do { + r = waitpid(pid, &status, 0); + } while (r == -1 && errno == EINTR); + return r == -1 ? -1 : status; +} +#endif + PHPAPI bool php_mail(const char *to, const char *subject, const char *message, const char *headers, const zend_string *extra_cmd) { FILE *sendmail; @@ -571,7 +630,8 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c * (e.g. the shell can't be executed) we explicitly set it to 0 to be * sure we don't catch any older errno value. */ errno = 0; - sendmail = popen(sendmail_cmd, "w"); + pid_t sendmail_pid = 0; + sendmail = php_mail_popen(sendmail_cmd, &sendmail_pid); #endif if (extra_cmd != NULL) { efree(sendmail_cmd); @@ -582,7 +642,7 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c #ifndef PHP_WIN32 if (EACCES == errno) { php_error_docref(NULL, E_WARNING, "Permission denied: unable to execute shell to run mail delivery binary '%s'", sendmail_path); - pclose(sendmail); + php_mail_pclose(sendmail, sendmail_pid); #if PHP_SIGCHILD /* Restore handler in case of error on Windows Not sure if this applicable on Win but just in case. */ @@ -646,7 +706,7 @@ PHPAPI bool php_mail(const char *to, const char *subject, const char *message, c } #endif #else - int wstatus = pclose(sendmail); + int wstatus = php_mail_pclose(sendmail, sendmail_pid); #if PHP_SIGCHILD if (sig_handler) { signal(SIGCHLD, sig_handler); diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index 8bd3361f59cb..71dbbf82dbc1 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -148,6 +148,8 @@ int openpty(int *master, int *slave, char *name, struct termios *termp, struct w #endif #include "proc_open.h" +#include "php_io.h" +#include "ext/standard/io_poll.h" static int le_proc_open; /* Resource number for `proc` resources */ @@ -254,7 +256,8 @@ static pid_t waitpid_cached(php_process_handle *proc, int *wait_status, int opti return proc->child; } - pid_t wait_pid = waitpid(proc->child, wait_status, options); + php_deadline dl = php_io_deadline_infinite(); + pid_t wait_pid = php_io_waitpid(NULL, proc->child, wait_status, options, &dl); /* The "exit" status is the final status of the process. * If we were to cache the status unconditionally, @@ -297,13 +300,18 @@ static void proc_open_rsrc_dtor(zend_resource *rsrc) * But if we're freeing the resource because of GC, don't wait. */ #ifdef PHP_WIN32 if (FG(pclose_wait)) { - WaitForSingleObject(proc->childHandle, INFINITE); - } - GetExitCodeProcess(proc->childHandle, &wstatus); - if (wstatus == STILL_ACTIVE) { - FG(pclose_ret) = -1; + /* The wait is an op, served by a ring or waited for by the core; the handle the + * resource holds keeps the pid valid until the completion arrives */ + int status = 0; + php_deadline dl = php_io_deadline_infinite(); + if (php_io_waitpid(NULL, (pid_t) proc->child, &status, 0, &dl) == (pid_t) proc->child) { + FG(pclose_ret) = status; + } else { + FG(pclose_ret) = -1; + } } else { - FG(pclose_ret) = wstatus; + GetExitCodeProcess(proc->childHandle, &wstatus); + FG(pclose_ret) = wstatus == STILL_ACTIVE ? -1 : (int) wstatus; } CloseHandle(proc->childHandle); @@ -371,6 +379,16 @@ PHP_FUNCTION(proc_terminate) /* }}} */ /* {{{ Close a process opened by `proc_open` */ +PHPAPI bool php_proc_open_get_pid(zval *zproc, php_process_id_t *pid) +{ + php_process_handle *proc = (php_process_handle*)zend_fetch_resource(Z_RES_P(zproc), "process", le_proc_open); + if (proc == NULL) { + return false; + } + *pid = proc->child; + return true; +} + PHP_FUNCTION(proc_close) { zval *zproc; @@ -1413,13 +1431,22 @@ PHP_FUNCTION(proc_open) } } + /* Signals blocked for a SignalHandle are the parent's business */ + posix_spawnattr_t attr; + sigset_t child_mask; + posix_spawnattr_init(&attr); + php_io_poll_signal_child_mask(&child_mask); + posix_spawnattr_setsigmask(&attr, &child_mask); + posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETSIGMASK); + if (argv) { - r = posix_spawnp(&child, ZSTR_VAL(command_str), &factions, NULL, argv, (env.envarray ? env.envarray : environ)); + r = posix_spawnp(&child, ZSTR_VAL(command_str), &factions, &attr, argv, (env.envarray ? env.envarray : environ)); } else { - r = posix_spawn(&child, "/bin/sh" , &factions, NULL, + r = posix_spawn(&child, "/bin/sh" , &factions, &attr, (char * const[]) {"sh", "-c", ZSTR_VAL(command_str), NULL}, env.envarray ? env.envarray : environ); } + posix_spawnattr_destroy(&attr); posix_spawn_file_actions_destroy(&factions); if (r != 0) { php_error_docref(NULL, E_WARNING, "posix_spawn() failed: %s", strerror(r)); @@ -1443,6 +1470,11 @@ PHP_FUNCTION(proc_open) php_ignore_value(chdir(cwd)); } + /* Signals blocked for a SignalHandle are the parent's business */ + sigset_t child_mask; + php_io_poll_signal_child_mask(&child_mask); + sigprocmask(SIG_SETMASK, &child_mask, NULL); + if (argv) { /* execvpe() is non-portable, use environ instead. */ if (env.envarray) { diff --git a/ext/standard/proc_open.h b/ext/standard/proc_open.h index 6a9e0533bfd2..55266401326d 100644 --- a/ext/standard/proc_open.h +++ b/ext/standard/proc_open.h @@ -48,3 +48,6 @@ typedef struct _php_process_handle { bool has_cached_exit_wait_status; #endif } php_process_handle; + +/* Throws and returns false when zproc is not a proc_open() resource */ +PHPAPI bool php_proc_open_get_pid(zval *zproc, php_process_id_t *pid); diff --git a/ext/standard/streamsfuncs.c b/ext/standard/streamsfuncs.c index 6502ac279e26..37c9d02ad4b6 100644 --- a/ext/standard/streamsfuncs.c +++ b/ext/standard/streamsfuncs.c @@ -20,6 +20,8 @@ #include "php_ini.h" #include "streamsfuncs.h" #include "php_network.h" +#include "php_io.h" +#include "ext/standard/io_poll.h" #include "php_string.h" #include "streams/php_streams_int.h" #ifdef HAVE_UNISTD_H @@ -89,10 +91,9 @@ PHP_FUNCTION(stream_socket_pair) array_init(return_value); - /* set the __exposed flag. - * php_stream_to_zval() does, add_next_index_resource() does not */ - php_stream_auto_cleanup(s1); - php_stream_auto_cleanup(s2); + /* What php_stream_to_zval() marks, add_next_index_resource() does not */ + php_stream_expose(s1); + php_stream_expose(s2); add_next_index_resource(return_value, s1->res); add_next_index_resource(return_value, s2->res); @@ -683,6 +684,257 @@ static int stream_array_to_fd_set(const HashTable *stream_array, fd_set *fds, ph return cnt ? 1 : 0; } +/* stream_select() under a provider: one Poll member per stream with the + * events of the sets it is in, a Timer member for the timeout, and the + * reported members put back into the fd sets for the usual filtering. */ +typedef struct php_select_member { + php_stream *stream; + php_socket_t fd; + uint32_t events; +} php_select_member; + +static void stream_array_collect_members(HashTable *stream_array, uint32_t events, + php_select_member **members, uint32_t *n, uint32_t *cap) +{ + zval *elem; + php_stream *stream; + + ZEND_HASH_FOREACH_VAL(stream_array, elem) { + php_socket_t this_fd; + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + if (php_stream_cast(stream, PHP_STREAM_AS_FD_FOR_SELECT | PHP_STREAM_CAST_INTERNAL, (void*)&this_fd, 1) != SUCCESS || this_fd == -1) { + continue; + } + bool merged = false; + for (uint32_t i = 0; i < *n; i++) { + if ((*members)[i].fd == this_fd) { + (*members)[i].events |= events; + merged = true; + break; + } + } + if (!merged) { + if (*n == *cap) { + *cap = *cap ? *cap * 2 : 8; + *members = safe_erealloc(*members, *cap, sizeof(**members), 0); + } + (*members)[*n].stream = stream; + (*members)[*n].fd = this_fd; + (*members)[*n].events = events; + (*n)++; + } + } ZEND_HASH_FOREACH_END(); +} + +/* The wait is an op on every stream of the sets: each is frozen once for + * its duration, and kept alive */ +static bool stream_array_freeze(HashTable *stream_array, php_stream ***frozen, uint32_t *n, uint32_t *cap) +{ + zval *elem; + php_stream *stream; + + ZEND_HASH_FOREACH_VAL(stream_array, elem) { + ZVAL_DEREF(elem); + php_stream_from_zval_no_verify(stream, elem); + if (stream == NULL) { + continue; + } + bool seen = false; + for (uint32_t i = 0; i < *n; i++) { + if ((*frozen)[i] == stream) { + seen = true; + break; + } + } + if (seen) { + continue; + } + if (stream->flags & PHP_STREAM_FLAG_IN_USE) { + zend_throw_error(NULL, "Concurrent access to a stream"); + return false; + } + if (*n == *cap) { + *cap = *cap ? *cap * 2 : 8; + *frozen = safe_erealloc(*frozen, *cap, sizeof(**frozen), 0); + } + stream->flags |= PHP_STREAM_FLAG_IN_USE; + GC_ADDREF(stream->res); + (*frozen)[(*n)++] = stream; + } ZEND_HASH_FOREACH_END(); + return true; +} + +static void stream_select_unfreeze(php_stream **frozen, uint32_t n) +{ + for (uint32_t i = 0; i < n; i++) { + zend_resource *res = frozen[i]->res; + frozen[i]->flags &= ~PHP_STREAM_FLAG_IN_USE; + zend_list_delete(res); + } + if (frozen) { + efree(frozen); + } +} + +/* Returns the number of ready descriptors, 0 on timeout, -1 with errno */ +static int stream_select_any(zval *r_array, zval *w_array, zval *e_array, struct timeval *tv, + fd_set *rfds, fd_set *wfds, fd_set *efds) +{ + php_select_member *members = NULL; + uint32_t n = 0, cap = 0; + bool priority = php_poll_backend_supports_priority(PHP_POLL_BACKEND_AUTO); + php_stream **frozen = NULL; + uint32_t n_frozen = 0, frozen_cap = 0; + + if ((r_array && !stream_array_freeze(Z_ARRVAL_P(r_array), &frozen, &n_frozen, &frozen_cap)) + || (w_array && !stream_array_freeze(Z_ARRVAL_P(w_array), &frozen, &n_frozen, &frozen_cap)) + || (e_array && !stream_array_freeze(Z_ARRVAL_P(e_array), &frozen, &n_frozen, &frozen_cap))) { + stream_select_unfreeze(frozen, n_frozen); + errno = ECANCELED; + return -1; + } + + if (r_array) { + stream_array_collect_members(Z_ARRVAL_P(r_array), PHP_POLL_READ, &members, &n, &cap); + } + if (w_array) { + stream_array_collect_members(Z_ARRVAL_P(w_array), PHP_POLL_WRITE, &members, &n, &cap); + } + if (e_array) { + stream_array_collect_members(Z_ARRVAL_P(e_array), priority ? PHP_POLL_PRI : 0, &members, &n, &cap); + } + + /* Syscall first, as for a read: a zero-timeout poll() over the sets answers a select + * with a stream ready now, or one with a zero timeout, without an op, and is the arm-time + * check of members on registered pairs; only a select that really waits builds the Any */ + bool checked = false; + if (n > 0) { + php_pollfd stack[16]; + php_pollfd *fds = n > sizeof(stack) / sizeof(stack[0]) ? safe_emalloc(n, sizeof(*fds), 0) : stack; + for (uint32_t i = 0; i < n; i++) { + fds[i].fd = members[i].fd; + fds[i].events = ((members[i].events & PHP_POLL_READ) ? POLLIN : 0) + | ((members[i].events & PHP_POLL_WRITE) ? POLLOUT : 0) + | ((members[i].events & PHP_POLL_PRI) ? POLLPRI : 0); + fds[i].revents = 0; + } + int ready = php_poll2(fds, n, 0); + int found = 0; + if (ready > 0) { + for (uint32_t i = 0; i < n; i++) { + short revents = fds[i].revents; + if (revents & POLLNVAL) { + /* The Any reports the descriptor's failure */ + found = 0; + break; + } + if ((revents & (POLLIN | POLLHUP | POLLERR)) && (members[i].events & PHP_POLL_READ)) { + PHP_SAFE_FD_SET(members[i].fd, rfds); + found++; + } + if ((revents & (POLLOUT | POLLERR)) && (members[i].events & PHP_POLL_WRITE)) { + PHP_SAFE_FD_SET(members[i].fd, wfds); + found++; + } + if ((revents & POLLPRI) && (members[i].events & PHP_POLL_PRI)) { + PHP_SAFE_FD_SET(members[i].fd, efds); + found++; + } + } + } + if (fds != stack) { + efree(fds); + } + if (found > 0 || (ready == 0 && tv && tv->tv_sec == 0 && tv->tv_usec == 0)) { + efree(members); + stream_select_unfreeze(frozen, n_frozen); + return found; + } + checked = ready == 0; + } + + php_io_op *ops = safe_emalloc(n + 1, sizeof(php_io_op), 0); + php_io_op **op_ptrs = safe_emalloc(n + 1, sizeof(php_io_op *), 0); + php_io_op_result *results = safe_emalloc(n + 1, sizeof(php_io_op_result), 0); + uint32_t n_members = 0; + + for (uint32_t i = 0; i < n; i++) { + if (members[i].events == 0) { + /* Only in the except set on a backend without priority events */ + continue; + } + php_io_op_poll(&ops[n_members], NULL, members[i].fd, members[i].events, + php_io_deadline_infinite()); + /* Frozen by this call, for the handle a provider may ask for */ + ops[n_members].stream = members[i].stream; + if (checked) { + ops[n_members].flags |= PHP_IO_OP_F_CHECKED; + } + op_ptrs[n_members] = &ops[n_members]; + n_members++; + } + uint32_t timer_index = UINT32_MAX; + if (tv) { + php_io_op_timer(&ops[n_members], php_io_deadline_from_timeval(tv)); + op_ptrs[n_members] = &ops[n_members]; + timer_index = n_members; + n_members++; + } + + int ret = 0; + if (n_members == 0) { + /* Nothing to wait for and no timeout: select() would block forever */ + errno = EINVAL; + ret = -1; + } else { + php_io_op any; + php_io_op_result any_result; + php_io_op_any(&any, op_ptrs, n_members, results); + if (php_io_run(&any, &any_result) == FAILURE) { + errno = ECANCELED; + ret = -1; + } else if (any_result.status == PHP_IO_INTERRUPTED) { + errno = EINTR; + ret = -1; + } else { + for (uint32_t i = 0; i < any.u.any.n_results; i++) { + uint32_t index = results[i].index; + if (index == timer_index || (results[i].status != PHP_IO_DONE && results[i].status != PHP_IO_READY)) { + continue; + } + /* Counted per set, as select() counts bits */ + php_socket_t fd = ops[index].fd; + uint32_t revents = (uint32_t) results[i].res; + if ((revents & (PHP_POLL_READ | PHP_POLL_HUP | PHP_POLL_ERROR)) && (ops[index].u.poll.events & PHP_POLL_READ)) { + PHP_SAFE_FD_SET(fd, rfds); + ret++; + } + if ((revents & (PHP_POLL_WRITE | PHP_POLL_ERROR)) && (ops[index].u.poll.events & PHP_POLL_WRITE)) { + PHP_SAFE_FD_SET(fd, wfds); + ret++; + } + if ((revents & PHP_POLL_PRI) && (ops[index].u.poll.events & PHP_POLL_PRI)) { + PHP_SAFE_FD_SET(fd, efds); + ret++; + } + } + } + } + + efree(results); + efree(op_ptrs); + efree(ops); + if (members) { + efree(members); + } + stream_select_unfreeze(frozen, n_frozen); + return ret; +} + static int stream_array_from_fd_set(zval *stream_array, const fd_set *fds) { zval *elem, *dest_elem; @@ -883,9 +1135,20 @@ PHP_FUNCTION(stream_select) } } - retval = php_select(max_fd+1, &rfds, &wfds, &efds, tv_p); + if (php_io_hooks_active()) { + /* The provider waits: the sets are rebuilt from what it reported */ + FD_ZERO(&rfds); + FD_ZERO(&wfds); + FD_ZERO(&efds); + retval = stream_select_any(r_array, w_array, e_array, tv_p, &rfds, &wfds, &efds); + } else { + retval = php_select(max_fd+1, &rfds, &wfds, &efds, tv_p); + } php_stream_error_operation_end(context); + if (retval == -1 && EG(exception)) { + RETURN_THROWS(); + } if (retval == -1) { php_error_docref(NULL, E_WARNING, "Unable to select [%d]: %s (max_fd=" PHP_SOCKET_FMT ")", errno, strerror(errno), max_fd); @@ -1839,3 +2102,4 @@ PHP_FUNCTION(stream_socket_shutdown) } /* }}} */ #endif + diff --git a/ext/standard/tests/poll/poll_backend_poll_nval.phpt b/ext/standard/tests/poll/poll_backend_poll_nval.phpt new file mode 100644 index 000000000000..09785412d897 --- /dev/null +++ b/ext/standard/tests/poll/poll_backend_poll_nval.phpt @@ -0,0 +1,43 @@ +--TEST-- +Io\Poll: the poll() backend reports a descriptor closed behind its back as an error, once +--EXTENSIONS-- +ffi +--SKIPIF-- +isAvailable()) die("skip poll backend not available"); +if (!is_dir('/proc/self/fd')) die("skip needs /proc/self/fd"); +?> +--INI-- +ffi.enable=1 +--FILE-- +add(new StreamPollHandle($a), [Event::Read], 'a'); +$libc->close(min($new)); +for ($i = 0; $i < 2; $i++) { + $events = $c->wait(Time\Duration::fromMilliseconds(100)); + echo count($events); + foreach ($events as $w) { + echo " ", $w->getData(), " ", $w->hasTriggered(Event::Error) ? "error" : "no error"; + } + echo "\n"; +} +?> +--EXPECT-- +1 a error +0 diff --git a/ext/standard/tests/poll/poll_backend_supportsPriority.phpt b/ext/standard/tests/poll/poll_backend_supportsPriority.phpt new file mode 100644 index 000000000000..3bfd5ca630a3 --- /dev/null +++ b/ext/standard/tests/poll/poll_backend_supportsPriority.phpt @@ -0,0 +1,28 @@ +--TEST-- +Io\Poll\Backend::supportsPriority(): kqueue, WSAPoll and Darwin's poll() cannot report Event::Priority +--FILE-- +isAvailable()) { + var_dump(true); + continue; + } + $expected = match ($backend) { + Io\Poll\Backend::Kqueue, Io\Poll\Backend::WSAPoll => false, + Io\Poll\Backend::Poll => PHP_OS_FAMILY !== 'Darwin', + default => true, + }; + var_dump($backend->supportsPriority() === $expected); +} +var_dump(is_bool(Io\Poll\Backend::Auto->supportsPriority())); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/poll/poll_ctx_wait_signal.phpt b/ext/standard/tests/poll/poll_ctx_wait_signal.phpt new file mode 100644 index 000000000000..4239df51be14 --- /dev/null +++ b/ext/standard/tests/poll/poll_ctx_wait_signal.phpt @@ -0,0 +1,40 @@ +--TEST-- +Io\Poll\Context::wait() returns early for a signal PHP has a handler for +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +wait()); + +pcntl_async_signals(false); +pcntl_alarm(1); +var_dump($ctx->wait()); +echo "dispatch\n"; +pcntl_signal_dispatch(); + +fwrite($w, "x"); +var_dump(count($ctx->wait())); +?> +--EXPECT-- +handler +array(0) { +} +array(0) { +} +dispatch +handler +int(1) diff --git a/ext/standard/tests/poll/poll_event_priority.phpt b/ext/standard/tests/poll/poll_event_priority.phpt new file mode 100644 index 000000000000..fce626a7204e --- /dev/null +++ b/ext/standard/tests/poll/poll_event_priority.phpt @@ -0,0 +1,34 @@ +--TEST-- +Io\Poll: Event::Priority reports TCP urgent data where the backend supports it +--EXTENSIONS-- +sockets +--SKIPIF-- +getBackend()->supportsPriority()) die("skip Priority is not supported by this backend"); +?> +--FILE-- +add(new StreamPollHandle($server), [Io\Poll\Event::Priority], "server"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +$sock = socket_import_stream($client); +socket_send($sock, "!", 1, MSG_OOB); +$events = $ctx->wait(Time\Duration::fromMicroseconds(200000)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0]->hasTriggered(Io\Poll\Event::Priority)); +var_dump(in_array(Io\Poll\Event::Priority, $events[0]->getWatchedEvents(), true)); +$watcher->remove(); +echo "done\n"; +?> +--EXPECTF-- +Events count: 0 +Events count: 1 +bool(true) +bool(true) +done diff --git a/ext/standard/tests/poll/poll_event_priority_unsupported.phpt b/ext/standard/tests/poll/poll_event_priority_unsupported.phpt new file mode 100644 index 000000000000..cdc38b3aee23 --- /dev/null +++ b/ext/standard/tests/poll/poll_event_priority_unsupported.phpt @@ -0,0 +1,33 @@ +--TEST-- +Io\Poll: Event::Priority is refused with ERROR_NOSUPPORT where the backend has no source for it +--SKIPIF-- +getBackend()->supportsPriority()) die("skip Priority is supported by this backend"); +?> +--FILE-- +add(new StreamPollHandle($server), [Io\Poll\Event::Priority]); +} catch (Io\Poll\FailedHandleAddException $e) { + var_dump($e->getCode() === Io\Poll\FailedHandleAddException::ERROR_NOSUPPORT); +} + +$watcher = $ctx->add(new StreamPollHandle($server), [Io\Poll\Event::Read]); +try { + $watcher->modifyEvents([Io\Poll\Event::Read, Io\Poll\Event::Priority]); +} catch (Io\Poll\FailedWatcherModificationException $e) { + var_dump($e->getCode() === Io\Poll\FailedWatcherModificationException::ERROR_NOSUPPORT); +} +$watcher->remove(); +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +done diff --git a/ext/standard/tests/poll/poll_notify_handle_basic.phpt b/ext/standard/tests/poll/poll_notify_handle_basic.phpt new file mode 100644 index 000000000000..3a991646ca7d --- /dev/null +++ b/ext/standard/tests/poll/poll_notify_handle_basic.phpt @@ -0,0 +1,71 @@ +--TEST-- +Io\Poll\NotifyHandle: raised by notify(), ready until clear() +--FILE-- +add($notify, [Io\Poll\Event::Notify], "notify"); +var_dump($watcher->getWatchedEvents()); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +$notify->notify(); +$notify->notify(); +$events = $ctx->wait(Time\Duration::fromMilliseconds(100)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0] === $watcher, $events[0]->getTriggeredEvents(), $events[0]->hasTriggered(Io\Poll\Event::Notify)); + +// Level: still reported until cleared +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +$notify->clear(); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +// Raised again after clearing +$notify->notify(); +echo "Events count: ", count($ctx->wait(Time\Duration::fromMilliseconds(100))), "\n"; +$notify->clear(); + +try { + $ctx->add(new Io\Poll\NotifyHandle(), [Io\Poll\Event::Read]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +try { + $ctx->add(new Io\Poll\NotifyHandle(), [Io\Poll\Event::Notify, Io\Poll\Event::Write]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +try { + [$r, $w] = pt_new_socket_pair(); + $ctx->add(new StreamPollHandle($r), [Io\Poll\Event::Notify]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +$watcher->remove(); +var_dump($watcher->isActive()); +?> +--EXPECT-- +bool(true) +array(1) { + [0]=> + enum(Io\Poll\Event::Notify) +} +Events count: 0 +Events count: 1 +bool(true) +array(1) { + [0]=> + enum(Io\Poll\Event::Notify) +} +bool(true) +Events count: 1 +Events count: 0 +Events count: 1 +Io\Poll\Context::add(): Argument #2 ($events) must be Event::Notify for a NotifyHandle +Io\Poll\Context::add(): Argument #2 ($events) must be Event::Notify for a NotifyHandle +Io\Poll\Context::add(): Argument #2 ($events) must not contain Event::Timer, Event::Notify, Event::Signal or Event::Process for this handle +bool(false) diff --git a/ext/standard/tests/poll/poll_on_watcher_removed_deferred.phpt b/ext/standard/tests/poll/poll_on_watcher_removed_deferred.phpt new file mode 100644 index 000000000000..87496e7fdc65 --- /dev/null +++ b/ext/standard/tests/poll/poll_on_watcher_removed_deferred.phpt @@ -0,0 +1,99 @@ +--TEST-- +Io\Poll: onWatcherRemoved runs on the next wait(), never from the close path +--FILE-- +add($h, [Event::Read], 'c1'); +$c2->add($h, [Event::Read], 'c2'); +unset($h); +$c1->onWatcherRemoved(function ($wt) { echo "c1: ", $wt->getData(), "\n"; unset($GLOBALS['c2']); }); +$c2->onWatcherRemoved(function ($wt) { echo "c2: ", $wt->getData(), "\n"; unset($GLOBALS['c1']); }); +fclose($r); +echo "closed\n"; +$c1->wait($zero); +var_dump(isset($c2)); + +echo "-- callback removes a watcher of the same stream\n"; +[$r, $w] = pt_new_socket_pair(); +$c1 = pt_new_stream_poll(); +$c2 = pt_new_stream_poll(); +$h = StreamPollWeakHandle::create($r); +$w1 = $c1->add($h, [Event::Read]); +$w2 = $c2->add($h, [Event::Read]); +$c1->onWatcherRemoved(function ($wt) use ($w2) { echo "c1 removal\n"; $w2->remove(); }); +fclose($r); +$c1->wait($zero); +var_dump($w1->isActive(), $w2->isActive()); + +echo "-- exceptions come from wait(), the rest stays queued\n"; +$c = pt_new_stream_poll(); +[$r1, $w1] = pt_new_socket_pair(); +[$r2, $w2] = pt_new_socket_pair(); +$c->add(new StreamPollHandle($r1), [Event::Read], 'one'); +$c->add(new StreamPollHandle($r2), [Event::Read], 'two'); +$c->onWatcherRemoved(function ($wt) { echo "cb ", $wt->getData(), "\n"; throw new Exception($wt->getData()); }); +fclose($r1); +fclose($r2); +echo "closed\n"; +foreach ([1, 2, 3] as $i) { + try { + var_dump(count($c->wait($zero))); + } catch (Exception $e) { + echo "caught ", $e->getMessage(), "\n"; + } +} + +echo "-- queued removals are dropped with the context\n"; +[$r, $w] = pt_new_socket_pair(); +$c = pt_new_stream_poll(); +$c->add(StreamPollWeakHandle::create($r), [Event::Read]); +$c->onWatcherRemoved(function ($wt) { echo "never\n"; }); +fclose($r); +unset($c); + +echo "-- GC of the stream runs no callback\n"; +class Holder { public $self; public $s; } +$c = pt_new_stream_poll(); +$c->onWatcherRemoved(function ($wt) { echo "removed, stream ", var_export($wt->getHandle()->getStream(), true), "\n"; }); +(function () use ($c) { + [$r, $w] = pt_new_socket_pair(); + $o = new Holder; + $o->self = $o; + $o->s = $r; + $c->add(StreamPollWeakHandle::create($r), [Event::Read]); +})(); +gc_collect_cycles(); +echo "collected\n"; +$c->wait($zero); +echo "done\n"; +?> +--EXPECT-- +-- callback drops the other context +closed +c1: c1 +bool(false) +-- callback removes a watcher of the same stream +c1 removal +bool(false) +bool(false) +-- exceptions come from wait(), the rest stays queued +closed +cb one +caught one +cb two +caught two +int(0) +-- queued removals are dropped with the context +-- GC of the stream runs no callback +collected +removed, stream NULL +done diff --git a/ext/standard/tests/poll/poll_process_handle_basic.phpt b/ext/standard/tests/poll/poll_process_handle_basic.phpt new file mode 100644 index 000000000000..9b5801b22187 --- /dev/null +++ b/ext/standard/tests/poll/poll_process_handle_basic.phpt @@ -0,0 +1,64 @@ +--TEST-- +Io\Poll\ProcessHandle: a child's exit as an event, left for proc_close() to collect +--EXTENSIONS-- +posix +pcntl +--SKIPIF-- +supportsProcessHandles()) die("skip no process handle source on this platform"); +?> +--FILE-- + 0 && PHP_INT_SIZE == 4) { + // No pid exceeds a 32-bit int: the line stands in for the check + echo "Io\\Poll\\ProcessHandle::__construct(): Argument #1 (\$pid) must be less than or equal to $pid\n"; + continue; + } + try { + new Io\Poll\ProcessHandle($pid); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } +} + +$ctx = new Io\Poll\Context(); +$proc = proc_open(['/bin/sh', '-c', 'sleep 0.2; exit 5'], [], $pipes); +$handle = Io\Poll\ProcessHandle::fromProcess($proc); +var_dump($handle->getPid() === proc_get_status($proc)['pid'], $handle->getStatus()); + +try { + $ctx->add($handle, [Io\Poll\Event::Read]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +$watcher = $ctx->add($handle, [Io\Poll\Event::Process]); +var_dump(count($ctx->wait(Time\Duration::fromSeconds(0)))); + +$events = $ctx->wait(Time\Duration::fromSeconds(5)); +var_dump(count($events), $events[0]->getTriggeredEvents(), $events[0]->getHandle() === $handle); +$status = $handle->getStatus(); +var_dump(pcntl_wifexited($status), pcntl_wexitstatus($status)); + +// Observed, not collected: proc_close() collects the child with the same status +var_dump(proc_get_status($proc)['running'], proc_close($proc)); +$watcher->remove(); +?> +--EXPECTF-- +Io\Poll\ProcessHandle::__construct(): Argument #1 ($pid) must be greater than 0 +Io\Poll\ProcessHandle::__construct(): Argument #1 ($pid) must be less than or equal to %d +bool(true) +NULL +Io\Poll\Context::add(): Argument #2 ($events) must be Event::Process for a ProcessHandle +int(0) +int(1) +array(1) { + [0]=> + enum(Io\Poll\Event::Process) +} +bool(true) +bool(true) +int(5) +bool(false) +int(5) diff --git a/ext/standard/tests/poll/poll_process_handle_collected_elsewhere.phpt b/ext/standard/tests/poll/poll_process_handle_collected_elsewhere.phpt new file mode 100644 index 000000000000..d49a86d6e561 --- /dev/null +++ b/ext/standard/tests/poll/poll_process_handle_collected_elsewhere.phpt @@ -0,0 +1,56 @@ +--TEST-- +Io\Poll\ProcessHandle: a process collected elsewhere or not our child is reported once, in every context +--EXTENSIONS-- +pcntl +--SKIPIF-- +supportsProcessHandles()) die("skip no process handle source on this platform"); +?> +--FILE-- +add($h, [Event::Process]); +fwrite($w, "x"); +var_dump(pcntl_waitpid($pid, $status) === $pid, pcntl_wexitstatus($status)); +$events = $c->wait(Time\Duration::fromSeconds(5)); +var_dump(count($events), $events[0]->hasTriggered(Event::Process), $h->getStatus()); +var_dump(count($c->wait($zero))); + +// Reaped by one context, reported by both, once each +$pid = pcntl_fork(); +if ($pid == 0) { + fread($r, 1); + exit(4); +} +$h = new ProcessHandle($pid); +$c1 = new Context; +$c2 = new Context; +$c1->add($h, [Event::Process]); +$c2->add($h, [Event::Process]); +fwrite($w, "x"); +var_dump(count($c1->wait(Time\Duration::fromSeconds(5))), pcntl_wexitstatus($h->getStatus())); +var_dump(count($c2->wait($zero)), count($c1->wait($zero)), count($c2->wait($zero))); +?> +--EXPECT-- +bool(true) +int(3) +int(1) +bool(true) +NULL +int(0) +int(1) +int(4) +int(1) +int(0) +int(0) diff --git a/ext/standard/tests/poll/poll_process_handle_exited.phpt b/ext/standard/tests/poll/poll_process_handle_exited.phpt new file mode 100644 index 000000000000..baf01ea7a929 --- /dev/null +++ b/ext/standard/tests/poll/poll_process_handle_exited.phpt @@ -0,0 +1,49 @@ +--TEST-- +Io\Poll\ProcessHandle: a child that exited before it was watched is reported at once +--EXTENSIONS-- +posix +pcntl +--SKIPIF-- +supportsProcessHandles()) die("skip no process handle source on this platform"); +?> +--FILE-- +getStatus()); + +$watcher = $ctx->add($handle, [Io\Poll\Event::Process]); +$events = $ctx->wait(Time\Duration::fromSeconds(5)); +var_dump(count($events), $events[0]->getTriggeredEvents()); +$status = $handle->getStatus(); +var_dump(pcntl_wifexited($status), pcntl_wexitstatus($status)); + +// Reported once, then no more +var_dump(count($ctx->wait(Time\Duration::fromSeconds(0)))); +var_dump(proc_close($proc)); +$watcher->remove(); + +// Reaped already: not a process to watch +proc_close(proc_open(['/bin/sh', '-c', 'exit 0'], [], $pipes)); +try { + $handle = new Io\Poll\ProcessHandle(999999); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +NULL +int(1) +array(1) { + [0]=> + enum(Io\Poll\Event::Process) +} +bool(true) +int(3) +int(0) +int(3) +Io\Poll\ProcessHandle::__construct(): Argument #1 ($pid) must be the id of a running process: %s diff --git a/ext/standard/tests/poll/poll_process_handle_nowait.phpt b/ext/standard/tests/poll/poll_process_handle_nowait.phpt new file mode 100644 index 000000000000..cd200b32a078 --- /dev/null +++ b/ext/standard/tests/poll/poll_process_handle_nowait.phpt @@ -0,0 +1,105 @@ +--TEST-- +Io\Poll\ProcessHandle: the exit is reported with its status and the child stays waitable, with rusage and by group +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- +supportsProcessHandles()) die("skip no process handle source on this platform"); +?> +--FILE-- +add($h, [Event::Process]); + $events = $c->wait(Time\Duration::fromSeconds(5)); + return [$pid, $h, count($events)]; +} + +echo "-- status recorded, child still waitable\n"; +[$a, $h, $n] = observe(3); +var_dump($n, pcntl_wexitstatus($h->getStatus())); +var_dump(pcntl_waitpid($a, $st, WNOHANG) === $a, pcntl_wexitstatus($st)); +var_dump(pcntl_waitpid($a, $st)); +var_dump(pcntl_wexitstatus($h->getStatus())); + +echo "-- killed\n"; +[$a, $h] = observe(-SIGKILL); +var_dump(pcntl_wifsignaled($h->getStatus()), pcntl_wtermsig($h->getStatus()) === SIGKILL); +var_dump(pcntl_waitpid($a, $st) === $a, pcntl_wtermsig($st) === SIGKILL); + +echo "-- rusage\n"; +[$a] = observe(4); +var_dump(pcntl_waitpid($a, $st, 0, $ru) === $a, pcntl_wexitstatus($st), is_array($ru)); +[$a] = observe(5); +var_dump(pcntl_wait($st, 0, $ru) === $a, pcntl_wexitstatus($st)); + +echo "-- by process group\n"; +[$a] = observe(6); +var_dump(pcntl_waitpid(0, $st) === $a, pcntl_wexitstatus($st)); +[$a] = observe(7); +var_dump(pcntl_waitpid(-posix_getpgrp(), $st) === $a, pcntl_wexitstatus($st)); + +echo "-- a second context reports it too\n"; +[$a, $h] = observe(8); +$c = new Context; +$c->add($h, [Event::Process]); +var_dump(count($c->wait(Time\Duration::fromSeconds(0)))); +$h2 = new ProcessHandle($a); +$c->add($h2, [Event::Process]); +var_dump(count($c->wait(Time\Duration::fromSeconds(5))), pcntl_wexitstatus($h2->getStatus())); +var_dump(pcntl_waitpid($a, $st) === $a); + +echo "-- collected elsewhere\n"; +$pid = pcntl_fork(); +if ($pid == 0) { + exit(9); +} +$h = new ProcessHandle($pid); +pcntl_waitpid($pid, $st); +$c = new Context; +$c->add($h, [Event::Process]); +var_dump(count($c->wait(Time\Duration::fromSeconds(5))), $h->getStatus()); +?> +--EXPECT-- +-- status recorded, child still waitable +int(1) +int(3) +bool(true) +int(3) +int(-1) +int(3) +-- killed +bool(true) +bool(true) +bool(true) +bool(true) +-- rusage +bool(true) +int(4) +bool(true) +bool(true) +int(5) +-- by process group +bool(true) +int(6) +bool(true) +int(7) +-- a second context reports it too +int(1) +int(1) +int(8) +bool(true) +-- collected elsewhere +int(1) +NULL diff --git a/ext/standard/tests/poll/poll_signal_handle_basic.phpt b/ext/standard/tests/poll/poll_signal_handle_basic.phpt new file mode 100644 index 000000000000..62a37f45dd47 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_basic.phpt @@ -0,0 +1,85 @@ +--TEST-- +Io\Poll\SignalHandle: signals as events, blocked while a context watches the handle +--EXTENSIONS-- +posix +pcntl +--SKIPIF-- +supportsSignalHandles()) die("skip no signal handle source on this platform"); +?> +--FILE-- +getMessage(), "\n"; +} +try { + new Io\Poll\SignalHandle([0]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +$ctx = new Io\Poll\Context(); +$handle = new Io\Poll\SignalHandle([SIGUSR1, SIGUSR2]); +var_dump($handle->getSignals() === [SIGUSR1, SIGUSR2]); + +// Not watched yet: nothing blocked +pcntl_sigprocmask(SIG_UNBLOCK, [SIGWINCH], $blocked); +var_dump(in_array(SIGUSR1, $blocked), in_array(SIGUSR2, $blocked)); + +try { + $ctx->add($handle, [Io\Poll\Event::Notify]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} + +$watcher = $ctx->add($handle, [Io\Poll\Event::Signal]); +// Watched: the handle blocked its signals +pcntl_sigprocmask(SIG_UNBLOCK, [SIGWINCH], $blocked); +var_dump(in_array(SIGUSR1, $blocked), in_array(SIGUSR2, $blocked)); +var_dump(count($ctx->wait(Time\Duration::fromSeconds(0)))); + +posix_kill(posix_getpid(), SIGUSR2); +$events = $ctx->wait(Time\Duration::fromSeconds(2)); +var_dump(count($events), $events[0]->getTriggeredEvents()); +var_dump($handle->getDelivered() === [SIGUSR2], $handle->getDelivered()); + +// Level: nothing pending, nothing reported +var_dump(count($ctx->wait(Time\Duration::fromSeconds(0)))); + +// Both at once +posix_kill(posix_getpid(), SIGUSR1); +posix_kill(posix_getpid(), SIGUSR2); +$events = $ctx->wait(Time\Duration::fromSeconds(2)); +var_dump(count($events), count($handle->getDelivered())); + +$watcher->remove(); + +// Removed: the signals are unblocked again while the handle lives on +pcntl_sigprocmask(SIG_UNBLOCK, [SIGWINCH], $blocked); +var_dump(in_array(SIGUSR1, $blocked), in_array(SIGUSR2, $blocked)); +?> +--EXPECTF-- +Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not be empty +Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be between 1 and %d +bool(true) +bool(false) +bool(false) +Io\Poll\Context::add(): Argument #2 ($events) must be Event::Signal for a SignalHandle +bool(true) +bool(true) +int(0) +int(1) +array(1) { + [0]=> + enum(Io\Poll\Event::Signal) +} +bool(true) +array(0) { +} +int(0) +int(1) +int(2) +bool(false) +bool(false) diff --git a/ext/standard/tests/poll/poll_signal_handle_child_mask.phpt b/ext/standard/tests/poll/poll_signal_handle_child_mask.phpt new file mode 100644 index 000000000000..cfa8a6c1b9a7 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_child_mask.phpt @@ -0,0 +1,54 @@ +--TEST-- +Io\Poll\SignalHandle: the signals it blocks while watched stay unblocked in exec'd children, mail() and pcntl_exec() included +--EXTENSIONS-- +pcntl +--SKIPIF-- +supportsSignalHandles()) die("skip no signal handle source on this platform"); +?> +--INI-- +sendmail_path="grep SigBlk /proc/self/status > {PWD}/poll_signal_handle_child_mask.out; cat > /dev/null" +--FILE-- +add($h, [Io\Poll\Event::Signal]); +echo "self: ", usr1_blocked(preg_replace('/.*^(SigBlk:[^\n]*).*/ms', '$1', file_get_contents('/proc/self/status'))), "\n"; + +$out = __DIR__ . '/poll_signal_handle_child_mask.out'; +var_dump(mail('nobody@example.com', 'subject', 'body')); +echo "mail: ", usr1_blocked(file_get_contents($out)), "\n"; + +$pid = pcntl_fork(); +if ($pid == 0) { + pcntl_exec('/bin/sh', ['-c', "grep SigBlk /proc/self/status > $out"]); + exit(1); +} +pcntl_waitpid($pid, $status); +echo "pcntl_exec: ", usr1_blocked(file_get_contents($out)), "\n"; + +// A failed exec leaves the mask as it was +$pid = pcntl_fork(); +if ($pid == 0) { + @pcntl_exec('/nonexistent/program'); + echo "failed exec: ", usr1_blocked(preg_replace('/.*^(SigBlk:[^\n]*).*/ms', '$1', file_get_contents('/proc/self/status'))), "\n"; + exit(0); +} +pcntl_waitpid($pid, $status); +?> +--CLEAN-- + +--EXPECT-- +self: blocked +bool(true) +mail: not blocked +pcntl_exec: not blocked +failed exec: blocked diff --git a/ext/standard/tests/poll/poll_signal_handle_engine_timeout.phpt b/ext/standard/tests/poll/poll_signal_handle_engine_timeout.phpt new file mode 100644 index 000000000000..703118520b41 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_engine_timeout.phpt @@ -0,0 +1,28 @@ +--TEST-- +Io\Poll\SignalHandle refuses the signal the execution timeout runs on +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +getMessage() === "Io\\Poll\\SignalHandle::__construct(): Argument #1 (\$signals) must not contain signal " . $sig . ", which the execution timeout uses" ? "refused\n" : $e->getMessage() . "\n"; +} + +// The timeout still fires with a handle on other signals +$h = new Io\Poll\SignalHandle([SIGUSR1]); +set_time_limit(1); +while (true) {} +?> +--EXPECTF-- +refused + +Fatal error: Maximum execution time of 1 second exceeded in %s on line %d diff --git a/ext/standard/tests/poll/poll_signal_handle_invalid.phpt b/ext/standard/tests/poll/poll_signal_handle_invalid.phpt new file mode 100644 index 000000000000..c3661bcedeb1 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_invalid.phpt @@ -0,0 +1,33 @@ +--TEST-- +Io\Poll\SignalHandle: signals that cannot be blocked, fault signals and non-integers are refused +--EXTENSIONS-- +pcntl +--FILE-- +getMessage()), "\n"; + } +} +$h = new Io\Poll\SignalHandle([SIGUSR1, SIGUSR1]); +var_dump($h->getSignals() === [SIGUSR1]); +?> +--EXPECTF-- +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not contain signal %d, which cannot be blocked +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not contain signal %d, which cannot be blocked +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not contain signal %d, which cannot be blocked +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not contain signal %d, which cannot be blocked +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not contain signal %d, which cannot be blocked +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) must not contain signal %d, which cannot be blocked +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be between 1 and N +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be between 1 and N +ValueError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be between 1 and N +TypeError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be of type int, string given +TypeError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be of type int, float given +TypeError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be of type int, null given +TypeError: Io\Poll\SignalHandle::__construct(): Argument #1 ($signals) signals must be of type int, string given +bool(true) diff --git a/ext/standard/tests/poll/poll_signal_handle_pending.phpt b/ext/standard/tests/poll/poll_signal_handle_pending.phpt new file mode 100644 index 000000000000..bb987e9cabe5 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_pending.phpt @@ -0,0 +1,66 @@ +--TEST-- +Io\Poll\SignalHandle: a signal pending before the handle is watched is reported at once +--EXTENSIONS-- +posix +pcntl +--SKIPIF-- +supportsSignalHandles()) die("skip no signal handle source on this platform"); +?> +--FILE-- +add($handle, [Io\Poll\Event::Signal]); +// FreeBSD announces a nested kqueue's readiness from a kernel task, so a zero timeout may miss +// what add() triggered; a bounded wait returns as soon as it is reported +$events = $ctx->wait(Time\Duration::fromSeconds(1)); +var_dump(count($events), $handle->getDelivered() === [SIGUSR1]); + +// Consumed: nothing pending, nothing reported +var_dump(count($ctx->wait(Time\Duration::fromSeconds(0)))); + +// Two signals of the set pending at once are both taken +posix_kill(posix_getpid(), SIGUSR1); +posix_kill(posix_getpid(), SIGUSR2); +$events = $ctx->wait(Time\Duration::fromSeconds(2)); +$delivered = $handle->getDelivered(); +sort($delivered); +var_dump(count($events), $delivered === [SIGUSR1, SIGUSR2]); +var_dump(count($ctx->wait(Time\Duration::fromSeconds(0)))); + +// A pending signal outside the set is left alone +pcntl_sigprocmask(SIG_BLOCK, [SIGTERM]); +posix_kill(posix_getpid(), SIGTERM); +posix_kill(posix_getpid(), SIGUSR2); +$events = $ctx->wait(Time\Duration::fromSeconds(2)); +var_dump(count($events), $handle->getDelivered() === [SIGUSR2]); +pcntl_sigprocmask(SIG_BLOCK, [SIGTERM], $blocked); +var_dump(in_array(SIGTERM, $blocked)); +pcntl_signal(SIGTERM, function () { echo "SIGTERM handled\n"; }); +pcntl_sigprocmask(SIG_UNBLOCK, [SIGTERM]); +pcntl_signal_dispatch(); + +$watcher->remove(); +unset($handle, $watcher, $events); +// Blocked before the handle, so still blocked after it +pcntl_sigprocmask(SIG_BLOCK, [SIGTERM], $blocked); +var_dump(in_array(SIGUSR1, $blocked), in_array(SIGUSR2, $blocked)); +?> +--EXPECT-- +int(1) +bool(true) +int(0) +int(1) +bool(true) +int(0) +int(1) +bool(true) +bool(true) +SIGTERM handled +bool(true) +bool(true) diff --git a/ext/standard/tests/poll/poll_signal_handle_registrations.phpt b/ext/standard/tests/poll/poll_signal_handle_registrations.phpt new file mode 100644 index 000000000000..bd8fcfd6baef --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_registrations.phpt @@ -0,0 +1,74 @@ +--TEST-- +Io\Poll\SignalHandle: the signals stay blocked until the last context stops watching the handle +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- +supportsSignalHandles()) die("skip no signal handle source on this platform"); +?> +--FILE-- +getDelivered()); + +// Two contexts: blocked until both removed +$a = new Context(); +$b = new Context(); +$wa = $a->add($h, [Event::Signal]); +$wb = $b->add($h, [Event::Signal]); +var_dump(usr1_blocked()); +$wa->remove(); +var_dump(usr1_blocked()); +posix_kill(posix_getpid(), SIGUSR1); +var_dump(count($b->wait(Time\Duration::fromSeconds(2))), $h->getDelivered() === [SIGUSR1]); + +// A context destroyed with the handle in it counts as a removal +unset($wb, $b); +var_dump(usr1_blocked()); + +// Two handles for the signal: blocked until the last watched one is removed +$h2 = new SignalHandle([SIGUSR1]); +$c = new Context(); +$w1 = $c->add($h, [Event::Signal]); +$w2 = $c->add($h2, [Event::Signal]); +$w1->remove(); +var_dump(usr1_blocked()); +$w2->remove(); +var_dump(usr1_blocked()); + +// A cycle through the watcher data, collected with the handle still watched +$d = new Context(); +$d->add($h, [Event::Signal], $d); +var_dump(usr1_blocked()); +unset($d); +gc_collect_cycles(); +var_dump(usr1_blocked()); +?> +--EXPECT-- +bool(false) +handler +array(0) { +} +bool(true) +bool(true) +int(1) +bool(true) +bool(false) +bool(true) +bool(false) +bool(true) +bool(false) diff --git a/ext/standard/tests/poll/poll_signal_handle_remove_pending.phpt b/ext/standard/tests/poll/poll_signal_handle_remove_pending.phpt new file mode 100644 index 000000000000..51d9261f24c3 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_remove_pending.phpt @@ -0,0 +1,48 @@ +--TEST-- +Io\Poll\SignalHandle: deliveries taken while watched are kept across the last removal, nothing is discarded +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- +supportsSignalHandles()) die("skip no signal handle source on this platform"); +?> +--FILE-- +add($h, [Io\Poll\Event::Signal]); +posix_kill(posix_getpid(), SIGUSR1); +posix_kill(posix_getpid(), SIGUSR2); +posix_kill(posix_getpid(), SIGTERM); +echo "pending\n"; +$watcher->remove(); +pcntl_signal_dispatch(); +echo "alive\n"; + +// Taken into the record before the unblock, none of them ran its handler or default action +$delivered = $h->getDelivered(); +sort($delivered); +$expected = [SIGUSR1, SIGUSR2, SIGTERM]; +sort($expected); +var_dump($delivered === $expected); + +// Unblocked again: a later delivery has its normal disposition +posix_kill(posix_getpid(), SIGUSR2); +pcntl_signal_dispatch(); +var_dump($h->getDelivered()); +var_dump(pcntl_sigprocmask(SIG_UNBLOCK, [SIGCHLD], $old), in_array(SIGUSR1, $old), in_array(SIGTERM, $old)); +?> +--EXPECT-- +pending +alive +bool(true) +handler SIGUSR2 +array(0) { +} +bool(true) +bool(false) +bool(false) diff --git a/ext/standard/tests/poll/poll_signal_handle_shared.phpt b/ext/standard/tests/poll/poll_signal_handle_shared.phpt new file mode 100644 index 000000000000..a5b6ddb419fd --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_shared.phpt @@ -0,0 +1,27 @@ +--TEST-- +Io\Poll\SignalHandle: of two handles for one signal only the one that took the delivery reports it +--EXTENSIONS-- +posix +pcntl +--SKIPIF-- +supportsSignalHandles()) die("skip no signal handle source on this platform"); +?> +--FILE-- +add($a, [Event::Signal], 'a'); +$c->add($b, [Event::Signal], 'b'); +posix_kill(posix_getpid(), SIGUSR1); +$events = $c->wait(Time\Duration::fromSeconds(2)); +var_dump(count($events)); +var_dump($events[0]->getHandle()->getDelivered() === [SIGUSR1]); +var_dump(count($c->wait(Time\Duration::fromSeconds(0)))); +?> +--EXPECT-- +int(1) +bool(true) +int(0) diff --git a/ext/standard/tests/poll/poll_signal_handle_zts.phpt b/ext/standard/tests/poll/poll_signal_handle_zts.phpt new file mode 100644 index 000000000000..73c4c63ec1a5 --- /dev/null +++ b/ext/standard/tests/poll/poll_signal_handle_zts.phpt @@ -0,0 +1,17 @@ +--TEST-- +Io\Poll\SignalHandle is refused outside the CLI in thread-safe builds +--CGI-- +--SKIPIF-- + +--FILE-- +getMessage(), "\n"; +} +?> +--EXPECT-- +Io\Poll\PollException: Io\Poll\SignalHandle is only available in the CLI in thread-safe builds diff --git a/ext/standard/tests/poll/poll_stream_oneshot_rearm_poll_backend.phpt b/ext/standard/tests/poll/poll_stream_oneshot_rearm_poll_backend.phpt new file mode 100644 index 000000000000..a7780c67c19d --- /dev/null +++ b/ext/standard/tests/poll/poll_stream_oneshot_rearm_poll_backend.phpt @@ -0,0 +1,59 @@ +--TEST-- +Io\Poll: a fired one-shot watcher stays registered and disarmed on the poll() backend +--SKIPIF-- +isAvailable()) die("skip poll backend required"); +?> +--FILE-- +add($handle, [Io\Poll\Event::Read, Io\Poll\Event::OneShot], "data"); + +fwrite($w, "a"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromMicroseconds(100000))), "\n"; +var_dump(fread($r, 10)); + +// Disarmed: not reported although readable, and not even polled +fwrite($w, "b"); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +var_dump($watcher->isActive()); + +// Still registered: adding the handle again is refused +try { + $ctx->add($handle, [Io\Poll\Event::Read]); +} catch (Io\Poll\HandleAlreadyWatchedException $e) { + echo $e::class, ': ', $e->getMessage(), "\n"; +} + +// Re-armed by modify, reports the pending byte at once +$watcher->modifyEvents([Io\Poll\Event::Read, Io\Poll\Event::OneShot]); +$events = $ctx->wait(Time\Duration::fromSeconds(0)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0] === $watcher); +var_dump(fread($r, 10)); + +// A disarmed watcher does not report the peer closing either +fclose($w); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +$watcher->modifyEvents([Io\Poll\Event::Read, Io\Poll\Event::OneShot]); +echo "Events count: ", count($ctx->wait(Time\Duration::fromMicroseconds(100000))), "\n"; + +$watcher->remove(); +var_dump($watcher->isActive()); +?> +--EXPECT-- +Events count: 1 +string(1) "a" +Events count: 0 +bool(true) +Io\Poll\HandleAlreadyWatchedException: Handle already added +Events count: 1 +bool(true) +string(1) "b" +Events count: 0 +Events count: 1 +bool(false) diff --git a/ext/standard/tests/poll/poll_timer_handle_basic.phpt b/ext/standard/tests/poll/poll_timer_handle_basic.phpt new file mode 100644 index 000000000000..68ddcf77c00e --- /dev/null +++ b/ext/standard/tests/poll/poll_timer_handle_basic.phpt @@ -0,0 +1,100 @@ +--TEST-- +Io\Poll\TimerHandle: one-shot and periodic timers on the context's deadline heap +--FILE-- +isPeriodic(), Time\Duration::compare($timer->getTimeout(), Time\Duration::fromMilliseconds(30))); + +$watcher = $ctx->add($timer, [Io\Poll\Event::Timer], "one-shot"); +var_dump($watcher->getWatchedEvents()); + +// Not yet +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; + +// The timer bounds an unbounded wait +$start = hrtime(true); +$events = $ctx->wait(); +$elapsed_ms = (hrtime(true) - $start) / 1e6; +echo "Events count: ", count($events), "\n"; +var_dump($events[0] === $watcher, $events[0]->getData(), $events[0]->getTriggeredEvents()); +var_dump($elapsed_ms >= 25 && $elapsed_ms < 1000); + +// Fired once: stays active, disarmed +var_dump($watcher->isActive()); +echo "Events count: ", count($ctx->wait(Time\Duration::fromMilliseconds(60))), "\n"; + +// Re-armed by modifyEvents() +$watcher->modifyEvents([Io\Poll\Event::Timer]); +echo "Events count: ", count($ctx->wait(Time\Duration::fromSeconds(0))), "\n"; +echo "Events count: ", count($ctx->wait(Time\Duration::fromMilliseconds(500))), "\n"; + +$watcher->remove(); +var_dump($watcher->isActive()); +try { + $watcher->remove(); +} catch (Io\Poll\InactiveWatcherException $e) { + echo $e->getMessage(), "\n"; +} + +// Periodic +$periodic = new Io\Poll\TimerHandle(Time\Duration::fromMilliseconds(10), true); +var_dump($periodic->isPeriodic()); +$pw = $ctx->add($periodic, [Io\Poll\Event::Timer], "periodic"); +$fired = 0; +$start = hrtime(true); +while ($fired < 3) { + foreach ($ctx->wait(Time\Duration::fromSeconds(2)) as $ev) { + var_dump($ev === $pw); + $fired++; + } +} +$elapsed_ms = (hrtime(true) - $start) / 1e6; +var_dump($elapsed_ms >= 25 && $elapsed_ms < 2000); +$pw->remove(); + +// Wrong events for a timer +try { + $ctx->add(new Io\Poll\TimerHandle(Time\Duration::fromSeconds(1)), [Io\Poll\Event::Read]); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +try { + new Io\Poll\TimerHandle(Time\Duration::fromSeconds(0), true); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} +?> +--EXPECT-- +bool(true) +bool(false) +int(0) +array(1) { + [0]=> + enum(Io\Poll\Event::Timer) +} +Events count: 0 +Events count: 1 +bool(true) +string(8) "one-shot" +array(1) { + [0]=> + enum(Io\Poll\Event::Timer) +} +bool(true) +bool(true) +Events count: 0 +Events count: 0 +Events count: 1 +bool(false) +Cannot remove inactive watcher +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +Io\Poll\Context::add(): Argument #2 ($events) must be Event::Timer for a TimerHandle +Io\Poll\TimerHandle::__construct(): Argument #1 ($timeout) must not be zero for a periodic timer diff --git a/ext/standard/tests/poll/poll_timer_handle_periodic_lag.phpt b/ext/standard/tests/poll/poll_timer_handle_periodic_lag.phpt new file mode 100644 index 000000000000..5386151f073a --- /dev/null +++ b/ext/standard/tests/poll/poll_timer_handle_periodic_lag.phpt @@ -0,0 +1,22 @@ +--TEST-- +Io\Poll\TimerHandle: a periodic timer far behind skips the missed periods at once +--FILE-- +add(new Io\Poll\TimerHandle(Time\Duration::fromNanoseconds(1), true), [Io\Poll\Event::Timer]); +$spent = 0; +for ($i = 0; $i < 3; $i++) { + usleep(300000); + $t0 = hrtime(true); + $events = $ctx->wait(Time\Duration::fromSeconds(1)); + $spent += hrtime(true) - $t0; + var_dump(count($events)); +} +// Stepping 1ns at a time through 300ms took seconds +var_dump($spent < 500000000); +?> +--EXPECT-- +int(1) +int(1) +int(1) +bool(true) diff --git a/ext/standard/tests/poll/poll_timer_handle_with_stream.phpt b/ext/standard/tests/poll/poll_timer_handle_with_stream.phpt new file mode 100644 index 000000000000..1e4c415fb26b --- /dev/null +++ b/ext/standard/tests/poll/poll_timer_handle_with_stream.phpt @@ -0,0 +1,49 @@ +--TEST-- +Io\Poll\TimerHandle: timers and descriptors in one context, timers reported first +--FILE-- +add(new StreamPollHandle($r), [Io\Poll\Event::Read], "stream"); +$tw = $ctx->add(new Io\Poll\TimerHandle(Time\Duration::fromMilliseconds(20)), [Io\Poll\Event::Timer], "timer"); + +// Data first: the stream is reported, the timer keeps waiting +fwrite($w, "x"); +$events = $ctx->wait(Time\Duration::fromSeconds(1)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0]->getData()); +fread($r, 10); + +// Then the timer fires on its own +$events = $ctx->wait(Time\Duration::fromSeconds(1)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0]->getData()); + +// Both due at once: the timer comes first +$tw->modifyEvents([Io\Poll\Event::Timer]); +usleep(30000); +fwrite($w, "y"); +$events = $ctx->wait(Time\Duration::fromSeconds(0)); +echo "Events count: ", count($events), "\n"; +var_dump($events[0]->getData(), $events[1]->getData()); + +// Destroying the context with an armed timer is fine +$tw->modifyEvents([Io\Poll\Event::Timer]); +unset($ctx); +var_dump($tw->isActive(), $sw->isActive()); +echo "done\n"; +?> +--EXPECT-- +Events count: 1 +string(6) "stream" +Events count: 1 +string(5) "timer" +Events count: 2 +string(5) "timer" +string(6) "stream" +bool(false) +bool(false) +done diff --git a/ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt b/ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt new file mode 100644 index 000000000000..11477236170c --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_context_destroyed_first.phpt @@ -0,0 +1,24 @@ +--TEST-- +Io\Poll: Context destroyed before stream: watcher deactivated +--FILE-- +add($handle, [Io\Poll\Event::Read]); + +unset($ctx); +gc_collect_cycles(); + +var_dump($watcher->isActive()); + +fclose($r); +fclose($w); + +echo "done\n"; +?> +--EXPECT-- +bool(false) +done diff --git a/ext/standard/tests/poll/poll_weakhandle_exposure.phpt b/ext/standard/tests/poll/poll_weakhandle_exposure.phpt new file mode 100644 index 000000000000..dd3dd0a59d38 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_exposure.phpt @@ -0,0 +1,31 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle is exposed through create(), one per stream, invalid once it is closed +--FILE-- +isExposed(), $handle->isValid(), $handle === StreamPollWeakHandle::create($r)); +var_dump($handle->getStream() === $r); + +$ctx = pt_new_stream_poll(); +$watcher = $ctx->add($handle, [Io\Poll\Event::Read]); +var_dump($watcher->getHandle() === $handle); + +fclose($r); +var_dump($handle->isValid(), $handle->getStream(), $handle->isExposed(), $watcher->isActive()); +fclose($w); +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +NULL +bool(true) +bool(false) +done diff --git a/ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt b/ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt new file mode 100644 index 000000000000..137ac1d623e9 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_getstream_null.phpt @@ -0,0 +1,21 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle::getStream() returns null after stream is freed +--FILE-- +getStream())); + +fclose($r); +fclose($w); + +var_dump($handle->getStream()); +echo "done\n"; +?> +--EXPECT-- +bool(true) +NULL +done diff --git a/ext/standard/tests/poll/poll_weakhandle_interface.phpt b/ext/standard/tests/poll/poll_weakhandle_interface.phpt new file mode 100644 index 000000000000..55c86cad8b48 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_interface.phpt @@ -0,0 +1,30 @@ +--TEST-- +Io\Poll: WeakHandle is the type of handles that hold their resource weakly +--FILE-- +isInterface()); +var_dump(class_implements(Io\Poll\WeakHandle::class)); + +try { + eval('class MyWeak implements Io\Poll\WeakHandle {}'); +} catch (Error $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} +?> +--EXPECTF-- +bool(true) +bool(true) +bool(false) +bool(true) +array(1) { + ["Io\Poll\Handle"]=> + string(14) "Io\Poll\Handle" +} + +Fatal error: Io\Poll\Handle cannot be implemented by user classes in %s on line %d diff --git a/ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt b/ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt new file mode 100644 index 000000000000..3000ab59b91b --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_manual_remove.phpt @@ -0,0 +1,34 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle watcher manual remove from single Context +--FILE-- +add($handle, [Io\Poll\Event::Read], 'my-data'); + +var_dump($watcher->isActive()); +var_dump($watcher->getData()); + +$watcher->remove(); + +var_dump($watcher->isActive()); + +try { + $watcher->remove(); +} catch (Io\Poll\InactiveWatcherException $e) { + echo $e->getMessage(), "\n"; +} + +fclose($r); +fclose($w); +echo "done\n"; +?> +--EXPECT-- +bool(true) +string(7) "my-data" +bool(false) +Cannot remove inactive watcher +done diff --git a/ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt b/ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt new file mode 100644 index 000000000000..59f1207165ee --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_multiple_contexts.phpt @@ -0,0 +1,36 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle auto-removed from multiple Contexts when stream is collected +--FILE-- +add($handle, [Io\Poll\Event::Read], 'ctx1'); +$watcher2 = $ctx2->add($handle, [Io\Poll\Event::Read], 'ctx2'); + +var_dump($watcher1->isActive()); +var_dump($watcher2->isActive()); + +$weakWatcher1 = WeakReference::create($watcher1); +$weakWatcher2 = WeakReference::create($watcher2); +unset($handle, $watcher1, $watcher2); + +fclose($r); +fclose($w); +gc_collect_cycles(); + +// Both watchers removed from their contexts and freed. +var_dump($weakWatcher1->get() === null); +var_dump($weakWatcher2->get() === null); +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +done diff --git a/ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt b/ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt new file mode 100644 index 000000000000..a56b13b0834f --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_on_watcher_removed.phpt @@ -0,0 +1,37 @@ +--TEST-- +Io\Poll: onWatcherRemoved reports stream GC on the next wait() but not a manual Watcher::remove() +--FILE-- +onWatcherRemoved(function (Io\Poll\Watcher $w) { + echo "removed: " . $w->getData() . "\n"; +}); + +$handle = StreamPollWeakHandle::create($r); +$watcher = $ctx->add($handle, [Io\Poll\Event::Read], 'manual'); +$watcher->remove(); // no callback expected +echo "after manual remove\n"; +unset($watcher, $handle); + +// --- Part 2: stream GC fires the callback --- +$handle = StreamPollWeakHandle::create($r); +$ctx->add($handle, [Io\Poll\Event::Read], 'stream-gc'); +unset($handle); + +fclose($r); // queues the removal for the next wait() +fclose($w); +echo "closed\n"; +$ctx->wait(Time\Duration::fromSeconds(0)); + +echo "done\n"; +?> +--EXPECT-- +after manual remove +closed +removed: stream-gc +done diff --git a/ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt b/ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt new file mode 100644 index 000000000000..289e80efdc88 --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_readd_after_remove.phpt @@ -0,0 +1,28 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle can be re-added to a Context after manual Watcher::remove() +--FILE-- +add($handle, [Io\Poll\Event::Read], 'first'); +$watcher1->remove(); +var_dump($watcher1->isActive()); + +$watcher2 = $ctx->add($handle, [Io\Poll\Event::Read], 'second'); +var_dump($watcher2->isActive()); +var_dump($watcher2->getData()); + +$watcher2->remove(); +fclose($r); +fclose($w); +echo "done\n"; +?> +--EXPECT-- +bool(false) +bool(true) +string(6) "second" +done diff --git a/ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt b/ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt new file mode 100644 index 000000000000..fbe72234485a --- /dev/null +++ b/ext/standard/tests/poll/poll_weakhandle_stream_gc.phpt @@ -0,0 +1,39 @@ +--TEST-- +Io\Poll: StreamPollWeakHandle watcher auto-removed and GC'd when stream is collected +--FILE-- +add($handle, [Io\Poll\Event::Read]); + +$weakHandle = WeakReference::create($handle); +$weakWatcher = WeakReference::create($watcher); + +// Refs to handle and watcher; context still holds a ref to +// watcher (and watcher holds a ref to handle), but only until the stream dies. +unset($handle, $watcher); + +// Stream is still alive: watcher should still be in the context. +var_dump($weakHandle->get() !== null); // handle alive (held by watcher in context) +var_dump($weakWatcher->get() !== null); // watcher alive (held by context) + +// Freeing the stream notifies handle and context +fclose($r); +fclose($w); + +// Context no longer holds the watcher ref; watcher held no external refs either. +gc_collect_cycles(); + +var_dump($weakHandle->get() === null); // handle freed +var_dump($weakWatcher->get() === null); // watcher freed +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +done diff --git a/ext/standard/tests/streams/hooks/any-members-submitted.phpt b/ext/standard/tests/streams/hooks/any-members-submitted.phpt new file mode 100644 index 000000000000..78927111b0ca --- /dev/null +++ b/ext/standard/tests/streams/hooks/any-members-submitted.phpt @@ -0,0 +1,49 @@ +--TEST-- +IO hooks: Any members a provider submitted on their own are withdrawn when the Any ends +--FILE-- +queue = new Io\Poll\OperationQueue(); } + public function getCapabilities(): array { return []; } + public function run(Io\Operation $op): Io\Completion + { + if ($op instanceof Io\Operation\Any) { + $done = []; + foreach ($op->getOperations() as $member) { + $c = $this->queue->submit($member); + if ($c !== null) { + $done[] = $c; + } + } + return $op->completeWith($done ?: $this->queue->waitCompletions()); + } + return $this->queue->submit($op) ?? $this->queue->waitCompletions()[0]; + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +$family = PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX; +[$a1, $a2] = stream_socket_pair($family, STREAM_SOCK_STREAM, 0); +[$b1, $b2] = stream_socket_pair($family, STREAM_SOCK_STREAM, 0); +$provider = new MemberProvider(); +Io\Hooks\set_hooks($provider); + +fwrite($a2, "x"); +$r = [$a1, $b1]; +$w = $e = null; +var_dump(stream_select($r, $w, $e, 5), $r === [$a1]); +var_dump($provider->queue->countPending()); + +fwrite($b2, "y"); +var_dump(count($provider->queue->waitCompletions(Time\Duration::fromNanoseconds(0)))); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +int(1) +bool(true) +int(0) +int(0) diff --git a/ext/standard/tests/streams/hooks/bridge-arguments.phpt b/ext/standard/tests/streams/hooks/bridge-arguments.phpt new file mode 100644 index 000000000000..85eb03219803 --- /dev/null +++ b/ext/standard/tests/streams/hooks/bridge-arguments.phpt @@ -0,0 +1,81 @@ +--TEST-- +IO hooks: argument checks of the bridge methods +--FILE-- +getMessage(), "\n"; +} + +final class Checks implements Io\Hooks\Hooks +{ + public Io\Poll\OperationQueue $queue; + public function __construct() { $this->queue = new Io\Poll\OperationQueue(); } + public function getCapabilities(): array { return []; } + public function run(Io\Operation $op): Io\Completion + { + if ($op instanceof Io\Operation\GetAddrInfo) { + try { + $op->completeWithAddresses(["127.0.0.1\0garbage"]); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } + return $op->completeWithAddresses(['127.0.0.1']); + } + if ($op instanceof Io\Operation\GetNameInfo) { + try { + $op->completeWithName("host\0garbage"); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } + return $op->completeWithName('localhost'); + } + if ($op instanceof Io\Operation\Any) { + $member = $op->getOperations()[0]; + try { + $member->completeReady([]); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } + $this->queue->submit($op); + try { + $this->queue->cancel($member); + } catch (Io\IoException $e) { + echo get_class($e), "\n"; + } + $this->queue->cancel($op); + $c = $member->completeReady([Io\Poll\Event::Read]); + try { + $op->completeWith([$c, $c]); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } + return $op->completeWith([$c]); + } + return $op->complete(Io\CompletionStatus::Unsupported); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +$checks = new Checks(); +Io\Hooks\set_hooks($checks); +var_dump(gethostbyname('bridge-arguments.invalid')); +var_dump(gethostbyaddr('192.0.2.1')); +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +$r = [$a]; +$w = $e = null; +var_dump(stream_select($r, $w, $e, 5)); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +Io\Poll\OperationQueue::__construct(): Argument #1 ($context) must be of type ?Io\Poll\Context, stdClass given +Io\Operation\GetAddrInfo::completeWithAddresses(): Argument #1 ($addresses) must not contain any null bytes +string(9) "127.0.0.1" +Io\Operation\GetNameInfo::completeWithName(): Argument #1 ($host) must not contain any null bytes +string(9) "localhost" +Io\Operation::completeReady(): Argument #1 ($events) must not be empty +Io\IoException +Io\Operation\Any::completeWith(): Argument #1 ($completions) must not contain two completions of the same member +int(1) diff --git a/ext/standard/tests/streams/hooks/cancel-after-completion.phpt b/ext/standard/tests/streams/hooks/cancel-after-completion.phpt new file mode 100644 index 000000000000..3a37e961dcbd --- /dev/null +++ b/ext/standard/tests/streams/hooks/cancel-after-completion.phpt @@ -0,0 +1,71 @@ +--TEST-- +IO hooks: a cancellation that arrives after the completion leaves the bytes with the stream +--SKIPIF-- + +--FILE-- +queue->getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $c = $this->queue->submit($op); + if ($this->peer) { + fwrite($this->peer, "hello"); + $this->peer = null; + } + while ($c === null) { + foreach ($this->queue->waitCompletions() as $done) { + $c = $done; + } + } + // The exception is pending when run() returns the completion + $cancel = new Cancel(); + return $c; + } +} + +$queues = ['poll' => new Io\Poll\OperationQueue(), 'ring' => new Io\Ring\Engine()]; +foreach ($queues as $name => $queue) { + foreach (['buffered', 'unbuffered'] as $mode) { + echo "-- $name $mode --\n"; + [$r, $w] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + stream_set_timeout($r, 1); + if ($mode === 'unbuffered') { + stream_set_read_buffer($r, 0); + } + Io\Hooks\set_hooks(new CancelAfterCompletion($queue, $w)); + try { + var_dump(fread($r, 5)); + } catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; + } + Io\Hooks\set_hooks(null); + fwrite($w, " world"); + var_dump(fread($r, 11)); + } +} +?> +--EXPECT-- +-- poll buffered -- +cancelled +string(11) "hello world" +-- poll unbuffered -- +cancelled +string(11) "hello world" +-- ring buffered -- +cancelled +string(11) "hello world" +-- ring unbuffered -- +cancelled +bool(false) diff --git a/ext/standard/tests/streams/hooks/concurrent-access-internal.phpt b/ext/standard/tests/streams/hooks/concurrent-access-internal.phpt new file mode 100644 index 000000000000..fdea8014f9f8 --- /dev/null +++ b/ext/standard/tests/streams/hooks/concurrent-access-internal.phpt @@ -0,0 +1,29 @@ +--TEST-- +IO hooks: an internal consumer of a stream frozen by another fiber gets the concurrent access Error +--FILE-- +spawn(function () use ($a) { + $data = fread($a, 10); + echo "A read: $data\n"; +}); +$scheduler->spawn(function () use ($a, $b) { + try { + /* The source stream is fetched without the argument parsing check */ + file_put_contents('php://memory', $a); + } catch (Error $e) { + echo "B: ", $e->getMessage(), "\n"; + } + fwrite($b, "hello"); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +B: Concurrent access to a stream +A read: hello diff --git a/ext/standard/tests/streams/hooks/connect-direct-ready.phpt b/ext/standard/tests/streams/hooks/connect-direct-ready.phpt new file mode 100644 index 000000000000..51945a160852 --- /dev/null +++ b/ext/standard/tests/streams/hooks/connect-direct-ready.phpt @@ -0,0 +1,43 @@ +--TEST-- +IO hooks: a direct provider answering Ready to a Connect it did not perform gets a connected socket +--SKIPIF-- + +--FILE-- +ops[] = $op::class; + return $op instanceof Io\Operation\Connect + ? $op->completeReady($op->getEvents()) + : $op->complete(Io\CompletionStatus::Unsupported); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +$path = sys_get_temp_dir() . '/connect-direct-ready-' . getmypid() . '.sock'; +@unlink($path); +$server = stream_socket_server("unix://$path"); +$provider = new ReadyOnly(); +Io\Hooks\set_hooks($provider); +$client = stream_socket_client("unix://$path", $errno, $errstr, 5); +Io\Hooks\set_hooks(null); +var_dump(is_resource($client), $provider->ops); +$peer = stream_socket_accept($server, 5); +var_dump(fwrite($client, "ping"), fread($peer, 4)); +@unlink($path); +?> +--EXPECT-- +bool(true) +array(1) { + [0]=> + string(20) "Io\Operation\Connect" +} +int(4) +string(4) "ping" diff --git a/ext/standard/tests/streams/hooks/context-provider.inc b/ext/standard/tests/streams/hooks/context-provider.inc new file mode 100644 index 000000000000..2aa4b4eee3d7 --- /dev/null +++ b/ext/standard/tests/streams/hooks/context-provider.inc @@ -0,0 +1,88 @@ +context = new \Io\Poll\Context(); + } + + public function getCapabilities(): array { return []; } + public function add(\Io\Registration $registration): void {} + public function remove(\Io\Registration $registration): void {} + + public function spawn(callable $fn): void + { + $this->ready[] = [new \Fiber($fn), null]; + } + + private function await(array $pairs): array + { + $fiber = \Fiber::getCurrent(); + $watchers = []; + foreach ($pairs as $i => [$handle, $events]) { + $watchers[$i] = $this->context->add($handle, [...$events, \Io\Poll\Event::OneShot], [$fiber, $i]); + } + $this->waiting++; + [$i, $triggered] = \Fiber::suspend(); + // A fired one-shot watcher stays registered and disarmed, so every + // watcher of this await goes, or the next await on the handle is refused + foreach ($watchers as $w) { + if ($w->isActive()) { + $w->remove(); + } + } + return [$i, $triggered]; + } + + public function run(\Io\Operation $op): \Io\Completion + { + if ($op instanceof \Io\Operation\Any) { + $members = $op->getOperations(); + $pairs = array_map(fn ($m) => [$m->getHandle(), $m->getEvents()], $members); + [$i, $triggered] = $this->await($pairs); + return $op->completeWith([$members[$i]->completeReady($triggered)]); + } + if ($op->getHandle() === null) { + return $op->complete(\Io\CompletionStatus::Unsupported); + } + $pairs = [[$op->getHandle(), $op->getEvents()]]; + if (!$op instanceof \Io\Operation\Timer && $op->getTimeout() !== null) { + $pairs[] = [new \Io\Poll\TimerHandle($op->getTimeout()), [\Io\Poll\Event::Timer]]; + } + [$i, $triggered] = $this->await($pairs); + return $i === 0 + ? $op->completeReady($triggered) + : $op->complete(\Io\CompletionStatus::Timeout); + } + + public function loop(): void + { + while ($this->ready || $this->waiting > 0) { + while ($this->ready) { + [$fiber, $value] = array_shift($this->ready); + $fiber->isStarted() ? $fiber->resume($value) : $fiber->start(); + } + if ($this->waiting === 0) { + break; + } + $resumed = []; + foreach ($this->context->wait() as $w) { + [$fiber, $i] = $w->getData(); + if (isset($resumed[spl_object_id($fiber)])) { + continue; + } + $resumed[spl_object_id($fiber)] = true; + $this->waiting--; + $this->ready[] = [$fiber, [$i, $w->getTriggeredEvents()]]; + } + } + } +} + diff --git a/ext/standard/tests/streams/hooks/context-provider.phpt b/ext/standard/tests/streams/hooks/context-provider.phpt new file mode 100644 index 000000000000..eeb3037c716d --- /dev/null +++ b/ext/standard/tests/streams/hooks/context-provider.phpt @@ -0,0 +1,46 @@ +--TEST-- +IO hooks: a provider on its own Poll context, timers included +--FILE-- +spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + usleep(20000); + fwrite($conn, "hello\n"); + fclose($conn); + echo "server done\n"; +}); +$provider->spawn(function () use ($addr) { + $c = stream_socket_client("tcp://$addr"); + $line = trim(fgets($c)); + echo "client got: $line\n"; +}); +$provider->spawn(function () { + usleep(5000); + echo "slept\n"; +}); +$provider->spawn(function () use ($addr) { + // A read that times out is a Timeout completion from the timer pair + $c = stream_socket_client("tcp://$addr"); + stream_set_timeout($c, 0, 50000); + var_dump(fgets($c)); + var_dump(stream_get_meta_data($c)['timed_out']); +}); + +$provider->loop(); +echo "done\n"; +?> +--EXPECT-- +slept +server done +client got: hello +bool(false) +bool(true) +done diff --git a/ext/standard/tests/streams/hooks/copy-to-stream.phpt b/ext/standard/tests/streams/hooks/copy-to-stream.phpt new file mode 100644 index 000000000000..c18141d2f94a --- /dev/null +++ b/ext/standard/tests/streams/hooks/copy-to-stream.phpt @@ -0,0 +1,40 @@ +--TEST-- +IO hooks: stream_copy_to_stream() from a socket waits through the provider +--SKIPIF-- + +--FILE-- +spawn(function () use ($a, $out, $file) { + echo "copy start\n"; + var_dump(stream_copy_to_stream($a, $file, 5)); + var_dump(stream_copy_to_stream($a, $out, 5)); + rewind($file); + rewind($out); + var_dump(stream_get_contents($file), stream_get_contents($out)); +}); +$s->spawn(function () use ($b) { + echo "writer runs while the copy waits\n"; + fwrite($b, "hello"); + usleep(1000); + fwrite($b, "world"); +}); +$s->loop(); +?> +--EXPECT-- +copy start +writer runs while the copy waits +int(5) +int(5) +string(5) "hello" +string(5) "world" diff --git a/ext/standard/tests/streams/hooks/data-ops-cancel.phpt b/ext/standard/tests/streams/hooks/data-ops-cancel.phpt new file mode 100644 index 000000000000..dd9d75f7e6b6 --- /dev/null +++ b/ext/standard/tests/streams/hooks/data-ops-cancel.phpt @@ -0,0 +1,126 @@ +--TEST-- +IO hooks: a provider that throws from run() cancels the operation, and a fiber destroyed while suspended in a read leaves the stream usable +--FILE-- +getMessage(), "\n"; +} +// The stream is not frozen afterwards +fwrite($w, "x"); +Io\Hooks\set_hooks(null); +var_dump(fread($r, 10)); + +// 2. A fiber destroyed while suspended in fread(): the operation is orphaned. +// This provider keeps the fibers itself, so dropping one really destroys it. +final class Dropping implements Io\Hooks\Hooks +{ + private array $fibers = []; + private array $ready = []; + private int $inFlight = 0; + public ?int $victim = null; + + public function __construct(private Io\OperationQueue $queue) {} + public function getCapabilities(): array { return Scheduler::capabilities($this->queue); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + + public function run(Io\Operation $op): Io\Completion + { + $fiber = Fiber::getCurrent(); + $id = spl_object_id($fiber); + $this->fibers[$id] = $fiber; + if ($op instanceof Io\Operation\Recv && $this->victim === null) { + $this->victim = $id; + } + $c = $this->queue->submit($op, $id); + if ($c !== null) { + return $c; + } + $this->inFlight++; + unset($fiber); + return Fiber::suspend(); + } + + public function drop(): void + { + unset($this->fibers[$this->victim]); + } + + public function spawn(callable $fn): void { $this->ready[] = [new Fiber($fn), null]; } + + public function loop(): void + { + // countPending() rather than a counter of its own: an operation whose + // fiber was destroyed is cancelled without a completion for anyone + while ($this->ready || $this->queue->countPending() > 0) { + while ($this->ready) { + [$fiber, $value] = array_shift($this->ready); + $fiber->isStarted() ? $fiber->resume($value) : $fiber->start(); + unset($fiber); + } + if ($this->queue->countPending() === 0) break; + foreach ($this->queue->waitCompletions() as $c) { + $this->inFlight--; + $id = $c->getData(); + if (isset($this->fibers[$id])) { + $this->ready[] = [$this->fibers[$id], $c]; + unset($this->fibers[$id]); + } + } + } + } +} + +$scheduler = new Dropping(Scheduler::defaultQueue()); +Io\Hooks\set_hooks($scheduler); +$scheduler->spawn(function () use ($r) { + fread($r, 10); + echo "read returned\n"; +}); +$scheduler->spawn(function () use ($scheduler, $r, $w) { + usleep(5000); + // Drop the only reference to the suspended fiber: its destructor unwinds it + $scheduler->drop(); + // A freeze the queue kept for the orphan is lifted once the operation settled + $tries = 0; + while (true) { + try { + stream_get_meta_data($r); + break; + } catch (Error $e) { + if (++$tries > 200) { echo "still frozen\n"; break; } + usleep(1000); + } + } + fwrite($w, "after"); + echo "writer done\n"; +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +stream_set_blocking($r, false); +var_dump(fread($r, 10)); +fclose($r); +fclose($w); +echo "done\n"; +?> +--EXPECT-- +cancelled Io\Operation\Recv +string(1) "x" +writer done +string(5) "after" +done diff --git a/ext/standard/tests/streams/hooks/data-ops-fork.phpt b/ext/standard/tests/streams/hooks/data-ops-fork.phpt new file mode 100644 index 000000000000..c3bca5b4d7bf --- /dev/null +++ b/ext/standard/tests/streams/hooks/data-ops-fork.phpt @@ -0,0 +1,36 @@ +--TEST-- +IO hooks: pcntl_fork() refuses while an operation is in flight +--EXTENSIONS-- +pcntl +--FILE-- +getMessage(), "\n"; + } + return $op->complete(Io\CompletionStatus::Timeout); + } +}); +var_dump(fread($r, 10)); +Io\Hooks\set_hooks(null); + +// Outside an operation forking works +$pid = pcntl_fork(); +if ($pid === 0) { + exit(0); +} +pcntl_waitpid($pid, $status); +var_dump($pid > 0); +?> +--EXPECT-- +Cannot fork while IO operations are in flight +bool(false) +bool(true) diff --git a/ext/standard/tests/streams/hooks/data-ops-shutdown.phpt b/ext/standard/tests/streams/hooks/data-ops-shutdown.phpt new file mode 100644 index 000000000000..a2d6ee1e5f1d --- /dev/null +++ b/ext/standard/tests/streams/hooks/data-ops-shutdown.phpt @@ -0,0 +1,22 @@ +--TEST-- +IO hooks: request shutdown with a fiber suspended in a read +--FILE-- +start(); // suspends inside fread() with the operation submitted +echo "suspended: ", var_export($fiber->isSuspended(), true), "\n"; +echo "done\n"; +// The fiber, the streams and the queue are destroyed at shutdown, in that order +?> +--EXPECT-- +suspended: true +done diff --git a/ext/standard/tests/streams/hooks/data-ops.phpt b/ext/standard/tests/streams/hooks/data-ops.phpt new file mode 100644 index 000000000000..ea66bf3021eb --- /dev/null +++ b/ext/standard/tests/streams/hooks/data-ops.phpt @@ -0,0 +1,63 @@ +--TEST-- +IO hooks: socket reads, writes, accepts and connects reach the provider as data operations +--FILE-- +seen[$op::class] = ($this->seen[$op::class] ?? 0) + 1; + $check = fn (bool $ok, string $what) => $ok || $this->problems[] = $op::class . ': ' . $what; + if ($op instanceof \Io\Operation\Recv || $op instanceof \Io\Operation\Send) { + $check($op->getLength() > 0, 'length'); + $check($op->getFlags() === 0, 'flags'); + $check($op->getHandle() instanceof \Io\Poll\WeakHandle, 'handle'); + $check($op->getEvents() === [$op instanceof \Io\Operation\Recv ? \Io\Poll\Event::Read : \Io\Poll\Event::Write], 'events'); + } elseif ($op instanceof \Io\Operation\Accept) { + $check($op->getHandle() instanceof \Io\Poll\WeakHandle, 'handle'); + $check($op->getEvents() === [\Io\Poll\Event::Read], 'events'); + $check($op->getTimeout() !== null, 'timeout'); + } elseif ($op instanceof \Io\Operation\Connect) { + $check(str_starts_with($op->getAddress(), '127.0.0.1:'), 'address ' . $op->getAddress()); + $check($op->getEvents() === [\Io\Poll\Event::Write], 'events'); + } + return parent::run($op); + } +} + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + // Read before the client writes, so the read has to wait + $line = fgets($conn); + fwrite($conn, strtoupper($line)); + fclose($conn); +}); +$scheduler->spawn(function () use ($addr) { + $c = stream_socket_client("tcp://$addr"); + usleep(10000); + fwrite($c, "hello\n"); + echo fgets($c); + fclose($c); +}); +$scheduler->loop(); + +var_dump(isset($scheduler->seen[Io\Operation\Accept::class]), isset($scheduler->seen[Io\Operation\Recv::class])); +var_dump($scheduler->problems); +?> +--EXPECT-- +HELLO +bool(true) +bool(true) +array(0) { +} diff --git a/ext/standard/tests/streams/hooks/dns-ops.phpt b/ext/standard/tests/streams/hooks/dns-ops.phpt new file mode 100644 index 000000000000..322aeba7e089 --- /dev/null +++ b/ext/standard/tests/streams/hooks/dns-ops.phpt @@ -0,0 +1,91 @@ +--TEST-- +IO hooks: name lookups reach the provider as GetAddrInfo and GetNameInfo, answered from userland or by the library +--FILE-- +seen[] = 'GetAddrInfo ' . $op->getHost() . ' ' . var_export($op->getService(), true) + . ' ' . var_export($op->getHandle(), true) . ' ' . var_export($op->getTimeout(), true); + if ($this->unsupported) { + return $op->complete(\Io\CompletionStatus::Unsupported); + } + return $op->completeWithAddresses($this->answer === null ? [] : [$this->answer]); + } + if ($op instanceof \Io\Operation\GetNameInfo) { + $this->seen[] = 'GetNameInfo ' . $op->getAddress(); + if ($this->unsupported) { + return $op->complete(\Io\CompletionStatus::Unsupported); + } + return $op->completeWithName($this->name); + } + return parent::run($op); + } +} + +$resolver = new Resolver(); +Io\Hooks\set_hooks($resolver); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$port = (int) substr(strrchr(stream_socket_get_name($server, false), ':'), 1); + +// 1. A userland answer: the name is whatever the provider says +$resolver->answer = '127.0.0.1'; +$resolver->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + fwrite($conn, "hi\n"); + fclose($conn); +}); +$resolver->spawn(function () use ($port) { + $c = stream_socket_client("tcp://resolved.by.provider:$port", $errno, $errstr, 5); + var_dump($c !== false, fgets($c)); +}); +$resolver->loop(); + +// 2. No address: the lookup fails +$resolver->answer = null; +$c = @stream_socket_client("tcp://nowhere.provider:$port", $errno, $errstr, 1); +var_dump($c, $errno); + +// 3. Unsupported: the library resolves it +$resolver->unsupported = true; +var_dump(gethostbyname('localhost')); +var_dump(gethostbynamel('localhost') !== false); + +// 4. Reverse lookups +$resolver->unsupported = false; +$resolver->name = 'named.by.provider'; +var_dump(gethostbyaddr('127.0.0.1')); +$resolver->unsupported = true; +var_dump(is_string(gethostbyaddr('127.0.0.1'))); + +Io\Hooks\set_hooks(null); +foreach ($resolver->seen as $line) { + echo $line, "\n"; +} +?> +--EXPECTF-- +bool(true) +string(3) "hi +" +bool(false) +int(%d) +string(9) "127.0.0.1" +bool(true) +string(17) "named.by.provider" +bool(true) +GetAddrInfo resolved.by.provider NULL NULL NULL +GetAddrInfo nowhere.provider NULL NULL NULL +GetAddrInfo localhost NULL NULL NULL +GetAddrInfo localhost NULL NULL NULL +GetNameInfo 127.0.0.1:0 +GetNameInfo 127.0.0.1:0 diff --git a/ext/standard/tests/streams/hooks/file-ops.phpt b/ext/standard/tests/streams/hooks/file-ops.phpt new file mode 100644 index 000000000000..7bb506401d24 --- /dev/null +++ b/ext/standard/tests/streams/hooks/file-ops.phpt @@ -0,0 +1,69 @@ +--TEST-- +IO hooks: regular file reads, writes and fsync reach a provider with the Files capability only +--FILE-- +seen[$op::class] = true; + if ($op instanceof \Io\Operation\Read || $op instanceof \Io\Operation\Write) { + /* -1 is the current position; a Windows overlapped file keeps none, so the + * wrapper passes the offset it tracks */ + $offsetOk = PHP_OS_FAMILY === 'Windows' ? $op->getOffset() >= 0 : $op->getOffset() === -1; + if ($op->getLength() <= 0 || !$offsetOk) { + $this->problems[] = $op::class; + } + } elseif ($op instanceof \Io\Operation\Fsync) { + if ($op->getHandle() === null || $op->getTimeout() !== null) { + $this->problems[] = $op::class; + } + $this->seen['dataonly=' . var_export($op->isDataOnly(), true)] = true; + } + return parent::run($op); + } +} + +$scheduler = new Tracing(null, [Io\Hooks\Capability::Files]); +Io\Hooks\set_hooks($scheduler); +$files = in_array(Io\Hooks\Capability::Files, $scheduler->getCapabilities(), true); + +$file = tempnam(sys_get_temp_dir(), 'iohooks'); +$scheduler->spawn(function () use ($file) { + $data = str_repeat("line of text\n", 20000); + $f = fopen($file, 'w'); + fwrite($f, $data); + fsync($f); + fdatasync($f); + fclose($f); + $f = fopen($file, 'r'); + $read = ''; + while (!feof($f)) { + $read .= fread($f, 65536); + } + fclose($f); + var_dump($read === $data); +}); +$scheduler->loop(); +unlink($file); + +// Without Files the file operations stay synchronous and never reach the provider +$seen = array_keys($scheduler->seen); +sort($seen); +$expected = $files + ? [Io\Operation\Fsync::class, Io\Operation\Read::class, Io\Operation\Write::class, 'dataonly=false', 'dataonly=true'] + : []; +sort($expected); +var_dump($seen === $expected); +var_dump($scheduler->problems); +?> +--EXPECT-- +bool(true) +bool(true) +array(0) { +} diff --git a/ext/standard/tests/streams/hooks/handle-exposure.phpt b/ext/standard/tests/streams/hooks/handle-exposure.phpt new file mode 100644 index 000000000000..e12a37519d75 --- /dev/null +++ b/ext/standard/tests/streams/hooks/handle-exposure.phpt @@ -0,0 +1,83 @@ +--TEST-- +IO hooks: a stream's handle is one object, created on demand, exposed only for a stream a script holds +--FILE-- +getHandle(); + if ($handle instanceof StreamPollWeakHandle) { + $key = $handle->isExposed() ? 'exposed' : 'internal'; + // Same object for the same stream, never the stream while the op runs + $this->seen[$key] = ($this->seen[$key] ?? true) + && $op->getHandle() === $handle && $handle->getStream() === null && $handle->isValid(); + if (!$handle->isExposed()) { + $this->inner = $handle; + } + } + return parent::run($op); + } +} + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + +// The factory returns the stream's one handle, exposed +$h = StreamPollWeakHandle::create($a); +var_dump($h === StreamPollWeakHandle::create($a), $h->isExposed(), $h->isValid(), $h->getStream() === $a); + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($a, $h, $server) { + // The op on $a carries $h; the ops on the wrapper's inner socket carry an internal handle + var_dump(fread($a, 10)); + $conn = stream_socket_accept($server, 5); + while (!str_ends_with($request ?? '', "\r\n\r\n")) { + $request = ($request ?? '') . fread($conn, 1024); + } + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok"); + fclose($conn); +}); +$scheduler->spawn(function () use ($b, $addr) { + fwrite($b, "hello"); + var_dump(file_get_contents("http://$addr/")); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); + +var_dump($scheduler->seen); +// The inner socket is closed: its handle answers invalid and hands nothing out +var_dump($scheduler->inner->isValid(), $scheduler->inner->isExposed(), $scheduler->inner->getStream()); + +// The handle outlives its stream as an invalid one +fclose($a); +var_dump($h->isValid(), $h->getStream(), $h->isExposed()); +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) +bool(true) +string(5) "hello" +string(2) "ok" +array(2) { + ["exposed"]=> + bool(true) + ["internal"]=> + bool(true) +} +bool(false) +bool(false) +NULL +bool(false) +NULL +bool(true) diff --git a/ext/standard/tests/streams/hooks/hook-close-fgets.phpt b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt new file mode 100644 index 000000000000..e42d6240193e --- /dev/null +++ b/ext/standard/tests/streams/hooks/hook-close-fgets.phpt @@ -0,0 +1,32 @@ +--TEST-- +IO hooks: fclose() from run() throws the concurrent access error +--FILE-- +getHandle()->getStream()); + fclose($this->stream); + return $op->completeReady($op->getEvents()); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +Io\Hooks\set_hooks(new CloseOnceHooks($client)); +try { + fgets($client); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} +?> +--EXPECT-- +NULL +Concurrent access to a stream diff --git a/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt new file mode 100644 index 000000000000..a2acd4235d07 --- /dev/null +++ b/ext/standard/tests/streams/hooks/hook-concurrent-access.phpt @@ -0,0 +1,47 @@ +--TEST-- +IO hooks: concurrent stream access from run() is rejected, the read arrives as a Recv +--FILE-- +getHandle()->getStream()); + try { + fgets($this->stream); + } catch (Error $e) { + echo $e->getMessage() . "\n"; + } + var_dump($op instanceof Io\Operation\Recv, $op->getEvents(), $op->getTimeout()); + return $op->complete(Io\CompletionStatus::Timeout); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +Io\Hooks\set_hooks(new ConcurrentHook($client)); +stream_set_timeout($client, 3); +var_dump(fgets($client)); +var_dump(stream_get_meta_data($client)['timed_out']); +?> +--EXPECTF-- +NULL +Concurrent access to a stream +bool(true) +array(1) { + [0]=> + enum(Io\Poll\Event::Read) +} +object(Time\Duration)#%d (%d) { + ["seconds"]=> + int(%d) + ["nanoseconds"]=> + int(%d) +%A} +bool(false) +bool(true) diff --git a/ext/standard/tests/streams/hooks/nonblocking-pipe.phpt b/ext/standard/tests/streams/hooks/nonblocking-pipe.phpt new file mode 100644 index 000000000000..2a7431b579d2 --- /dev/null +++ b/ext/standard/tests/streams/hooks/nonblocking-pipe.phpt @@ -0,0 +1,36 @@ +--TEST-- +IO hooks: a read on a non-blocking pipe is the plain syscall, never a wait +--SKIPIF-- + +--FILE-- +ops[] = $op::class; + return $op->complete(Io\CompletionStatus::Timeout); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +$proc = proc_open([PHP_BINARY, '-n', '-r', 'fgets(STDIN);'], [0 => ['pipe', 'r'], 1 => ['pipe', 'w']], $pipes); +stream_set_blocking($pipes[1], false); +$provider = new Recording(); +Io\Hooks\set_hooks($provider); +var_dump(fread($pipes[1], 100)); +Io\Hooks\set_hooks(null); +var_dump($provider->ops); +fclose($pipes[0]); +fclose($pipes[1]); +proc_close($proc); +?> +--EXPECT-- +string(0) "" +array(0) { +} diff --git a/ext/standard/tests/streams/hooks/operation-final.phpt b/ext/standard/tests/streams/hooks/operation-final.phpt new file mode 100644 index 000000000000..482e27c62ed9 --- /dev/null +++ b/ext/standard/tests/streams/hooks/operation-final.phpt @@ -0,0 +1,8 @@ +--TEST-- +IO hooks: Io\Operation cannot be extended in userland +--FILE-- + +--EXPECTF-- +Fatal error: Class MyOperation cannot extend final class Io\Operation in %s on line %d diff --git a/ext/standard/tests/streams/hooks/overlapped-file-casts.phpt b/ext/standard/tests/streams/hooks/overlapped-file-casts.phpt new file mode 100644 index 000000000000..3cf5c461b619 --- /dev/null +++ b/ext/standard/tests/streams/hooks/overlapped-file-casts.phpt @@ -0,0 +1,54 @@ +--TEST-- +Windows overlapped files: the FILE* and descriptor casts are served from a synchronous reopen +--EXTENSIONS-- +curl +--SKIPIF-- + +--ENV-- +PHP_IO_OVERLAPPED_FILES=1 +--FILE-- + $in, 1 => $childOut], $pipes); +var_dump(proc_close($proc)); +fclose($in); +fclose($childOut); +var_dump(file_get_contents("$dir/child.txt")); + +// A select on a file: always ready +$f = fopen("$dir/source.txt", 'rb'); +$r = [$f]; $w = $e = null; +var_dump(stream_select($r, $w, $e, 0), fread($f, 4)); +fclose($f); + +unlink("$dir/source.txt"); +unlink("$dir/curl.txt"); +unlink("$dir/child.txt"); +rmdir($dir); +?> +--EXPECT-- +bool(true) +bool(true) +int(0) +string(10) "9876543210" +int(1) +string(4) "0123" diff --git a/ext/standard/tests/streams/hooks/pipe-mode-fork.phpt b/ext/standard/tests/streams/hooks/pipe-mode-fork.phpt new file mode 100644 index 000000000000..6e7d62f2e66e --- /dev/null +++ b/ext/standard/tests/streams/hooks/pipe-mode-fork.phpt @@ -0,0 +1,45 @@ +--TEST-- +IO hooks: a forked child closing its copy of a pipe leaves the parent's descriptor non-blocking +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => ['pipe', 'w']], $pipes); +stream_set_read_buffer($pipes[1], 0); +$probe = pipe_mode_probe($pipes[1]); + +var_dump(pipe_mode_read_hooked($pipes[1], 1)); +echo "switched: ", pipe_mode_ask($probe), "\n"; + +$pid = pcntl_fork(); +if ($pid === 0) { + fclose($pipes[1]); + exit(0); +} +pcntl_waitpid($pid, $status); +echo "child closed: ", pipe_mode_ask($probe), "\n"; + +var_dump(pipe_mode_read_hooked($pipes[1], 1)); +fwrite($pipes[0], "go\n"); +var_dump(fread($pipes[1], 1)); + +fclose($pipes[1]); +echo "parent closed: ", pipe_mode_ask($probe), "\n"; + +pipe_mode_probe_close($probe); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECT-- +string(1) "A" +switched: 0 +child closed: 0 +string(1) "B" +string(1) "C" +parent closed: 1 diff --git a/ext/standard/tests/streams/hooks/pipe-mode-proc-open-handover.phpt b/ext/standard/tests/streams/hooks/pipe-mode-proc-open-handover.phpt new file mode 100644 index 000000000000..375a841ed38c --- /dev/null +++ b/ext/standard/tests/streams/hooks/pipe-mode-proc-open-handover.phpt @@ -0,0 +1,35 @@ +--TEST-- +IO hooks: a pipe handed to a proc_open child is blocking again once PHP closed its end +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => ['pipe', 'w']], $wpipes); +stream_set_read_buffer($wpipes[1], 0); +var_dump(pipe_mode_read_hooked($wpipes[1], 4)); + +// Reads its stdin, the writer's output, once the gate opens +$reader = proc_open([PHP_BINARY, '-n', '-r', 'fgets(fopen("php://fd/3", "r")); var_dump(fgets(STDIN));'], + [0 => $wpipes[1], 1 => ['pipe', 'w'], 3 => ['pipe', 'r']], $rpipes); +fclose($wpipes[1]); +fwrite($rpipes[3], "go\n"); +usleep(50000); +fwrite($wpipes[0], "go\n"); +echo stream_get_contents($rpipes[1]); + +fclose($rpipes[3]); +fclose($rpipes[1]); +proc_close($reader); +fclose($wpipes[0]); +proc_close($writer); +?> +--EXPECT-- +string(4) "one +" +string(4) "two +" diff --git a/ext/standard/tests/streams/hooks/pipe-mode-ring.phpt b/ext/standard/tests/streams/hooks/pipe-mode-ring.phpt new file mode 100644 index 000000000000..198ec8a3e51b --- /dev/null +++ b/ext/standard/tests/streams/hooks/pipe-mode-ring.phpt @@ -0,0 +1,38 @@ +--TEST-- +IO hooks: a blocking pipe the ring saw stays blocking without a provider and is given back blocking +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => ['pipe', 'w']], $pipes); +stream_set_read_buffer($pipes[1], 0); +$probe = pipe_mode_probe($pipes[1]); +echo "before: ", pipe_mode_ask($probe), "\n"; + +var_dump(pipe_mode_read_hooked($pipes[1], 1, new Io\Ring\Engine())); +echo "switched: ", pipe_mode_ask($probe), "\n"; + +var_dump(stream_get_meta_data($pipes[1])['blocked']); +fwrite($pipes[0], "go\n"); +var_dump(fread($pipes[1], 1)); + +fclose($pipes[1]); +echo "closed: ", pipe_mode_ask($probe), "\n"; + +pipe_mode_probe_close($probe); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECT-- +before: 1 +string(1) "A" +switched: 0 +bool(true) +string(1) "B" +closed: 1 diff --git a/ext/standard/tests/streams/hooks/pipe-mode-user-nonblocking.phpt b/ext/standard/tests/streams/hooks/pipe-mode-user-nonblocking.phpt new file mode 100644 index 000000000000..7eeaaa420fed --- /dev/null +++ b/ext/standard/tests/streams/hooks/pipe-mode-user-nonblocking.phpt @@ -0,0 +1,67 @@ +--TEST-- +IO hooks: a pipe the script made non-blocking stays non-blocking and never waits +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes); +stream_set_read_buffer($pipes[1], 0); +stream_set_read_buffer($pipes[2], 0); +$probe1 = pipe_mode_probe($pipes[1]); +$probe2 = pipe_mode_probe($pipes[2]); + +echo "-- switched by the provider, then made non-blocking\n"; +var_dump(pipe_mode_read_hooked($pipes[1], 1)); +var_dump(stream_set_blocking($pipes[1], false)); +var_dump(stream_get_meta_data($pipes[1])['blocked']); +echo "kernel: ", pipe_mode_ask($probe1), "\n"; +var_dump(pipe_mode_read_hooked($pipes[1], 1)); +var_dump(fread($pipes[1], 1)); + +echo "-- non-blocking before any provider\n"; +var_dump(stream_set_blocking($pipes[2], false)); +var_dump(pipe_mode_read_hooked($pipes[2], 1)); +var_dump(stream_get_meta_data($pipes[2])['blocked']); +var_dump(fread($pipes[2], 1)); + +echo "-- blocking again\n"; +var_dump(stream_set_blocking($pipes[2], true)); +var_dump(stream_get_meta_data($pipes[2])['blocked']); +echo "kernel: ", pipe_mode_ask($probe2), "\n"; +var_dump(stream_set_blocking($pipes[2], false)); + +echo "-- the script's mode is kept at close\n"; +fclose($pipes[1]); +fclose($pipes[2]); +echo "kernel: ", pipe_mode_ask($probe1), " ", pipe_mode_ask($probe2), "\n"; + +pipe_mode_probe_close($probe1); +pipe_mode_probe_close($probe2); +fclose($pipes[0]); +proc_close($proc); +?> +--EXPECT-- +-- switched by the provider, then made non-blocking +string(1) "A" +bool(true) +bool(false) +kernel: 0 +string(0) "" +string(0) "" +-- non-blocking before any provider +bool(true) +string(0) "" +bool(false) +string(0) "" +-- blocking again +bool(true) +bool(true) +kernel: 1 +bool(true) +-- the script's mode is kept at close +kernel: 0 0 diff --git a/ext/standard/tests/streams/hooks/pipe-mode.inc b/ext/standard/tests/streams/hooks/pipe-mode.inc new file mode 100644 index 000000000000..9fa4577c6bd1 --- /dev/null +++ b/ext/standard/tests/streams/hooks/pipe-mode.inc @@ -0,0 +1,37 @@ + $stream, 1 => ['pipe', 'w'], 3 => ['pipe', 'r']], $pipes); + return [$proc, $pipes[3], $pipes[1]]; +} + +function pipe_mode_ask(array $probe): string +{ + fwrite($probe[1], "?\n"); + return trim(fgets($probe[2])); +} + +function pipe_mode_probe_close(array $probe): void +{ + fclose($probe[1]); + fclose($probe[2]); + proc_close($probe[0]); +} + +/* Reads through a provider, which switches the pipe to non-blocking */ +function pipe_mode_read_hooked($stream, int $len, ?Io\OperationQueue $queue = null): string|false +{ + $s = new Scheduler($queue); + $ret = null; + Io\Hooks\set_hooks($s); + $s->spawn(function () use ($stream, $len, &$ret) { $ret = fread($stream, $len); }); + $s->loop(); + Io\Hooks\set_hooks(null); + return $ret; +} diff --git a/ext/standard/tests/streams/hooks/poll-queue-retained-hangup.phpt b/ext/standard/tests/streams/hooks/poll-queue-retained-hangup.phpt new file mode 100644 index 000000000000..c0dc8061423b --- /dev/null +++ b/ext/standard/tests/streams/hooks/poll-queue-retained-hangup.phpt @@ -0,0 +1,58 @@ +--TEST-- +IO hooks: a retained registration nobody waits on does not report the peer's hangup +--FILE-- +added[] = [$registration->getEvent(), $registration->getTrigger()]; + $this->q->add($registration); + } + public function remove(Io\Registration $registration): void { $this->q->remove($registration); } + public function run(Io\Operation $op): Io\Completion { + if ($this->runs++ == 0) { + fwrite($this->b, "x"); + } + // Answered from the record at submit, or by the wait + $c = $this->q->submit($op); + if ($c === null) { + foreach ($this->q->waitCompletions() as $c) {} + } + return $c; + } +}; +Io\Hooks\set_hooks($h); +var_dump(fread($a, 10)); +// Uninstalling ends the registration without remove(): the queue keeps the record +Io\Hooks\set_hooks(null); +var_dump($h->runs > 0, $h->added); +fclose($b); + +$cpu = function () { + $r = getrusage(); + return $r['ru_utime.tv_sec'] + $r['ru_utime.tv_usec'] / 1e6 + $r['ru_stime.tv_sec'] + $r['ru_stime.tv_usec'] / 1e6; +}; +$before = $cpu(); +var_dump(count($q->waitCompletions(Time\Duration::fromMilliseconds(500)))); +// Waiting on a hangup nobody asked about spun for the whole wait +var_dump($cpu() - $before < 0.25); +?> +--EXPECT-- +string(1) "x" +bool(true) +array(1) { + [0]=> + array(2) { + [0]=> + enum(Io\Poll\Event::Read) + [1]=> + enum(Io\Poll\Trigger::Level) + } +} +int(0) +bool(true) diff --git a/ext/standard/tests/streams/hooks/process-handle-windows.phpt b/ext/standard/tests/streams/hooks/process-handle-windows.phpt new file mode 100644 index 000000000000..752f64687ab5 --- /dev/null +++ b/ext/standard/tests/streams/hooks/process-handle-windows.phpt @@ -0,0 +1,38 @@ +--TEST-- +Io\Poll\ProcessHandle on Windows: a process object that answers the exit code, no source to poll +--SKIPIF-- + +--FILE-- +getPid() === proc_get_status($proc)['pid'], $handle->getStatus()); + +// No source for the poll queue: the context refuses it +$ctx = new Io\Poll\Context(); +try { + $ctx->add($handle, [Io\Poll\Event::Process]); +} catch (Io\Poll\FailedHandleAddException $e) { + echo get_class($e), ": ", $e->getMessage(), "\n"; +} + +// The exit code once the process ended, whoever waited for it +var_dump(proc_close($proc)); +var_dump($handle->getStatus()); + +// A pid nobody runs under +try { + new Io\Poll\ProcessHandle(0x7FFFFFFE); +} catch (ValueError $e) { + echo get_class($e), ": ", substr($e->getMessage(), 0, 72), "\n"; +} +?> +--EXPECTF-- +bool(true) +NULL +Io\Poll\FailedHandleAddException: This backend has no source for the handle +int(5) +int(5) +ValueError: Io\Poll\ProcessHandle::__construct(): Argument #1 ($pid) must be t%s diff --git a/ext/standard/tests/streams/hooks/process-ops-context.phpt b/ext/standard/tests/streams/hooks/process-ops-context.phpt new file mode 100644 index 000000000000..5e4318acccb5 --- /dev/null +++ b/ext/standard/tests/streams/hooks/process-ops-context.phpt @@ -0,0 +1,75 @@ +--TEST-- +IO hooks: WaitPid and SigWait through their handles on a provider's own Poll context +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- +supportsProcessHandles()) die("skip no process handle source"); +if (!Io\Poll\Backend::Auto->supportsSignalHandles()) die("skip no signal handle source"); +if (!function_exists("pcntl_sigwaitinfo")) die("skip pcntl_sigwaitinfo() is not available"); +?> +--FILE-- + $e->name, $op->getEvents()); + $this->seen[] = substr(get_class($op), 13) . ':' . get_class($op->getHandle()) . ':' . implode(',', $events); + } + return parent::run($op); + } +} + +$provider = new Tracing(); +\Io\Hooks\set_hooks($provider); +pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGUSR2]); + +$provider->spawn(function () { + // The context observes the exit through the ProcessHandle; proc_close() + // collects the child after Ready. It exits well after the timed wait + // below runs out, so the output order is fixed. + $proc = proc_open(['/bin/sh', '-c', 'sleep 0.5; exit 6'], [], $pipes); + var_dump(proc_close($proc)); +}); +$provider->spawn(function () { + // The context consumes the signal through the SignalHandle; the wait + // gets the recorded info + $signo = pcntl_sigwaitinfo([SIGUSR1], $info); + var_dump($signo === SIGUSR1, $info['signo'] === SIGUSR1, $info['pid'] === posix_getpid()); +}); +$provider->spawn(function () { + // A timed wait that runs out is the timer pair's Timeout + var_dump(pcntl_sigtimedwait([SIGUSR2], $info, 0, 100000000)); +}); +$provider->spawn(function () { + usleep(30000); + posix_kill(posix_getpid(), SIGUSR1); + echo "sent\n"; +}); + +$provider->loop(); +var_dump($provider->seen); +?> +--EXPECT-- +sent +bool(true) +bool(true) +bool(true) +bool(false) +int(6) +array(3) { + [0]=> + string(37) "WaitPid:Io\Poll\ProcessHandle:Process" + [1]=> + string(35) "SigWait:Io\Poll\SignalHandle:Signal" + [2]=> + string(35) "SigWait:Io\Poll\SignalHandle:Signal" +} diff --git a/ext/standard/tests/streams/hooks/process-ops-proc-open.phpt b/ext/standard/tests/streams/hooks/process-ops-proc-open.phpt new file mode 100644 index 000000000000..77e904b69773 --- /dev/null +++ b/ext/standard/tests/streams/hooks/process-ops-proc-open.phpt @@ -0,0 +1,66 @@ +--TEST-- +IO hooks: proc_close() waits with a WaitPid operation, so waits for several children overlap +--SKIPIF-- + +--FILE-- +seen[] = ($op->getPid() > 0 ? "pid" : "any") . ":" + . ($op->getHandle() === null ? "null" : get_class($op->getHandle())); + } + return parent::run($op); + } +} + +// One child alone, for the bound below: a slow build (ASAN) starts php slowly +$start = hrtime(true); +$proc = proc_open([PHP_BINARY, '-n', '-r', "usleep(300000); exit(0);"], [], $pipes); +proc_close($proc); +$one = (hrtime(true) - $start) / 1e9; + +$scheduler = new Counting(); +Io\Hooks\set_hooks($scheduler); +$results = []; +$start = hrtime(true); + +foreach ([7, 8] as $code) { + $scheduler->spawn(function () use (&$results, $code) { + $proc = proc_open([PHP_BINARY, '-n', '-r', "usleep(300000); exit($code);"], [], $pipes); + $results[$code] = proc_close($proc); + }); +} +$scheduler->loop(); +ksort($results); +var_dump($results); +// The two waits overlapped instead of adding up to twice one child +var_dump((hrtime(true) - $start) / 1e9 < 1.6 * $one); +var_dump($scheduler->seen); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +array(2) { + [7]=> + int(7) + [8]=> + int(8) +} +bool(true) +array(2) { + [0]=> + string(25) "pid:Io\Poll\ProcessHandle" + [1]=> + string(25) "pid:Io\Poll\ProcessHandle" +} diff --git a/ext/standard/tests/streams/hooks/process-ops-two-waiters.phpt b/ext/standard/tests/streams/hooks/process-ops-two-waiters.phpt new file mode 100644 index 000000000000..b233fb772468 --- /dev/null +++ b/ext/standard/tests/streams/hooks/process-ops-two-waiters.phpt @@ -0,0 +1,44 @@ +--TEST-- +IO hooks: two waits for one child, the one that collects it gets the status and the other ECHILD +--EXTENSIONS-- +pcntl +posix +--FILE-- +spawn(function () use ($pid, $name, &$results) { + $r = pcntl_waitpid($pid, $status); + $results[$name] = $r === $pid ? pcntl_wexitstatus($status) : [$r, pcntl_get_last_error() === PCNTL_ECHILD]; + }); +} +$scheduler->loop(); +Io\Hooks\set_hooks(null); + +sort($results); +var_dump($results); +?> +--EXPECT-- +array(2) { + [0]=> + int(3) + [1]=> + array(2) { + [0]=> + int(-1) + [1]=> + bool(true) + } +} diff --git a/ext/standard/tests/streams/hooks/process-ops.phpt b/ext/standard/tests/streams/hooks/process-ops.phpt new file mode 100644 index 000000000000..62b4915c636b --- /dev/null +++ b/ext/standard/tests/streams/hooks/process-ops.phpt @@ -0,0 +1,97 @@ +--TEST-- +IO hooks: waiting for a child is an operation in proc_close(), pclose() and pcntl_waitpid() +--EXTENSIONS-- +pcntl +posix +--FILE-- +seen[] = ($op->getPid() > 0 ? "pid" : "any") . ":" + . ($op->getHandle() === null ? "null" : get_class($op->getHandle())); + } + return parent::run($op); + } +} + +$scheduler = new Counting(); +Io\Hooks\set_hooks($scheduler); +$results = []; +$start = hrtime(true); + +// A forked child, waited for by pid; forked first, before anything is in +// flight. The child inherits the provider and a queue it must not use, so +// it drops the hooks before it blocks. +$scheduler->spawn(function () use (&$results) { + $pid = pcntl_fork(); + if ($pid === 0) { + Io\Hooks\set_hooks(null); + usleep(200000); + exit(9); + } + $r = pcntl_waitpid($pid, $status); + $results['fork'] = [$r === $pid, pcntl_wexitstatus($status)]; +}); + +// proc_open() and proc_close() +$scheduler->spawn(function () use (&$results) { + $proc = proc_open(['/bin/sh', '-c', 'sleep 0.3; exit 7'], [], $pipes); + $results['proc'] = proc_close($proc); +}); + +// popen() and pclose() +$scheduler->spawn(function () use (&$results) { + $fp = popen('sleep 0.1; echo out; exit 4', 'r'); + $results['popen'] = [fread($fp, 10), pclose($fp)]; +}); + +$scheduler->loop(); +ksort($results); +var_dump($results); +// The three waits overlapped instead of adding up +var_dump((hrtime(true) - $start) / 1e9 < 0.55); +var_dump($scheduler->seen); + +// Any child, with WNOHANG, never reaches the provider +$before = count($scheduler->seen); +var_dump(pcntl_waitpid(-1, $status, WNOHANG)); +var_dump(count($scheduler->seen) === $before); +?> +--EXPECT-- +array(3) { + ["fork"]=> + array(2) { + [0]=> + bool(true) + [1]=> + int(9) + } + ["popen"]=> + array(2) { + [0]=> + string(4) "out +" + [1]=> + int(4) + } + ["proc"]=> + int(7) +} +bool(true) +array(3) { + [0]=> + string(25) "pid:Io\Poll\ProcessHandle" + [1]=> + string(25) "pid:Io\Poll\ProcessHandle" + [2]=> + string(25) "pid:Io\Poll\ProcessHandle" +} +int(-1) +bool(true) diff --git a/ext/standard/tests/streams/hooks/provider-completes.phpt b/ext/standard/tests/streams/hooks/provider-completes.phpt new file mode 100644 index 000000000000..a97cf5a169c5 --- /dev/null +++ b/ext/standard/tests/streams/hooks/provider-completes.phpt @@ -0,0 +1,105 @@ +--TEST-- +IO hooks: a provider completing Timer, Poll and Any operations itself +--EXTENSIONS-- +curl +--FILE-- +seen[$op::class] = true; + if ($op instanceof Io\Operation\Timer) { + $this->timerOk = $this->timerOk + && $op->getHandle() instanceof Io\Poll\TimerHandle + && $op->getEvents() === [Io\Poll\Event::Timer] + && $op->getTimeout() instanceof Time\Duration; + return $op->complete(Io\CompletionStatus::Done); + } + if ($op instanceof Io\Operation\Any) { + $this->anyRuns++; + $members = $op->getOperations(); + $completions = []; + foreach ($members as $m) { + if ($m instanceof Io\Operation\Timer) { + $completions[] = $m->complete(Io\CompletionStatus::Done); + } else { + /* Poll members carry a weak handle and no deadline of their own */ + $this->membersOk = $this->membersOk + && $m instanceof Io\Operation\Poll + && $m->getHandle() instanceof Io\Poll\WeakHandle + && $m->getTimeout() === null + && $m->getEvents() !== []; + /* Report every member ready: libcurl copes with spurious readiness */ + $completions[] = $m->completeReady($m->getEvents()); + } + } + $c = $op->completeWith($completions); + $this->membersOk = $this->membersOk && count($c->getCompletions()) === count($members); + return $c; + } + return $op->completeReady($op->getEvents()); + } + + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +$recorder = new Recorder(); +Io\Hooks\set_hooks($recorder); + +usleep(1); + +$ch = curl_init("http://127.0.0.1:1/"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 200); +var_dump(curl_exec($ch)); +var_dump(curl_errno($ch) !== 0); + +Io\Hooks\set_hooks(null); +var_dump(array_keys($recorder->seen), $recorder->anyRuns >= 1, $recorder->membersOk, $recorder->timerOk); + +/* Operations end when run() returns */ +$last = null; +Io\Hooks\set_hooks(new class($last) implements Io\Hooks\Hooks { + public function __construct(private &$last) {} + public function getCapabilities(): array { return []; } + public function run(Io\Operation $op): Io\Completion { + $this->last = $op; + return $op->complete(Io\CompletionStatus::Done); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +}); +usleep(1); +var_dump($last instanceof Io\Operation\Timer, $last->isValid()); +try { + $last->getTimeout(); +} catch (Io\InvalidOperationException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +bool(false) +bool(true) +array(2) { + [0]=> + string(18) "Io\Operation\Timer" + [1]=> + string(16) "Io\Operation\Any" +} +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) +The operation has ended diff --git a/ext/standard/tests/streams/hooks/provider-invalid-results.phpt b/ext/standard/tests/streams/hooks/provider-invalid-results.phpt new file mode 100644 index 000000000000..960602c59982 --- /dev/null +++ b/ext/standard/tests/streams/hooks/provider-invalid-results.phpt @@ -0,0 +1,106 @@ +--TEST-- +IO hooks: completions a provider could not have produced are refused +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +caps; } + public function run(Io\Operation $op): Io\Completion + { + return ($this->complete)($op) ?? $op->complete(Io\CompletionStatus::Unsupported); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +function attempt(string $label, Closure $call, string $type, Closure $complete, array $caps = []): void +{ + Io\Hooks\set_hooks(new Fake(fn ($op) => $op instanceof $type ? $complete($op) : null, $caps)); + try { + $r = $call(); + echo "$label: ", var_export($r, true), "\n"; + } catch (Error $e) { + echo "$label: ", $e::class, ": ", $e->getMessage(), "\n"; + } + Io\Hooks\set_hooks(null); +} + +$file = __DIR__ . '/provider-invalid-results.txt'; +file_put_contents($file, "hello"); +$f = fopen($file, 'r+'); +$files = [Io\Hooks\Capability::Files]; +$direct = [Io\Hooks\Capability::DirectData]; +$accept = [Io\Hooks\Capability::DirectAccept]; + +attempt('read', fn () => @fread($f, 100), Io\Operation\Read::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, 1 << 20), $files); +attempt('read eof', fn () => @fread($f, 100), Io\Operation\Read::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, 0), $files); +attempt('write', fn () => @fwrite($f, "0123456789"), Io\Operation\Write::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, $op->getLength() + 1), $files); +attempt('write short', fn () => @fwrite($f, "0123456789"), Io\Operation\Write::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, -1), $files); + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +attempt('recv', fn () => @fread($a, 100), Io\Operation\Recv::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, 5), $direct); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +attempt('accept', fn () => @stream_socket_accept($server, 5), Io\Operation\Accept::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, 1), $accept); + +$pid = pcntl_fork(); +if ($pid === 0) { + exit(3); +} +attempt('waitpid', fn () => @pcntl_waitpid($pid, $status), Io\Operation\WaitPid::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, $op->getPid())); +var_dump(pcntl_waitpid($pid, $status) === $pid, pcntl_wexitstatus($status)); + +pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]); +attempt('sigwait', fn () => @pcntl_sigtimedwait([SIGUSR1], $info, 5), Io\Operation\SigWait::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, SIGUSR1)); +attempt('sigwait timeout', fn () => @pcntl_sigtimedwait([SIGUSR1], $info, 0, 1000), Io\Operation\SigWait::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done, -1, 11)); + +attempt('getaddrinfo', fn () => gethostbyname('provider-invalid-results.invalid'), Io\Operation\GetAddrInfo::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done)); +attempt('getnameinfo', fn () => gethostbyaddr('192.0.2.1'), Io\Operation\GetNameInfo::class, + fn ($op) => $op->complete(Io\CompletionStatus::Done)); + +/* An operation still submitted to a queue completes from that queue */ +$queue = new Io\Poll\OperationQueue(); +attempt('submitted', fn () => usleep(1), Io\Operation\Timer::class, function ($op) use ($queue) { + $queue->submit($op); + return $op->complete(Io\CompletionStatus::Done); +}); +var_dump($queue->countPending()); +?> +--CLEAN-- + +--EXPECT-- +read: Error: Io\Hooks\Hooks::run() cannot complete Io\Operation\Read with a result it did not produce, only a queue can +read eof: '' +write: Error: The IO provider completed an operation with an invalid result +write short: Error: The IO provider completed an operation with an invalid result +recv: Error: Io\Hooks\Hooks::run() cannot complete Io\Operation\Recv with a result it did not produce, only a queue can +accept: Error: Io\Hooks\Hooks::run() cannot complete Io\Operation\Accept with a result it did not produce, only a queue can +waitpid: Error: Io\Hooks\Hooks::run() cannot complete Io\Operation\WaitPid with a result it did not produce, only a queue can +bool(true) +int(3) +sigwait: Error: Io\Hooks\Hooks::run() cannot complete Io\Operation\SigWait with a result it did not produce, only a queue can +sigwait timeout: false +getaddrinfo: 'provider-invalid-results.invalid' +getnameinfo: '192.0.2.1' +submitted: Error: Io\Hooks\Hooks::run() must cancel the operation or wait for its completion from the queue it was submitted to +int(0) diff --git a/ext/standard/tests/streams/hooks/provider-no-fiber-switch.phpt b/ext/standard/tests/streams/hooks/provider-no-fiber-switch.phpt new file mode 100644 index 000000000000..cd647ac4a90a --- /dev/null +++ b/ext/standard/tests/streams/hooks/provider-no-fiber-switch.phpt @@ -0,0 +1,69 @@ +--TEST-- +IO hooks: getCapabilities() and add() cannot switch fibers +--EXTENSIONS-- +curl +--FILE-- +complete(Io\CompletionStatus::Unsupported); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +final class CapsSuspends extends Base +{ + public function getCapabilities(): array + { + Fiber::suspend(); + return []; + } +} + +final class AddSuspends extends Base +{ + private bool $reported = false; + + public function add(Io\Registration $registration): void + { + try { + Fiber::suspend(); + } catch (FiberError $e) { + /* libcurl may report the socket more than once for one connect */ + if (!$this->reported) { + echo "add: ", $e->getMessage(), "\n"; + $this->reported = true; + } + } + } +} + +$fiber = new Fiber(function () { + try { + Io\Hooks\set_hooks(new CapsSuspends()); + } catch (FiberError $e) { + echo "getCapabilities: ", $e->getMessage(), "\n"; + } + var_dump(Io\Hooks\get_hooks()); + + Io\Hooks\set_hooks(new AddSuspends()); + $ch = curl_init("http://127.0.0.1:1/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 200); + var_dump(curl_exec($ch)); + /* Not left locked */ + Io\Hooks\set_hooks(null); + echo "uninstalled\n"; +}); +$fiber->start(); +var_dump($fiber->isTerminated()); +?> +--EXPECT-- +getCapabilities: Cannot switch fibers in current execution context +NULL +add: Cannot switch fibers in current execution context +bool(false) +uninstalled +bool(true) diff --git a/ext/standard/tests/streams/hooks/registrations-curl.phpt b/ext/standard/tests/streams/hooks/registrations-curl.phpt new file mode 100644 index 000000000000..32d1a02cbb7c --- /dev/null +++ b/ext/standard/tests/streams/hooks/registrations-curl.phpt @@ -0,0 +1,131 @@ +--TEST-- +IO hooks: curl's socket directions and the streams' pairs are registrations bracketed by add() and remove() +--EXTENSIONS-- +curl +--FILE-- +getRegistration(); + $id = $reg === null ? -1 : spl_object_id($reg); + $this->runs[$id] = ($this->runs[$id] ?? 0) + 1; + $this->membersOk = $this->membersOk && $reg !== null && $reg->isValid() + && isset($this->added[$id]) && $this->added[$id] === $reg + && $op->getEvents() === [$reg->getEvent()] + && $op->getHandle() === $reg->getHandle(); + } + + public function run(\Io\Operation $op): \Io\Completion + { + if ($op instanceof \Io\Operation\Any) { + // curl: one Poll member per registered direction + foreach ($op->getOperations() as $m) { + if ($m instanceof \Io\Operation\Poll) { + $this->seen($m); + } + } + } elseif ($op instanceof \Io\Operation\Accept || $op instanceof \Io\Operation\Recv) { + // the server's streams: the wait after EAGAIN is on the stream's pair + $this->seen($op); + } + return parent::run($op); + } + + public function add(\Io\Registration $registration): void + { + $this->added[spl_object_id($registration)] = $registration; + $handle = $registration->getHandle(); + if ($handle instanceof \CurlSocketPollWeakHandle) { + $this->curl[spl_object_id($registration)] = true; + } + // The stream pairs want Edge and get Level, all this provider takes + $this->addsOk = $this->addsOk && $registration->isValid() + && $registration->getTrigger() === \Io\Poll\Trigger::Level + && ($handle instanceof \CurlSocketPollWeakHandle || $handle instanceof \StreamPollWeakHandle) + && in_array($registration->getEvent(), [\Io\Poll\Event::Read, \Io\Poll\Event::Write], true); + parent::add($registration); + } + + public function remove(\Io\Registration $registration): void + { + $this->removed[spl_object_id($registration)] = true; + $this->removesOk = $this->removesOk && isset($this->added[spl_object_id($registration)]) + && $this->added[spl_object_id($registration)] === $registration && $registration->isValid(); + parent::remove($registration); + } +} + +// The Poll queue takes Level registrations, the ring does not +$scheduler = new Tracing(new Io\Poll\OperationQueue(), [Io\Hooks\Capability::LevelRegistrations]); +Io\Hooks\set_hooks($scheduler); + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$addr = stream_socket_get_name($server, false); + +$scheduler->spawn(function () use ($server) { + $conn = stream_socket_accept($server, 5); + $request = ''; + while (!str_ends_with($request, "\r\n\r\n")) { + $chunk = fread($conn, 1024); + if ($chunk === false || $chunk === '') break; + $request .= $chunk; + } + fwrite($conn, "HTTP/1.0 200 OK\r\nContent-Length: 2\r\n\r\nok"); + fclose($conn); +}); + +$scheduler->spawn(function () use ($addr) { + $ch = curl_init("http://$addr/"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + var_dump(curl_exec($ch)); +}); + +$scheduler->loop(); +fclose($server); + +// Every pair was added once, waited on with the same object, and removed +var_dump(count($scheduler->curl) >= 1, count($scheduler->added) > count($scheduler->curl)); +$removed = array_keys($scheduler->removed); +$added = array_keys($scheduler->added); +sort($removed); +sort($added); +var_dump($removed === $added); +var_dump($scheduler->addsOk, $scheduler->removesOk, $scheduler->membersOk); +$ran = true; +$invalid = true; +foreach ($scheduler->added as $id => $reg) { + $ran = $ran && ($scheduler->runs[$id] ?? 0) >= 1; + $invalid = $invalid && !$reg->isValid(); +} +var_dump($ran, $invalid); +try { + reset($scheduler->added)->getEvent(); +} catch (Io\InvalidRegistrationException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +string(2) "ok" +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +The registration has ended diff --git a/ext/standard/tests/streams/hooks/registrations-edge-select.phpt b/ext/standard/tests/streams/hooks/registrations-edge-select.phpt new file mode 100644 index 000000000000..416a2096c741 --- /dev/null +++ b/ext/standard/tests/streams/hooks/registrations-edge-select.phpt @@ -0,0 +1,78 @@ +--TEST-- +IO hooks: stream_select() on a partly read Edge-registered stream reports it readable +--FILE-- +getRegistration(); + $this->log[] = substr($op::class, 13) . ($reg ? " on " . $reg->getTrigger()->name : "") . " " . $c->getStatus()->name; + } + return $c; + } + + public function add(\Io\Registration $registration): void + { + $this->log[] = "add " . $registration->getEvent()->name . " " . $registration->getTrigger()->name; + parent::add($registration); + } +} + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +$scheduler = new Tracing(new Io\Poll\OperationQueue(), [Io\Hooks\Capability::EdgeRegistrations]); +Io\Hooks\set_hooks($scheduler); + +$scheduler->spawn(function () use ($a) { + // Unbuffered: every fread() is a Recv op + stream_set_read_buffer($a, 0); + // The first wait registers the read pair; the writer's bytes complete it + var_dump(fread($a, 8)); + // No new edge comes for the rest: the select checks at arm time, with the zero-timeout + // poll that precedes its Any, so nothing reaches the provider + $r = [$a]; $w = null; $e = null; + var_dump(stream_select($r, $w, $e, 0)); + var_dump(fread($a, 100)); + // Drained: the next wait is a real one, answered by the next write + var_dump(fread($a, 100)); + // Bytes arriving while nothing waits set the ready bit and the syscall takes them first, so + // the next wait is answered from the record once (one spurious wakeup), then waits for real + usleep(20000); + var_dump(fread($a, 100)); + var_dump(fread($a, 100)); + var_dump(feof($a)); +}); +$scheduler->spawn(function () use ($b) { + fwrite($b, "0123456789abcdefghij"); + usleep(10000); + fwrite($b, "more"); + usleep(10000); + fwrite($b, "late"); + usleep(30000); + fclose($b); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +// A backend that observes edges records the bytes that arrived while nothing waited, so one wait +// is answered from the record (one spurious wakeup); a level backend re-arms per wait and sees none +$spurious = (new Io\Poll\Context())->getBackend()->supportsEdgeTriggering() ? 1 : 0; +$ready = count(array_filter($scheduler->log, fn ($l) => $l === 'Recv on Edge Ready')); +var_dump($scheduler->log[0], $ready === 3 + $spurious, count($scheduler->log) === $ready + 1); +?> +--EXPECT-- +string(8) "01234567" +int(1) +string(12) "89abcdefghij" +string(4) "more" +string(4) "late" +string(0) "" +bool(true) +string(13) "add Read Edge" +bool(true) +bool(true) diff --git a/ext/standard/tests/streams/hooks/registrations-replace.phpt b/ext/standard/tests/streams/hooks/registrations-replace.phpt new file mode 100644 index 000000000000..442b7e861140 --- /dev/null +++ b/ext/standard/tests/streams/hooks/registrations-replace.phpt @@ -0,0 +1,120 @@ +--TEST-- +IO hooks: replacing the provider ends its registrations without remove(), the next one sees add() again +--FILE-- +q = new \Io\Poll\OperationQueue(); + parent::__construct($this->q, [\Io\Hooks\Capability::EdgeRegistrations]); + } + + public function run(\Io\Operation $op): \Io\Completion + { + $reg = $op->getRegistration(); + $this->log[] = "$this->name run " . substr($op::class, 13) . ($reg ? " on " . $reg->getEvent()->name : " unregistered"); + return parent::run($op); + } + + public function add(\Io\Registration $registration): void + { + $this->regs[] = $registration; + $this->log[] = "$this->name add " . $registration->getEvent()->name . " " . $registration->getTrigger()->name; + parent::add($registration); + } + + public function remove(\Io\Registration $registration): void + { + $this->log[] = "$this->name remove " . $registration->getEvent()->name; + parent::remove($registration); + } +} + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + +$first = new Tracing('first'); +Io\Hooks\set_hooks($first); +$first->spawn(function () use ($a) { + $first = Io\Hooks\get_hooks(); + // Two waits on the read pair: one add() + var_dump(fread($a, 10)); + var_dump(fread($a, 10)); + // The same object every time + var_dump(count($first->regs), $first->regs[0]->isValid()); +}); +$first->spawn(function () use ($b) { + fwrite($b, "1"); + usleep(1000); + fwrite($b, "2"); +}); +$first->loop(); + +// Replaced with a registration in place: no remove(), the object is detached +$second = new Tracing('second'); +Io\Hooks\set_hooks($second); +var_dump($first->regs[0]->isValid()); +try { + $first->regs[0]->getHandle(); +} catch (Io\InvalidRegistrationException $e) { + echo $e->getMessage(), "\n"; +} +try { + $second->q->add($first->regs[0]); +} catch (Io\InvalidRegistrationException $e) { + echo "queue: ", $e->getMessage(), "\n"; +} +$second->spawn(function () use ($a) { + var_dump(fread($a, 10)); + // The pair is removed before the descriptor closes + fclose($a); +}); +$second->spawn(function () use ($b) { + fwrite($b, "3"); +}); +$second->loop(); +Io\Hooks\set_hooks(null); + +// A provider installed a second time sees add() again +[$c, $d] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +Io\Hooks\set_hooks($first); +$first->spawn(function () use ($c) { + stream_set_timeout($c, 0, 10000); + var_dump(fread($c, 10)); +}); +$first->loop(); +Io\Hooks\set_hooks(null); +print_r($first->log); +print_r($second->log); +?> +--EXPECT-- +string(1) "1" +string(1) "2" +int(1) +bool(true) +bool(false) +The registration has ended +queue: The registration has ended +string(1) "3" +bool(false) +Array +( + [0] => first add Read Edge + [1] => first run Recv on Read + [2] => first run Timer unregistered + [3] => first run Recv on Read + [4] => first add Read Edge + [5] => first run Recv on Read +) +Array +( + [0] => second add Read Edge + [1] => second run Recv on Read + [2] => second remove Read +) diff --git a/ext/standard/tests/streams/hooks/registrations-ring-accept.phpt b/ext/standard/tests/streams/hooks/registrations-ring-accept.phpt new file mode 100644 index 000000000000..4a22c4d49680 --- /dev/null +++ b/ext/standard/tests/streams/hooks/registrations-ring-accept.phpt @@ -0,0 +1,133 @@ +--TEST-- +Io\Ring\Engine: a burst of connections under DirectAccept is served in order of arrival from a multishot accept +--SKIPIF-- +getSupportedHookCapabilities(), true)) { + die("skip the backend does not support DirectAccept"); +} +if (!in_array(Io\Hooks\Capability::EdgeRegistrations, (new Io\Ring\Engine())->getSupportedHookCapabilities(), true)) { + die("skip the listener's pair needs EdgeRegistrations"); +} +?> +--FILE-- +accepts++; + if ($op->getRegistration() !== null) { + $this->registered++; + } + } + return parent::run($op); + } + + public function add(\Io\Registration $registration): void + { + // Only the listener's pairs; the clients register theirs too. The handle is the + // stream's one object, and the stream is frozen by the accept, so getStream() is null + if ($registration->getHandle() === StreamPollWeakHandle::create($this->listener)) { + $this->regs[] = $registration->getEvent()->name . " " . $registration->getTrigger()->name; + } + parent::add($registration); + } +} + +$n = 40; +$ring = new Io\Ring\Engine(); +$scheduler = new Tracing($ring, [Io\Hooks\Capability::EdgeRegistrations, Io\Hooks\Capability::DirectAccept]); +Io\Hooks\set_hooks($scheduler); + +$ctx = stream_context_create(['socket' => ['backlog' => 128]]); +$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, $ctx); +$addr = stream_socket_get_name($server, false); +$scheduler->listener = $server; + +$order = []; +$peers = true; +$scheduler->spawn(function () use ($server, $n, &$order, &$peers) { + // The first accept arms the multishot and parks; the burst then fills the buffer past its + // cap, so the multishot is let go and comes back once the takes brought it below half + for ($i = 0; $i < $n; $i++) { + $conn = stream_socket_accept($server, 5, $peer); + $peers = $peers && $peer !== '' && $peer === stream_socket_get_name($conn, true); + $order[] = (int) fread($conn, 10); + fclose($conn); + } +}); +for ($i = 0; $i < $n; $i++) { + $scheduler->spawn(function () use ($addr, $i) { + $c = stream_socket_client("tcp://$addr", $errno, $errstr, 5); + fwrite($c, (string) $i); + // Kept open until the server read it + fread($c, 1); + }); +} +$scheduler->loop(); + +var_dump(count($order), $order === range(0, $n - 1), $peers); +var_dump($scheduler->accepts, $scheduler->registered, $scheduler->regs); +// Accepts served from the buffer complete at submit: the accepting fiber takes them without a pass +var_dump($scheduler->inline >= 20); + +// A parked accept keeps its deadline without an entry of its own +$scheduler->spawn(function () use ($server) { + $start = hrtime(true); + var_dump(@stream_socket_accept($server, 0.2)); + var_dump((hrtime(true) - $start) / 1e9 >= 0.15); +}); +$scheduler->loop(); + +// The multishot is armed with no waiter: not pending +var_dump($ring->countPending()); + +// accept_multishot => false keeps a listener unregistered: its accepts stay one-shot +$shared = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN, + stream_context_create(['socket' => ['accept_multishot' => false]])); +$saddr = stream_socket_get_name($shared, false); +$scheduler->listener = $shared; +$before = [$scheduler->accepts, $scheduler->registered, count($scheduler->regs)]; +$scheduler->spawn(function () use ($shared) { + $conn = stream_socket_accept($shared, 5); + var_dump(fread($conn, 10)); +}); +$scheduler->spawn(function () use ($saddr) { + $c = stream_socket_client("tcp://$saddr", $errno, $errstr, 5); + fwrite($c, "shared"); + fread($c, 1); +}); +$scheduler->loop(); +var_dump($scheduler->accepts - $before[0], $scheduler->registered - $before[1], count($scheduler->regs) - $before[2]); + +// Closing the listener removes the pair and lets the multishot go +fclose($server); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +int(40) +bool(true) +bool(true) +int(40) +int(40) +array(1) { + [0]=> + string(9) "Read Edge" +} +bool(true) +bool(false) +bool(true) +int(0) +string(6) "shared" +int(1) +int(0) +int(0) diff --git a/ext/standard/tests/streams/hooks/registrations-ring-edge.phpt b/ext/standard/tests/streams/hooks/registrations-ring-edge.phpt new file mode 100644 index 000000000000..2c3dd090618a --- /dev/null +++ b/ext/standard/tests/streams/hooks/registrations-ring-edge.phpt @@ -0,0 +1,103 @@ +--TEST-- +Io\Ring\Engine: waits after a drain on an Edge pair are served from one multishot poll +--SKIPIF-- +getSupportedHookCapabilities(), true)) { + die("skip the backend's multishot poll does not report edges"); +} +?> +--FILE-- +getRegistration(); + $this->log[] = substr($op::class, 13) . ($reg ? " on " . $reg->getTrigger()->name : "") + . " " . $c->getStatus()->name . " " . implode("|", array_map(fn ($e) => $e->name, $c->getEvents())); + } + return $c; + } + + public function add(\Io\Registration $registration): void + { + $this->log[] = "add " . $registration->getEvent()->name; + parent::add($registration); + } + + public function remove(\Io\Registration $registration): void + { + $this->log[] = "remove " . $registration->getEvent()->name; + parent::remove($registration); + } +} + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +$ring = new Io\Ring\Engine(); +$scheduler = new Tracing($ring, [Io\Hooks\Capability::EdgeRegistrations]); +Io\Hooks\set_hooks($scheduler); + +$scheduler->spawn(function () use ($a) { + // recvfrom waits with a Poll op after EAGAIN: the first one registers the pair and arms + var_dump(stream_socket_recvfrom($a, 100)); + var_dump(stream_socket_recvfrom($a, 100)); + // Bytes that arrived while nothing waited set the ready bits; the syscall takes them first, + // so the next wait is answered from the record once, then waits for real + usleep(20000); + var_dump(stream_socket_recvfrom($a, 100)); + var_dump(stream_socket_recvfrom($a, 100)); + // The peer's close is an edge too + var_dump(stream_socket_recvfrom($a, 100), feof($a)); + // Removed before the descriptor closes + fclose($a); +}); +$scheduler->spawn(function () use ($b) { + usleep(5000); + fwrite($b, "one"); + usleep(5000); + fwrite($b, "two"); + usleep(5000); + fwrite($b, "three"); + usleep(30000); + fwrite($b, "four"); + usleep(30000); + fclose($b); +}); +$scheduler->loop(); + +// An armed multishot with no waiter is not pending: nothing can complete +var_dump($ring->countPending()); +try { + $ring->waitCompletions(); +} catch (Io\IoException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +print_r($scheduler->log); +?> +--EXPECT-- +string(3) "one" +string(3) "two" +string(5) "three" +string(4) "four" +string(0) "" +bool(true) +int(0) +No operation can complete: nothing pending has a descriptor or a deadline +Array +( + [0] => add Read + [1] => Poll on Edge Done Read + [2] => Poll on Edge Done Read + [3] => Poll on Edge Done Read + [4] => Poll on Edge Done Read + [5] => Poll on Edge Done Read|HangUp + [6] => remove Read +) diff --git a/ext/standard/tests/streams/hooks/registrations-two-queues.phpt b/ext/standard/tests/streams/hooks/registrations-two-queues.phpt new file mode 100644 index 000000000000..e4b3f6fd3f33 --- /dev/null +++ b/ext/standard/tests/streams/hooks/registrations-two-queues.phpt @@ -0,0 +1,77 @@ +--TEST-- +IO hooks: a queue ignores the record another queue kept on a registration, and its own survives a provider swap +--FILE-- +adds++; + $this->other->add($registration); + } + public function remove(\Io\Registration $registration): void + { + $this->other->remove($registration); + } +} + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +stream_set_read_buffer($a, 0); + +$queue = new \Io\Poll\OperationQueue(); +$split = new Split(new \Io\Poll\OperationQueue(), $queue); +Io\Hooks\set_hooks($split); +$split->spawn(function () use ($a) { + var_dump(fread($a, 10), fread($a, 10)); +}); +$split->spawn(function () use ($b) { + usleep(20000); + fwrite($b, "one"); + usleep(20000); + fwrite($b, "two"); +}); +$split->loop(); +var_dump($split->adds); + +// The same queue under a second provider: the pair is added again and the record found through it +$second = new Scheduler($queue, [\Io\Hooks\Capability::EdgeRegistrations]); +Io\Hooks\set_hooks($second); +$second->spawn(function () use ($a) { + var_dump(fread($a, 10)); +}); +$second->spawn(function () use ($b) { + usleep(20000); + fwrite($b, "three"); +}); +$second->loop(); + +// And under a provider on a fresh queue, while the old one is gone +unset($queue, $split); +$third = new Scheduler(new \Io\Poll\OperationQueue(), [\Io\Hooks\Capability::EdgeRegistrations]); +Io\Hooks\set_hooks($third); +$third->spawn(function () use ($a) { + var_dump(fread($a, 10)); + fclose($a); +}); +$third->spawn(function () use ($b) { + usleep(20000); + fwrite($b, "four"); +}); +$third->loop(); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +string(3) "one" +string(3) "two" +int(1) +string(5) "three" +string(4) "four" diff --git a/ext/standard/tests/streams/hooks/ring-accept-fd-zero.phpt b/ext/standard/tests/streams/hooks/ring-accept-fd-zero.phpt new file mode 100644 index 000000000000..efd1c982698b --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-accept-fd-zero.phpt @@ -0,0 +1,42 @@ +--TEST-- +Io\Ring\Engine: an accepted descriptor 0 is a valid result +--SKIPIF-- + +--FILE-- +ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $c = $this->ring->submit($op); + while ($c === null) { + foreach ($this->ring->waitCompletions() as $done) { + $c = $done; + } + } + return $c; + } +} + +$provider = new Waiting(new Io\Ring\Engine()); +$server = stream_socket_server('tcp://127.0.0.1:0'); +$client = stream_socket_client('tcp://' . stream_socket_get_name($server, false)); +// The lowest free descriptor goes to the accepted connection +fclose(STDIN); +Io\Hooks\set_hooks($provider); +$conn = stream_socket_accept($server, 5); +Io\Hooks\set_hooks(null); +var_dump(is_resource($conn)); +fwrite($client, "hello"); +var_dump(fread($conn, 5)); +?> +--EXPECT-- +bool(true) +string(5) "hello" diff --git a/ext/standard/tests/streams/hooks/ring-basic.phpt b/ext/standard/tests/streams/hooks/ring-basic.phpt new file mode 100644 index 000000000000..a5d455f8face --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-basic.phpt @@ -0,0 +1,103 @@ +--TEST-- +Io\Ring\Engine: backend, notification handle and a Timer operation +--SKIPIF-- + +--FILE-- +getBackend() instanceof Io\Ring\Backend); +var_dump($ring->countPending()); + +$handle = $ring->getHandle(); +var_dump($handle instanceof Io\Poll\NotifyHandle); +try { + $handle->notify(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} + +// A Timer op through a provider that submits to the ring and reaps it +final class RingProvider implements Io\Hooks\Hooks +{ + public function __construct(private Io\Ring\Engine $ring) {} + public function getCapabilities(): array { return $this->ring->getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op, "data"); + var_dump($this->ring->countPending()); + $completions = []; + while (!$completions) { + $completions = $this->ring->waitCompletions(Time\Duration::fromSeconds(2)); + } + var_dump(count($completions), $completions[0]->getOperation() === $op, $completions[0]->getData()); + var_dump($completions[0]->getStatus(), $this->ring->countPending()); + return $completions[0]; + } +} + +Io\Hooks\set_hooks(new RingProvider($ring)); +$start = hrtime(true); +usleep(20000); +$elapsed_ms = (hrtime(true) - $start) / 1e6; +var_dump($elapsed_ms >= 15 && $elapsed_ms < 1000); +Io\Hooks\set_hooks(null); + +// The notification descriptor reports a completion posted while nobody waited +$ctx = new Io\Poll\Context(); +$watcher = $ctx->add($handle, [Io\Poll\Event::Notify]); +Io\Hooks\set_hooks(new class($ring) implements Io\Hooks\Hooks { + public function __construct(private Io\Ring\Engine $ring) {} + public function getCapabilities(): array { return []; } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + global $ctx, $handle; + $this->ring->submit($op); + // Clear, then reap until empty; a wakeup that finds nothing is a stale + // notification from an earlier blocking wait and is harmless + $completions = []; + $wakeups = 0; + while (!$completions) { + $events = $ctx->wait(Time\Duration::fromSeconds(2)); + $wakeups++; + if (count($events) !== 1 || !$events[0]->hasTriggered(Io\Poll\Event::Notify)) { + echo "unexpected events\n"; + break; + } + $completions = $this->ring->waitCompletions(Time\Duration::fromSeconds(0)); + } + var_dump($wakeups >= 1, count($completions)); + var_dump($this->ring->waitCompletions(Time\Duration::fromSeconds(0))); + return $completions[0]; + } +}); +usleep(1000); +Io\Hooks\set_hooks(null); +$watcher->remove(); +echo "done\n"; +?> +--EXPECT-- +bool(true) +bool(true) +int(0) +bool(true) +This Io\Poll\NotifyHandle is raised by its owner and cannot be notified +int(1) +int(1) +bool(true) +string(4) "data" +enum(Io\CompletionStatus::Done) +int(0) +bool(true) +bool(true) +int(1) +array(0) { +} +done diff --git a/ext/standard/tests/streams/hooks/ring-cancel-read-commit.phpt b/ext/standard/tests/streams/hooks/ring-cancel-read-commit.phpt new file mode 100644 index 000000000000..b5a900e37542 --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-cancel-read-commit.phpt @@ -0,0 +1,118 @@ +--TEST-- +Io\Ring\Engine: a read the provider cancelled after it took bytes keeps them for the stream +--SKIPIF-- + +--FILE-- +countPending()) { + $ring->waitCompletions(); + } +} + +function pair() +{ + $pair = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + stream_set_timeout($pair[0], 1); + return $pair; +} + +$ring = new Io\Ring\Engine(); + +echo "-- cancelled in flight --\n"; +final class CancelIt implements Io\Hooks\Hooks +{ + public function __construct(private Io\Ring\Engine $ring, private $peer) {} + public function getCapabilities(): array { return $this->ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op); + Io\Hooks\set_hooks(null); + fwrite($this->peer, "hello"); + $until = hrtime(true) + 20000000; + while (hrtime(true) < $until); + $this->ring->cancel($op); + return $op->complete(Io\CompletionStatus::Cancelled); + } +} +[$r, $w] = pair(); +Io\Hooks\set_hooks(new CancelIt($ring, $w)); +var_dump(@fread($r, 5)); +Io\Hooks\set_hooks(null); +// The backend may still fill the buffer: frozen until the op settled +try { + stream_get_meta_data($r); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +settle($ring); +fwrite($w, " world"); +var_dump(fread($r, 11)); + +echo "-- cancelled once reaped --\n"; +final class Parked implements Io\Hooks\Hooks +{ + public array $ops = []; + public function __construct(private Io\Ring\Engine $ring) {} + public function getCapabilities(): array { return $this->ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op, Fiber::getCurrent()); + $this->ops[spl_object_id(Fiber::getCurrent())] = $op; + return Fiber::suspend(); + } +} +$provider = new Parked($ring); +$pairs = []; +$fibers = []; +for ($i = 0; $i < 2; $i++) { + $pairs[$i] = pair(); + $r = $pairs[$i][0]; + $fibers[$i] = new Fiber(function () use ($r) { return @fread($r, 5); }); +} +Io\Hooks\set_hooks($provider); +foreach ($fibers as $f) { + $f->start(); +} +Io\Hooks\set_hooks(null); +foreach ($pairs as [$r, $w]) { + fwrite($w, "hello"); +} +$until = hrtime(true) + 50000000; +while (hrtime(true) < $until); + +// Both are reaped, one is delivered and the other cancelled +[$c] = $ring->waitCompletions(null, 1); +$done = $c->getData(); +Io\Hooks\set_hooks($provider); +$done->resume($c); +foreach ($fibers as $i => $f) { + if ($f !== $done) { + $op = $provider->ops[spl_object_id($f)]; + $ring->cancel($op); + $f->resume($op->complete(Io\CompletionStatus::Cancelled)); + $victim = $i; + } +} +Io\Hooks\set_hooks(null); +settle($ring); +var_dump($done->getReturn(), $fibers[$victim]->getReturn()); +fwrite($pairs[$victim][1], " world"); +var_dump(fread($pairs[$victim][0], 11)); +?> +--EXPECT-- +-- cancelled in flight -- +bool(false) +Concurrent access to a stream +string(11) "hello world" +-- cancelled once reaped -- +string(5) "hello" +bool(false) +string(11) "hello world" diff --git a/ext/standard/tests/streams/hooks/ring-capabilities.phpt b/ext/standard/tests/streams/hooks/ring-capabilities.phpt new file mode 100644 index 000000000000..29e9f7dadf6c --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-capabilities.phpt @@ -0,0 +1,40 @@ +--TEST-- +Io\Ring\Engine: EdgeRegistrations and DirectAccept by default, Files and DirectData on request +--SKIPIF-- + +--FILE-- +getBackend() !== Io\Ring\Backend::Iocp; +$default = [Io\Hooks\Capability::DirectAccept]; +if ($edge) { + $default[] = Io\Hooks\Capability::EdgeRegistrations; +} +var_dump($ring->getHookCapabilities() === $default); +var_dump((new Io\Poll\OperationQueue())->getHookCapabilities()); + +// DirectData where the backend completes ops itself: io_uring and IOCP, not the thread pool; +// DirectAccept everywhere, from a multishot accept +$expected = [Io\Hooks\Capability::Files]; +if ($ring->getBackend() !== Io\Ring\Backend::Threads) { + $expected[] = Io\Hooks\Capability::DirectData; +} +$expected[] = Io\Hooks\Capability::DirectAccept; +if ($edge) { + $expected[] = Io\Hooks\Capability::EdgeRegistrations; +} +var_dump($ring->getSupportedHookCapabilities() === $expected); +?> +--EXPECT-- +bool(true) +array(2) { + [0]=> + enum(Io\Hooks\Capability::EdgeRegistrations) + [1]=> + enum(Io\Hooks\Capability::LevelRegistrations) +} +bool(true) diff --git a/ext/standard/tests/streams/hooks/ring-capacity.phpt b/ext/standard/tests/streams/hooks/ring-capacity.phpt new file mode 100644 index 000000000000..bbab15d8781f --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-capacity.phpt @@ -0,0 +1,61 @@ +--TEST-- +Io\Ring\Engine: the smallest ring takes more operations than it has entries and keeps their deadlines +--SKIPIF-- + +--FILE-- +spawn(function () use ($i, $pairs, &$results) { + stream_set_timeout($pairs[$i][0], 0, 200000); + $r = fread($pairs[$i][0], 10); + $results[] = var_export($r, true) . " timed_out=" . var_export(stream_get_meta_data($pairs[$i][0])['timed_out'], true); + }); +} +$scheduler->loop(); +$counts = array_count_values($results); +ksort($counts); +print_r($counts); + +// Operations still pending when the script ends +$ring = new Io\Ring\Engine(1); +Io\Hooks\set_hooks(new class($ring) implements Io\Hooks\Hooks { + public function __construct(private Io\Ring\Engine $ring) {} + public function getCapabilities(): array { return Scheduler::capabilities($this->ring); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion { + return $this->ring->submit($op) ?? Fiber::suspend(); + } +}); +$fibers = []; +for ($i = 0; $i < $n; $i++) { + stream_set_timeout($pairs[$i][0], -1); + $fibers[$i] = new Fiber(function () use ($i, $pairs) { fread($pairs[$i][0], 10); }); + $fibers[$i]->start(); +} +var_dump($ring->countPending()); +echo "end\n"; +?> +--EXPECT-- +Array +( + ['seven' timed_out=false] => 1 + [false timed_out=true] => 99 +) +int(100) +end diff --git a/ext/standard/tests/streams/hooks/ring-destroy-signal.phpt b/ext/standard/tests/streams/hooks/ring-destroy-signal.phpt new file mode 100644 index 000000000000..5b9608f5f34e --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-destroy-signal.phpt @@ -0,0 +1,54 @@ +--TEST-- +Io\Ring\Engine: a signal while the ring is drained on destruction +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- +ring->getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion { + $this->ring->submit($op); + throw new RuntimeException("gave up"); + } +}); + +$parent = getmypid(); +$pid = pcntl_fork(); +if ($pid === 0) { + Io\Hooks\set_hooks(null); + usleep(200000); + posix_kill($parent, SIGUSR1); + usleep(200000); + exit(3); +} + +// A wait for any child blocks a worker and cannot be cancelled: the +// destruction waits for it and is interrupted by the signal meanwhile +try { + pcntl_waitpid(-1, $status); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +unset($ring); +echo "ring destroyed\n"; +var_dump(pcntl_waitpid($pid, $status) === $pid, pcntl_wexitstatus($status)); +pcntl_signal_dispatch(); +?> +--EXPECT-- +gave up +ring destroyed +bool(true) +int(3) +SIGUSR1 handled diff --git a/ext/standard/tests/streams/hooks/ring-direct.phpt b/ext/standard/tests/streams/hooks/ring-direct.phpt new file mode 100644 index 000000000000..7026aca182eb --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-direct.phpt @@ -0,0 +1,44 @@ +--TEST-- +Io\Ring\Engine: with DirectData a read or write reaches the ring even when the socket is ready +--SKIPIF-- +getSupportedHookCapabilities(), true)) { + die("skip the backend does not support DirectData"); +} +?> +--FILE-- +seen[] = $op::class; + return parent::run($op); + } +} + +foreach ([[], [Io\Hooks\Capability::DirectData]] as $caps) { + [$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + $scheduler = new Tracing(new Io\Ring\Engine(), $caps); + Io\Hooks\set_hooks($scheduler); + $scheduler->spawn(function () use ($a, $b) { + var_dump(fwrite($b, "ready")); + var_dump(fread($a, 5)); + }); + $scheduler->loop(); + Io\Hooks\set_hooks(null); + echo implode(",", $scheduler->seen) ?: "none", "\n"; +} +?> +--EXPECT-- +int(5) +string(5) "ready" +none +int(5) +string(5) "ready" +Io\Operation\Send,Io\Operation\Recv diff --git a/ext/standard/tests/streams/hooks/ring-engine-freed.phpt b/ext/standard/tests/streams/hooks/ring-engine-freed.phpt new file mode 100644 index 000000000000..046b8da3529f --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-engine-freed.phpt @@ -0,0 +1,53 @@ +--TEST-- +Io\Ring\Engine: freeing the ring while a fiber is suspended in an operation on it +--SKIPIF-- + +--FILE-- +getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion { + $c = $GLOBALS['ring']->submit($op, "data"); + $GLOBALS['kept'] = $op; + return $c ?? Fiber::suspend(); + } +}); + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +$f = new Fiber(function () use ($a) { + try { + fread($a, 10); + } finally { + echo "fiber unwinding\n"; + } +}); +$f->start(); +echo "suspended\n"; + +// The ring cancels and drains the operation; the fiber's frame still has it +unset($GLOBALS['ring']); +echo "ring freed\n"; +$reuse = str_repeat("z", 200); +var_dump($kept->isValid()); + +$f = null; +echo "fiber destroyed\n"; +var_dump($kept->isValid()); + +Io\Hooks\set_hooks(null); +fwrite($b, "x"); +var_dump(fread($a, 10)); +?> +--EXPECT-- +suspended +ring freed +bool(true) +fiber unwinding +fiber destroyed +bool(false) +string(1) "x" diff --git a/ext/standard/tests/streams/hooks/ring-fork.phpt b/ext/standard/tests/streams/hooks/ring-fork.phpt new file mode 100644 index 000000000000..7486c696e1f7 --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-fork.phpt @@ -0,0 +1,119 @@ +--TEST-- +Io\Ring\Engine: a forked child cannot use the parent's ring and does not keep its descriptors +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- +ring->getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $c = $this->ring->submit($op); + if ($this->giveUp) { + throw new RuntimeException("gave up"); + } + if ($c !== null) { + return $c; + } + do { + $completions = $this->ring->waitCompletions(); + } while (!$completions); + return $completions[0]; + } +} + +pcntl_sigprocmask(SIG_BLOCK, [SIGUSR2]); +$pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +$before = fds(); +$ring = new Io\Ring\Engine(); +$handle = $ring->getHandle(); +$provider = new RingProvider($ring); +Io\Hooks\set_hooks($provider); +usleep(1000); +// Left in flight: the child inherits a record whose completion it never sees +$provider->giveUp = true; +try { + usleep(200000); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +// What ior opens on demand: the thread backend's poller, io_uring's signalfd +foreach ([fn() => fread($pair[0], 1), fn() => pcntl_sigwaitinfo([SIGUSR2])] as $call) { + try { + @$call(); + } catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; + } +} +$provider->giveUp = false; +Io\Hooks\set_hooks(null); + +$pid = pcntl_fork(); +if ($pid === 0) { + var_dump(fds() === $before); + $calls = [ + 'getBackend' => fn() => $ring->getBackend(), + 'getHandle' => fn() => $ring->getHandle(), + 'getHookCapabilities' => fn() => $ring->getHookCapabilities(), + 'countPending' => fn() => $ring->countPending(), + 'waitCompletions' => fn() => $ring->waitCompletions(Time\Duration::fromSeconds(0)), + ]; + foreach ($calls as $name => $call) { + try { + $call(); + echo "$name: no exception\n"; + } catch (Io\Ring\RingException $e) { + echo "$name: ", $e->getMessage(), "\n"; + } + } + // Neither submits nor waits for the parent's operations + unset($calls, $ring, $provider); + var_dump(fds() === $before); + exit(3); +} + +var_dump(pcntl_waitpid($pid, $status) === $pid, pcntl_wexitstatus($status)); + +// The parent's ring is unaffected +var_dump($ring->waitCompletions(), $ring->countPending()); +Io\Hooks\set_hooks($provider); +$start = hrtime(true); +usleep(20000); +var_dump((hrtime(true) - $start) / 1e6 >= 15); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +gave up +gave up +gave up +bool(true) +getBackend: The ring was created in another process +getHandle: The ring was created in another process +getHookCapabilities: The ring was created in another process +countPending: The ring was created in another process +waitCompletions: The ring was created in another process +bool(true) +bool(true) +int(3) +array(0) { +} +int(0) +bool(true) diff --git a/ext/standard/tests/streams/hooks/ring-inflight-window.phpt b/ext/standard/tests/streams/hooks/ring-inflight-window.phpt new file mode 100644 index 000000000000..4418cb194d0d --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-inflight-window.phpt @@ -0,0 +1,43 @@ +--TEST-- +Io\Ring\Engine: pending operations beyond the completion queue size do not hold back later ones +--SKIPIF-- + +--FILE-- +spawn(function () use ($i, $pairs, &$result, &$timedOut) { + $data = fread($pairs[$i][0], 10); + $timedOut += (int) stream_get_meta_data($pairs[$i][0])['timed_out']; + if ($i > 0) { + fwrite($pairs[$i - 1][1], $data); + } else { + $result = $data; + } + }); +} +$scheduler->spawn(function () use ($n, $pairs) { + fwrite($pairs[$n - 1][1], "relay"); +}); +$scheduler->loop(); +var_dump($result, $timedOut); +?> +--EXPECT-- +string(5) "relay" +int(0) diff --git a/ext/standard/tests/streams/hooks/ring-orphan-caller-buffer.phpt b/ext/standard/tests/streams/hooks/ring-orphan-caller-buffer.phpt new file mode 100644 index 000000000000..3acf9623e5fa --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-orphan-caller-buffer.phpt @@ -0,0 +1,90 @@ +--TEST-- +Io\Ring\Engine: an orphaned Read or Write never touches the caller's buffer after the call returned +--SKIPIF-- + +--FILE-- +ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op); + // Spin, so that the backend is busy with the op when the frame goes + $until = hrtime(true) + $this->delay; + while (hrtime(true) < $until); + throw new RuntimeException("gave up"); + } +} + +// The stream stays frozen until the orphaned op settled +function settle(Io\Ring\Engine $ring): void +{ + if ($ring->countPending()) { + $ring->waitCompletions(); + } +} + +$size = 1500000; +$src = __DIR__ . '/ring-orphan-caller-buffer.src'; +$dst = __DIR__ . '/ring-orphan-caller-buffer.dst'; +file_put_contents($src, str_repeat('A', $size)); +$ring = new Io\Ring\Engine(); +$provider = new GiveUp($ring); +$read_changed = 0; +$write_foreign = 0; + +for ($i = 0; $i < 100; $i++) { + $provider->delay = ($i % 50) * 10000; + // The provider before the opens: Windows opens a file for the ring only with one in place + Io\Hooks\set_hooks($provider); + + // An unbuffered read lands in a string the failed fread() frees + $fp = fopen($src, 'r'); + stream_set_read_buffer($fp, 0); + try { + @fread($fp, $size); + } catch (RuntimeException $e) { + } + Io\Hooks\set_hooks(null); + $victim = str_repeat('.', $size - 32); + $until = hrtime(true) + 2000000; + while (hrtime(true) < $until); + $read_changed += strpos($victim, 'A') !== false; + unset($victim); + settle($ring); + fclose($fp); + + // A write sends from a string the failed fwrite() frees + Io\Hooks\set_hooks($provider); + $fp = fopen($dst, 'w'); + try { + @fwrite($fp, str_repeat('B', $size)); + } catch (RuntimeException $e) { + } + Io\Hooks\set_hooks(null); + $victim = str_repeat('.', $size - 32); + $until = hrtime(true) + 2000000; + while (hrtime(true) < $until); + unset($victim); + settle($ring); + fclose($fp); + $write_foreign += trim(file_get_contents($dst), 'B') !== ''; +} + +var_dump($read_changed, $write_foreign); +?> +--CLEAN-- + +--EXPECT-- +int(0) +int(0) diff --git a/ext/standard/tests/streams/hooks/ring-orphan-outputs.phpt b/ext/standard/tests/streams/hooks/ring-orphan-outputs.phpt new file mode 100644 index 000000000000..9ad0a4cba93d --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-orphan-outputs.phpt @@ -0,0 +1,63 @@ +--TEST-- +Io\Ring\Engine: operations without a stream outlive their frame on the ring's own buffers +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- +ring->getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion { + // Submitted, then the frame goes away with the operation still in flight + $this->ring->submit($op); + throw new RuntimeException("gave up on " . $op::class); + } +}); + +$pid = pcntl_fork(); +if ($pid === 0) { + Io\Hooks\set_hooks(null); + usleep(100000); + exit(7); +} + +try { + pcntl_waitpid(-1, $status); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +try { + gethostbynamel("localhost"); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); + +function deep($n) { $a = str_repeat('x', 100); return $n ? deep($n - 1) . '' : $a; } +deep(50); + +// Only orphans are left: they are counted, and the wait ends once they settled +var_dump($ring->countPending()); +var_dump($ring->waitCompletions()); +var_dump($ring->countPending()); + +// The ring observed the child without collecting it, so it is still there for the next wait +var_dump(pcntl_waitpid($pid, $status) === $pid, pcntl_wexitstatus($status)); +?> +--EXPECT-- +gave up on Io\Operation\WaitPid +gave up on Io\Operation\GetAddrInfo +int(2) +array(0) { +} +int(0) +bool(true) +int(7) diff --git a/ext/standard/tests/streams/hooks/ring-orphan-read-commit.phpt b/ext/standard/tests/streams/hooks/ring-orphan-read-commit.phpt new file mode 100644 index 000000000000..d19654da64e2 --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-orphan-read-commit.phpt @@ -0,0 +1,85 @@ +--TEST-- +Io\Ring\Engine: bytes an orphaned read took are the stream's next ones, or the stream is broken +--SKIPIF-- + +--FILE-- +ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op); + // The data arrives once the op is in flight, and the backend takes it before the frame goes + Io\Hooks\set_hooks(null); + if ($this->peer) { + fwrite($this->peer, "hello"); + } + $until = hrtime(true) + 20000000; + while (hrtime(true) < $until); + throw new RuntimeException("gave up on " . $op::class); + } +} + +function orphaned_read(Io\Ring\Engine $ring, $fp, $peer, int $len): void +{ + Io\Hooks\set_hooks(new GiveUp($ring, $peer)); + try { + @fread($fp, $len); + } catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; + } + Io\Hooks\set_hooks(null); + // The stream stays frozen until the orphan settled + while ($ring->countPending()) { + $ring->waitCompletions(); + } +} + +$ring = new Io\Ring\Engine(); + +echo "-- buffered socket --\n"; +[$r, $w] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +stream_set_timeout($r, 1); +orphaned_read($ring, $r, $w, 5); +fwrite($w, " world"); +var_dump(fread($r, 11)); + +echo "-- unbuffered socket --\n"; +[$r, $w] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +stream_set_timeout($r, 1); +stream_set_read_buffer($r, 0); +orphaned_read($ring, $r, $w, 5); +var_dump(fread($r, 5), feof($r)); + +echo "-- buffered file --\n"; +$file = __DIR__ . '/ring-orphan-read-commit.txt'; +file_put_contents($file, "hello world"); +// A Files provider at open time: Windows opens the file for the ring only then +Io\Hooks\set_hooks(new GiveUp($ring, null)); +$fp = fopen($file, 'r'); +orphaned_read($ring, $fp, null, 5); +var_dump(fread($fp, 11), ftell($fp)); +fclose($fp); +?> +--CLEAN-- + +--EXPECT-- +-- buffered socket -- +gave up on Io\Operation\Recv +string(11) "hello world" +-- unbuffered socket -- +gave up on Io\Operation\Recv +bool(false) +bool(true) +-- buffered file -- +gave up on Io\Operation\Read +string(11) "hello world" +int(11) diff --git a/ext/standard/tests/streams/hooks/ring-poll-priority.phpt b/ext/standard/tests/streams/hooks/ring-poll-priority.phpt new file mode 100644 index 000000000000..60f576eaa4f0 --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-poll-priority.phpt @@ -0,0 +1,51 @@ +--TEST-- +Io\Ring\Engine: stream_select() reports the except set although ior has no priority event +--SKIPIF-- +supportsPriority()) die("skip the poll backend has no priority event"); +?> +--FILE-- +ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $c = $this->ring->submit($op); + // The urgent byte arrives once the wait is submitted + if ($this->peer) { + $peer = $this->peer; + $this->peer = null; + Io\Hooks\set_hooks(null); + stream_socket_sendto($peer, "!", STREAM_OOB); + Io\Hooks\set_hooks($this); + } + while ($c === null) { + foreach ($this->ring->waitCompletions() as $done) { + $c = $done; + } + } + return $c; + } +} + +$server = stream_socket_server('tcp://127.0.0.1:0'); +$client = stream_socket_client('tcp://' . stream_socket_get_name($server, false)); +$conn = stream_socket_accept($server); +$provider = new Waiting(new Io\Ring\Engine()); +$provider->peer = $client; +Io\Hooks\set_hooks($provider); +$r = []; +$w = []; +$e = [$conn]; +var_dump(stream_select($r, $w, $e, 5), count($e)); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +int(1) +int(1) diff --git a/ext/standard/tests/streams/hooks/ring-sigwait-requeue.phpt b/ext/standard/tests/streams/hooks/ring-sigwait-requeue.phpt new file mode 100644 index 000000000000..a6d9ac4fd3fa --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-sigwait-requeue.phpt @@ -0,0 +1,47 @@ +--TEST-- +Io\Ring\Engine: a signal taken by an abandoned SigWait goes back to the process +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- +ring->getHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion { + $this->ring->submit($op); + // The signal arrives and the ring takes it, then the caller leaves without the completion + posix_kill(getmypid(), SIGUSR1); + $until = hrtime(true) + 100_000_000; + while (hrtime(true) < $until); + throw new RuntimeException("gave up on " . $op::class); + } +}); + +try { + @pcntl_sigwaitinfo([SIGUSR1], $info); +} catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; +} +Io\Hooks\set_hooks(null); +var_dump($ring->waitCompletions(), $ring->countPending()); + +var_dump(pcntl_sigtimedwait([SIGUSR1], $info, 1) === SIGUSR1, $info['pid'] === getmypid()); +?> +--EXPECT-- +gave up on Io\Operation\SigWait +array(0) { +} +int(0) +bool(true) +bool(true) diff --git a/ext/standard/tests/streams/hooks/ring-socket-orphan.phpt b/ext/standard/tests/streams/hooks/ring-socket-orphan.phpt new file mode 100644 index 000000000000..bccdc41feb9c --- /dev/null +++ b/ext/standard/tests/streams/hooks/ring-socket-orphan.phpt @@ -0,0 +1,60 @@ +--TEST-- +Io\Ring\Engine: a Socket whose op outlived its frame stays busy until the op settled, and closing it drains the op +--EXTENSIONS-- +sockets +--SKIPIF-- + +--FILE-- +ring->getSupportedHookCapabilities(); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} + public function run(Io\Operation $op): Io\Completion + { + $this->ring->submit($op); + throw new RuntimeException("gave up on " . $op::class); + } +} + +$ring = new Io\Ring\Engine(); +foreach (['close', 'settle'] as $how) { + socket_create_pair(AF_UNIX, SOCK_STREAM, 0, $pair); + [$a, $b] = $pair; + Io\Hooks\set_hooks(new GiveUp($ring)); + try { + socket_recv($a, $buf, 5, 0); + } catch (RuntimeException $e) { + echo $e->getMessage(), "\n"; + } + Io\Hooks\set_hooks(null); + try { + socket_recv($a, $buf, 5, MSG_DONTWAIT); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + if ($how === 'close') { + socket_close($a); + var_dump($ring->countPending()); + } else { + while ($ring->countPending()) { + $ring->waitCompletions(); + } + socket_write($b, "hello"); + var_dump(socket_recv($a, $buf, 5, 0), $buf); + } +} +?> +--EXPECT-- +gave up on Io\Operation\Recv +Concurrent access to a socket +int(0) +gave up on Io\Operation\Recv +Concurrent access to a socket +int(5) +string(5) "hello" diff --git a/ext/standard/tests/streams/hooks/scheduler.inc b/ext/standard/tests/streams/hooks/scheduler.inc new file mode 100644 index 000000000000..4e8924639f9c --- /dev/null +++ b/ext/standard/tests/streams/hooks/scheduler.inc @@ -0,0 +1,118 @@ +queue = $queue ?? self::defaultQueue(); + $this->capabilities = self::capabilities($this->queue, $capabilities); + } + + /* IO_HOOKS_QUEUE=ring runs the same tests on Io\Ring\Engine */ + public static function defaultQueue(): \Io\OperationQueue + { + if (getenv('IO_HOOKS_QUEUE') === 'ring') { + if (!class_exists(\Io\Ring\Engine::class)) { + die("skip Io\\Ring\\Engine not available\n"); + } + return new \Io\Ring\Engine(); + } + return new \Io\Poll\OperationQueue(); + } + + /* IO_HOOKS_CAPS=files,directData,directAccept opts in to what the queue supports, default is its own set */ + public static function capabilities(\Io\OperationQueue $queue, ?array $wanted = null): array + { + if ($wanted === null) { + $env = (string) getenv('IO_HOOKS_CAPS'); + if ($env === '') { + return $queue->getHookCapabilities(); + } + $wanted = array_map( + fn ($name) => constant(\Io\Hooks\Capability::class . '::' . ucfirst(trim($name))), + explode(',', $env)); + } + $supported = $queue instanceof \Io\Ring\Engine + ? $queue->getSupportedHookCapabilities() + : $queue->getHookCapabilities(); + return array_values(array_filter($supported, fn ($c) => in_array($c, $wanted, true))); + } + + /** Schedule a task; it starts on the next pass of loop(). */ + public function spawn(callable $fn): void + { + $this->ready[] = [new \Fiber($fn), null]; + } + + /* The Hooks interface, called by the core. */ + + public function getCapabilities(): array + { + return $this->capabilities; + } + + public function run(\Io\Operation $op): \Io\Completion + { + $c = $this->queue->submit($op, \Fiber::getCurrent()); + if ($c !== null) { + /* Completed at submit: no suspend, no pass of the loop */ + $this->inline++; + return $c; + } + $this->inFlight++; + return \Fiber::suspend(); + } + + public function add(\Io\Registration $registration): void + { + $this->queue->add($registration); + } + + public function remove(\Io\Registration $registration): void + { + $this->queue->remove($registration); + } + + /* The event loop, called by the script on the main fiber. */ + public function loop(): void + { + while ($this->ready || $this->inFlight > 0) { + while ($this->ready) { + [$fiber, $value] = array_shift($this->ready); + $fiber->isStarted() ? $fiber->resume($value) : $fiber->start(); + } + if ($this->inFlight === 0) { + break; + } + foreach ($this->queue->waitCompletions() as $c) { + $this->inFlight--; + $this->ready[] = [$c->getData(), $c]; + } + } + } + + /* Test helper: start a task and let it run before continuing the caller. */ + public function go(callable $fn): void + { + $this->ready[] = [new \Fiber($fn), null]; + $fiber = \Fiber::getCurrent(); + if ($fiber === null) { + return; + } + $this->ready[] = [$fiber, null]; + \Fiber::suspend(); + } +} diff --git a/ext/standard/tests/streams/hooks/set-hooks-reentrant.phpt b/ext/standard/tests/streams/hooks/set-hooks-reentrant.phpt new file mode 100644 index 000000000000..65f2052995a2 --- /dev/null +++ b/ext/standard/tests/streams/hooks/set-hooks-reentrant.phpt @@ -0,0 +1,95 @@ +--TEST-- +IO hooks: set_hooks() from getCapabilities() and add() is refused, from run() it is safe +--EXTENSIONS-- +curl +--FILE-- +complete(Io\CompletionStatus::Unsupported); } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +final class Installed extends Base +{ + public function __destruct() { echo "Installed destroyed\n"; } +} + +final class CapsReplaces extends Base +{ + public function getCapabilities(): array + { + try { + Io\Hooks\set_hooks(null); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } + return []; + } +} + +final class RunDrops extends Base +{ + public string $state = ''; + + public function run(Io\Operation $op): Io\Completion + { + Io\Hooks\set_hooks(null); + $this->state = str_repeat('x', 3); + echo "run still has its object: {$this->state}\n"; + return $op->complete(Io\CompletionStatus::Unsupported); + } + + public function __destruct() { echo "RunDrops destroyed\n"; } +} + +final class AddReplaces extends Base +{ + private bool $reported = false; + + public function add(Io\Registration $registration): void + { + try { + Io\Hooks\set_hooks(new Base()); + } catch (Error $e) { + /* libcurl may report the socket more than once for one connect */ + if (!$this->reported) { + echo $e->getMessage(), "\n"; + $this->reported = true; + } + } + } +} + +Io\Hooks\set_hooks(new Installed()); +$previous = Io\Hooks\set_hooks(new CapsReplaces()); +var_dump($previous instanceof Installed); +unset($previous); + +Io\Hooks\set_hooks(new RunDrops()); +usleep(1); +var_dump(Io\Hooks\get_hooks()); + +Io\Hooks\set_hooks(new AddReplaces()); +$ch = curl_init("http://127.0.0.1:1/"); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, 200); +var_dump(curl_exec($ch)); +var_dump(Io\Hooks\get_hooks() instanceof AddReplaces); +Io\Hooks\set_hooks(null); +echo "done\n"; +?> +--EXPECT-- +Io\Hooks\set_hooks() cannot be called from getCapabilities(), add(), remove() or a provider destructor +bool(true) +Installed destroyed +run still has its object: xxx +RunDrops destroyed +NULL +Io\Hooks\set_hooks() cannot be called from getCapabilities(), add(), remove() or a provider destructor +bool(false) +bool(true) +done diff --git a/ext/standard/tests/streams/hooks/shutdown-bailout-frozen.phpt b/ext/standard/tests/streams/hooks/shutdown-bailout-frozen.phpt new file mode 100644 index 000000000000..944ff0b6149c --- /dev/null +++ b/ext/standard/tests/streams/hooks/shutdown-bailout-frozen.phpt @@ -0,0 +1,28 @@ +--TEST-- +IO hooks: a fatal error while a fiber is suspended in an operation leaves no stream frozen for the shutdown +--EXTENSIONS-- +zlib +--FILE-- +start(); +echo "suspended\n"; +eval('function strlen() {}'); +?> +--EXPECTF-- +suspended + +Fatal error: Cannot redeclare function strlen() in %s : eval()'d code on line %d%A diff --git a/ext/standard/tests/streams/hooks/shutdown-close-no-queue.phpt b/ext/standard/tests/streams/hooks/shutdown-close-no-queue.phpt new file mode 100644 index 000000000000..deff824c07be --- /dev/null +++ b/ext/standard/tests/streams/hooks/shutdown-close-no-queue.phpt @@ -0,0 +1,21 @@ +--TEST-- +IO hooks: a stream closed after the request shutdown waits without recreating the core queue +--EXTENSIONS-- +zlib +--FILE-- + 0); +stream_set_blocking($a, true); +stream_set_timeout($a, 0, 100000); +stream_filter_append($a, 'zlib.deflate', STREAM_FILTER_WRITE); +fwrite($a, random_bytes(100)); +echo "end\n"; +?> +--EXPECTF-- +end + +%s: PHP Request Shutdown: Send of %d bytes failed with errno=%d %s in Unknown on line 0 diff --git a/ext/standard/tests/streams/hooks/signal-ops-deadline.phpt b/ext/standard/tests/streams/hooks/signal-ops-deadline.phpt new file mode 100644 index 000000000000..edb7f83c79b9 --- /dev/null +++ b/ext/standard/tests/streams/hooks/signal-ops-deadline.phpt @@ -0,0 +1,31 @@ +--TEST-- +IO hooks: a signal wait the provider keeps reporting as taken elsewhere ends at its deadline +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +complete(Io\CompletionStatus::Done, -1, PCNTL_EAGAIN); + } + return $op->complete(Io\CompletionStatus::Unsupported); + } + public function add(Io\Registration $registration): void {} + public function remove(Io\Registration $registration): void {} +} + +pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1]); +Io\Hooks\set_hooks(new Again()); +var_dump(pcntl_sigtimedwait([SIGUSR1], $info, 0, 1000000)); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +bool(false) diff --git a/ext/standard/tests/streams/hooks/signal-ops.phpt b/ext/standard/tests/streams/hooks/signal-ops.phpt new file mode 100644 index 000000000000..4d99bc600a63 --- /dev/null +++ b/ext/standard/tests/streams/hooks/signal-ops.phpt @@ -0,0 +1,85 @@ +--TEST-- +IO hooks: pcntl_sigwaitinfo() and pcntl_sigtimedwait() are SigWait operations +--EXTENSIONS-- +pcntl +posix +--SKIPIF-- + +--FILE-- + $s === SIGUSR1 ? 'USR1' : ($s === SIGUSR2 ? 'USR2' : $s), $op->getSignals()); + $this->seen[] = [$names, get_class($op->getHandle()), $op->getTimeout() !== null]; + } + return parent::run($op); + } +} + +$scheduler = new Counting(); +Io\Hooks\set_hooks($scheduler); + +pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1, SIGUSR2]); + +$scheduler->spawn(function () { + $signo = pcntl_sigwaitinfo([SIGUSR1], $info); + var_dump($signo === SIGUSR1, $info['signo'] === SIGUSR1, $info['pid'] === posix_getpid()); +}); + +$scheduler->spawn(function () { + $start = hrtime(true); + var_dump(pcntl_sigtimedwait([SIGUSR2], $info, 0, 200000000)); + var_dump((hrtime(true) - $start) / 1e6 >= 150); +}); + +$scheduler->spawn(function () { + usleep(50000); + echo "sending\n"; + posix_kill(posix_getpid(), SIGUSR1); +}); + +$scheduler->loop(); +var_dump($scheduler->seen); +?> +--EXPECT-- +sending +bool(true) +bool(true) +bool(true) +bool(false) +bool(true) +array(2) { + [0]=> + array(3) { + [0]=> + array(1) { + [0]=> + string(4) "USR1" + } + [1]=> + string(20) "Io\Poll\SignalHandle" + [2]=> + bool(false) + } + [1]=> + array(3) { + [0]=> + array(1) { + [0]=> + string(4) "USR2" + } + [1]=> + string(20) "Io\Poll\SignalHandle" + [2]=> + bool(true) + } +} diff --git a/ext/standard/tests/streams/hooks/sleep.phpt b/ext/standard/tests/streams/hooks/sleep.phpt new file mode 100644 index 000000000000..d3e144be8ba0 --- /dev/null +++ b/ext/standard/tests/streams/hooks/sleep.phpt @@ -0,0 +1,41 @@ +--TEST-- +IO hooks: the sleep family is a Timer operation and fibers sleep concurrently +--FILE-- +spawn(function () { + usleep(60000); + echo "usleep done\n"; +}); +$scheduler->spawn(function () { + time_nanosleep(0, 20000000); + echo "time_nanosleep done\n"; +}); +$scheduler->spawn(function () { + var_dump(sleep(0)); + echo "sleep done\n"; +}); +$scheduler->loop(); + +$elapsed_ms = (hrtime(true) - $start) / 1e6; +var_dump($elapsed_ms >= 60, $elapsed_ms < 500); +var_dump(Io\Hooks\get_hooks() === $scheduler, Io\Hooks\is_active()); +var_dump(Io\Hooks\set_hooks(null) === $scheduler, Io\Hooks\is_active()); +?> +--EXPECT-- +int(0) +sleep done +time_nanosleep done +usleep done +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) +bool(false) diff --git a/ext/standard/tests/streams/hooks/stream-errors-fiber-attribution.phpt b/ext/standard/tests/streams/hooks/stream-errors-fiber-attribution.phpt new file mode 100644 index 000000000000..6e2f0621754f --- /dev/null +++ b/ext/standard/tests/streams/hooks/stream-errors-fiber-attribution.phpt @@ -0,0 +1,86 @@ +--TEST-- +IO hooks: stream errors of interleaved fibers reach the handler of their own stream +--FILE-- +socket = $GLOBALS['sockets'][parse_url($path, PHP_URL_HOST)]; + return true; + } + + public function stream_read(int $count): string + { + $this->reads++; + if ($this->reads === 2) { + fread($this->socket, 16); + } + return $this->reads <= 2 ? str_repeat('x', $count + 1) : ''; + } + + public function stream_eof(): bool + { + return $this->reads >= 3; + } + + public function stream_stat(): array + { + return []; + } +} + +class StreamA extends ProxyStream {} +class StreamB extends ProxyStream {} + +stream_wrapper_register('proxy-a', StreamA::class); +stream_wrapper_register('proxy-b', StreamB::class); + +function context(string $name): mixed +{ + return stream_context_create(['stream' => [ + 'error_mode' => StreamErrorMode::Silent, + 'error_handler' => static function (array $errors) use ($name): void { + foreach ($errors as $error) { + echo "$name: ", strstr($error->message, ' - ', true), "\n"; + } + }, + ]]); +} + +$scheduler = new Scheduler(); +Io\Hooks\set_hooks($scheduler); + +[$a, $feedA] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); +[$b, $feedB] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); +$sockets = ['a' => $a, 'b' => $b]; + +$scheduler->spawn(function () use ($feedB) { + $stream = fopen('proxy-a://a', 'r', false, context('A')); + stream_get_contents($stream); + echo "A done\n"; + fwrite($feedB, 'b'); +}); +$scheduler->spawn(function () { + $stream = fopen('proxy-b://b', 'r', false, context('B')); + stream_get_contents($stream); + echo "B done\n"; +}); +$scheduler->spawn(function () use ($feedA) { + fwrite($feedA, 'a'); +}); +$scheduler->loop(); +?> +--EXPECT-- +A: StreamA::stream_read +A: StreamA::stream_read +A done +B: StreamB::stream_read +B: StreamB::stream_read +B done diff --git a/ext/standard/tests/streams/hooks/stream-errors-many-fibers.phpt b/ext/standard/tests/streams/hooks/stream-errors-many-fibers.phpt new file mode 100644 index 000000000000..04bc043c8092 --- /dev/null +++ b/ext/standard/tests/streams/hooks/stream-errors-many-fibers.phpt @@ -0,0 +1,48 @@ +--TEST-- +IO hooks: stream error operations of many suspended fibers do not add up to the depth limit +--SKIPIF-- + +--FILE-- +spawn(function () use ($i, &$pairs, &$results) { + $results[$i] = fread($pairs[$i][0], 16); + }); +} +$scheduler->spawn(function () use ($n, &$pairs) { + for ($i = 0; $i < $n; $i++) { + fwrite($pairs[$i][1], "r$i"); + } +}); +$scheduler->loop(); + +$ok = 0; +for ($i = 0; $i < $n; $i++) { + $ok += ($results[$i] ?? null) === "r$i"; +} +var_dump($ok); +?> +--EXPECT-- +int(1500) diff --git a/ext/standard/tests/streams/hooks/stream-free-frozen.phpt b/ext/standard/tests/streams/hooks/stream-free-frozen.phpt new file mode 100644 index 000000000000..cc9ae62a49dc --- /dev/null +++ b/ext/standard/tests/streams/hooks/stream-free-frozen.phpt @@ -0,0 +1,39 @@ +--TEST-- +IO hooks: a stream frozen by a suspended op is not freed under it +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => ['pipe', 'w']], $pipes); +$out = []; +$scheduler->spawn(function () use ($pipes, &$out) { + $out['read'] = fread($pipes[1], 10); + $out['after'] = is_resource($pipes[1]); +}); +$scheduler->spawn(function () use ($p, &$out) { + $out['proc_close'] = proc_close($p); +}); +$scheduler->loop(); +var_dump($out['read'], $out['after'], $out['proc_close']); +var_dump(fclose($pipes[1])); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +string(3) "bye" +bool(true) +int(0) +bool(true) diff --git a/ext/standard/tests/streams/hooks/stream-select-frozen.phpt b/ext/standard/tests/streams/hooks/stream-select-frozen.phpt new file mode 100644 index 000000000000..1cf6da021f55 --- /dev/null +++ b/ext/standard/tests/streams/hooks/stream-select-frozen.phpt @@ -0,0 +1,44 @@ +--TEST-- +IO hooks: stream_select() freezes the streams of its sets while it waits +--FILE-- +spawn(function () use ($a, $b) { + /* The same stream in two sets is frozen once */ + $r = [$a]; + $w = null; + $e = [$a]; + var_dump(stream_select($r, $w, $e, 10)); + var_dump(count($r)); + fclose($a); + echo "A closed\n"; +}); +$scheduler->spawn(function () use ($a, $b) { + foreach ([ + 'fclose' => fn () => fclose($a), + 'fread' => fn () => fread($a, 1), + 'stream_select' => function () use ($a) { $r = [$a]; $w = $e = null; stream_select($r, $w, $e, 0); }, + ] as $name => $fn) { + try { + $fn(); + } catch (Error $e) { + echo "B $name: ", $e->getMessage(), "\n"; + } + } + fwrite($b, "x"); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +B fclose: Concurrent access to a stream +B fread: Concurrent access to a stream +B stream_select: Concurrent access to a stream +int(1) +int(1) +A closed diff --git a/ext/standard/tests/streams/hooks/stream-select.phpt b/ext/standard/tests/streams/hooks/stream-select.phpt new file mode 100644 index 000000000000..7b6f4ed80a25 --- /dev/null +++ b/ext/standard/tests/streams/hooks/stream-select.phpt @@ -0,0 +1,79 @@ +--TEST-- +IO hooks: stream_select() polls first and waits with one Any operation, reporting every ready stream +--FILE-- +getOperations(); + $this->anys[] = count(array_filter($members, fn ($m) => $m instanceof \Io\Operation\Poll)) + . '+' . count(array_filter($members, fn ($m) => $m instanceof \Io\Operation\Timer)); + } + return parent::run($op); + } +} + +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +[$a, $b] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); +[$c, $d] = stream_socket_pair(PHP_OS_FAMILY === 'Windows' ? STREAM_PF_INET : STREAM_PF_UNIX, STREAM_SOCK_STREAM, 0); + +$scheduler->spawn(function () use ($a, $c) { + // Nothing readable yet: the select times out + $r = [$a, $c]; $w = null; $e = null; + $start = hrtime(true); + var_dump(stream_select($r, $w, $e, 0, 50000), $r); + var_dump((hrtime(true) - $start) / 1e6 >= 40); + + // Both readable by now (the writer's sends are ops of their own on a + // direct queue, so leave it a wide margin): both reported, the writable side too, + // by the zero-timeout poll that precedes the Any, so no op reaches the provider + usleep(150000); + $r = [$a, $c]; $w = [$a]; $e = null; + var_dump(stream_select($r, $w, $e, 1), count($r), $w === [$a]); + var_dump(fread($a, 10), fread($c, 10)); + + // Blocking select woken by the other fiber, told to go on + fwrite($a, "go"); + $r = [$a]; $w = null; $e = null; + var_dump(stream_select($r, $w, $e, null), $r === [$a], fread($a, 10)); +}); +$scheduler->spawn(function () use ($b, $d) { + usleep(70000); + fwrite($b, "one"); + fwrite($d, "two"); + fread($b, 2); + // Let the select block before the wakeup: a direct queue sends without a fiber switch + usleep(20000); + fwrite($b, "three"); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +var_dump($scheduler->anys); +?> +--EXPECT-- +int(0) +array(0) { +} +bool(true) +int(3) +int(2) +bool(true) +string(3) "one" +string(3) "two" +int(1) +bool(true) +string(5) "three" +array(2) { + [0]=> + string(3) "2+1" + [1]=> + string(3) "1+0" +} diff --git a/ext/standard/tests/streams/hooks/submit-inline.phpt b/ext/standard/tests/streams/hooks/submit-inline.phpt new file mode 100644 index 000000000000..282c531296e8 --- /dev/null +++ b/ext/standard/tests/streams/hooks/submit-inline.phpt @@ -0,0 +1,73 @@ +--TEST-- +IO hooks: submit() returns an operation the queue completed at submit, and the provider does not suspend +--SKIPIF-- + +--FILE-- +q = self::defaultQueue(); + parent::__construct($this->q, [Io\Hooks\Capability::EdgeRegistrations]); + } + + public function run(\Io\Operation $op): \Io\Completion + { + if ($op instanceof \Io\Operation\Timer) { + return parent::run($op); + } + $c = $this->q->submit($op, \Fiber::getCurrent()); + if ($c !== null) { + $this->log[] = substr($op::class, 13) . ' at submit ' . $c->getStatus()->name; + return $c; + } + $this->log[] = substr($op::class, 13) . ' waited'; + $this->inFlight++; + return \Fiber::suspend(); + } +} + +// Datagram waits are readiness ops after EAGAIN on both queues +[$a, $b] = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_DGRAM, 0); +$scheduler = new Tracing(); +Io\Hooks\set_hooks($scheduler); + +$scheduler->spawn(function () use ($a) { + // The first wait is real. A datagram arriving while nothing waits sets the pair's ready bit + // and the syscall takes it without an op, so the wait after that is answered at submit, once, + // and the next waits for real. + var_dump(stream_socket_recvfrom($a, 10)); + usleep(30000); + var_dump(stream_socket_recvfrom($a, 10)); + var_dump(stream_socket_recvfrom($a, 10)); +}); +$scheduler->spawn(function () use ($b) { + usleep(10000); + fwrite($b, "one"); + usleep(10000); + fwrite($b, "two"); + usleep(40000); + fwrite($b, "three"); +}); +$scheduler->loop(); +Io\Hooks\set_hooks(null); +print_r($scheduler->log); +?> +--EXPECT-- +string(3) "one" +string(3) "two" +string(5) "three" +Array +( + [0] => Poll waited + [1] => Poll at submit Done + [2] => Poll waited +) diff --git a/ext/standard/tests/streams/hooks/use-case.phpt b/ext/standard/tests/streams/hooks/use-case.phpt new file mode 100644 index 000000000000..77b08d29e995 --- /dev/null +++ b/ext/standard/tests/streams/hooks/use-case.phpt @@ -0,0 +1,69 @@ +--TEST-- +Stream hook: use case +--FILE-- +go($fn); +} + +$scheduler->spawn(function () { + $server = stream_socket_server('tcp://localhost:0'); + $socket_name = stream_socket_get_name($server, false); + if (!preg_match('/:(\d+)$/', $socket_name, $m)) { + die("Could not extract port from '$socket_name'"); + } + $port = $m[1]; + + go(function () use ($server) { + $client = stream_socket_accept($server); + go(function () use ($client) { + $headers = []; + while (!feof($client)) { + $line = fgets($client); + if ($line === false) { + break; + } + if ($line === "\r\n") { + break; + } + $headers[] = $line; + } + foreach ($headers as $header) { + echo "> " . trim($header) . "\n"; + } + fwrite($client, "HTTP/1.0 200 OK\r\n"); + fwrite($client, "\r\n"); + fwrite($client, "Hello world!\n"); + }); + }); + + go(function () use ($port) { + $fd = stream_socket_client("tcp://localhost:$port"); + fwrite($fd, "GET / HTTP/1.0\r\n"); + fwrite($fd, "Host: localhost\r\n"); + fwrite($fd, "\r\n"); + /* Not feof(): on Windows the server's close lingers as a Poll op, so + * the client may read the last line before the close and see EOF only + * from the next read */ + while (($line = fgets($fd)) !== false) { + echo trim("< " . $line) . "\n"; + } + }); +}); +$scheduler->loop(); + +?> +--EXPECT-- +> GET / HTTP/1.0 +> Host: localhost +< HTTP/1.0 200 OK +< +< Hello world! diff --git a/ext/standard/tests/streams/hooks/wait-completions-signal.phpt b/ext/standard/tests/streams/hooks/wait-completions-signal.phpt new file mode 100644 index 000000000000..b2a452f93d4c --- /dev/null +++ b/ext/standard/tests/streams/hooks/wait-completions-signal.phpt @@ -0,0 +1,54 @@ +--TEST-- +IO hooks: waitCompletions() returns early for a signal, so its handler runs while ops are pending +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- +spawn(function () use ($a) { + var_dump(fread($a, 10)); +}); +pcntl_alarm(1); +$scheduler->loop(); + +/* The dispatch style: an empty array first */ +pcntl_async_signals(false); +$fiber = new Fiber(function () use ($a) { + var_dump(fread($a, 10)); +}); +$fiber->start(); +pcntl_alarm(1); +var_dump($queue->waitCompletions()); +pcntl_signal_dispatch(); +$completions = $queue->waitCompletions(); +var_dump(count($completions)); +$fiber->resume($completions[0]); +Io\Hooks\set_hooks(null); +?> +--EXPECT-- +handler +string(1) "x" +array(0) { +} +handler +int(1) +string(1) "x" diff --git a/ext/standard/tests/streams/socket_read_keeps_waiting_through_signal.phpt b/ext/standard/tests/streams/socket_read_keeps_waiting_through_signal.phpt new file mode 100644 index 000000000000..f8167baeb82e --- /dev/null +++ b/ext/standard/tests/streams/socket_read_keeps_waiting_through_signal.phpt @@ -0,0 +1,35 @@ +--TEST-- +Without a provider a blocking socket read keeps waiting through a handled signal, as on master +--EXTENSIONS-- +pcntl +posix +--FILE-- + +--EXPECT-- +string(5) "data +" +int(1) diff --git a/ext/standard/tests/streams/stream_errors_fiber_bailout_in_operation.phpt b/ext/standard/tests/streams/stream_errors_fiber_bailout_in_operation.phpt new file mode 100644 index 000000000000..c796a04dd51c --- /dev/null +++ b/ext/standard/tests/streams/stream_errors_fiber_bailout_in_operation.phpt @@ -0,0 +1,57 @@ +--TEST-- +Stream errors: a fiber bailing out inside a stream operation leaves the main flow clean +--FILE-- +reads++; + if ($this->reads === 2 && Fiber::getCurrent()) { + eval('function strlen() {}'); + } + return $this->reads <= 2 ? str_repeat('x', $count + 1) : ''; + } + + public function stream_eof(): bool + { + return $this->reads >= 3; + } + + public function stream_stat(): array + { + return []; + } +} + +stream_wrapper_register('fatal', FatalStream::class); + +register_shutdown_function(function () { + include __DIR__ . '/stream_errors_fiber_bailout_in_operation_missing.inc'; + $stream = fopen('fatal://x', 'r'); + var_dump(strlen(fread($stream, 10))); +}); + +$fiber = new Fiber(function () { + $stream = fopen('fatal://x', 'r'); + stream_get_contents($stream); +}); +$fiber->start(); +?> +--EXPECTF-- +Fatal error: Cannot redeclare function strlen() in %s : eval()'d code on line %d + +Warning: include(): Failed to open stream: No such file or directory in %s on line %d + +Warning: include(): Failed opening '%s' for inclusion (include_path=%s) in %s on line %d + +Warning: fread(): FatalStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost in %s on line %d +int(10) diff --git a/ext/standard/tests/streams/stream_errors_fiber_destroyed_in_operation.phpt b/ext/standard/tests/streams/stream_errors_fiber_destroyed_in_operation.phpt new file mode 100644 index 000000000000..f52c4d33134a --- /dev/null +++ b/ext/standard/tests/streams/stream_errors_fiber_destroyed_in_operation.phpt @@ -0,0 +1,63 @@ +--TEST-- +Stream errors: a fiber destroyed while suspended inside a stream operation +--FILE-- +reads++; + if ($this->reads === 2 && Fiber::getCurrent()) { + Fiber::suspend(); + } + return $this->reads <= 2 ? str_repeat('x', $count + 1) : ''; + } + + public function stream_eof(): bool + { + return $this->reads >= 3; + } + + public function stream_stat(): array + { + return []; + } +} + +stream_wrapper_register('suspend', SuspendingStream::class); + +$context = stream_context_create(['stream' => [ + 'error_mode' => StreamErrorMode::Silent, + 'error_store' => StreamErrorStore::None, +]]); + +$fiber = new Fiber(function () use ($context) { + $stream = fopen('suspend://x', 'r', false, $context); + stream_get_contents($stream); + echo "not reached\n"; +}); +$fiber->start(); +unset($fiber); +gc_collect_cycles(); +echo "destroyed\n"; + +$stream = fopen('suspend://x', 'r'); +var_dump(strlen(stream_get_contents($stream))); +var_dump(count(stream_last_errors())); +?> +--EXPECTF-- +destroyed + +Warning: stream_get_contents(): SuspendingStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost in %s on line %d + +Warning: stream_get_contents(): SuspendingStream::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost in %s on line %d +int(16384) +int(0) diff --git a/ext/standard/tests/streams/stream_errors_fiber_suspend_in_wrapper.phpt b/ext/standard/tests/streams/stream_errors_fiber_suspend_in_wrapper.phpt new file mode 100644 index 000000000000..8f130c123733 --- /dev/null +++ b/ext/standard/tests/streams/stream_errors_fiber_suspend_in_wrapper.phpt @@ -0,0 +1,74 @@ +--TEST-- +Stream errors: operations of fibers suspended in a user wrapper keep their own errors +--FILE-- +reads++; + if ($this->reads === 2) { + Fiber::suspend(); + } + return $this->reads <= 2 ? str_repeat('x', $count + 1) : ''; + } + + public function stream_eof(): bool + { + return $this->reads >= 3; + } + + public function stream_stat(): array + { + return []; + } +} + +class StreamA extends SuspendingStream {} +class StreamB extends SuspendingStream {} + +stream_wrapper_register('suspend-a', StreamA::class); +stream_wrapper_register('suspend-b', StreamB::class); + +function read_all(string $name, string $url): void +{ + $context = stream_context_create(['stream' => [ + 'error_mode' => StreamErrorMode::Silent, + 'error_handler' => static function (array $errors) use ($name): void { + foreach ($errors as $error) { + echo "$name: ", strstr($error->message, ' - ', true), "\n"; + } + }, + ]]); + $stream = fopen($url, 'r', false, $context); + var_dump(strlen(stream_get_contents($stream))); +} + +$a = new Fiber(fn () => read_all('A', 'suspend-a://x')); +$b = new Fiber(fn () => read_all('B', 'suspend-b://x')); +$a->start(); +$b->start(); +$a->resume(); +$b->resume(); + +$stream = fopen('suspend-a://x', 'r'); +var_dump(strlen(fread($stream, 10))); +?> +--EXPECTF-- +A: StreamA::stream_read +A: StreamA::stream_read +int(16384) +B: StreamB::stream_read +B: StreamB::stream_read +int(16384) + +Warning: fread(): StreamA::stream_read - read 1 bytes more data than requested (8193 read, 8192 max) - excess data will be lost in %s on line %d +int(10) diff --git a/ext/standard/tests/streams/stream_socket_blocking_mode_fork_child.phpt b/ext/standard/tests/streams/stream_socket_blocking_mode_fork_child.phpt new file mode 100644 index 000000000000..66f0c0a3924a --- /dev/null +++ b/ext/standard/tests/streams/stream_socket_blocking_mode_fork_child.phpt @@ -0,0 +1,32 @@ +--TEST-- +A forked child closing a socket stream leaves the parent's descriptor non-blocking +--EXTENSIONS-- +pcntl +--SKIPIF-- + +--FILE-- + +--EXPECT-- +int(0) +bool(false) +bool(true) diff --git a/ext/standard/tests/streams/stream_socket_blocking_mode_restored.phpt b/ext/standard/tests/streams/stream_socket_blocking_mode_restored.phpt new file mode 100644 index 000000000000..10caf3b5bd44 --- /dev/null +++ b/ext/standard/tests/streams/stream_socket_blocking_mode_restored.phpt @@ -0,0 +1,38 @@ +--TEST-- +Socket streams give a shared descriptor back in blocking mode +--SKIPIF-- + +--FILE-- + ['pipe', 'r'], 1 => $b, 2 => ['pipe', 'w']], $pipes); +/* The parent's own stream on the same socket also gives it back blocking */ +fclose($b); +var_dump(fgets($a)); +fwrite($pipes[0], "go\n"); +fclose($pipes[0]); +/* Let the writer fill the socket buffer before reading: in non-blocking + * mode it fails with EAGAIN and reports it on stderr */ +$r = [$pipes[2]]; $w = $e = null; +stream_select($r, $w, $e, 0, 500000); +$n = 0; +while (!feof($a)) { + $n += strlen(fread($a, 65536)); +} +var_dump($n, stream_get_contents($pipes[2]), proc_close($p)); +?> +--EXPECT-- +string(4) "php +" +int(1000000) +string(0) "" +int(0) diff --git a/ext/standard/tests/streams/stream_socket_read_timed_out.phpt b/ext/standard/tests/streams/stream_socket_read_timed_out.phpt new file mode 100644 index 000000000000..42db2215ece4 --- /dev/null +++ b/ext/standard/tests/streams/stream_socket_read_timed_out.phpt @@ -0,0 +1,42 @@ +--TEST-- +Socket stream reads report timed_out only when the timeout expired +--SKIPIF-- + +--FILE-- + +--EXPECT-- +string(2) "a +" +string(3) "bcd" +bool(false) +string(0) "" +bool(false) +string(1) "x" +bool(false) +bool(true) +bool(false) +bool(true) diff --git a/ext/standard/tests/streams/stream_socket_recvfrom_blocking_no_timeout.phpt b/ext/standard/tests/streams/stream_socket_recvfrom_blocking_no_timeout.phpt new file mode 100644 index 000000000000..23438b52a456 --- /dev/null +++ b/ext/standard/tests/streams/stream_socket_recvfrom_blocking_no_timeout.phpt @@ -0,0 +1,26 @@ +--TEST-- +stream_socket_recvfrom() on a blocking stream is not bound by default_socket_timeout +--SKIPIF-- + +--INI-- +default_socket_timeout=1 +--FILE-- + +--EXPECT-- +string(5) "hello" diff --git a/ext/xmlreader/php_xmlreader.c b/ext/xmlreader/php_xmlreader.c index 6a7fb65e2af0..a608899862f4 100644 --- a/ext/xmlreader/php_xmlreader.c +++ b/ext/xmlreader/php_xmlreader.c @@ -388,6 +388,8 @@ ZEND_GET_MODULE(xmlreader) /* {{{ xmlreader_free_resources */ static void xmlreader_free_resources(xmlreader_object *intern) { + /* Closing the input can wait too */ + intern->in_use = true; if (intern->input) { xmlFreeParserInputBuffer(intern->input); intern->input = NULL; @@ -403,6 +405,7 @@ static void xmlreader_free_resources(xmlreader_object *intern) { intern->schema = NULL; } #endif + intern->in_use = false; } /* }}} */ @@ -430,6 +433,18 @@ zend_object *xmlreader_objects_new(zend_class_entry *class_type) } /* }}} */ +/* The libxml IO callbacks can suspend the fiber or run user code, which must + * not free, reset or re-enter the reader under the running call. The caller's + * frame keeps the object alive. */ +static bool xmlreader_check_not_in_use(const xmlreader_object *intern) +{ + if (UNEXPECTED(intern->in_use)) { + zend_throw_error(NULL, "Attempt to use XMLReader while it is reading"); + return false; + } + return true; +} + /* {{{ php_xmlreader_string_arg */ static void php_xmlreader_string_arg(INTERNAL_FUNCTION_PARAMETERS, xmlreader_read_one_char_t internal_function) { zval *id; @@ -496,8 +511,13 @@ static void php_xmlreader_no_arg_string(INTERNAL_FUNCTION_PARAMETERS, xmlreader_ id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { + intern->in_use = true; retchar = (char *)internal_function(intern->ptr); + intern->in_use = false; } if (retchar) { RETVAL_STRING(retchar); @@ -525,12 +545,17 @@ static void php_xmlreader_set_relaxng_schema(INTERNAL_FUNCTION_PARAMETERS, int t #ifdef LIBXML_SCHEMAS_ENABLED xmlreader_object *intern = Z_XMLREADER_P(ZEND_THIS); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { int retval = -1; xmlRelaxNGPtr schema = NULL; if (source) { + intern->in_use = true; schema = _xmlreader_get_relaxNG(source, source_len, type, NULL, NULL); + intern->in_use = false; if (schema) { retval = xmlTextReaderRelaxNGSetSchema(intern->ptr, schema); } @@ -573,6 +598,9 @@ PHP_METHOD(XMLReader, close) id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } /* libxml is segfaulting in versions up to 2.6.8 using xmlTextReaderClose so for now we will free the whole reader when close is called as it would get rebuilt on a new load anyways */ @@ -715,6 +743,9 @@ PHP_METHOD(XMLReader, moveToAttribute) id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { retval = xmlTextReaderMoveToAttribute(intern->ptr, (xmlChar *)name); if (retval == 1) { @@ -742,6 +773,9 @@ PHP_METHOD(XMLReader, moveToAttributeNo) id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { retval = xmlTextReaderMoveToAttributeNo(intern->ptr, attr_pos); if (retval == 1) { @@ -780,6 +814,9 @@ PHP_METHOD(XMLReader, moveToAttributeNs) id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { retval = xmlTextReaderMoveToAttributeNs(intern->ptr, (xmlChar *)name, (xmlChar *)ns_uri); if (retval == 1) { @@ -794,6 +831,9 @@ PHP_METHOD(XMLReader, moveToAttributeNs) /* {{{ Moves the position of the current instance to the node that contains the current Attribute node. */ PHP_METHOD(XMLReader, moveToElement) { + if (!xmlreader_check_not_in_use(Z_XMLREADER_P(ZEND_THIS))) { + RETURN_THROWS(); + } php_xmlreader_no_arg(INTERNAL_FUNCTION_PARAM_PASSTHRU, xmlTextReaderMoveToElement); } /* }}} */ @@ -801,6 +841,9 @@ PHP_METHOD(XMLReader, moveToElement) /* {{{ Moves the position of the current instance to the first attribute associated with the current node. */ PHP_METHOD(XMLReader, moveToFirstAttribute) { + if (!xmlreader_check_not_in_use(Z_XMLREADER_P(ZEND_THIS))) { + RETURN_THROWS(); + } php_xmlreader_no_arg(INTERNAL_FUNCTION_PARAM_PASSTHRU, xmlTextReaderMoveToFirstAttribute); } /* }}} */ @@ -808,6 +851,9 @@ PHP_METHOD(XMLReader, moveToFirstAttribute) /* {{{ Moves the position of the current instance to the next attribute associated with the current node. */ PHP_METHOD(XMLReader, moveToNextAttribute) { + if (!xmlreader_check_not_in_use(Z_XMLREADER_P(ZEND_THIS))) { + RETURN_THROWS(); + } php_xmlreader_no_arg(INTERNAL_FUNCTION_PARAM_PASSTHRU, xmlTextReaderMoveToNextAttribute); } /* }}} */ @@ -823,12 +869,17 @@ PHP_METHOD(XMLReader, read) id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (!intern->ptr) { zend_throw_error(NULL, "Data must be loaded before reading"); RETURN_THROWS(); } + intern->in_use = true; retval = xmlTextReaderRead(intern->ptr); + intern->in_use = false; if (retval == -1) { RETURN_FALSE; } else { @@ -852,14 +903,20 @@ PHP_METHOD(XMLReader, next) id = ZEND_THIS; intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { + intern->in_use = true; retval = xmlTextReaderNext(intern->ptr); while (name != NULL && retval == 1) { if (xmlStrEqual(xmlTextReaderConstLocalName(intern->ptr), (xmlChar *)name)) { + intern->in_use = false; RETURN_TRUE; } retval = xmlTextReaderNext(intern->ptr); } + intern->in_use = false; if (retval == -1) { RETURN_FALSE; } else { @@ -907,6 +964,9 @@ static void xml_reader_from_uri(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * if (id != NULL) { ZEND_ASSERT(instanceof_function(Z_OBJCE_P(id), xmlreader_class_entry)); intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } xmlreader_free_resources(intern); } @@ -923,9 +983,15 @@ static void xml_reader_from_uri(INTERNAL_FUNCTION_PARAMETERS, zend_class_entry * valid_file = _xmlreader_get_valid_file_path(source, resolved_path, MAXPATHLEN ); if (valid_file) { + if (intern) { + intern->in_use = true; + } PHP_LIBXML_SANITIZE_GLOBALS(reader_for_file); reader = xmlReaderForFile(valid_file, encoding, options); PHP_LIBXML_RESTORE_GLOBALS(reader_for_file); + if (intern) { + intern->in_use = false; + } } if (reader == NULL) { @@ -968,7 +1034,8 @@ PHP_METHOD(XMLReader, fromUri) static int xml_reader_stream_read(void *context, char *buffer, int len) { zend_resource *resource = context; - if (EXPECTED(resource->ptr)) { + /* libxml retries a failed read, which must not run on an unwinding fiber */ + if (EXPECTED(resource->ptr) && !EG(exception)) { php_stream *stream = resource->ptr; return php_stream_read(stream, buffer, len); } @@ -1080,9 +1147,14 @@ PHP_METHOD(XMLReader, setSchema) #ifdef LIBXML_SCHEMAS_ENABLED xmlreader_object *intern = Z_XMLREADER_P(ZEND_THIS); - if (intern && intern->ptr) { + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } + if (intern->ptr) { PHP_LIBXML_SANITIZE_GLOBALS(schema); + intern->in_use = true; int retval = xmlTextReaderSchemaValidate(intern->ptr, source); + intern->in_use = false; PHP_LIBXML_RESTORE_GLOBALS(schema); if (retval == 0) { @@ -1118,7 +1190,10 @@ PHP_METHOD(XMLReader, setParserProperty) id = ZEND_THIS; intern = Z_XMLREADER_P(id); - if (!intern || !intern->ptr) { + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } + if (!intern->ptr) { zend_throw_error(NULL, "Cannot access parser properties before loading data"); RETURN_THROWS(); } @@ -1174,6 +1249,9 @@ static void xml_reader_from_string(INTERNAL_FUNCTION_PARAMETERS, zend_class_entr if (id != NULL) { ZEND_ASSERT(instanceof_function(Z_OBJCE_P(id), xmlreader_class_entry)); intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } xmlreader_free_resources(intern); } @@ -1290,9 +1368,14 @@ PHP_METHOD(XMLReader, expand) } intern = Z_XMLREADER_P(id); + if (!xmlreader_check_not_in_use(intern)) { + RETURN_THROWS(); + } if (intern->ptr) { + intern->in_use = true; node = xmlTextReaderExpand(intern->ptr); + intern->in_use = false; if (node == NULL) { php_error_docref(NULL, E_WARNING, "An Error Occurred while expanding"); diff --git a/ext/xmlreader/php_xmlreader.h b/ext/xmlreader/php_xmlreader.h index d33938f40506..f7648ecc97d9 100644 --- a/ext/xmlreader/php_xmlreader.h +++ b/ext/xmlreader/php_xmlreader.h @@ -41,6 +41,7 @@ typedef struct _xmlreader_object { /* strings must be set in input buffer as copy is required */ xmlParserInputBufferPtr input; void *schema; + bool in_use; zend_object std; } xmlreader_object; diff --git a/ext/xmlreader/tests/io_hooks_concurrent_access.phpt b/ext/xmlreader/tests/io_hooks_concurrent_access.phpt new file mode 100644 index 000000000000..8a971d5b8be3 --- /dev/null +++ b/ext/xmlreader/tests/io_hooks_concurrent_access.phpt @@ -0,0 +1,74 @@ +--TEST-- +XMLReader under io_hooks: the reader refuses other use while a read waits for input +--EXTENSIONS-- +xmlreader +dom +--SKIPIF-- + +--FILE-- +'); +$reader = XMLReader::fromStream($input); + +$scheduler = new Scheduler(); +Io\Hooks\set_hooks($scheduler); + +$scheduler->spawn(function () use (&$reader) { + var_dump($reader->read()); + echo "A: done\n"; +}); + +$scheduler->spawn(function () use (&$reader, $writer) { + $calls = [ + 'close' => fn() => $reader->close(), + 'read' => fn() => $reader->read(), + 'next' => fn() => $reader->next(), + 'expand' => fn() => $reader->expand(), + 'readOuterXml' => fn() => $reader->readOuterXml(), + 'moveToFirstAttribute' => fn() => $reader->moveToFirstAttribute(), + 'setParserProperty' => fn() => $reader->setParserProperty(XMLReader::SUBST_ENTITIES, true), + 'setSchema' => fn() => $reader->setSchema(null), + 'setRelaxNGSchema' => fn() => $reader->setRelaxNGSchema(null), + 'open' => fn() => $reader->open(__FILE__), + 'XML' => fn() => $reader->XML(''), + ]; + foreach ($calls as $name => $call) { + try { + $call(); + echo "$name: no error\n"; + } catch (Error $e) { + echo "$name: ", $e->getMessage(), "\n"; + } + } + var_dump($reader->depth, $reader->getParserProperty(XMLReader::SUBST_ENTITIES)); + /* only the frame of the running read keeps the object alive */ + $reader = null; + fwrite($writer, ''); + fclose($writer); +}); + +$scheduler->loop(); +var_dump($reader); +?> +--EXPECT-- +close: Attempt to use XMLReader while it is reading +read: Attempt to use XMLReader while it is reading +next: Attempt to use XMLReader while it is reading +expand: Attempt to use XMLReader while it is reading +readOuterXml: Attempt to use XMLReader while it is reading +moveToFirstAttribute: Attempt to use XMLReader while it is reading +setParserProperty: Attempt to use XMLReader while it is reading +setSchema: Attempt to use XMLReader while it is reading +setRelaxNGSchema: Attempt to use XMLReader while it is reading +open: Attempt to use XMLReader while it is reading +XML: Attempt to use XMLReader while it is reading +int(0) +bool(false) +bool(true) +A: done +NULL diff --git a/ext/xmlreader/tests/io_hooks_fiber_destroyed.phpt b/ext/xmlreader/tests/io_hooks_fiber_destroyed.phpt new file mode 100644 index 000000000000..a7c38fba4dd0 --- /dev/null +++ b/ext/xmlreader/tests/io_hooks_fiber_destroyed.phpt @@ -0,0 +1,39 @@ +--TEST-- +XMLReader under io_hooks: a fiber destroyed at shutdown while a read waits for input unwinds it +--EXTENSIONS-- +xmlreader +--SKIPIF-- + +--FILE-- +'); +$reader = XMLReader::fromStream($input); + +$scheduler = new Scheduler(); +Io\Hooks\set_hooks($scheduler); + +$fiber = new Fiber(function () use (&$reader) { + try { + $reader->read(); + } finally { + echo "A: unwound\n"; + } +}); +$fiber->start(); +try { + $reader->close(); +} catch (Error $e) { + echo $e->getMessage(), "\n"; +} +unset($reader, $input, $fiber); +echo "end\n"; +?> +--EXPECT-- +Attempt to use XMLReader while it is reading +end +A: unwound diff --git a/ext/xmlreader/tests/reentrant_from_stream_wrapper.phpt b/ext/xmlreader/tests/reentrant_from_stream_wrapper.phpt new file mode 100644 index 000000000000..f9f7447536d5 --- /dev/null +++ b/ext/xmlreader/tests/reentrant_from_stream_wrapper.phpt @@ -0,0 +1,52 @@ +--TEST-- +XMLReader: the reader refuses use from a stream wrapper it is reading from +--EXTENSIONS-- +xmlreader +--FILE-- +'; + private $pos = 0; + public function url_stat($path, $flags) { return []; } + public function stream_open($path, $mode, $options, &$opened_path) { return true; } + public function stream_read($count) { + global $reader; + foreach (['close', 'read', 'next', 'expand', 'moveToFirstAttribute', 'XML'] as $method) { + try { + $method === 'XML' ? $reader->XML('') : $reader->$method(); + self::$log[] = "$method: no error"; + } catch (Error $e) { + self::$log[] = "$method: " . $e->getMessage(); + } + } + $chunk = substr($this->data, $this->pos, 4); + $this->pos += strlen($chunk); + return $chunk; + } + public function stream_eof() { return $this->pos >= strlen($this->data); } + public function stream_stat() { return []; } + public function stream_close() {} +} +stream_wrapper_register('test', 'Wrapper'); + +$reader = new XMLReader(); +var_dump($reader->open('test://doc')); +while ($reader->read()) { + echo $reader->name, "\n"; +} +echo implode("\n", array_unique(Wrapper::$log)), "\n"; +?> +--EXPECT-- +bool(true) +root +a +b +root +close: Attempt to use XMLReader while it is reading +read: Attempt to use XMLReader while it is reading +next: Attempt to use XMLReader while it is reading +expand: Attempt to use XMLReader while it is reading +moveToFirstAttribute: Attempt to use XMLReader while it is reading +XML: Attempt to use XMLReader while it is reading diff --git a/ext/xmlwriter/php_xmlwriter.c b/ext/xmlwriter/php_xmlwriter.c index fe7604edb52a..6e3c64fe54f4 100644 --- a/ext/xmlwriter/php_xmlwriter.c +++ b/ext/xmlwriter/php_xmlwriter.c @@ -33,6 +33,9 @@ typedef int (*xmlwriter_read_int_t)(xmlTextWriterPtr writer); #define XMLWRITER_FROM_OBJECT(ptr, object) \ { \ ze_xmlwriter_object *obj = Z_XMLWRITER_P(object); \ + if (!xmlwriter_check_not_in_use(obj)) { \ + RETURN_THROWS(); \ + } \ ptr = obj->ptr; \ if (!ptr) { \ zend_throw_error(NULL, "Invalid or uninitialized XMLWriter object"); \ @@ -43,13 +46,36 @@ typedef int (*xmlwriter_read_int_t)(xmlTextWriterPtr writer); static zend_object_handlers xmlwriter_object_handlers; +static bool xmlwriter_check_not_in_use(const ze_xmlwriter_object *intern) +{ + if (UNEXPECTED(intern->in_use)) { + zend_throw_error(NULL, "Attempt to use XMLWriter while it is writing"); + return false; + } + return true; +} + +/* The output callbacks can suspend the fiber or run user code, which must not + * free or re-enter the writer under the running call */ +static zend_always_inline void xmlwriter_write_begin(zval *object) +{ + Z_XMLWRITER_P(object)->in_use = true; +} + +static zend_always_inline void xmlwriter_write_end(zval *object) +{ + Z_XMLWRITER_P(object)->in_use = false; +} + static zend_always_inline void xmlwriter_destroy_libxml_objects(ze_xmlwriter_object *intern) { if (intern->ptr) { + intern->in_use = true; /* Note: this call will also free the output pointer. */ xmlFreeTextWriter(intern->ptr); intern->ptr = NULL; intern->output = NULL; + intern->in_use = false; } } @@ -59,7 +85,9 @@ static void xmlwriter_object_dtor(zend_object *object) ze_xmlwriter_object *intern = php_xmlwriter_fetch_object(object); /* freeing the resource here may leak, but otherwise we may use it after it has been freed */ - xmlwriter_destroy_libxml_objects(intern); + if (!intern->in_use) { + xmlwriter_destroy_libxml_objects(intern); + } zend_objects_destroy_object(object); } /* }}} */ @@ -225,7 +253,9 @@ static void php_xmlwriter_string_arg(INTERNAL_FUNCTION_PARAMETERS, xmlwriter_rea XMLW_NAME_CHK(2, subject_name); } + xmlwriter_write_begin(self); retval = internal_function(ptr, (xmlChar *) name); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -241,7 +271,9 @@ static void php_xmlwriter_end(INTERNAL_FUNCTION_PARAMETERS, xmlwriter_read_int_t } XMLWRITER_FROM_OBJECT(ptr, self); + xmlwriter_write_begin(self); retval = internal_function(ptr); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -303,7 +335,9 @@ PHP_FUNCTION(xmlwriter_start_attribute_ns) XMLW_NAME_CHK(3, "attribute name"); + xmlwriter_write_begin(self); retval = xmlTextWriterStartAttributeNS(ptr, (xmlChar *)prefix, (xmlChar *)name, (xmlChar *)uri); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -326,7 +360,9 @@ PHP_FUNCTION(xmlwriter_write_attribute) XMLW_NAME_CHK(2, "attribute name"); + xmlwriter_write_begin(self); retval = xmlTextWriterWriteAttribute(ptr, (xmlChar *)name, (xmlChar *)content); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -349,7 +385,9 @@ PHP_FUNCTION(xmlwriter_write_attribute_ns) XMLW_NAME_CHK(3, "attribute name"); + xmlwriter_write_begin(self); retval = xmlTextWriterWriteAttributeNS(ptr, (xmlChar *)prefix, (xmlChar *)name, (xmlChar *)uri, (xmlChar *)content); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -379,7 +417,9 @@ PHP_FUNCTION(xmlwriter_start_element_ns) XMLW_NAME_CHK(3, "element name"); + xmlwriter_write_begin(self); retval = xmlTextWriterStartElementNS(ptr, (xmlChar *)prefix, (xmlChar *)name, (xmlChar *)uri); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -417,13 +457,19 @@ PHP_FUNCTION(xmlwriter_write_element) XMLW_NAME_CHK(2, "element name"); if (!content) { + xmlwriter_write_begin(self); retval = xmlTextWriterStartElement(ptr, (xmlChar *)name); + xmlwriter_write_end(self); if (retval == -1) { RETURN_FALSE; } + xmlwriter_write_begin(self); retval = xmlTextWriterEndElement(ptr); + xmlwriter_write_end(self); } else { + xmlwriter_write_begin(self); retval = xmlTextWriterWriteElement(ptr, (xmlChar *)name, (xmlChar *)content); + xmlwriter_write_end(self); } RETURN_BOOL(retval != -1); @@ -448,13 +494,19 @@ PHP_FUNCTION(xmlwriter_write_element_ns) XMLW_NAME_CHK(3, "element name"); if (!content) { + xmlwriter_write_begin(self); retval = xmlTextWriterStartElementNS(ptr,(xmlChar *)prefix, (xmlChar *)name, (xmlChar *)uri); + xmlwriter_write_end(self); if (retval == -1) { RETURN_FALSE; } + xmlwriter_write_begin(self); retval = xmlTextWriterEndElement(ptr); + xmlwriter_write_end(self); } else { + xmlwriter_write_begin(self); retval = xmlTextWriterWriteElementNS(ptr, (xmlChar *)prefix, (xmlChar *)name, (xmlChar *)uri, (xmlChar *)content); + xmlwriter_write_end(self); } RETURN_BOOL(retval != -1); @@ -492,7 +544,9 @@ PHP_FUNCTION(xmlwriter_write_pi) XMLW_NAME_CHK(2, "PI target"); + xmlwriter_write_begin(self); retval = xmlTextWriterWritePI(ptr, (xmlChar *)name, (xmlChar *)content); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -510,7 +564,9 @@ PHP_FUNCTION(xmlwriter_start_cdata) } XMLWRITER_FROM_OBJECT(ptr, self); + xmlwriter_write_begin(self); retval = xmlTextWriterStartCDATA(ptr); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -556,7 +612,9 @@ PHP_FUNCTION(xmlwriter_start_comment) } XMLWRITER_FROM_OBJECT(ptr, self); + xmlwriter_write_begin(self); retval = xmlTextWriterStartComment(ptr); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -590,7 +648,9 @@ PHP_FUNCTION(xmlwriter_start_document) } XMLWRITER_FROM_OBJECT(ptr, self); + xmlwriter_write_begin(self); retval = xmlTextWriterStartDocument(ptr, version, enc, alone); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -617,7 +677,9 @@ PHP_FUNCTION(xmlwriter_start_dtd) } XMLWRITER_FROM_OBJECT(ptr, self); + xmlwriter_write_begin(self); retval = xmlTextWriterStartDTD(ptr, (xmlChar *)name, (xmlChar *)pubid, (xmlChar *)sysid); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -644,7 +706,9 @@ PHP_FUNCTION(xmlwriter_write_dtd) } XMLWRITER_FROM_OBJECT(ptr, self); + xmlwriter_write_begin(self); retval = xmlTextWriterWriteDTD(ptr, (xmlChar *)name, (xmlChar *)pubid, (xmlChar *)sysid, (xmlChar *)subset); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -681,7 +745,9 @@ PHP_FUNCTION(xmlwriter_write_dtd_element) XMLW_NAME_CHK(2, "element name"); + xmlwriter_write_begin(self); retval = xmlTextWriterWriteDTDElement(ptr, (xmlChar *)name, (xmlChar *)content); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -718,7 +784,9 @@ PHP_FUNCTION(xmlwriter_write_dtd_attlist) XMLW_NAME_CHK(2, "element name"); + xmlwriter_write_begin(self); retval = xmlTextWriterWriteDTDAttlist(ptr, (xmlChar *)name, (xmlChar *)content); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -741,7 +809,9 @@ PHP_FUNCTION(xmlwriter_start_dtd_entity) XMLW_NAME_CHK(2, "attribute name"); + xmlwriter_write_begin(self); retval = xmlTextWriterStartDTDEntity(ptr, isparm, (xmlChar *)name); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -776,7 +846,9 @@ PHP_FUNCTION(xmlwriter_write_dtd_entity) XMLW_NAME_CHK(2, "element name"); + xmlwriter_write_begin(self); retval = xmlTextWriterWriteDTDEntity(ptr, pe, (xmlChar *)name, (xmlChar *)pubid, (xmlChar *)sysid, (xmlChar *)ndataid, (xmlChar *)content); + xmlwriter_write_end(self); RETURN_BOOL(retval != -1); } @@ -800,6 +872,9 @@ PHP_FUNCTION(xmlwriter_open_uri) if (self) { /* We do not use XMLWRITER_FROM_OBJECT, xmlwriter init function here */ ze_obj = Z_XMLWRITER_P(self); + if (!xmlwriter_check_not_in_use(ze_obj)) { + RETURN_THROWS(); + } } if (source_len == 0) { @@ -813,7 +888,13 @@ PHP_FUNCTION(xmlwriter_open_uri) RETURN_FALSE; } + if (ze_obj) { + ze_obj->in_use = true; + } ptr = xmlNewTextWriterFilename(valid_file, 0); + if (ze_obj) { + ze_obj->in_use = false; + } if (!ptr) { RETURN_FALSE; @@ -910,6 +991,9 @@ PHP_FUNCTION(xmlwriter_open_memory) if (self) { /* We do not use XMLWRITER_FROM_OBJECT, xmlwriter init function here */ ze_obj = Z_XMLWRITER_P(self); + if (!xmlwriter_check_not_in_use(ze_obj)) { + RETURN_THROWS(); + } } smart_str *output; @@ -1015,7 +1099,9 @@ static void php_xmlwriter_flush(INTERNAL_FUNCTION_PARAMETERS, int force_string) if (force_string == 1 && output == NULL) { RETURN_EMPTY_STRING(); } + xmlwriter_write_begin(self); output_bytes = xmlTextWriterFlush(ptr); + xmlwriter_write_end(self); if (output) { if (empty) { RETURN_STR(smart_str_extract(output)); diff --git a/ext/xmlwriter/php_xmlwriter.h b/ext/xmlwriter/php_xmlwriter.h index 542c6e838c5e..93f18f15ddcb 100644 --- a/ext/xmlwriter/php_xmlwriter.h +++ b/ext/xmlwriter/php_xmlwriter.h @@ -34,6 +34,7 @@ extern zend_module_entry xmlwriter_module_entry; typedef struct _ze_xmlwriter_object { xmlTextWriterPtr ptr; smart_str *output; + bool in_use; zend_object std; } ze_xmlwriter_object; diff --git a/ext/xmlwriter/tests/io_hooks_concurrent_access.phpt b/ext/xmlwriter/tests/io_hooks_concurrent_access.phpt new file mode 100644 index 000000000000..020cb0f46d32 --- /dev/null +++ b/ext/xmlwriter/tests/io_hooks_concurrent_access.phpt @@ -0,0 +1,67 @@ +--TEST-- +XMLWriter under io_hooks: the writer refuses other use while a write waits for the output +--EXTENSIONS-- +xmlwriter +--SKIPIF-- + +--FILE-- +spawn(function () use (&$writer, $len) { + $w = $writer; + $w->startElement('r'); + /* more than the socket buffer takes */ + var_dump($w->text(str_repeat('x', $len))); + var_dump($w->endElement(), $w->flush()); +}); + +$scheduler->spawn(function () use (&$writer, $in, $len) { + $calls = [ + 'text' => fn() => $writer->text('y'), + 'endElement' => fn() => $writer->endElement(), + 'flush' => fn() => $writer->flush(), + 'outputMemory' => fn() => $writer->outputMemory(), + 'xmlwriter_text' => fn() => xmlwriter_text($writer, 'y'), + 'openMemory' => fn() => $writer->openMemory(), + 'openUri' => fn() => $writer->openUri('php://memory'), + ]; + foreach ($calls as $name => $call) { + try { + $call(); + echo "$name: no error\n"; + } catch (Error $e) { + echo "$name: ", $e->getMessage(), "\n"; + } + } + $writer = null; + $data = ''; + while (strlen($data) < $len + 7 && !feof($in)) { + $data .= fread($in, 65536); + } + var_dump($data === '' . str_repeat('x', $len) . ''); +}); + +$scheduler->loop(); +?> +--EXPECT-- +text: Attempt to use XMLWriter while it is writing +endElement: Attempt to use XMLWriter while it is writing +flush: Attempt to use XMLWriter while it is writing +outputMemory: Attempt to use XMLWriter while it is writing +xmlwriter_text: Attempt to use XMLWriter while it is writing +openMemory: Attempt to use XMLWriter while it is writing +openUri: Attempt to use XMLWriter while it is writing +bool(true) +bool(true) +int(4) +bool(true) diff --git a/ext/xmlwriter/tests/reentrant_from_stream_wrapper.phpt b/ext/xmlwriter/tests/reentrant_from_stream_wrapper.phpt new file mode 100644 index 000000000000..1779c735eae9 --- /dev/null +++ b/ext/xmlwriter/tests/reentrant_from_stream_wrapper.phpt @@ -0,0 +1,44 @@ +--TEST-- +XMLWriter: the writer refuses use from a stream wrapper it is writing to +--EXTENSIONS-- +xmlwriter +--FILE-- +text('y') : $writer->$method(); + self::$log[] = "$method: no error"; + } catch (Error $e) { + self::$log[] = "$method: " . $e->getMessage(); + } + } + self::$data .= $data; + return strlen($data); + } + public function stream_close() {} +} +stream_wrapper_register('test', 'Wrapper'); + +$writer = new XMLWriter(); +var_dump($writer->openUri('test://doc')); +$writer->startElement('r'); +$writer->text(str_repeat('x', 10000)); +$writer->endElement(); +var_dump($writer->flush()); +var_dump(Wrapper::$data === '' . str_repeat('x', 10000) . ''); +echo implode("\n", array_unique(Wrapper::$log)), "\n"; +?> +--EXPECTF-- +bool(true) +int(%d) +bool(true) +text: Attempt to use XMLWriter while it is writing +flush: Attempt to use XMLWriter while it is writing +openMemory: Attempt to use XMLWriter while it is writing diff --git a/main/io/php_io_copy_windows.c b/main/io/php_io_copy_windows.c index de75d2529987..377c297cb815 100644 --- a/main/io/php_io_copy_windows.c +++ b/main/io/php_io_copy_windows.c @@ -19,13 +19,22 @@ #include #include #include +#include "win32/ioutil.h" +/* A file opened overlapped reads and writes at the position its stream keeps */ static inline ssize_t php_io_win_read(php_io_fd *fd, char *buf, int len) { if (fd->fd_type == PHP_IO_FD_SOCKET) { int result = recv(fd->socket, buf, len, 0); return (result == SOCKET_ERROR) ? -1 : (ssize_t) result; } + if (fd->position) { + ssize_t result = php_win32_ioutil_pread(fd->fd, buf, len, *fd->position); + if (result > 0) { + *fd->position += result; + } + return result; + } return (ssize_t) _read(fd->fd, buf, len); } @@ -35,6 +44,13 @@ static inline ssize_t php_io_win_write(php_io_fd *fd, const char *buf, int len) int result = send(fd->socket, buf, len, 0); return (result == SOCKET_ERROR) ? -1 : (ssize_t) result; } + if (fd->position) { + ssize_t result = php_win32_ioutil_pwrite(fd->fd, buf, len, *fd->position); + if (result > 0) { + *fd->position += result; + } + return result; + } return (ssize_t) _write(fd->fd, buf, len); } @@ -170,7 +186,8 @@ static ssize_t php_io_win_transmit_file(int src_fd, SOCKET dest_sock, size_t max zend_result php_io_windows_copy(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied) { - if (src->fd_type != PHP_IO_FD_SOCKET && dest->fd_type == PHP_IO_FD_SOCKET) { + /* TransmitFile takes the file's own position, which an overlapped file has none of */ + if (src->fd_type != PHP_IO_FD_SOCKET && dest->fd_type == PHP_IO_FD_SOCKET && !src->position) { ssize_t result = php_io_win_transmit_file(src->fd, dest->socket, maxlen); if (result >= 0) { *copied = (size_t) result; diff --git a/main/io/php_io_hooks.c b/main/io/php_io_hooks.c new file mode 100644 index 000000000000..6bcb2d0f4a0f --- /dev/null +++ b/main/io/php_io_hooks.c @@ -0,0 +1,2093 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_io.h" +#include "ext/standard/file.h" +#include "ext/standard/io_poll.h" + +#include +#ifndef PHP_WIN32 +# include +# include +#endif +#include +#include "zend_fibers.h" +#if defined(HAVE_FDATASYNC) && defined(__APPLE__) +/* The symbol is present, the headers do not declare it */ +extern int fdatasync(int); +#endif +#ifndef PHP_WIN32 +# include +# include +# include +#endif + +PHPAPI void (*php_io_op_zobj_detach)(zend_object *zobj) = NULL; +PHPAPI bool (*php_io_signal_pending)(void) = NULL; + +PHPAPI bool php_io_interrupt_pending(void) +{ + return atomic_load(&EG(vm_interrupt)) || (php_io_signal_pending && php_io_signal_pending()); +} + +/* The callers of the socket entry points read php_socket_errno(), which on + * Windows is the Winsock error rather than errno */ +#ifdef PHP_WIN32 +static int php_io_wsa_error(int err) +{ + switch (err) { + case EINTR: return WSAEINTR; + case EAGAIN: return WSAEWOULDBLOCK; + case EBADF: return WSAEBADF; + case EACCES: return WSAEACCES; + case EINVAL: return WSAEINVAL; + case EMFILE: return WSAEMFILE; + case ETIMEDOUT: return WSAETIMEDOUT; + case ECANCELED: return WSAECANCELLED; + case ENOTSUP: return WSAEOPNOTSUPP; + case EALREADY: return WSAEALREADY; + case EISCONN: return WSAEISCONN; + case ENOTCONN: return WSAENOTCONN; + case ECONNREFUSED: return WSAECONNREFUSED; + case ECONNRESET: return WSAECONNRESET; + case ECONNABORTED: return WSAECONNABORTED; + case ENETUNREACH: return WSAENETUNREACH; + case EHOSTUNREACH: return WSAEHOSTUNREACH; + case EADDRINUSE: return WSAEADDRINUSE; + default: return err; + } +} +#endif + +static zend_always_inline void php_io_set_errno(int err) +{ +#ifdef PHP_WIN32 + WSASetLastError(php_io_wsa_error(err)); +#endif + errno = err; +} + +/* A stream or a handle whose in-flight op outlived its frame, kept by a queue */ +typedef struct php_io_orphan { + php_io_queue *queue; + php_stream *stream; /* NULL for a handle */ + zend_object *handle; + zend_resource *res; /* NULL for a stream kept off the resource list */ + bool freeing; /* php_stream_free() is draining it */ +} php_io_orphan; + +/* Operation constructors */ + +static void php_io_op_init(php_io_op *op, php_io_op_type type, zend_object *handle, + php_socket_t fd, uint32_t ready_events, php_deadline dl) +{ + memset(op, 0, sizeof(*op)); + op->type = type; + op->handle = handle; + op->fd = fd; + op->ready_events = ready_events; + op->deadline = dl; +} + +PHPAPI void php_io_op_poll(php_io_op *op, zend_object *handle, php_socket_t fd, uint32_t events, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_POLL, handle, fd, events, dl); + op->u.poll.events = events; +} + +PHPAPI void php_io_op_timer(php_io_op *op, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_TIMER, NULL, SOCK_ERR, PHP_POLL_TIMER, dl); +} + +PHPAPI void php_io_op_waitpid(php_io_op *op, zend_object *handle, pid_t pid, int options, int *status, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_WAITPID, handle, SOCK_ERR, PHP_POLL_PROCESS, dl); + op->u.waitpid.pid = pid; + op->u.waitpid.options = options; + op->u.waitpid.status = status; +} + +PHPAPI void php_io_op_sigwait(php_io_op *op, zend_object *handle, const php_sigset_t *set, php_siginfo_t *info, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_SIGWAIT, handle, SOCK_ERR, PHP_POLL_SIGNAL, dl); + op->u.sigwait.set = set; + op->u.sigwait.info = info; + op->u.sigwait.taken = 0; +} + +PHPAPI void php_io_op_read(php_io_op *op, zend_object *handle, php_socket_t fd, void *buf, size_t len, int64_t off, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_READ, handle, fd, PHP_POLL_READ, dl); + op->u.io.buf = buf; + op->u.io.len = len; + op->u.io.offset = off; +} + +PHPAPI void php_io_op_write(php_io_op *op, zend_object *handle, php_socket_t fd, const void *buf, size_t len, int64_t off, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_WRITE, handle, fd, PHP_POLL_WRITE, dl); + op->u.io.buf = (void *) buf; + op->u.io.len = len; + op->u.io.offset = off; +} + +PHPAPI void php_io_op_recv(php_io_op *op, zend_object *handle, php_socket_t fd, void *buf, size_t len, int flags, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_RECV, handle, fd, PHP_POLL_READ, dl); + op->u.io.buf = buf; + op->u.io.len = len; + op->u.io.offset = -1; + op->u.io.flags = flags; +} + +PHPAPI void php_io_op_send(php_io_op *op, zend_object *handle, php_socket_t fd, const void *buf, size_t len, int flags, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_SEND, handle, fd, PHP_POLL_WRITE, dl); + op->u.io.buf = (void *) buf; + op->u.io.len = len; + op->u.io.offset = -1; + op->u.io.flags = flags; +} + +PHPAPI void php_io_op_accept(php_io_op *op, zend_object *handle, php_socket_t fd, struct sockaddr *addr, socklen_t *addrlen, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_ACCEPT, handle, fd, PHP_POLL_READ, dl); + op->u.accept.addr = addr; + op->u.accept.addrlen = addrlen; +} + +PHPAPI void php_io_op_connect(php_io_op *op, zend_object *handle, php_socket_t fd, const struct sockaddr *addr, socklen_t addrlen, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_CONNECT, handle, fd, PHP_POLL_WRITE, dl); + op->u.connect.addr = addr; + op->u.connect.addrlen = addrlen; +} + +PHPAPI void php_io_op_getaddrinfo(php_io_op *op, const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_GETADDRINFO, NULL, SOCK_ERR, 0, dl); + op->u.getaddrinfo.node = node; + op->u.getaddrinfo.service = service; + op->u.getaddrinfo.hints = hints; + op->u.getaddrinfo.res = res; +} + +PHPAPI void php_io_op_getnameinfo(php_io_op *op, const struct sockaddr *addr, socklen_t addrlen, int flags, char *host, size_t hostlen, char *service, size_t servicelen, php_deadline dl) +{ + php_io_op_init(op, PHP_IO_OP_GETNAMEINFO, NULL, SOCK_ERR, 0, dl); + op->u.getnameinfo.addr = addr; + op->u.getnameinfo.addrlen = addrlen; + op->u.getnameinfo.flags = flags; + op->u.getnameinfo.host = host; + op->u.getnameinfo.hostlen = hostlen; + op->u.getnameinfo.service = service; + op->u.getnameinfo.servicelen = servicelen; +} + +PHPAPI void php_io_op_fsync(php_io_op *op, zend_object *handle, php_socket_t fd, bool data_only) +{ + php_io_op_init(op, PHP_IO_OP_FSYNC, handle, fd, 0, php_io_deadline_infinite()); + op->u.fsync.data_only = data_only; +} + +PHPAPI void php_io_op_any(php_io_op *op, php_io_op **members, uint32_t n, php_io_op_result *results) +{ + php_io_op_init(op, PHP_IO_OP_ANY, NULL, SOCK_ERR, 0, php_io_deadline_infinite()); + op->u.any.ops = members; + op->u.any.n = n; + op->u.any.results = results; + op->u.any.n_results = 0; +} + +/* Registrations */ + +PHPAPI void (*php_io_registration_zobj_detach)(zend_object *zobj) = NULL; + +PHPAPI void php_io_hooks_lock(void) +{ + FG(io_hooks_locked)++; + zend_fiber_switch_block(); +} + +PHPAPI void php_io_hooks_unlock(void) +{ + zend_fiber_switch_unblock(); + FG(io_hooks_locked)--; +} + +static void php_io_registration_detach_zobj(php_io_registration *reg) +{ + if (reg->zobj) { + zend_object *zobj = reg->zobj; + reg->zobj = NULL; + if (php_io_registration_zobj_detach) { + php_io_registration_zobj_detach(zobj); + } + OBJ_RELEASE(zobj); + } +} + +static zend_always_inline bool php_io_hooks_take_trigger(const php_io_hooks *hooks, php_io_trigger trigger) +{ + uint32_t flag = trigger == PHP_IO_TRIGGER_EDGE + ? PHP_IO_HOOKS_F_EDGE_REGISTRATIONS : PHP_IO_HOOKS_F_LEVEL_REGISTRATIONS; + return (hooks->flags & flag) != 0; +} + +static void php_io_registration_free(php_io_registration *reg) +{ + if (reg->handle) { + OBJ_RELEASE(®->handle->std); + } + efree(reg); +} + +static void php_io_registration_unlink(php_io_registration *reg) +{ + php_io_registration **link = reg->stream ? ®->stream->io_registrations : ®->handle->registrations; + while (*link != reg) { + link = &(*link)->next; + } + *link = reg->next; + if (reg->gprev) { + reg->gprev->gnext = reg->gnext; + } else { + FG(io_registrations) = reg->gnext; + } + if (reg->gnext) { + reg->gnext->gprev = reg->gprev; + } +} + +/* The record of the pair on the registrant's list, created on the first call, and added to the + * current provider when it has not seen it: on its first wait, and again after a replacement. + * The trigger is settled when the record is added, as what the caller wants or Level when that + * is all the provider takes. */ +static php_io_registration *php_io_register(php_io_registration **list, php_stream *stream, + php_poll_handle_object *handle, php_socket_t fd, uint32_t event, php_io_trigger trigger) +{ + php_io_hooks *hooks = FG(io_hooks); + + if (!hooks || fd == SOCK_ERR) { + return NULL; + } + if (!php_io_hooks_take_trigger(hooks, trigger)) { + if (trigger != PHP_IO_TRIGGER_EDGE || !php_io_hooks_take_trigger(hooks, PHP_IO_TRIGGER_LEVEL)) { + return NULL; + } + trigger = PHP_IO_TRIGGER_LEVEL; + } + + php_io_registration *reg; + for (reg = *list; reg; reg = reg->next) { + if (reg->event == event) { + break; + } + } + if (!reg) { + reg = ecalloc(1, sizeof(*reg)); + reg->fd = fd; + reg->event = event; + reg->stream = stream; + reg->handle = handle; + if (handle) { + GC_ADDREF(&handle->std); + } + reg->next = *list; + *list = reg; + reg->gnext = FG(io_registrations); + if (reg->gnext) { + reg->gnext->gprev = reg; + } + FG(io_registrations) = reg; + } + if (reg->generation == FG(io_hooks_generation)) { + return reg; + } + + reg->trigger = trigger; + /* What an earlier provider kept is not the new one's */ + reg->provider_data = NULL; + reg->generation = FG(io_hooks_generation); + if (hooks->ops->add) { + /* An add() that unregisters the pair (closes the stream) leaves the record to this frame */ + reg->in_add = true; + php_io_hooks_lock(); + hooks->ops->add(hooks, reg); + php_io_hooks_unlock(); + reg->in_add = false; + if (reg->dead) { + php_io_registration_free(reg); + return NULL; + } + } + return reg; +} + +PHPAPI php_io_registration *php_io_register_stream(php_stream *stream, php_socket_t fd, uint32_t event, + php_io_trigger trigger) +{ + return php_io_register(&stream->io_registrations, stream, NULL, fd, event, trigger); +} + +PHPAPI php_io_registration *php_io_register_handle(php_poll_handle_object *handle, uint32_t event, php_io_trigger trigger) +{ + return php_io_register(&handle->registrations, NULL, handle, php_poll_handle_get_fd(handle), event, trigger); +} + +PHPAPI void php_io_unregister(php_io_registration *reg) +{ + php_io_hooks *hooks = FG(io_hooks); + + php_io_registration_unlink(reg); + if (hooks && reg->generation == FG(io_hooks_generation) && hooks->ops->remove) { + php_io_hooks_lock(); + hooks->ops->remove(hooks, reg); + php_io_hooks_unlock(); + } + php_io_registration_detach_zobj(reg); + if (reg->in_add) { + reg->dead = true; + return; + } + php_io_registration_free(reg); +} + +PHPAPI void php_io_unregister_all(php_io_registration **list) +{ + while (*list) { + php_io_unregister(*list); + } +} + +PHPAPI php_poll_handle_object *php_io_registration_get_handle(php_io_registration *reg) +{ + if (!reg->handle && reg->stream) { + zend_object *handle = php_stream_get_poll_handle(reg->stream, reg->stream->userland); + GC_ADDREF(handle); + reg->handle = PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(handle); + } + return reg->handle; +} + +PHPAPI zend_object *php_io_op_get_handle(php_io_op *op) +{ + if (!op->handle && op->stream) { + op->handle = php_stream_get_poll_handle(op->stream, op->stream->userland); + } + return op->handle; +} + +/* Queues */ + +ZEND_TLS uint64_t php_io_queue_ids = 0; + +PHPAPI uint64_t php_io_queue_new_id(void) +{ + return ++php_io_queue_ids; +} + +/* Hooks */ + +PHPAPI zend_result php_io_hooks_register(php_io_hooks *hooks) +{ + php_io_hooks *current = FG(io_hooks); + + /* Not from add, remove or a dtor of the provider being replaced */ + if (FG(io_hooks_locked)) { + return FAILURE; + } + + if (hooks == NULL) { + if (current) { + FG(io_hooks) = NULL; + /* Its registrations end without remove(): the next provider sees add() again */ + for (php_io_registration *reg = FG(io_registrations); reg; reg = reg->gnext) { + php_io_registration_detach_zobj(reg); + } + if (current->ops->dtor) { + php_io_hooks_lock(); + current->ops->dtor(current); + php_io_hooks_unlock(); + } + } + return SUCCESS; + } + + if (current) { + return FAILURE; + } + + FG(io_hooks_generation)++; + FG(io_hooks) = hooks; + return SUCCESS; +} + +PHPAPI php_io_hooks *php_io_hooks_current(void) +{ + return FG(io_hooks); +} + +PHPAPI bool php_io_hooks_active(void) +{ + return FG(io_hooks) != NULL; +} + +/* A bailout while an op was suspended skipped its frame end */ +static void php_io_unfreeze_list(HashTable *list) +{ + zend_resource *res; + ZEND_HASH_FOREACH_PTR(list, res) { + if (res->type == php_file_le_stream() || res->type == php_file_le_pstream()) { + ((php_stream *) res->ptr)->flags &= ~PHP_STREAM_FLAG_IN_USE; + } + } ZEND_HASH_FOREACH_END(); +} + +PHPAPI void php_io_hooks_request_shutdown(void) +{ + FG(io_hooks_locked) = 0; + php_io_hooks_register(NULL); + /* A persistent stream outlives the request; its records must not */ + while (FG(io_registrations)) { + php_io_unregister(FG(io_registrations)); + } + FG(io_shut_down) = true; + if (FG(io_queue)) { + php_io_queue *q = FG(io_queue); + FG(io_queue) = NULL; + q->ops->destroy(q); + } + if (FG(io_orphans)) { + /* A queue that outlived the provider (an object held elsewhere) + * still owns the buffers of its orphans: settle them before the + * resource list closes the streams, which happens before the + * object store frees the queue */ + while (zend_hash_num_elements(FG(io_orphans)) > 0) { + zend_hash_internal_pointer_reset(FG(io_orphans)); + php_io_orphan *o = zend_hash_get_current_data_ptr(FG(io_orphans)); + if (o->stream) { + php_stream *stream = o->stream; + if (o->queue->ops->drain) { + o->queue->ops->drain(o->queue, stream); + } + php_io_stream_unfreeze(stream); + } else { + php_io_handle_drain(o->handle); + } + } + zend_hash_destroy(FG(io_orphans)); + efree(FG(io_orphans)); + FG(io_orphans) = NULL; + } + if (FG(io_ops_in_flight)) { + php_io_unfreeze_list(&EG(regular_list)); + php_io_unfreeze_list(&EG(persistent_list)); + FG(io_ops_in_flight) = 0; + } + if (FG(io_addrinfo)) { + struct addrinfo *head; + ZEND_HASH_FOREACH_PTR(FG(io_addrinfo), head) { + php_io_addrinfo_free_list(head); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(FG(io_addrinfo)); + efree(FG(io_addrinfo)); + FG(io_addrinfo) = NULL; + } +} + +PHPAPI void php_io_addrinfo_free_list(struct addrinfo *head) +{ + while (head) { + struct addrinfo *next = head->ai_next; + free(head); + head = next; + } +} + +PHPAPI void php_io_addrinfo_register(struct addrinfo *head) +{ + if (!FG(io_addrinfo)) { + FG(io_addrinfo) = emalloc(sizeof(HashTable)); + zend_hash_init(FG(io_addrinfo), 4, NULL, NULL, 0); + } + zend_hash_index_add_new_ptr(FG(io_addrinfo), (zend_ulong) (uintptr_t) head, head); +} + +PHPAPI void php_io_freeaddrinfo(struct addrinfo *res) +{ + if (!res) { + return; + } + if (FG(io_addrinfo) && zend_hash_index_del(FG(io_addrinfo), (zend_ulong) (uintptr_t) res) == SUCCESS) { + php_io_addrinfo_free_list(res); + return; + } + freeaddrinfo(res); +} + +PHPAPI void php_io_child_forget(pid_t pid) +{ +#ifdef HAVE_IOR + if (pid == 0) { + php_io_ring_after_fork(); + } +#else + (void) pid; +#endif +} + +PHPAPI uint32_t php_io_ops_in_flight(void) +{ + return FG(io_ops_in_flight); +} + +/* Orphans */ + +static zend_always_inline zend_ulong php_io_orphan_key(const void *owner) +{ + zend_ulong key = (zend_ulong) (uintptr_t) owner; + return (key >> 3) | (key << ((sizeof(key) * 8) - 3)); +} + +static void php_io_orphan_dtor(zval *zv) +{ + efree(Z_PTR_P(zv)); +} + +static php_io_orphan *php_io_orphan_add(const void *owner, php_io_queue *queue) +{ + if (!FG(io_orphans)) { + FG(io_orphans) = emalloc(sizeof(HashTable)); + zend_hash_init(FG(io_orphans), 4, NULL, php_io_orphan_dtor, 0); + } + if (zend_hash_index_exists(FG(io_orphans), php_io_orphan_key(owner))) { + return NULL; + } + php_io_orphan *o = ecalloc(1, sizeof(*o)); + o->queue = queue; + zend_hash_index_add_new_ptr(FG(io_orphans), php_io_orphan_key(owner), o); + return o; +} + +static zend_always_inline php_io_orphan *php_io_orphan_find(const void *owner) +{ + return FG(io_orphans) ? zend_hash_index_find_ptr(FG(io_orphans), php_io_orphan_key(owner)) : NULL; +} + +PHPAPI void php_io_stream_orphan(php_stream *stream, php_io_queue *queue) +{ + php_io_orphan *o = php_io_orphan_add(stream, queue); + if (o) { + o->stream = stream; + o->res = stream->res; + /* The buffer belongs to the backend until the completion arrives */ + if (o->res) { + GC_ADDREF(o->res); + } + } +} + +PHPAPI void php_io_stream_unfreeze(php_stream *stream) +{ + php_io_orphan *o = php_io_orphan_find(stream); + if (!o) { + return; + } + zend_resource *res = o->res; + bool freeing = o->freeing; + zend_hash_index_del(FG(io_orphans), php_io_orphan_key(stream)); + stream->flags &= ~PHP_STREAM_FLAG_IN_USE; + if (!res) { + return; + } + if (!freeing) { + zend_list_delete(res); + } else if (GC_REFCOUNT(res) > 1) { + /* The free still uses it: a last reference goes with the resource list */ + GC_DELREF(res); + } +} + +PHPAPI bool php_io_stream_busy(php_stream *stream) +{ + return (stream->flags & PHP_STREAM_FLAG_IN_USE) && !php_io_orphan_find(stream); +} + +/* Called from php_stream_free() with the stream still frozen */ +PHPAPI void php_io_stream_drain(php_stream *stream) +{ + php_io_orphan *o = php_io_orphan_find(stream); + if (!o) { + return; + } + o->freeing = true; + if (o->queue->ops->drain) { + o->queue->ops->drain(o->queue, stream); + } + php_io_stream_unfreeze(stream); +} + +/* The op of a handle without a stream: its descriptor must stay open until the op settled */ +PHPAPI void php_io_handle_orphan(zend_object *handle, php_io_queue *queue) +{ + php_io_orphan *o = php_io_orphan_add(handle, queue); + if (o) { + o->handle = handle; + GC_ADDREF(handle); + } +} + +PHPAPI void php_io_handle_unfreeze(zend_object *handle) +{ + if (php_io_orphan_find(handle)) { + zend_hash_index_del(FG(io_orphans), php_io_orphan_key(handle)); + OBJ_RELEASE(handle); + } +} + +PHPAPI bool php_io_handle_busy(zend_object *handle) +{ + return php_io_orphan_find(handle) != NULL; +} + +/* Before the owner closes the descriptor */ +PHPAPI void php_io_handle_drain(zend_object *handle) +{ + php_io_orphan *o = php_io_orphan_find(handle); + if (!o) { + return; + } + GC_ADDREF(handle); + if (o->queue->ops->drain) { + o->queue->ops->drain(o->queue, handle); + } + php_io_handle_unfreeze(handle); + OBJ_RELEASE(handle); +} + +/* Entry point */ + +static void php_io_op_detach_zobj(php_io_op *op) +{ + if (op->zobj) { + zend_object *zobj = op->zobj; + op->zobj = NULL; + if (php_io_op_zobj_detach) { + php_io_op_zobj_detach(zobj); + } + OBJ_RELEASE(zobj); + } +} + +/* The op is over from the caller's point of view: make sure no queue still + * references it and invalidate every userland wrapper. */ +static void php_io_op_finish(php_io_op *op) +{ + if (op->queue) { + op->queue->ops->orphan(op->queue, op); + } + if (op->type == PHP_IO_OP_ANY) { + for (uint32_t i = 0; i < op->u.any.n; i++) { + php_io_op *m = op->u.any.ops[i]; + /* A member the provider submitted on its own */ + if (m->queue) { + m->queue->ops->orphan(m->queue, m); + } + php_io_op_detach_zobj(m); + } + } + php_io_op_detach_zobj(op); +} + +static php_io_queue *php_io_core_queue(void) +{ +#ifndef PHP_WIN32 + if (FG(io_queue) && FG(io_queue_pid) != getpid()) { + /* Inherited across fork: its context is inert here (nothing was in + * flight, the fork guard saw to that), so it is dropped for a new one */ + php_io_queue *q = FG(io_queue); + FG(io_queue) = NULL; + q->ops->destroy(q); + } +#endif + if (!FG(io_queue)) { + /* poll(2) handles regular files and needs no registration syscalls for + * the one-shot waits of the synchronous path */ + FG(io_queue) = php_io_queue_create_poll(PHP_POLL_BACKEND_POLL); + if (!FG(io_queue)) { + FG(io_queue) = php_io_queue_create_poll(PHP_POLL_BACKEND_AUTO); + } +#ifndef PHP_WIN32 + FG(io_queue_pid) = getpid(); +#endif + } + return FG(io_queue); +} + +static zend_result php_io_run_sync_timer(php_io_op *op, php_io_op_result *result) +{ + bool infinite = php_deadline_is_infinite(&op->deadline); + + result->index = 0; + result->res = 0; + result->error = 0; + result->status = PHP_IO_DONE; + + for (;;) { + zend_hrtime_t remaining = infinite ? ZEND_HRTIME_T_MAX : php_io_deadline_remaining(&op->deadline, zend_hrtime()); + if (remaining == 0) { + break; + } +#ifdef PHP_WIN32 + zend_hrtime_t ms = (remaining + 999999) / 1000000; + Sleep(ms >= INFINITE ? INFINITE - 1 : (DWORD) ms); +#else + zend_hrtime_t sec = remaining / ZEND_NANO_IN_SEC; + struct timespec ts = { + .tv_sec = sec > INT_MAX ? INT_MAX : (time_t) sec, + .tv_nsec = sec > INT_MAX ? 0 : (long) (remaining % ZEND_NANO_IN_SEC), + }; + if (nanosleep(&ts, NULL) != 0) { + if (errno == EINTR) { + result->status = PHP_IO_INTERRUPTED; + } + break; + } +#endif + } + + return SUCCESS; +} + +/* After the request shutdown destroyed the core queue: streams closed later + * wait with a plain poll(2) rather than creating a queue nobody frees */ +static zend_result php_io_run_sync_direct(php_io_op *op, php_io_op_result *result) +{ + uint32_t events = op->type == PHP_IO_OP_POLL ? op->u.poll.events : op->ready_events; + int pevents = ((events & PHP_POLL_READ) ? POLLIN : 0) | ((events & PHP_POLL_WRITE) ? POLLOUT : 0) + | ((events & PHP_POLL_PRI) ? POLLPRI : 0); + + result->index = 0; + result->res = 0; + result->error = 0; + + if (op->fd == SOCK_ERR || op->type == PHP_IO_OP_ANY || !pevents) { + result->status = PHP_IO_UNSUPPORTED; + return SUCCESS; + } + + int n; + for (;;) { + int timeout = -1; + if (!php_deadline_is_infinite(&op->deadline)) { + zend_hrtime_t ms = (php_io_deadline_remaining(&op->deadline, zend_hrtime()) + 999999) / 1000000; + timeout = ms > INT_MAX ? INT_MAX : (int) ms; + } + n = php_pollfd_for_ms(op->fd, pevents, timeout); + if (n >= 0 || php_socket_errno() != PHP_IO_SOCK_EINTR) { + break; + } + } + + if (n < 0) { + result->status = PHP_IO_DONE; + result->res = -1; + result->error = php_socket_errno(); + } else if (n == 0) { + result->status = PHP_IO_TIMEOUT; + } else { + uint32_t revents = ((n & POLLIN) ? PHP_POLL_READ : 0) | ((n & POLLOUT) ? PHP_POLL_WRITE : 0) + | ((n & POLLPRI) ? PHP_POLL_PRI : 0) | ((n & (POLLERR | POLLNVAL)) ? PHP_POLL_ERROR : 0) + | ((n & POLLHUP) ? PHP_POLL_HUP : 0); + result->status = op->type == PHP_IO_OP_POLL ? PHP_IO_DONE : PHP_IO_READY; + result->res = revents; + } + return SUCCESS; +} + +static zend_result php_io_run_sync(php_io_op *op, php_io_op_result *result) +{ + if (op->type == PHP_IO_OP_TIMER) { + return php_io_run_sync_timer(op, result); + } + if (FG(io_shut_down)) { + return php_io_run_sync_direct(op, result); + } + + php_io_queue *q = php_io_core_queue(); + + result->index = 0; + result->res = -1; + result->error = 0; + + if (!q) { + result->status = PHP_IO_DONE; + result->error = ENOMEM; + return SUCCESS; + } + + if (q->ops->submit(q, op, NULL) == FAILURE) { + result->status = PHP_IO_DONE; + result->error = errno ? errno : EINVAL; + return SUCCESS; + } + + php_io_queue_completion c; + int n; + if (q->ops->take_inline(q, op, &c)) { + *result = c.result; + return SUCCESS; + } + /* Without a provider a blocking op keeps waiting through signals, as + * the poll loops it replaced did; the handler runs when it returns */ + do { + n = q->ops->wait(q, &c, 1, NULL); + } while (n == 0 || (n < 0 && errno == EINTR)); + + if (n < 0) { + int err = errno; + if (op->queue) { + q->ops->orphan(q, op); + } + result->status = PHP_IO_DONE; + result->error = err ? err : EIO; + return SUCCESS; + } + + ZEND_ASSERT(c.op == op); + *result = c.result; + return SUCCESS; +} + +static zend_result php_io_run_ex(php_io_op *op, php_io_op_result *result); + +PHPAPI zend_result php_io_run(php_io_op *op, php_io_op_result *result) +{ + FG(io_ops_in_flight)++; + zend_result rc = php_io_run_ex(op, result); + /* Reset by the request shutdown after a bailout abandoned frames */ + if (FG(io_ops_in_flight)) { + FG(io_ops_in_flight)--; + } + return rc; +} + +/* What a provider may leave behind must never be read uninitialized */ +static void php_io_op_clear_outputs(php_io_op *op) +{ + switch (op->type) { + case PHP_IO_OP_GETADDRINFO: + *op->u.getaddrinfo.res = NULL; + break; + case PHP_IO_OP_GETNAMEINFO: + if (op->u.getnameinfo.host && op->u.getnameinfo.hostlen) { + op->u.getnameinfo.host[0] = '\0'; + } + if (op->u.getnameinfo.service && op->u.getnameinfo.servicelen) { + op->u.getnameinfo.service[0] = '\0'; + } + break; + case PHP_IO_OP_WAITPID: + if (op->u.waitpid.status) { + *op->u.waitpid.status = 0; + } + break; + case PHP_IO_OP_SIGWAIT: + if (op->u.sigwait.info) { + memset(op->u.sigwait.info, 0, sizeof(*op->u.sigwait.info)); + } + break; + default: + break; + } +} + +/* A readiness Any with a member the provider cannot wait for runs here as a whole */ +static bool php_io_any_member_unsupported(const php_io_op *op) +{ + if (op->type != PHP_IO_OP_ANY) { + return false; + } + bool unsupported = false; + for (uint32_t i = 0; i < op->u.any.n_results; i++) { + unsupported |= op->u.any.results[i].status == PHP_IO_UNSUPPORTED; + } + if (!unsupported) { + return false; + } + for (uint32_t i = 0; i < op->u.any.n; i++) { + if (op->u.any.ops[i]->type != PHP_IO_OP_POLL && op->u.any.ops[i]->type != PHP_IO_OP_TIMER) { + return false; + } + } + return true; +} + +/* A successful Done must fit the op: no more bytes than the buffer holds */ +static bool php_io_result_valid(const php_io_op *op, const php_io_op_result *result) +{ + if (result->status != PHP_IO_DONE || result->error) { + return true; + } + switch (op->type) { + case PHP_IO_OP_READ: + case PHP_IO_OP_WRITE: + case PHP_IO_OP_RECV: + case PHP_IO_OP_SEND: + return result->res >= 0 && (uint64_t) result->res <= op->u.io.len; + case PHP_IO_OP_ACCEPT: + return result->res >= 0; + case PHP_IO_OP_WAITPID: + case PHP_IO_OP_SIGWAIT: + return result->res > 0; + case PHP_IO_OP_CONNECT: + case PHP_IO_OP_FSYNC: + return result->res >= 0; + case PHP_IO_OP_GETADDRINFO: + return *op->u.getaddrinfo.res != NULL; + default: + return true; + } +} + +static zend_result php_io_run_ex(php_io_op *op, php_io_op_result *result) +{ + php_io_hooks *hooks = FG(io_hooks); + if (hooks && UNEXPECTED(EG(exception))) { + /* A provider must not suspend with it pending, or a cancellation is lost */ + php_io_op_finish(op); + result->status = PHP_IO_CANCELLED; + result->index = 0; + result->res = -1; + result->error = ECANCELED; + return FAILURE; + } + if (hooks) { + php_io_op_clear_outputs(op); + result->status = PHP_IO_UNSUPPORTED; + result->index = 0; + result->res = 0; + result->error = 0; + zend_result rc = hooks->ops->run(hooks, op, result); + php_io_op_finish(op); + + if (rc == SUCCESS && !php_io_result_valid(op, result)) { + if (op->type == PHP_IO_OP_GETADDRINFO) { + result->error = EAI_FAIL; + result->res = -1; + } else { + zend_throw_error(NULL, "The IO provider completed an operation with an invalid result"); + rc = FAILURE; + } + } + if (rc == FAILURE) { + result->status = PHP_IO_CANCELLED; + result->res = -1; + result->error = ECANCELED; + return FAILURE; + } + if ((result->status != PHP_IO_UNSUPPORTED && !php_io_any_member_unsupported(op)) + || EG(exception)) { + return SUCCESS; + } + } + + zend_result rc = php_io_run_sync(op, result); + php_io_op_finish(op); + return rc; +} + +/* Wrappers */ + +static int php_io_poll_result_to_revents(const php_io_op_result *result, uint32_t events) +{ + switch (result->status) { + case PHP_IO_DONE: + case PHP_IO_READY: + if (result->error) { + php_io_set_errno(result->error); + return -1; + } + return result->res > 0 && result->res <= INT_MAX ? (int) result->res : (int) events; + case PHP_IO_TIMEOUT: + php_io_set_errno(ETIMEDOUT); + return 0; + case PHP_IO_INTERRUPTED: + php_io_set_errno(EINTR); + return -1; + case PHP_IO_CANCELLED: + php_io_set_errno(ECANCELED); + return -1; + case PHP_IO_UNSUPPORTED: + default: + php_io_set_errno(ENOTSUP); + return -1; + } +} + +/* The stream is frozen for the duration; a queue keeping the op past this + * frame (the ring on an abnormal exit) keeps it frozen until the op settled */ +typedef struct php_io_frame { + php_stream *stream; + zend_resource *res; +} php_io_frame; + +/* Fails with the Error of argument parsing when the stream is frozen by an + * op of another flow: internal consumers reach streams without it */ +static zend_result php_io_frame_begin(php_io_frame *f, php_stream *stream) +{ + f->stream = stream; + f->res = NULL; + if (stream) { + if (UNEXPECTED(stream->flags & PHP_STREAM_FLAG_IN_USE)) { + f->stream = NULL; + zend_throw_error(NULL, "Concurrent access to a stream"); + php_io_set_errno(ECANCELED); + return FAILURE; + } + /* No resource release can free the stream under the op */ + if (stream->res) { + f->res = stream->res; + GC_ADDREF(f->res); + } + stream->flags |= PHP_STREAM_FLAG_IN_USE; + } + return SUCCESS; +} + +static void php_io_frame_end(php_io_frame *f, php_io_op *op) +{ + if (f->stream && !(op && op->in_flight)) { + f->stream->flags &= ~PHP_STREAM_FLAG_IN_USE; + } + /* A last reference dropped meanwhile leaves the stream to the request + * shutdown: the stream layer above still uses it */ + if (f->res) { + GC_DELREF(f->res); + } +} + +PHPAPI void php_io_stream_keep_read(php_stream *stream, bool in_buffer, ssize_t res) +{ + if (res < 0) { + return; + } + if (res > 0 && !in_buffer) { + php_stream_mark_read_lost(stream); + } else { + php_stream_read_buffer_commit(stream, res); + } +} + +/* The caller is being cancelled: a finished read keeps its bytes and nothing more runs */ +static bool php_io_run_cancelled(php_stream *stream, const php_io_op *op, + const php_io_op_result *result) +{ + if (EXPECTED(!EG(exception))) { + return false; + } + if (result->status == PHP_IO_DONE) { + if (op->type != PHP_IO_OP_READ && op->type != PHP_IO_OP_RECV) { + return false; + } + if (stream && !result->error && php_io_op_read_advances(op)) { + php_io_stream_keep_read(stream, op->flags & PHP_IO_OP_F_STREAM_BUF, result->res); + } + } else if (result->status != PHP_IO_READY && result->status != PHP_IO_UNSUPPORTED) { + return false; + } + php_io_set_errno(ECANCELED); + return true; +} + +static zend_always_inline uint32_t php_io_hook_flags(void) +{ + return FG(io_hooks) ? FG(io_hooks)->flags : 0; +} + +/* A wait on a registrant's pair that reaches the provider registers the pair, as Edge or as Level + * when that is all the provider takes; an add() that threw cancels the wait. The registrant is the + * stream, or the handle of an op without one. */ +static zend_result php_io_op_register_wait(php_io_op *op, php_stream *stream, uint32_t event) +{ + if (!FG(io_hooks) || !(event == PHP_POLL_READ || event == PHP_POLL_WRITE)) { + return SUCCESS; + } + zend_object *pending = EG(exception); + if (stream) { + if (stream->flags & PHP_STREAM_FLAG_NO_IO_REGISTRATION) { + return SUCCESS; + } + op->registration = php_io_register_stream(stream, op->fd, event, PHP_IO_TRIGGER_EDGE); + } else if (op->handle) { + op->registration = php_io_register_handle(PHP_POLL_HANDLE_OBJ_FROM_ZOBJ(op->handle), event, + PHP_IO_TRIGGER_EDGE); + } else { + return SUCCESS; + } + if (EG(exception) != pending) { + php_io_set_errno(ECANCELED); + return FAILURE; + } + return SUCCESS; +} + +PHPAPI int php_io_poll_ex(php_stream *stream, zend_object *handle, php_socket_t fd, uint32_t events, + php_deadline *dl, uint32_t op_flags) +{ + php_io_op op; + php_io_op_result result; + php_io_frame f; + + if (php_io_frame_begin(&f, stream) == FAILURE) { + return -1; + } + php_io_op_poll(&op, handle, fd, events, *dl); + op.flags |= op_flags; + op.stream = stream; + zend_result rc = php_io_op_register_wait(&op, stream, events); + if (rc == SUCCESS) { + rc = php_io_run(&op, &result); + } + php_io_frame_end(&f, &op); + + if (rc == FAILURE) { + php_io_set_errno(ECANCELED); + return -1; + } + return php_io_poll_result_to_revents(&result, events); +} + +PHPAPI int php_io_poll(php_stream *stream, php_socket_t fd, uint32_t events, php_deadline *dl) +{ + return php_io_poll_ex(stream, NULL, fd, events, dl, 0); +} + +/* Status to a syscall-like return for a data op; true when the caller is done */ +static bool php_io_data_result(const php_io_op_result *result, ssize_t *ret) +{ + switch (result->status) { + case PHP_IO_DONE: + if (result->error) { + php_io_set_errno(result->error); + *ret = -1; + } else { + *ret = (ssize_t) result->res; + } + return true; + case PHP_IO_TIMEOUT: + php_io_set_errno(ETIMEDOUT); + *ret = -1; + return true; + case PHP_IO_INTERRUPTED: + php_io_set_errno(EINTR); + *ret = -1; + return true; + case PHP_IO_CANCELLED: + php_io_set_errno(ECANCELED); + *ret = -1; + return true; + case PHP_IO_READY: + case PHP_IO_UNSUPPORTED: + default: + return false; + } +} + +/* The arguments of a socket call made through the ladders below */ +typedef struct php_io_sock_call { + php_socket_t fd; + void *buf; + size_t len; + int flags; + struct sockaddr *addr; + socklen_t *addrlen; + socklen_t addrlen_in; +} php_io_sock_call; + +typedef ssize_t (*php_io_sock_syscall)(const php_io_sock_call *c); +typedef void (*php_io_sock_prep)(php_io_op *op, php_stream *stream, const php_io_sock_call *c, + php_deadline dl); + +/* The descriptor ladder: the syscall first and the op on EAGAIN, or the op first with the direct + * flag; Ready means retry the syscall, Unsupported means syscall first from now on. */ +static zend_always_inline ssize_t php_io_descriptor_op(php_stream *stream, zend_object *handle, + php_deadline *dl, uint32_t direct_flag, php_io_sock_syscall syscall_fn, php_io_sock_prep prep, + const php_io_sock_call *c) +{ + php_io_frame f; + php_io_op op; + php_io_op_result result; + ssize_t ret; + /* A stream at EOF gets the syscall first, as without the flag: recv() reports the end again + * and no op reaches the provider for a handle that already answers invalid */ + bool direct = (php_io_hook_flags() & direct_flag) != 0 && !(stream && stream->eof); + bool waited = false; + + if (php_io_frame_begin(&f, stream) == FAILURE) { + return -1; + } + memset(&op, 0, sizeof(op)); + for (;;) { + if (!direct) { + ret = syscall_fn(c); + if (ret >= 0 || !PHP_IS_TRANSIENT_ERROR(php_socket_errno())) { + break; + } + if (waited && dl->hrtime == 0) { + /* A non-blocking deadline gets one readiness check */ + break; + } + } + prep(&op, stream, c, *dl); + op.handle = handle; + op.stream = stream; + if (!direct) { + op.flags |= PHP_IO_OP_F_AFTER_DRAIN; + } + if (php_io_op_register_wait(&op, stream, op.ready_events) == FAILURE + || php_io_run(&op, &result) == FAILURE + || php_io_run_cancelled(stream, &op, &result)) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + if (php_io_data_result(&result, &ret)) { + break; + } + waited = true; + if (result.status == PHP_IO_UNSUPPORTED && !direct) { + php_io_set_errno(ENOTSUP); + ret = -1; + break; + } + direct = false; + } + php_io_frame_end(&f, &op); + return ret; +} + +/* Where _php_stream_fill_read_buffer() reads: memory the stream owns and + * frees only after its orphans were drained. A queue may leave an op on it + * past the frame; any other buffer is the caller's. */ +static zend_always_inline uint32_t php_io_stream_buf_flag(php_stream *stream, const void *buf) +{ + return stream && buf == php_stream_read_buffer_tail(stream) ? PHP_IO_OP_F_STREAM_BUF : 0; +} + +static zend_always_inline bool php_io_stream_read_lost(php_stream *stream) +{ + if (stream && php_stream_is_read_lost(stream)) { + php_io_set_errno(EIO); + return true; + } + return false; +} + +#ifdef PHP_WIN32 +# define PHP_IO_SOCKLEN(n) ((int) (n)) +#else +# define PHP_IO_SOCKLEN(n) (n) +#endif + +static ssize_t php_io_recv_syscall(const php_io_sock_call *c) +{ + return recv(c->fd, c->buf, c->len, c->flags); +} + +static void php_io_recv_prep(php_io_op *op, php_stream *stream, const php_io_sock_call *c, + php_deadline dl) +{ + php_io_op_recv(op, NULL, c->fd, c->buf, c->len, c->flags, dl); + op->flags |= php_io_stream_buf_flag(stream, c->buf); +} + +PHPAPI ssize_t php_io_recv_ex(php_stream *stream, zend_object *handle, php_socket_t fd, void *buf, + size_t len, int flags, php_deadline *dl) +{ + if (php_io_stream_read_lost(stream)) { + return -1; + } + php_io_sock_call c = { .fd = fd, .buf = buf, .len = len, .flags = flags }; + return php_io_descriptor_op(stream, handle, dl, PHP_IO_HOOKS_F_DIRECT_DATA, php_io_recv_syscall, + php_io_recv_prep, &c); +} + +PHPAPI ssize_t php_io_recv(php_stream *stream, php_socket_t fd, void *buf, size_t len, int flags, + php_deadline *dl) +{ + return php_io_recv_ex(stream, NULL, fd, buf, len, flags, dl); +} + +static ssize_t php_io_send_syscall(const php_io_sock_call *c) +{ + return send(c->fd, c->buf, c->len, c->flags); +} + +static void php_io_send_prep(php_io_op *op, php_stream *stream, const php_io_sock_call *c, + php_deadline dl) +{ + php_io_op_send(op, NULL, c->fd, c->buf, c->len, c->flags, dl); +} + +PHPAPI ssize_t php_io_send_ex(php_stream *stream, zend_object *handle, php_socket_t fd, const void *buf, + size_t len, int flags, php_deadline *dl) +{ + php_io_sock_call c = { .fd = fd, .buf = (void *) buf, .len = len, .flags = flags }; + return php_io_descriptor_op(stream, handle, dl, PHP_IO_HOOKS_F_DIRECT_DATA, php_io_send_syscall, + php_io_send_prep, &c); +} + +PHPAPI ssize_t php_io_send(php_stream *stream, php_socket_t fd, const void *buf, size_t len, int flags, + php_deadline *dl) +{ + return php_io_send_ex(stream, NULL, fd, buf, len, flags, dl); +} + +/* The readiness form of the ladder, for calls without a data op: a Poll op on EAGAIN, bounded by + * the caller's deadline; a non-blocking deadline gets one readiness check. */ +static zend_always_inline ssize_t php_io_readiness_op(php_stream *stream, zend_object *handle, + uint32_t events, php_deadline *dl, php_io_sock_syscall syscall_fn, const php_io_sock_call *c) +{ + ssize_t ret; + bool waited = false; + + for (;;) { + ret = syscall_fn(c); + if (ret >= 0 || !dl || !PHP_IS_TRANSIENT_ERROR(php_socket_errno())) { + break; + } + if (waited && dl->hrtime == 0) { + break; + } + if (php_io_poll_ex(stream, handle, c->fd, events, dl, PHP_IO_OP_F_AFTER_DRAIN) <= 0) { + /* errno: ETIMEDOUT, ECANCELED or the failure */ + ret = -1; + break; + } + if (EG(exception)) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + waited = true; + } + return ret; +} + +static ssize_t php_io_sendto_syscall(const php_io_sock_call *c) +{ + if (c->addr) { + return sendto(c->fd, c->buf, PHP_IO_SOCKLEN(c->len), c->flags, c->addr, PHP_IO_SOCKLEN(c->addrlen_in)); + } + return send(c->fd, c->buf, PHP_IO_SOCKLEN(c->len), c->flags); +} + +PHPAPI ssize_t php_io_sendto_ex(php_stream *stream, zend_object *handle, php_socket_t fd, + const void *buf, size_t len, int flags, const struct sockaddr *addr, socklen_t addrlen, + php_deadline *dl) +{ + php_io_sock_call c = { .fd = fd, .buf = (void *) buf, .len = len, .flags = flags, + .addr = (struct sockaddr *) addr, .addrlen_in = addrlen }; + return php_io_readiness_op(stream, handle, PHP_POLL_WRITE, dl, php_io_sendto_syscall, &c); +} + +PHPAPI ssize_t php_io_sendto(php_stream *stream, php_socket_t fd, const void *buf, size_t len, int flags, + const struct sockaddr *addr, socklen_t addrlen, php_deadline *dl) +{ + return php_io_sendto_ex(stream, NULL, fd, buf, len, flags, addr, addrlen, dl); +} + +static ssize_t php_io_recvfrom_syscall(const php_io_sock_call *c) +{ + if (c->addr) { + return recvfrom(c->fd, c->buf, PHP_IO_SOCKLEN(c->len), c->flags, c->addr, c->addrlen); + } + return recv(c->fd, c->buf, PHP_IO_SOCKLEN(c->len), c->flags); +} + +PHPAPI ssize_t php_io_recvfrom_ex(php_stream *stream, zend_object *handle, php_socket_t fd, void *buf, + size_t len, int flags, struct sockaddr *addr, socklen_t *addrlen, php_deadline *dl) +{ + if (php_io_stream_read_lost(stream)) { + return -1; + } + php_io_sock_call c = { .fd = fd, .buf = buf, .len = len, .flags = flags, .addr = addr, .addrlen = addrlen }; + return php_io_readiness_op(stream, handle, PHP_POLL_READ, dl, php_io_recvfrom_syscall, &c); +} + +PHPAPI ssize_t php_io_recvfrom(php_stream *stream, php_socket_t fd, void *buf, size_t len, int flags, + struct sockaddr *addr, socklen_t *addrlen, php_deadline *dl) +{ + return php_io_recvfrom_ex(stream, NULL, fd, buf, len, flags, addr, addrlen, dl); +} + +static ssize_t php_io_accept_syscall(const php_io_sock_call *c) +{ + return (ssize_t) accept(c->fd, c->addr, c->addrlen); +} + +static void php_io_accept_prep(php_io_op *op, php_stream *stream, const php_io_sock_call *c, + php_deadline dl) +{ + php_io_op_accept(op, NULL, c->fd, c->addr, c->addrlen, dl); +} + +PHPAPI php_socket_t php_io_accept_ex(php_stream *stream, zend_object *handle, php_socket_t fd, + struct sockaddr *addr, socklen_t *addrlen, php_deadline *dl) +{ + php_io_sock_call c = { .fd = fd, .addr = addr, .addrlen = addrlen }; + /* A listener kept unregistered gets no multishot accept, so a direct accept on it would take + * one connection per pass: accept() first instead */ + uint32_t direct_flag = stream && (stream->flags & PHP_STREAM_FLAG_NO_IO_REGISTRATION) + ? 0 : PHP_IO_HOOKS_F_DIRECT_ACCEPT; + return php_io_descriptor_op(stream, handle, dl, direct_flag, php_io_accept_syscall, + php_io_accept_prep, &c); +} + +PHPAPI php_socket_t php_io_accept(php_stream *stream, php_socket_t fd, struct sockaddr *addr, + socklen_t *addrlen, php_deadline *dl) +{ + return php_io_accept_ex(stream, NULL, fd, addr, addrlen, dl); +} + +/* A non-blocking connect that is under way: EINPROGRESS, EAGAIN on some + * systems, WSAEWOULDBLOCK on Windows (where EINPROGRESS is defined as it), + * EALREADY for one started before */ +#ifdef PHP_WIN32 +# define PHP_IO_IS_EALREADY(err) ((err) == EALREADY || (err) == WSAEALREADY) +#else +# define PHP_IO_IS_EALREADY(err) ((err) == EALREADY) +#endif +#define PHP_IO_CONNECT_PENDING(err) ((err) == EINPROGRESS || (err) == EAGAIN || (err) == EWOULDBLOCK || PHP_IO_IS_EALREADY(err)) + +/* The connect is started once; the wait completes as Ready and the result + * is read from SO_ERROR, or as Done when the provider connected itself */ +PHPAPI int php_io_connect_ex(php_stream *stream, zend_object *handle, php_socket_t fd, + const struct sockaddr *addr, socklen_t addrlen, php_deadline *dl) +{ + php_io_frame f; + php_io_op op; + php_io_op_result result; + int ret = 0; + bool direct = (php_io_hook_flags() & PHP_IO_HOOKS_F_DIRECT_DATA) != 0; + bool started = false; /* our own connect() is in progress */ + + if (php_io_frame_begin(&f, stream) == FAILURE) { + return -1; + } + memset(&op, 0, sizeof(op)); + + if (!direct) { + if (connect(fd, addr, addrlen) == 0) { + goto out; + } + if (!PHP_IO_CONNECT_PENDING(php_socket_errno())) { + ret = -1; + goto out; + } + started = true; + } + + for (;;) { + php_io_op_connect(&op, handle, fd, addr, addrlen, *dl); + op.stream = stream; + if (started) { + op.flags |= PHP_IO_OP_F_AFTER_DRAIN; + } + if (php_io_op_register_wait(&op, stream, PHP_POLL_WRITE) == FAILURE + || php_io_run(&op, &result) == FAILURE) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + if ((result.status == PHP_IO_READY && !started) + || (result.status == PHP_IO_UNSUPPORTED && direct)) { + /* The provider only waited, or does not connect: start it + * ourselves and wait for writability */ + direct = false; + if (connect(fd, addr, addrlen) == 0) { + break; + } + if (!PHP_IO_CONNECT_PENDING(php_socket_errno())) { + ret = -1; + break; + } + started = true; + continue; + } + if (started && result.status == PHP_IO_DONE && result.res < 0 && PHP_IO_IS_EALREADY(result.error)) { + /* A provider that performs the op found our connect still under + * way: wait for its outcome like after a readiness report */ + php_io_op_poll(&op, handle, fd, PHP_POLL_WRITE, *dl); + op.flags |= PHP_IO_OP_F_AFTER_DRAIN; + op.stream = stream; + if (php_io_op_register_wait(&op, stream, PHP_POLL_WRITE) == FAILURE + || php_io_run(&op, &result) == FAILURE) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + int n = php_io_poll_result_to_revents(&result, PHP_POLL_WRITE); + if (n <= 0) { + ret = -1; + break; + } + result.status = PHP_IO_READY; + } + /* A provider that performs the op connects a socket whose connect + * we started already: EISCONN then means it completed meanwhile + * and the outcome is in SO_ERROR, as after a readiness report */ + if (result.status == PHP_IO_READY + || (started && result.status == PHP_IO_DONE && result.res < 0 && result.error == EISCONN)) { + int error = 0; + socklen_t len = sizeof(error); + if (getsockopt(fd, SOL_SOCKET, SO_ERROR, (char *) &error, &len) != 0) { + ret = -1; + } else if (error) { + php_io_set_errno(error); + ret = -1; + } + break; + } + ssize_t r; + if (php_io_data_result(&result, &r)) { + ret = r < 0 ? -1 : 0; + break; + } + php_io_set_errno(ENOTSUP); + ret = -1; + break; + } + +out: + php_io_frame_end(&f, &op); + return ret; +} + +PHPAPI int php_io_connect(php_stream *stream, php_socket_t fd, const struct sockaddr *addr, + socklen_t addrlen, php_deadline *dl) +{ + return php_io_connect_ex(stream, NULL, fd, addr, addrlen, dl); +} + +/* Regular files have no readiness form: without F_FILES the call is synchronous. Other descriptors + * wait for readiness unless the provider performs the op or the deadline is non-blocking. */ +static ssize_t php_io_file_op(php_stream *stream, int fd, php_deadline *dl, bool regular, + ssize_t (*syscall_fn)(int, void *, size_t, int64_t), void *buf, size_t len, int64_t offset, + void (*prep)(php_io_op *, zend_object *, php_socket_t, void *, size_t, int64_t, php_deadline)) +{ + uint32_t flags = php_io_hook_flags(); + php_io_frame f; + php_io_op op; + php_io_op_result result; + ssize_t ret; + + if (php_io_frame_begin(&f, stream) == FAILURE) { + return -1; + } + memset(&op, 0, sizeof(op)); + + bool nonblock = !regular && dl->hrtime == 0; + bool offload = FG(io_hooks) && !nonblock + && (regular ? (flags & PHP_IO_HOOKS_F_FILES) : (flags & (PHP_IO_HOOKS_F_FILES | PHP_IO_HOOKS_F_DIRECT_DATA))); + bool ready = nonblock || !FG(io_hooks); + bool drained = false; /* the syscall returned EAGAIN: the next wait follows a drain */ + for (;;) { + if (offload) { + prep(&op, NULL, fd, buf, len, offset, *dl); + if (prep == php_io_op_read) { + op.flags |= php_io_stream_buf_flag(stream, buf); + } + op.stream = stream; + if ((!regular && php_io_op_register_wait(&op, stream, op.ready_events) == FAILURE) + || php_io_run(&op, &result) == FAILURE + || php_io_run_cancelled(stream, &op, &result)) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + offload = false; + if (php_io_data_result(&result, &ret)) { + if (ret < 0 && !regular && PHP_IS_TRANSIENT_ERROR(errno)) { + ready = false; + drained = true; + continue; + } + break; + } + /* Ready or Unsupported: perform it here */ + ready = result.status == PHP_IO_READY; + } + if (!regular && !ready) { + /* Wait for readiness before the syscall */ + uint32_t events = prep == php_io_op_read ? PHP_POLL_READ : PHP_POLL_WRITE; + php_io_op_poll(&op, NULL, fd, events, *dl); + if (drained) { + op.flags |= PHP_IO_OP_F_AFTER_DRAIN; + } + op.stream = stream; + if (php_io_op_register_wait(&op, stream, events) == FAILURE + || php_io_run(&op, &result) == FAILURE + || php_io_run_cancelled(stream, &op, &result)) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + int n = php_io_poll_result_to_revents(&result, events); + if (n < 0) { + ret = -1; + break; + } + if (n == 0) { + php_io_set_errno(ETIMEDOUT); + ret = -1; + break; + } + } + ret = syscall_fn(fd, buf, len, offset); + if (ret < 0 && errno == EINTR) { + /* Retried once; a second signal is left to the script */ + ret = syscall_fn(fd, buf, len, offset); + } + if (ret < 0 && !regular && !nonblock && PHP_IS_TRANSIENT_ERROR(errno)) { + ready = false; + drained = true; + continue; + } + break; + } + + php_io_frame_end(&f, &op); + return ret; +} + +/* The synchronous forms; an offset means pread and pwrite, on Windows the + * overlapped call an overlapped descriptor needs */ +static ssize_t php_io_read_syscall(int fd, void *buf, size_t len, int64_t offset) +{ + if (offset < 0) { + return read(fd, buf, len); + } +#ifdef PHP_WIN32 + return php_win32_ioutil_pread(fd, buf, len, offset); +#else + return pread(fd, buf, len, (off_t) offset); +#endif +} + +static ssize_t php_io_write_syscall(int fd, void *buf, size_t len, int64_t offset) +{ + if (offset < 0) { + return write(fd, buf, len); + } +#ifdef PHP_WIN32 + return php_win32_ioutil_pwrite(fd, buf, len, offset); +#else + return pwrite(fd, buf, len, (off_t) offset); +#endif +} + +PHPAPI ssize_t php_io_read_at(php_stream *stream, int fd, void *buf, size_t len, int64_t offset, php_deadline *dl) +{ + if (php_io_stream_read_lost(stream)) { + return -1; + } + bool regular = !stream || !(stream->flags & PHP_STREAM_FLAG_NO_SEEK); + return php_io_file_op(stream, fd, dl, regular, php_io_read_syscall, buf, len, offset, php_io_op_read); +} + +PHPAPI ssize_t php_io_write_at(php_stream *stream, int fd, const void *buf, size_t len, int64_t offset, php_deadline *dl) +{ + bool regular = !stream || !(stream->flags & PHP_STREAM_FLAG_NO_SEEK); + return php_io_file_op(stream, fd, dl, regular, php_io_write_syscall, (void *) buf, len, offset, + (void (*)(php_io_op *, zend_object *, php_socket_t, void *, size_t, int64_t, php_deadline)) php_io_op_write); +} + +PHPAPI ssize_t php_io_read(php_stream *stream, int fd, void *buf, size_t len, php_deadline *dl) +{ + return php_io_read_at(stream, fd, buf, len, -1, dl); +} + +PHPAPI ssize_t php_io_write(php_stream *stream, int fd, const void *buf, size_t len, php_deadline *dl) +{ + return php_io_write_at(stream, fd, buf, len, -1, dl); +} + +PHPAPI int php_io_fsync(php_stream *stream, int fd, bool data_only) +{ + if (FG(io_hooks) && (php_io_hook_flags() & PHP_IO_HOOKS_F_FILES)) { + php_io_frame f; + php_io_op op; + php_io_op_result result; + ssize_t ret; + + if (php_io_frame_begin(&f, stream) == FAILURE) { + return -1; + } + php_io_op_fsync(&op, NULL, fd, data_only); + op.stream = stream; + zend_result rc = php_io_run(&op, &result); + php_io_frame_end(&f, &op); + if (rc == FAILURE) { + php_io_set_errno(ECANCELED); + return -1; + } + if (php_io_data_result(&result, &ret)) { + return ret < 0 ? -1 : 0; + } + } +#ifdef HAVE_FDATASYNC + return data_only ? fdatasync(fd) : fsync(fd); +#else + return fsync(fd); +#endif +} + +/* DNS: always handed to a provider, which may answer from its own + * resolver; Unsupported means the library call */ +/* A numeric host needs no lookup and must not reach a provider */ +static bool php_io_host_is_numeric(const char *node, const struct addrinfo *hints) +{ + if (!node) { + return true; + } + if (hints && (hints->ai_flags & AI_NUMERICHOST)) { + return true; + } + struct in_addr in4; + if (inet_pton(AF_INET, node, &in4) == 1) { + return true; + } +#ifdef HAVE_IPV6 + struct in6_addr in6; + if (inet_pton(AF_INET6, node, &in6) == 1) { + return true; + } +#endif + return false; +} + +PHPAPI int php_io_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res, php_deadline *dl) +{ + if (FG(io_hooks) && !php_io_host_is_numeric(node, hints)) { + php_io_op op; + php_io_op_result result; + php_io_op_getaddrinfo(&op, node, service, hints, res, *dl); + if (php_io_run(&op, &result) == FAILURE) { + return EAI_SYSTEM; + } + if (result.status == PHP_IO_DONE) { + return result.error; + } + if (result.status == PHP_IO_TIMEOUT) { + return EAI_AGAIN; + } + if (result.status == PHP_IO_CANCELLED) { + return EAI_SYSTEM; + } + } + return getaddrinfo(node, service, hints, res); +} + +PHPAPI int php_io_getnameinfo(const struct sockaddr *addr, socklen_t addrlen, int flags, char *host, size_t hostlen, char *service, size_t servicelen, php_deadline *dl) +{ + if (FG(io_hooks)) { + php_io_op op; + php_io_op_result result; + php_io_op_getnameinfo(&op, addr, addrlen, flags, host, hostlen, service, servicelen, *dl); + if (php_io_run(&op, &result) == FAILURE) { + return EAI_SYSTEM; + } + if (result.status == PHP_IO_DONE) { + return result.error; + } + if (result.status == PHP_IO_TIMEOUT) { + return EAI_AGAIN; + } + if (result.status == PHP_IO_CANCELLED) { + return EAI_SYSTEM; + } + } + return getnameinfo(addr, addrlen, host, hostlen, service, servicelen, flags); +} + +#ifdef PHP_WIN32 +/* The synchronous wait: the process opened by id, WaitForSingleObject() up to the deadline, the + * exit code as the status. 0 when a non-blocking deadline finds it running, like WNOHANG. */ +static pid_t php_io_waitpid_sync(pid_t pid, int *status, php_deadline *dl) +{ + HANDLE process = OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE, (DWORD) pid); + if (!process) { + _set_errno(GetLastError() == ERROR_INVALID_PARAMETER ? ECHILD : EACCES); + return -1; + } + int remaining = dl ? php_deadline_to_timeout_ms(dl) : -1; + DWORD waited = WaitForSingleObject(process, remaining < 0 ? INFINITE : (DWORD) remaining); + pid_t ret; + if (waited == WAIT_OBJECT_0) { + DWORD code = 0; + if (GetExitCodeProcess(process, &code)) { + if (status) { + *status = (int) code; + } + ret = pid; + } else { + _set_errno(EACCES); + ret = -1; + } + } else if (waited == WAIT_TIMEOUT) { + if (dl && dl->hrtime == 0) { + ret = 0; + } else { + _set_errno(ETIMEDOUT); + ret = -1; + } + } else { + _set_errno(EACCES); + ret = -1; + } + CloseHandle(process); + return ret; +} + +/* The wait is the provider's where there is one: a ring serves it from a process handle, the + * poll queue has no source for it and answers Unsupported, and the core waits itself then. pid + * names any process, not only a child, and options are ignored: a non-blocking deadline is the + * WNOHANG of this platform. */ +PHPAPI pid_t php_io_waitpid(zend_object *handle, pid_t pid, int *status, int options, php_deadline *dl) +{ + if (pid <= 0) { + _set_errno(ENOTSUP); + return -1; + } + + if (FG(io_hooks) && !(dl && dl->hrtime == 0)) { + php_io_op op; + php_io_op_result result; + pid_t ret; + + for (;;) { + php_io_op_waitpid(&op, handle, pid, options, status, *dl); + if (php_io_run(&op, &result) == FAILURE) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + if (result.status == PHP_IO_READY) { + /* A handle the provider watched: the process asked without waiting */ + php_deadline now; + php_deadline_init_nonblock(&now); + ret = php_io_waitpid_sync(pid, status, &now); + if (ret != 0) { + break; + } + continue; + } + if (result.status == PHP_IO_UNSUPPORTED) { + ret = php_io_waitpid_sync(pid, status, dl); + break; + } + ssize_t r; + php_io_data_result(&result, &r); + ret = (pid_t) r; + break; + } + return ret; + } + + return php_io_waitpid_sync(pid, status, dl); +} +#endif + +#ifndef PHP_WIN32 +/* The wait is the provider's and collecting the child is the core's. After Ready the core collects + * with waitpid(WNOHANG) and waits again when nothing changed yet. A Done names a child the ring + * observed without collecting it: the core collects that pid, and waits again when another waiter + * collected it first. The op's descriptor is the process or signal source, so the poll queue + * completes it Ready without a handle object. */ +PHPAPI pid_t php_io_waitpid(zend_object *handle, pid_t pid, int *status, int options, php_deadline *dl) +{ + if (FG(io_hooks) && !(options & WNOHANG)) { + php_io_op op; + php_io_op_result result; + pid_t ret; + int pidfd = -1; + int watchable = 0; +#ifdef WUNTRACED + watchable |= WUNTRACED; +#endif +#ifdef WCONTINUED + watchable |= WCONTINUED; +#endif + if (pid > 0 && !(options & watchable)) { + pidfd = php_poll_process_source_open(pid); + } + + for (;;) { + php_io_op_waitpid(&op, handle, pid, options, status, *dl); + if (pidfd >= 0) { + op.fd = pidfd; + } + if (php_io_run(&op, &result) == FAILURE) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + if (result.status == PHP_IO_READY) { + ret = waitpid(pid, status, options | WNOHANG); + if (ret != 0) { + break; + } + continue; + } + if (result.status == PHP_IO_UNSUPPORTED) { + ret = waitpid(pid, status, options); + break; + } + ssize_t r; + php_io_data_result(&result, &r); + ret = (pid_t) r; + if (ret > 0 && waitpid(ret, status, options | WNOHANG) != ret) { + continue; + } + break; + } + if (pidfd >= 0) { + close(pidfd); + } + return ret; + } + + return waitpid(pid, status, options); +} + +PHPAPI int php_io_sigwait(zend_object *handle, const php_sigset_t *set, php_siginfo_t *info, php_deadline *dl) +{ +#ifdef HAVE_SIGTIMEDWAIT + bool as_op = FG(io_hooks) != NULL; +#else + /* Without sigtimedwait() the synchronous wait is the op on the core + * queue too, which serves the deadline from the signal source */ + bool as_op = true; +#endif + if (as_op) { + php_io_op op; + php_io_op_result result; + int ret; + int sfd = php_poll_signal_source_open(set); + + for (;;) { + php_io_op_sigwait(&op, handle, set, info, *dl); + if (sfd >= 0) { + op.fd = sfd; + } + if (php_io_run(&op, &result) == FAILURE) { + php_io_set_errno(ECANCELED); + ret = -1; + break; + } + bool again = result.status == PHP_IO_DONE && result.error == EAGAIN; + if (result.status == PHP_IO_READY) { + if (op.u.sigwait.taken > 0) { + ret = op.u.sigwait.taken; + break; + } + ret = php_poll_signal_source_take(sfd, set, info); + if (ret > 0) { + break; + } + again = true; + } + if (again) { + /* Another wait collected the signal first */ + if (!php_deadline_is_infinite(dl) && php_io_deadline_remaining(dl, zend_hrtime()) == 0) { + php_io_set_errno(EAGAIN); + ret = -1; + break; + } + continue; + } + if (result.status == PHP_IO_UNSUPPORTED) { + ret = -2; + break; + } + if (result.status == PHP_IO_TIMEOUT) { + if (op.u.sigwait.taken > 0) { + /* The handle consumed it in the same reap as the deadline */ + ret = op.u.sigwait.taken; + break; + } + php_io_set_errno(EAGAIN); + ret = -1; + break; + } + ssize_t r; + php_io_data_result(&result, &r); + ret = (int) r; + break; + } + if (sfd >= 0) { + close(sfd); + } + if (ret != -2) { + return ret; + } + } + + /* Unsupported by the provider and by the core queue: no source */ +#ifdef HAVE_SIGTIMEDWAIT + if (php_deadline_is_infinite(dl)) { + return sigwaitinfo(set, info); + } + zend_hrtime_t remaining = php_io_deadline_remaining(dl, zend_hrtime()); + struct timespec ts = { + .tv_sec = remaining / ZEND_NANO_IN_SEC, + .tv_nsec = remaining % ZEND_NANO_IN_SEC, + }; + return sigtimedwait(set, info, &ts); +#else + if (php_deadline_is_infinite(dl)) { + int signo; + int err = sigwait(set, &signo); + if (err != 0) { + php_io_set_errno(err); + return -1; + } + if (info) { + memset(info, 0, sizeof(*info)); + info->si_signo = signo; + } + return signo; + } + /* A timed wait without a source has nothing to wait on here */ + php_io_set_errno(ENOSYS); + return -1; +#endif +} +#endif + +PHPAPI zend_result php_io_sleep(php_deadline dl, bool *interrupted) +{ + php_io_op op; + php_io_op_result result; + + php_io_op_timer(&op, dl); + zend_result rc = php_io_run(&op, &result); + if (interrupted) { + *interrupted = rc == SUCCESS && result.status == PHP_IO_INTERRUPTED; + } + return rc; +} diff --git a/main/io/php_io_queue_poll.c b/main/io/php_io_queue_poll.c new file mode 100644 index 000000000000..1a5930a44214 --- /dev/null +++ b/main/io/php_io_queue_poll.c @@ -0,0 +1,788 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +/* The poll queue: a Poll context and its timers. Poll ops complete as Done with the triggered + * events, other ops with a descriptor as Ready, ops without one as Unsupported. + * + * One record per descriptor carries the union of the submitted ops' events. A record nobody waits + * on is taken out of the context, since epoll reports hangups with no events armed. A registration + * retains the record between waits. An Edge pair also stays in the context edge-triggered for the + * pair's life, its reports recorded as ready bits when no wait consumes them, so a readiness wait + * on it is answered from the record and costs no syscall; a Poll op or a Level wait on such a + * descriptor re-checks readiness through a modify. Without edge-triggering in the backend an Edge + * pair is served as Level. */ + +#include "php.h" +#include "php_io.h" +#include "main/php_poll.h" + +#include + +#define PHP_IO_POLL_MIN_EVENTS 64 + +typedef struct _php_io_poll_req php_io_poll_req; +typedef struct _php_io_poll_fdreg php_io_poll_fdreg; + +/* One descriptor in the context */ +struct _php_io_poll_fdreg { + int fd; + uint32_t armed; /* events registered in the context */ + bool in_ctx; + bool stale; /* removal from the context failed */ + bool dead; /* dropped while stale */ + bool hup; /* a hangup or error was reported: every edge wait completes at once */ + uint32_t edge; /* events of the Edge registrations, kept armed edge-triggered */ + uint32_t level; /* events of the Level registrations, the record only */ + uint32_t ready; /* edge events reported and consumed by no wait */ + php_io_poll_req *reqs; /* submitted ops on the descriptor */ +}; + +struct _php_io_poll_req { + php_io_op *op; + void *data; + php_io_op_result result; + php_io_poll_req *group; /* member: the Any's request */ + uint32_t index; /* member: position in the Any */ + php_poll_timer *timer; /* the deadline, or the Timer op itself */ + php_io_poll_fdreg *fdreg; /* the descriptor the op waits on */ + uint32_t events; /* the interest on fdreg */ + php_io_poll_req *fd_next; /* fdreg->reqs */ + bool done; /* member: result recorded */ + bool ready; /* top-level: in the ready list */ + bool fired; /* group: in the fired list */ + php_io_poll_req **members; /* group */ + uint32_t n_members; /* group */ + php_io_poll_req *prev; /* outstanding list, top-level only */ + php_io_poll_req *next; +}; + +typedef struct php_io_poll_queue { + php_io_queue base; + uint64_t id; + php_poll_ctx *ctx; + HashTable fdregs; /* fd -> php_io_poll_fdreg */ + HashTable dead; /* records the context may still report */ + bool et; /* the backend keeps edge-triggered entries */ + uint32_t n_waiting; /* submitted ops armed on a descriptor */ + php_io_poll_req *outstanding; + uint32_t pending; + php_io_poll_req **ready; + uint32_t n_ready; + uint32_t ready_cap; + php_io_poll_req **fired; + uint32_t n_fired; + uint32_t fired_cap; + php_poll_event *events; + uint32_t events_cap; +} php_io_poll_queue; + +static int php_io_poll_error_to_errno(php_poll_error err) +{ + switch (err) { + case PHP_POLL_ERR_NOMEM: return ENOMEM; + case PHP_POLL_ERR_INVALID: return EBADF; + case PHP_POLL_ERR_EXISTS: return EEXIST; + case PHP_POLL_ERR_NOTFOUND: return ENOENT; + case PHP_POLL_ERR_INTERRUPTED: return EINTR; + case PHP_POLL_ERR_PERMISSION: return EPERM; + case PHP_POLL_ERR_TOOBIG: return EMFILE; + case PHP_POLL_ERR_AGAIN: return EAGAIN; + case PHP_POLL_ERR_NOSUPPORT: return ENOTSUP; + default: return EIO; + } +} + +/* Lists */ + +static void php_io_poll_list_push(php_io_poll_req ***list, uint32_t *n, uint32_t *cap, php_io_poll_req *req) +{ + if (*n == *cap) { + *cap = *cap ? *cap * 2 : 16; + *list = safe_erealloc(*list, *cap, sizeof(**list), 0); + } + (*list)[(*n)++] = req; +} + +static void php_io_poll_list_remove(php_io_poll_req **list, uint32_t *n, php_io_poll_req *req) +{ + for (uint32_t i = 0; i < *n; i++) { + if (list[i] == req) { + memmove(&list[i], &list[i + 1], (*n - i - 1) * sizeof(*list)); + (*n)--; + return; + } + } + ZEND_UNREACHABLE(); +} + +/* Descriptor registrations */ + +static php_io_poll_fdreg *php_io_poll_fdreg_get(php_io_poll_queue *q, int fd, bool create) +{ + php_io_poll_fdreg *reg = zend_hash_index_find_ptr(&q->fdregs, (zend_ulong) fd); + if (!reg && create) { + reg = ecalloc(1, sizeof(*reg)); + reg->fd = fd; + zend_hash_index_add_new_ptr(&q->fdregs, (zend_ulong) fd, reg); + } + return reg; +} + +/* The record of a registered pair through the registration, which add() pointed at it; a record + * another queue left there is told by the queue id, and looked up by descriptor. A registered pair + * keeps its record alive. */ +static zend_always_inline php_io_poll_fdreg *php_io_poll_fdreg_of(php_io_poll_queue *q, + php_io_registration *registration, int fd, bool create) +{ + if (registration && registration->queue_id == q->id && registration->queue_data) { + return registration->queue_data; + } + return php_io_poll_fdreg_get(q, fd, create); +} + +/* Out of the context. The removal fails when the descriptor was closed + * while registered, or its number reused: epoll keeps the entry as long as + * another descriptor refers to the old file, and may still report it. */ +static void php_io_poll_fdreg_leave(php_io_poll_queue *q, php_io_poll_fdreg *reg) +{ + if (reg->in_ctx) { + if (php_poll_remove(q->ctx, reg->fd) != SUCCESS) { + reg->stale = true; + } + reg->in_ctx = false; + } + reg->armed = 0; +} + +static void php_io_poll_fdreg_drop(php_io_poll_queue *q, php_io_poll_fdreg *reg) +{ + php_io_poll_fdreg_leave(q, reg); + zend_hash_index_del(&q->fdregs, (zend_ulong) reg->fd); + if (!reg->stale) { + efree(reg); + } else { + /* Kept so that a late report finds it, and ignored */ + reg->dead = true; + zend_hash_index_add_new_ptr(&q->dead, (zend_ulong) (uintptr_t) reg, reg); + } +} + +/* Bring the context's interest in line with the submitted ops and the Edge pairs. A modify with + * an unchanged mask is skipped unless forced, which re-checks readiness of an edge-triggered + * entry. On failure *err says why and the record is out of the context. */ +static zend_result php_io_poll_fdreg_sync(php_io_poll_queue *q, php_io_poll_fdreg *reg, php_poll_error *err, + bool force) +{ + uint32_t wanted = reg->edge; + for (php_io_poll_req *r = reg->reqs; r; r = r->fd_next) { + wanted |= r->events; + } + if (reg->edge) { + wanted |= PHP_POLL_ET; + } + + if (!reg->reqs && !reg->edge && !reg->level) { + php_io_poll_fdreg_drop(q, reg); + return SUCCESS; + } + if (!wanted) { + php_io_poll_fdreg_leave(q, reg); + return SUCCESS; + } + if (reg->in_ctx && wanted == reg->armed && !force) { + return SUCCESS; + } + if (reg->in_ctx && php_poll_modify(q->ctx, reg->fd, wanted, reg) != SUCCESS) { + *err = php_poll_get_error(q->ctx); + php_io_poll_fdreg_leave(q, reg); + return FAILURE; + } + if (!reg->in_ctx) { + if (php_poll_add(q->ctx, reg->fd, wanted, reg) != SUCCESS) { + *err = php_poll_get_error(q->ctx); + return FAILURE; + } + reg->in_ctx = true; + } + reg->armed = wanted; + return SUCCESS; +} + +static void php_io_poll_fdreg_unlink(php_io_poll_req *req) +{ + php_io_poll_req **link = &req->fdreg->reqs; + while (*link != req) { + link = &(*link)->fd_next; + } + *link = req->fd_next; + req->fd_next = NULL; +} + +/* Requests */ + +static php_io_poll_req *php_io_poll_req_create(php_io_poll_queue *q, php_io_op *op, void *data) +{ + php_io_poll_req *req = ecalloc(1, sizeof(*req)); + req->op = op; + req->data = data; + op->queue = &q->base; + op->queue_data = req; + op->in_flight = false; + return req; +} + +static void php_io_poll_req_unregister(php_io_poll_queue *q, php_io_poll_req *req) +{ + if (req->fdreg) { + php_io_poll_fdreg *reg = req->fdreg; + php_io_poll_fdreg_unlink(req); + req->fdreg = NULL; + q->n_waiting--; + /* On failure the record is left out of the context, which the + * remaining ops re-arm on their next submit */ + php_poll_error err; + php_io_poll_fdreg_sync(q, reg, &err, false); + } + if (req->timer) { + php_poll_timer_remove(q->ctx, req->timer); + req->timer = NULL; + } +} + +static void php_io_poll_req_free(php_io_poll_req *req) +{ + req->op->queue = NULL; + req->op->queue_data = NULL; + efree(req); +} + +static void php_io_poll_req_free_top(php_io_poll_queue *q, php_io_poll_req *req) +{ + if (req->prev) { + req->prev->next = req->next; + } else { + q->outstanding = req->next; + } + if (req->next) { + req->next->prev = req->prev; + } + q->pending--; + php_io_poll_req_free(req); +} + +static void php_io_poll_req_complete(php_io_poll_queue *q, php_io_poll_req *req, + php_io_status status, int64_t res, int error) +{ + php_io_poll_req_unregister(q, req); + + if (req->group) { + if (!req->done) { + req->done = true; + req->result.status = status; + req->result.index = req->index; + req->result.res = res; + req->result.error = error; + if (!req->group->fired) { + req->group->fired = true; + php_io_poll_list_push(&q->fired, &q->n_fired, &q->fired_cap, req->group); + } + } + return; + } + + ZEND_ASSERT(!req->ready); + req->ready = true; + req->result.status = status; + req->result.index = 0; + req->result.res = res; + req->result.error = error; + php_io_poll_list_push(&q->ready, &q->n_ready, &q->ready_cap, req); +} + +static void php_io_poll_req_arm(php_io_poll_queue *q, php_io_poll_req *req) +{ + php_io_op *op = req->op; + + if (op->type == PHP_IO_OP_TIMER) { + if (!php_deadline_is_infinite(&op->deadline)) { + req->timer = php_poll_timer_add(q->ctx, op->deadline.hrtime, 0, req); + } + return; + } + + uint32_t events = op->type == PHP_IO_OP_POLL ? op->u.poll.events : op->ready_events; + if (events & (PHP_POLL_PROCESS | PHP_POLL_SIGNAL)) { + /* A pidfd or signalfd, readable when there is something to take */ + events = PHP_POLL_READ; + } + events &= PHP_POLL_READ | PHP_POLL_WRITE | PHP_POLL_ERROR | PHP_POLL_HUP | PHP_POLL_RDHUP | PHP_POLL_PRI; + if (op->fd == SOCK_ERR || events == 0) { + php_io_poll_req_complete(q, req, PHP_IO_UNSUPPORTED, 0, 0); + return; + } + + php_io_poll_fdreg *reg = php_io_poll_fdreg_of(q, op->registration, (int) op->fd, true); + + /* A wait after a drain on an Edge pair: answered from the record when it can be */ + bool edge_wait = (op->flags & PHP_IO_OP_F_AFTER_DRAIN) && op->registration + && op->registration->trigger == PHP_IO_TRIGGER_EDGE && (reg->edge & events) == events; + if (edge_wait && (reg->hup || (reg->ready & events))) { + uint32_t revents = reg->hup ? (events | PHP_POLL_HUP) : (reg->ready & events); + reg->ready &= ~events; + php_io_poll_req_complete(q, req, op->type == PHP_IO_OP_POLL ? PHP_IO_DONE : PHP_IO_READY, revents, 0); + return; + } + + req->fdreg = reg; + req->events = events; + req->fd_next = reg->reqs; + reg->reqs = req; + q->n_waiting++; + + /* A Poll op or a Level wait on an edge-triggered entry checks readiness now, unless its + * caller just did: an edge since then is queued for the wait either way */ + php_poll_error err; + bool force = !edge_wait && reg->edge && !(op->flags & PHP_IO_OP_F_CHECKED); + if (php_io_poll_fdreg_sync(q, reg, &err, force) != SUCCESS) { + php_poll_error ignored; + php_io_poll_fdreg_unlink(req); + req->fdreg = NULL; + q->n_waiting--; + php_io_poll_fdreg_sync(q, reg, &ignored, false); + if (err == PHP_POLL_ERR_NOSUPPORT) { + php_io_poll_req_complete(q, req, PHP_IO_UNSUPPORTED, 0, 0); + } else { + php_io_poll_req_complete(q, req, PHP_IO_DONE, -1, php_io_poll_error_to_errno(err)); + } + return; + } + + if (!php_deadline_is_infinite(&op->deadline)) { + req->timer = php_poll_timer_add(q->ctx, op->deadline.hrtime, 0, req); + } +} + +/* A descriptor reported ready: every op whose interest it meets completes; + * an error or hangup completes all of them. What no wait consumed is kept + * for the Edge pairs, whose next edge only comes after a drain. */ +static void php_io_poll_fdreg_fire(php_io_poll_queue *q, php_io_poll_fdreg *reg, uint32_t revents) +{ + if (reg->dead) { + return; + } + bool failure = (revents & (PHP_POLL_ERROR | PHP_POLL_HUP)) != 0; + uint32_t consumed = 0; + /* A registration retains the record; without one it goes with its last request */ + uint32_t edge = reg->edge; + php_io_poll_req *r = reg->reqs; + while (r) { + php_io_poll_req *next = r->fd_next; + if (failure || (r->events & revents)) { + php_io_status status = r->op->type == PHP_IO_OP_POLL ? PHP_IO_DONE : PHP_IO_READY; + consumed |= r->events; + php_io_poll_req_complete(q, r, status, revents, 0); + } + r = next; + } + if (edge) { + if (failure) { + reg->hup = true; + } + reg->ready |= revents & reg->edge & ~consumed; + } +} + +static void php_io_poll_group_release_members(php_io_poll_queue *q, php_io_poll_req *req) +{ + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_poll_req *m = req->members[i]; + if (m) { + php_io_poll_req_unregister(q, m); + php_io_poll_req_free(m); + } + } + if (req->members) { + efree(req->members); + req->members = NULL; + } + req->n_members = 0; +} + +/* The Any completes once at least one member did: collect the members that + * completed, withdraw the interest of the rest. */ +static void php_io_poll_group_fold(php_io_poll_queue *q, php_io_poll_req *req) +{ + php_io_op *op = req->op; + uint32_t n_results = 0; + + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_poll_req *m = req->members[i]; + if (m && m->done) { + if (op->u.any.results) { + op->u.any.results[n_results] = m->result; + } + n_results++; + } + } + op->u.any.n_results = n_results; + php_io_poll_group_release_members(q, req); + + req->fired = false; + req->ready = true; + req->result.status = PHP_IO_DONE; + req->result.index = 0; + req->result.res = 0; + req->result.error = 0; + php_io_poll_list_push(&q->ready, &q->n_ready, &q->ready_cap, req); +} + +static void php_io_poll_fold_all(php_io_poll_queue *q) +{ + while (q->n_fired) { + php_io_poll_req *req = q->fired[0]; + php_io_poll_list_remove(q->fired, &q->n_fired, req); + php_io_poll_group_fold(q, req); + } +} + +/* Queue operations */ + +static zend_result php_io_poll_queue_submit(php_io_queue *base, php_io_op *op, void *data) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + + if (op->queue) { + errno = EALREADY; + return FAILURE; + } + + php_io_poll_req *req = php_io_poll_req_create(q, op, data); + req->next = q->outstanding; + if (q->outstanding) { + q->outstanding->prev = req; + } + q->outstanding = req; + q->pending++; + + if (op->type == PHP_IO_OP_ANY) { + uint32_t n = op->u.any.n; + op->u.any.n_results = 0; + req->n_members = n; + req->members = n ? safe_emalloc(n, sizeof(*req->members), 0) : NULL; + for (uint32_t i = 0; i < n; i++) { + php_io_poll_req *m = php_io_poll_req_create(q, op->u.any.ops[i], NULL); + m->group = req; + m->index = i; + req->members[i] = m; + } + for (uint32_t i = 0; i < n; i++) { + php_io_poll_req_arm(q, req->members[i]); + } + if (n == 0 && !req->fired) { + /* Nothing could ever complete it */ + req->fired = true; + php_io_poll_list_push(&q->fired, &q->n_fired, &q->fired_cap, req); + } + } else { + php_io_poll_req_arm(q, req); + } + + return SUCCESS; +} + +static zend_result php_io_poll_queue_cancel(php_io_queue *base, php_io_op *op) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + php_io_poll_req *req = op->queue_data; + + if (op->queue != base || !req) { + errno = ENOENT; + return FAILURE; + } + if (req->group) { + /* Members are withdrawn by their Any */ + errno = EINVAL; + return FAILURE; + } + + if (op->type == PHP_IO_OP_ANY) { + php_io_poll_group_release_members(q, req); + if (req->fired) { + php_io_poll_list_remove(q->fired, &q->n_fired, req); + } + } else { + php_io_poll_req_unregister(q, req); + } + if (req->ready) { + php_io_poll_list_remove(q->ready, &q->n_ready, req); + } + php_io_poll_req_free_top(q, req); + return SUCCESS; +} + +/* A registration retains the descriptor's record between waits, and an Edge pair arms it + * edge-triggered for good. The record is found through the registration while the queue's id + * matches and by descriptor otherwise, so a record a replaced provider left behind costs nothing + * more than its memory. */ +static zend_result php_io_poll_queue_add(php_io_queue *base, php_io_registration *registration) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + uint32_t event = registration->event; + + if (registration->fd == SOCK_ERR || !(event == PHP_POLL_READ || event == PHP_POLL_WRITE)) { + errno = EBADF; + return FAILURE; + } + php_io_poll_fdreg *reg = php_io_poll_fdreg_get(q, (int) registration->fd, true); + registration->queue_data = reg; + registration->queue_id = q->id; + if (registration->trigger == PHP_IO_TRIGGER_EDGE && q->et) { + php_poll_error err; + reg->edge |= event; + reg->level &= ~event; + if (php_io_poll_fdreg_sync(q, reg, &err, false) != SUCCESS) { + /* A descriptor the backend refuses edge-triggered: served as Level */ + reg->edge &= ~event; + reg->level |= event; + php_io_poll_fdreg_sync(q, reg, &err, false); + } + } else { + reg->level |= event; + } + return SUCCESS; +} + +static void php_io_poll_queue_remove(php_io_queue *base, php_io_registration *registration) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + uint32_t event = registration->event; + + if (registration->fd == SOCK_ERR) { + return; + } + php_io_poll_fdreg *reg = php_io_poll_fdreg_of(q, registration, (int) registration->fd, false); + registration->queue_data = NULL; + if (reg && ((reg->edge | reg->level) & event)) { + php_poll_error err; + reg->edge &= ~event; + reg->level &= ~event; + reg->ready &= ~event; + if (!reg->edge) { + reg->hup = false; + } + php_io_poll_fdreg_sync(q, reg, &err, false); + } +} + +static bool php_io_poll_queue_take_inline(php_io_queue *base, php_io_op *op, php_io_queue_completion *out) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + php_io_poll_req *req = op->queue_data; + + if (!req || !req->ready || op->type == PHP_IO_OP_ANY) { + return false; + } + php_io_poll_list_remove(q->ready, &q->n_ready, req); + out->op = op; + out->data = req->data; + out->result = req->result; + php_io_poll_req_free_top(q, req); + return true; +} + +static uint32_t php_io_poll_queue_deliver(php_io_poll_queue *q, php_io_queue_completion *out, uint32_t max) +{ + uint32_t n = MIN(max, q->n_ready); + + for (uint32_t i = 0; i < n; i++) { + php_io_poll_req *req = q->ready[i]; + out[i].op = req->op; + out[i].data = req->data; + out[i].result = req->result; + php_io_poll_req_free_top(q, req); + } + memmove(q->ready, &q->ready[n], (q->n_ready - n) * sizeof(*q->ready)); + q->n_ready -= n; + return n; +} + +static int php_io_poll_queue_wait(php_io_queue *base, php_io_queue_completion *out, uint32_t max, + const php_deadline *dl) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + zend_hrtime_t limit = dl ? dl->hrtime : ZEND_HRTIME_T_MAX; + + if (max == 0) { + return 0; + } + + if (q->events_cap < max) { + q->events_cap = max; + q->events = safe_erealloc(q->events, q->events_cap, sizeof(*q->events), 0); + } + + for (;;) { + php_io_poll_fold_all(q); + if (q->n_ready) { + return (int) php_io_poll_queue_deliver(q, out, max); + } + + if (limit == ZEND_HRTIME_T_MAX && q->n_waiting == 0 && php_poll_timer_count(q->ctx) == 0) { + /* Nothing can ever complete: an infinite timer, or nothing at all */ + errno = EDEADLK; + return -1; + } + + /* The context bounds the wait by its own timers */ + struct timespec ts, *pts = NULL; + if (limit != ZEND_HRTIME_T_MAX) { + zend_hrtime_t now = zend_hrtime(); + zend_hrtime_t remaining = limit > now ? limit - now : 0; + ts.tv_sec = remaining / ZEND_NANO_IN_SEC; + ts.tv_nsec = remaining % ZEND_NANO_IN_SEC; + pts = &ts; + } + + int n = php_poll_wait(q->ctx, q->events, (int) q->events_cap, pts); + if (n < 0) { + /* EINTR when a signal handler is pending: the context restarts on any other */ + errno = php_io_poll_error_to_errno(php_poll_get_error(q->ctx)); + return -1; + } + + /* Descriptors first: completing a timed out request drops its + * registration when nothing else holds it, and the same reap may + * still carry an event for that registration. A request whose + * readiness came in this reap keeps it, and its timer is skipped. */ + for (int i = 0; i < n; i++) { + if (!(q->events[i].revents & PHP_POLL_TIMER)) { + php_io_poll_fdreg_fire(q, q->events[i].data, q->events[i].revents); + } + } + for (int i = 0; i < n; i++) { + if (q->events[i].revents & PHP_POLL_TIMER) { + php_io_poll_req *req = q->events[i].data; + if (req->ready || req->done) { + /* Its deadline and its readiness landed in the same reap */ + continue; + } + php_io_status status = req->op->type == PHP_IO_OP_TIMER ? PHP_IO_DONE : PHP_IO_TIMEOUT; + php_io_poll_req_complete(q, req, status, 0, 0); + } + } + + php_io_poll_fold_all(q); + if (q->n_ready) { + return (int) php_io_poll_queue_deliver(q, out, max); + } + if (limit != ZEND_HRTIME_T_MAX && zend_hrtime() >= limit) { + return 0; + } + } +} + +static void php_io_poll_queue_orphan(php_io_queue *base, php_io_op *op) +{ + php_io_poll_req *req = op->queue_data; + if (op->queue == base && req && req->group) { + /* A member going away before its Any: the group forgets it */ + req->group->members[req->index] = NULL; + php_io_poll_req_unregister((php_io_poll_queue *) base, req); + php_io_poll_req_free(req); + return; + } + /* Readiness ops never reference a buffer, so this is a plain cancel */ + php_io_poll_queue_cancel(base, op); +} + +static uint32_t php_io_poll_queue_count_pending(php_io_queue *base) +{ + return ((php_io_poll_queue *) base)->pending; +} + +static uint32_t php_io_poll_queue_hook_flags(php_io_queue *base) +{ + return PHP_IO_HOOKS_F_EDGE_REGISTRATIONS | PHP_IO_HOOKS_F_LEVEL_REGISTRATIONS; +} + +static void php_io_poll_queue_destroy(php_io_queue *base) +{ + php_io_poll_queue *q = (php_io_poll_queue *) base; + + while (q->outstanding) { + php_io_poll_queue_cancel(base, q->outstanding->op); + } + ZEND_ASSERT(q->pending == 0 && q->n_ready == 0 && q->n_fired == 0); + + /* Records of registrations nobody removed */ + php_io_poll_fdreg *reg; + ZEND_HASH_FOREACH_PTR(&q->fdregs, reg) { + if (reg->in_ctx) { + php_poll_remove(q->ctx, reg->fd); + } + efree(reg); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(&q->fdregs); + ZEND_HASH_FOREACH_PTR(&q->dead, reg) { + efree(reg); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(&q->dead); + + php_poll_destroy(q->ctx); + if (q->ready) { + efree(q->ready); + } + if (q->fired) { + efree(q->fired); + } + if (q->events) { + efree(q->events); + } + efree(q); +} + +static const php_io_queue_ops php_io_poll_queue_ops = { + .submit = php_io_poll_queue_submit, + .take_inline = php_io_poll_queue_take_inline, + .cancel = php_io_poll_queue_cancel, + .add = php_io_poll_queue_add, + .remove = php_io_poll_queue_remove, + .wait = php_io_poll_queue_wait, + .orphan = php_io_poll_queue_orphan, + .drain = NULL, + .count_pending = php_io_poll_queue_count_pending, + .hook_flags = php_io_poll_queue_hook_flags, + .destroy = php_io_poll_queue_destroy, +}; + +PHPAPI php_io_queue *php_io_queue_create_poll(php_poll_backend_type backend) +{ + php_poll_ctx *ctx = php_poll_create(backend, 0); + if (!ctx) { + return NULL; + } + if (php_poll_init(ctx) != SUCCESS) { + php_poll_destroy(ctx); + return NULL; + } + + php_io_poll_queue *q = ecalloc(1, sizeof(*q)); + q->base.ops = &php_io_poll_queue_ops; + q->id = php_io_queue_new_id(); + q->ctx = ctx; + q->et = php_poll_supports_et(ctx); + zend_hash_init(&q->fdregs, 8, NULL, NULL, 0); + zend_hash_init(&q->dead, 0, NULL, NULL, 0); + q->events_cap = PHP_IO_POLL_MIN_EVENTS; + q->events = safe_emalloc(q->events_cap, sizeof(*q->events), 0); + return &q->base; +} diff --git a/main/io/php_io_ring.c b/main/io/php_io_ring.c new file mode 100644 index 000000000000..b2bcb98979b3 --- /dev/null +++ b/main/io/php_io_ring.c @@ -0,0 +1,2238 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#include "php.h" +#include "php_network.h" +#include "php_io.h" + +#ifdef HAVE_IOR + +#include +#include +#ifndef PHP_WIN32 +# include +# include +# include +#endif +#if defined(HAVE_FDATASYNC) && defined(__APPLE__) +/* The symbol is present, the headers do not declare it */ +extern int fdatasync(int); +#endif + +/* The op layer's signal types are handed to ior as they are */ +static_assert(sizeof(php_sigset_t) == sizeof(ior_sigset_t), "php_sigset_t must match ior_sigset_t"); +static_assert(sizeof(php_siginfo_t) == sizeof(ior_siginfo_t), "php_siginfo_t must match ior_siginfo_t"); + +/* One submitted op, alive until the main cqe and its linked timeout's were reaped. What the backend + * reads or writes, apart from a stream's read buffer, lives in the record and reaches the op only + * at delivery; it is malloc'ed, since the record outlives the request when the backend cannot be + * stopped. */ +typedef struct _php_io_ring_req php_io_ring_req; +typedef struct _php_io_ring_reg php_io_ring_reg; + +/* An Edge-registered descriptor: what the ring keeps between the waits on it. The first wait after + * a drain arms a multishot poll whose edges answer the waits parked on the record, or are kept as + * ready bits for the next one; a Poll op that checks readiness at arm time stays single-shot. */ +struct _php_io_ring_reg { + php_socket_t fd; + uint32_t edge; /* PHP_POLL_* events registered Edge */ + uint32_t ready; /* IOR_POLL_* reported by the multishot and consumed by no wait */ + bool hup; /* a hangup or error was reported: every wait completes at once */ + php_io_ring_req *poll; /* the armed multishot poll, NULL when none */ + uint32_t poll_mask; /* what it watches */ + php_io_ring_req *accept; /* the armed multishot accept of a listener, NULL when none */ + bool accept_failed; /* the backend refused it: accepts take their own entry */ + int *fds; /* connections accepted with no wait to take them, oldest first */ + uint32_t n_fds; + uint32_t fds_cap; +}; + +/* Buffered connections past which the multishot accept is let go, until a take brings the buffer + * below half: the listen backlog keeps the rest, where a process sharing the listener can still + * take them */ +#define PHP_IO_RING_ACCEPT_CAP 32 + +struct _php_io_ring_req { + php_io_op *op; + void *data; + php_io_op_type type; + php_deadline deadline; + php_io_op_result result; + int32_t main_res; /* the main cqe's result, -1 until then */ + ior_timespec ts; /* the timer's or the linked timeout's */ + php_io_ring_req *group; /* member: the Any's request */ + uint32_t index; /* member: position in the Any */ + bool has_lt; /* a linked timeout was submitted */ + bool main_done; /* the main cqe was reaped */ + bool lt_done; /* the linked timeout's cqe was reaped */ + bool early; /* completed at its linked timeout, before its main cqe */ + bool cancelled; /* cancel() was called */ + bool cancel_pending; /* the cancel still needs an entry */ + bool backlogged; /* waiting for room in the ring */ + bool orphaned; /* nobody wants the completion */ + bool delivered; /* the output went to the op */ + php_stream *orphan_stream; /* frozen until the record settled */ + zend_object *orphan_handle; /* the same for a handle's op without a stream */ + bool ready; /* top-level: completion to deliver */ + bool fired; /* group: in the fired list */ + bool group_done; /* member: the group folded already */ + bool multishot; /* an Edge record's multishot poll, without an op */ + php_io_ring_reg *reg; /* multishot: its record, NULL once removed */ + php_io_ring_reg *waiting; /* a wait parked on an Edge record, without an entry */ + uint32_t w_mask; /* IOR_POLL_* the parked wait wants */ + php_io_ring_req *w_prev; /* the parked waits */ + php_io_ring_req *w_next; + php_io_ring_req **members; /* group */ + uint32_t n_members; + php_io_ring_req *prev; /* live list */ + php_io_ring_req *next; + php_io_ring_req *bl_prev; /* backlog */ + php_io_ring_req *bl_next; + union { + struct { char *node; char *service; struct addrinfo hints; bool has_hints; + struct addrinfo *res; } gai; + struct { struct sockaddr *addr; socklen_t addrlen; int flags; + char *host; size_t hostlen; char *service; size_t servicelen; } gni; + struct { struct sockaddr *addr; socklen_t addrlen; socklen_t cap; } sock; + struct { php_socket_t fd; bool data_only; } fsync; + struct { int status; } waitpid; + struct { php_sigset_t set; php_siginfo_t info; } sigwait; + struct { char *buf; bool advances; } io; /* bounce buffer, or NULL */ + } u; +}; + +typedef struct php_io_ring_cqe { + uintptr_t data; + int32_t res; + bool more; /* IOR_CQE_F_MORE: a multishot's edge, not its last completion */ +} php_io_ring_cqe; + +struct php_io_ring { + ior_ctx *ctx; + uint64_t id; /* tells this ring's records on a registration from another's */ + uint32_t features; + bool fd_nonblock; + pid_t owner_pid; /* a forked child must not touch the ring */ + php_io_queue *queue; /* the queue over the ring, if any */ + php_io_ring *prev_ring; /* the rings of this thread */ + php_io_ring *next_ring; + uint32_t unsubmitted; /* taken, not submitted yet */ + php_io_ring_req *live; /* records with a cqe or a completion pending */ + uint32_t pending; /* not delivered yet, orphans excluded */ + uint32_t n_cancel_pending; + php_io_ring_req *bl_head; /* ops waiting for room, oldest first */ + php_io_ring_req *bl_tail; + HashTable regs; /* fd -> php_io_ring_reg */ + php_io_ring_req *waiting; /* waits parked on Edge records */ + php_io_ring_req **ready; + uint32_t n_ready; + uint32_t ready_cap; + php_io_ring_req **fired; + uint32_t n_fired; + uint32_t fired_cap; + ior_cqe **cqes; + php_io_ring_cqe *batch; + uint32_t cqes_cap; + bool notify_created; +}; + +/* Tags in the cqe user data: the linked timeout's cqe and a cancel's cqe + * carry the record pointer with a low bit set */ +#define PHP_IO_RING_TAG_LT ((uintptr_t) 1) +#define PHP_IO_RING_TAG_CANCEL ((uintptr_t) 2) +#define PHP_IO_RING_TAG_MASK ((uintptr_t) 3) + +/* Every cqe of the record arrived */ +static zend_always_inline bool php_io_ring_req_settled(php_io_ring_req *req) +{ + return req->main_done && (!req->has_lt || req->lt_done); +} + +static void php_io_ring_list_push(php_io_ring_req ***list, uint32_t *n, uint32_t *cap, php_io_ring_req *req) +{ + if (*n == *cap) { + *cap = *cap ? *cap * 2 : 16; + *list = safe_erealloc(*list, *cap, sizeof(**list), 0); + } + (*list)[(*n)++] = req; +} + +static void php_io_ring_list_remove(php_io_ring_req **list, uint32_t *n, php_io_ring_req *req) +{ + for (uint32_t i = 0; i < *n; i++) { + if (list[i] == req) { + memmove(&list[i], &list[i + 1], (*n - i - 1) * sizeof(*list)); + (*n)--; + return; + } + } + ZEND_UNREACHABLE(); +} + +ZEND_TLS php_io_ring *php_io_rings = NULL; + +/* Inherited across fork: the kernel ring is shared with the parent and the + * worker threads do not exist here, so ior drops the child's copy */ +static bool php_io_ring_foreign(php_io_ring *ring) +{ + if (EXPECTED(ring->owner_pid == getpid())) { + return false; + } +#ifndef PHP_WIN32 + if (ring->ctx) { + ior_queue_forget(ring->ctx); + ring->ctx = NULL; + } +#endif + return true; +} + +PHPAPI bool php_io_ring_inherited(php_io_ring *ring) +{ + return php_io_ring_foreign(ring); +} + +PHPAPI void php_io_ring_after_fork(void) +{ + for (php_io_ring *ring = php_io_rings; ring; ring = ring->next_ring) { + php_io_ring_foreign(ring); + } +} + +PHPAPI php_io_ring *php_io_ring_create(uint32_t entries, bool fd_nonblock) +{ + ior_params params; + memset(¶ms, 0, sizeof(params)); + params.backend = IOR_BACKEND_AUTO; + if (fd_nonblock) { + params.flags |= IOR_SETUP_FD_NONBLOCK; + } + + /* An op and its linked timeout go in one submission */ + params.sq_entries = MAX(entries ? entries : PHP_IO_RING_DEFAULT_ENTRIES, 2); + + ior_ctx *ctx; + int rc = ior_queue_init_params(params.sq_entries, &ctx, ¶ms); + if (rc < 0) { + errno = -rc; + return NULL; + } + + php_io_ring *ring = ecalloc(1, sizeof(*ring)); + ring->id = php_io_queue_new_id(); + ring->next_ring = php_io_rings; + if (php_io_rings) { + php_io_rings->prev_ring = ring; + } + php_io_rings = ring; + ring->ctx = ctx; + ring->features = params.features; + ring->owner_pid = getpid(); + ring->fd_nonblock = fd_nonblock; + ring->cqes_cap = 64; + ring->cqes = safe_emalloc(ring->cqes_cap, sizeof(*ring->cqes), 0); + ring->batch = safe_emalloc(ring->cqes_cap, sizeof(*ring->batch), 0); + zend_hash_init(&ring->regs, 8, NULL, NULL, 0); + return ring; +} + +PHPAPI uint32_t php_io_ring_features(php_io_ring *ring) +{ + return ring->features; +} + +PHPAPI php_io_ring_backend_type php_io_ring_get_backend_type(php_io_ring *ring) +{ + switch (ior_get_backend_type(ring->ctx)) { + case IOR_BACKEND_IOURING: return PHP_IO_RING_BACKEND_IO_URING; + case IOR_BACKEND_IOCP: return PHP_IO_RING_BACKEND_IOCP; + default: return PHP_IO_RING_BACKEND_THREADS; + } +} + +PHPAPI const char *php_io_ring_backend_name(php_io_ring *ring) +{ + return ior_get_backend_name(ring->ctx); +} + +/* What a provider on a ring registers with unless it chooses: Edge registrations cost nothing and + * let the ring keep its multishots, and a direct accept is served from the multishot accept's + * buffer at submit, so the accepting fiber takes a burst without a pass per connection. Direct + * data ops and file ops lose when data is usually ready and files are in the page cache, so they + * stay opt-in. */ +PHPAPI uint32_t php_io_ring_hook_flags(php_io_ring *ring) +{ + return php_io_ring_supported_hook_flags(ring) + & (PHP_IO_HOOKS_F_EDGE_REGISTRATIONS | PHP_IO_HOOKS_F_DIRECT_ACCEPT); +} + +PHPAPI uint32_t php_io_ring_supported_hook_flags(php_io_ring *ring) +{ + /* Every ior backend keeps a multishot accept, which is what makes a direct + * accept serve a burst in order of arrival rather than one per pass */ + uint32_t flags = PHP_IO_HOOKS_F_FILES | PHP_IO_HOOKS_F_DIRECT_ACCEPT; +#ifndef PHP_WIN32 + /* Edge registrations need a multishot poll that reports edges. IOCP's + * re-reports readiness that persists (ior_prep_poll_multishot), so the + * ring's Edge records would keep completing; not offered there. */ + flags |= PHP_IO_HOOKS_F_EDGE_REGISTRATIONS; +#endif + if (ring->features & IOR_FEAT_NATIVE_ASYNC) { + flags |= PHP_IO_HOOKS_F_DIRECT_DATA; + } + return flags; +} + +static php_io_ring_reg *php_io_ring_reg_get(php_io_ring *ring, php_socket_t fd, bool create) +{ + php_io_ring_reg *rec = zend_hash_index_find_ptr(&ring->regs, (zend_ulong) fd); + if (!rec && create) { + rec = ecalloc(1, sizeof(*rec)); + rec->fd = fd; + zend_hash_index_add_new_ptr(&ring->regs, (zend_ulong) fd, rec); + } + return rec; +} + +/* The record through the registration, which php_io_ring_add() pointed at it; one another ring + * left there is told by the id and looked up by descriptor. A registered pair keeps its record. */ +static zend_always_inline php_io_ring_reg *php_io_ring_reg_of(php_io_ring *ring, + php_io_registration *registration, php_socket_t fd) +{ + if (registration && registration->queue_id == ring->id && registration->queue_data) { + return registration->queue_data; + } + return php_io_ring_reg_get(ring, fd, false); +} + +static void php_io_ring_reg_disarm(php_io_ring *ring, php_io_ring_reg *rec); +static void php_io_ring_reg_disarm_accept(php_io_ring *ring, php_io_ring_reg *rec, bool keep); + +/* The connections nobody took: their clients see a reset, as with a listener closed with a backlog */ +static void php_io_ring_reg_close_fds(php_io_ring_reg *rec) +{ + for (uint32_t i = 0; i < rec->n_fds; i++) { + closesocket(rec->fds[i]); + } + rec->n_fds = 0; + if (rec->fds) { + efree(rec->fds); + rec->fds = NULL; + rec->fds_cap = 0; + } +} + +/* Nothing is armed here: the first wait after a drain does that, when it knows what to keep */ +PHPAPI zend_result php_io_ring_add(php_io_ring *ring, php_io_registration *reg) +{ + if (php_io_ring_foreign(ring)) { + errno = EPERM; + return FAILURE; + } + if (reg->fd == SOCK_ERR || !(reg->event == PHP_POLL_READ || reg->event == PHP_POLL_WRITE)) { + errno = EBADF; + return FAILURE; + } + if (!(ring->features & IOR_FEAT_POLL_ADD)) { + /* Served as one-shot waits */ + return SUCCESS; + } + php_io_ring_reg *rec = php_io_ring_reg_get(ring, reg->fd, true); + rec->edge |= reg->event; + reg->queue_data = rec; + reg->queue_id = ring->id; + return SUCCESS; +} + +PHPAPI void php_io_ring_remove(php_io_ring *ring, php_io_registration *reg) +{ + php_io_ring_reg *rec = php_io_ring_reg_of(ring, reg, reg->fd); + reg->queue_data = NULL; + if (!rec) { + return; + } + rec->edge &= ~reg->event; + if (rec->poll) { + /* Cancelled before the descriptor closes; a remaining direction re-arms at its next wait */ + php_io_ring_reg_disarm(ring, rec); + } + if (!(rec->edge & PHP_POLL_READ)) { + if (rec->accept) { + php_io_ring_reg_disarm_accept(ring, rec, false); + } + php_io_ring_reg_close_fds(rec); + } + if (!rec->edge) { + zend_hash_index_del(&ring->regs, (zend_ulong) rec->fd); + efree(rec); + } +} + +PHPAPI php_socket_t php_io_ring_notify_fd(php_io_ring *ring) +{ + if (php_io_ring_foreign(ring)) { + errno = EPERM; + return SOCK_ERR; + } + ior_fd_t fd = ior_notify_fd(ring->ctx); + if (fd == IOR_INVALID_FD) { + return SOCK_ERR; + } + ring->notify_created = true; + return (php_socket_t) fd; +} + +PHPAPI void php_io_ring_notify_clear(php_io_ring *ring) +{ + if (ring->notify_created && !php_io_ring_foreign(ring)) { + ior_notify_clear(ring->ctx); + } +} + +PHPAPI uint32_t php_io_ring_count_pending(php_io_ring *ring) +{ + if (php_io_ring_foreign(ring)) { + return 0; + } + /* Orphans included: a loop must keep reaping until they settled. A + * multishot poll owns nothing of a caller's and settles when it can. */ + uint32_t n = ring->pending; + for (php_io_ring_req *r = ring->live; r; r = r->next) { + if (r->orphaned && !r->ready && !r->multishot && !php_io_ring_req_settled(r)) { + n++; + } + } + return n; +} + +static uint32_t php_io_ring_reap(php_io_ring *ring); + +/* Records */ + +/* What one submission of a data op transfers at most */ +static zend_always_inline unsigned php_io_ring_io_len(php_io_op *op) +{ + return (unsigned) MIN(op->u.io.len, UINT32_MAX); +} + +/* The inputs are copied and the outputs get slots of their own, so that a + * record outliving the op's frame never touches it */ +static void php_io_ring_req_capture(php_io_ring_req *req, php_io_op *op) +{ + switch (op->type) { + case PHP_IO_OP_GETADDRINFO: + req->u.gai.node = op->u.getaddrinfo.node ? pestrdup(op->u.getaddrinfo.node, 1) : NULL; + req->u.gai.service = op->u.getaddrinfo.service ? pestrdup(op->u.getaddrinfo.service, 1) : NULL; + if (op->u.getaddrinfo.hints) { + req->u.gai.hints.ai_flags = op->u.getaddrinfo.hints->ai_flags; + req->u.gai.hints.ai_family = op->u.getaddrinfo.hints->ai_family; + req->u.gai.hints.ai_socktype = op->u.getaddrinfo.hints->ai_socktype; + req->u.gai.hints.ai_protocol = op->u.getaddrinfo.hints->ai_protocol; + req->u.gai.has_hints = true; + } + break; + case PHP_IO_OP_GETNAMEINFO: + req->u.gni.addrlen = op->u.getnameinfo.addrlen; + req->u.gni.addr = pemalloc(MAX(req->u.gni.addrlen, 1), 1); + memcpy(req->u.gni.addr, op->u.getnameinfo.addr, req->u.gni.addrlen); + req->u.gni.flags = op->u.getnameinfo.flags; + if (op->u.getnameinfo.host && op->u.getnameinfo.hostlen) { + req->u.gni.hostlen = op->u.getnameinfo.hostlen; + req->u.gni.host = pemalloc(req->u.gni.hostlen, 1); + } + if (op->u.getnameinfo.service && op->u.getnameinfo.servicelen) { + req->u.gni.servicelen = op->u.getnameinfo.servicelen; + req->u.gni.service = pemalloc(req->u.gni.servicelen, 1); + } + break; + case PHP_IO_OP_ACCEPT: + if (op->u.accept.addr && op->u.accept.addrlen) { + req->u.sock.cap = *op->u.accept.addrlen; + req->u.sock.addrlen = req->u.sock.cap; + req->u.sock.addr = pemalloc(MAX(req->u.sock.cap, 1), 1); + } + break; + case PHP_IO_OP_CONNECT: + req->u.sock.addrlen = op->u.connect.addrlen; + req->u.sock.addr = pemalloc(MAX(req->u.sock.addrlen, 1), 1); + memcpy(req->u.sock.addr, op->u.connect.addr, req->u.sock.addrlen); + break; + case PHP_IO_OP_FSYNC: + req->u.fsync.fd = op->fd; + req->u.fsync.data_only = op->u.fsync.data_only; + break; + case PHP_IO_OP_SIGWAIT: + req->u.sigwait.set = *op->u.sigwait.set; + break; + case PHP_IO_OP_READ: + case PHP_IO_OP_RECV: + req->u.io.advances = php_io_op_read_advances(op); + if (!(op->flags & PHP_IO_OP_F_STREAM_BUF)) { + req->u.io.buf = pemalloc(MAX(php_io_ring_io_len(op), 1), 1); + } + break; + case PHP_IO_OP_WRITE: + case PHP_IO_OP_SEND: + req->u.io.buf = pemalloc(MAX(php_io_ring_io_len(op), 1), 1); + memcpy(req->u.io.buf, op->u.io.buf, php_io_ring_io_len(op)); + break; + default: + break; + } +} + +/* The completion is delivered: the op gets what the record collected */ +static void php_io_ring_req_output(php_io_ring_req *req, php_io_op *op) +{ + int32_t res = req->main_res; + + /* Completed early: what the op produces later is released with the record */ + if (!req->main_done) { + return; + } + req->delivered = true; + switch (req->type) { + case PHP_IO_OP_GETADDRINFO: + if (op->u.getaddrinfo.res) { + *op->u.getaddrinfo.res = req->u.gai.res; + req->u.gai.res = NULL; + } + break; + case PHP_IO_OP_GETNAMEINFO: + if (res == 0) { + if (req->u.gni.host) { + memcpy(op->u.getnameinfo.host, req->u.gni.host, req->u.gni.hostlen); + } + if (req->u.gni.service) { + memcpy(op->u.getnameinfo.service, req->u.gni.service, req->u.gni.servicelen); + } + } + break; + case PHP_IO_OP_ACCEPT: + if (res >= 0 && req->u.sock.addr) { + memcpy(op->u.accept.addr, req->u.sock.addr, MIN(req->u.sock.addrlen, req->u.sock.cap)); + *op->u.accept.addrlen = req->u.sock.addrlen; + } + break; + case PHP_IO_OP_WAITPID: + if (res > 0 && op->u.waitpid.status) { + *op->u.waitpid.status = req->u.waitpid.status; + } + break; + case PHP_IO_OP_SIGWAIT: + if (res > 0 && op->u.sigwait.info) { + *op->u.sigwait.info = req->u.sigwait.info; + } + break; + case PHP_IO_OP_READ: + case PHP_IO_OP_RECV: + if (res > 0 && req->u.io.buf) { + memcpy(op->u.io.buf, req->u.io.buf, res); + } + break; + default: + break; + } +} + +/* The record goes away: what the op produced and nobody took is released */ +static void php_io_ring_req_discard(php_io_ring_req *req) +{ + bool unclaimed = req->main_done && !req->delivered; + int32_t res = req->main_res; + + switch (req->type) { + case PHP_IO_OP_GETADDRINFO: + if (req->u.gai.res) { + freeaddrinfo(req->u.gai.res); + } + if (req->u.gai.node) { + pefree(req->u.gai.node, 1); + } + if (req->u.gai.service) { + pefree(req->u.gai.service, 1); + } + break; + case PHP_IO_OP_GETNAMEINFO: + pefree(req->u.gni.addr, 1); + if (req->u.gni.host) { + pefree(req->u.gni.host, 1); + } + if (req->u.gni.service) { + pefree(req->u.gni.service, 1); + } + break; + case PHP_IO_OP_ACCEPT: + if (unclaimed && res >= 0) { + closesocket((php_socket_t) res); + } + ZEND_FALLTHROUGH; + case PHP_IO_OP_CONNECT: + if (req->u.sock.addr) { + pefree(req->u.sock.addr, 1); + } + break; + case PHP_IO_OP_READ: + case PHP_IO_OP_RECV: + case PHP_IO_OP_WRITE: + case PHP_IO_OP_SEND: + if (req->u.io.buf) { + pefree(req->u.io.buf, 1); + } + break; + case PHP_IO_OP_SIGWAIT: + /* The op took the signal: it goes back to the process */ + if (unclaimed && res > 0) { + ior_sigrequeue((const ior_siginfo_t *) &req->u.sigwait.info); + } + break; + default: + break; + } +} + +static php_io_ring_req *php_io_ring_req_alloc(php_io_ring *ring) +{ + php_io_ring_req *req = pecalloc(1, sizeof(*req), 1); + req->main_res = -1; + req->next = ring->live; + if (ring->live) { + ring->live->prev = req; + } + ring->live = req; + return req; +} + +static php_io_ring_req *php_io_ring_req_create(php_io_ring *ring, php_io_op *op, void *data) +{ + php_io_ring_req *req = php_io_ring_req_alloc(ring); + req->op = op; + req->data = data; + req->type = op->type; + req->deadline = op->deadline; + php_io_ring_req_capture(req, op); + return req; +} + +/* Records with a completion still to come, the armed multishots aside: what a wait with nothing + * pending has to see settled before it returns */ +static bool php_io_ring_settling(php_io_ring *ring) +{ + for (php_io_ring_req *r = ring->live; r; r = r->next) { + if (!r->multishot) { + return true; + } + } + return false; +} + +/* Parked waits */ + +static void php_io_ring_waiter_park(php_io_ring *ring, php_io_ring_req *req, php_io_ring_reg *rec, uint32_t mask) +{ + req->waiting = rec; + req->w_mask = mask; + req->w_prev = NULL; + req->w_next = ring->waiting; + if (ring->waiting) { + ring->waiting->w_prev = req; + } + ring->waiting = req; +} + +static void php_io_ring_waiter_settle(php_io_ring *ring, php_io_ring_req *req) +{ + if (req->w_prev) { + req->w_prev->w_next = req->w_next; + } else { + ring->waiting = req->w_next; + } + if (req->w_next) { + req->w_next->w_prev = req->w_prev; + } + req->w_prev = req->w_next = NULL; + req->waiting = NULL; +} + +static void php_io_ring_backlog_push(php_io_ring *ring, php_io_ring_req *req) +{ + req->backlogged = true; + req->bl_next = NULL; + req->bl_prev = ring->bl_tail; + if (ring->bl_tail) { + ring->bl_tail->bl_next = req; + } else { + ring->bl_head = req; + } + ring->bl_tail = req; +} + +static void php_io_ring_backlog_remove(php_io_ring *ring, php_io_ring_req *req) +{ + if (req->bl_prev) { + req->bl_prev->bl_next = req->bl_next; + } else { + ring->bl_head = req->bl_next; + } + if (req->bl_next) { + req->bl_next->bl_prev = req->bl_prev; + } else { + ring->bl_tail = req->bl_prev; + } + req->bl_prev = req->bl_next = NULL; + req->backlogged = false; +} + +static void php_io_ring_req_free(php_io_ring *ring, php_io_ring_req *req) +{ + if (req->orphan_stream) { + php_io_stream_unfreeze(req->orphan_stream); + req->orphan_stream = NULL; + } + if (req->orphan_handle) { + php_io_handle_unfreeze(req->orphan_handle); + req->orphan_handle = NULL; + } + if (req->backlogged) { + php_io_ring_backlog_remove(ring, req); + } + if (req->waiting) { + php_io_ring_waiter_settle(ring, req); + } + if (req->reg) { + if (req->type == PHP_IO_OP_ACCEPT) { + if (req->reg->accept == req) { + req->reg->accept = NULL; + } + } else if (req->reg->poll == req) { + req->reg->poll = NULL; + } + req->reg = NULL; + } + if (req->cancel_pending) { + ring->n_cancel_pending--; + } + if (req->prev) { + req->prev->next = req->next; + } else { + ring->live = req->next; + } + if (req->next) { + req->next->prev = req->prev; + } + php_io_ring_req_discard(req); + if (req->members) { + efree(req->members); + } + pefree(req, 1); +} + +/* Read and Write ops carry a file descriptor: on Windows a CRT one, whose + * handle is what IOCP works on (an overlapped one, the plain wrapper's + * promise for the files it hands over) */ +static zend_always_inline ior_fd_t php_io_ring_file_fd(php_io_op *op) +{ +#ifdef PHP_WIN32 + return (ior_fd_t) _get_osfhandle((int) op->fd); +#else + return (ior_fd_t) op->fd; +#endif +} + +/* The bounce buffer, or the stream's read buffer the op points at */ +static zend_always_inline void *php_io_ring_io_buf(php_io_ring_req *req, php_io_op *op) +{ + return req->u.io.buf ? req->u.io.buf : op->u.io.buf; +} + +/* Work callbacks: they run on a worker and see only the record */ + +static int32_t php_io_ring_work_getaddrinfo(ior_work_token *token, void *arg) +{ + php_io_ring_req *req = arg; + return getaddrinfo(req->u.gai.node, req->u.gai.service, + req->u.gai.has_hints ? &req->u.gai.hints : NULL, &req->u.gai.res); +} + +static int32_t php_io_ring_work_getnameinfo(ior_work_token *token, void *arg) +{ + php_io_ring_req *req = arg; + return getnameinfo(req->u.gni.addr, req->u.gni.addrlen, + req->u.gni.host, req->u.gni.hostlen, + req->u.gni.service, req->u.gni.servicelen, req->u.gni.flags); +} + +static int32_t php_io_ring_work_fsync(ior_work_token *token, void *arg) +{ + php_io_ring_req *req = arg; +#ifdef HAVE_FDATASYNC + int rc = req->u.fsync.data_only ? fdatasync((int) req->u.fsync.fd) : fsync((int) req->u.fsync.fd); +#else + int rc = fsync((int) req->u.fsync.fd); +#endif + return rc == 0 ? 0 : -errno; +} + +static uint32_t php_io_ring_poll_mask_to_ior(uint32_t events) +{ + uint32_t mask = 0; + if (events & PHP_POLL_READ) { + mask |= IOR_POLL_IN; + } + if (events & PHP_POLL_WRITE) { + mask |= IOR_POLL_OUT; + } + return mask; +} + +static uint32_t php_io_ring_poll_mask_from_ior(uint32_t mask) +{ + uint32_t events = 0; + if (mask & IOR_POLL_IN) { + events |= PHP_POLL_READ; + } + if (mask & IOR_POLL_OUT) { + events |= PHP_POLL_WRITE; + } + if (mask & (IOR_POLL_ERR | IOR_POLL_NVAL)) { + events |= PHP_POLL_ERROR; + } + if (mask & IOR_POLL_HUP) { + events |= PHP_POLL_HUP; + } + return events; +} + +/* Relative, because zend_hrtime() runs on CLOCK_MONOTONIC_RAW where it + * exists and ior's absolute deadlines offer the monotonic, boot-time and + * wall clocks */ +static void php_io_ring_deadline_to_ts(const php_deadline *dl, ior_timespec *ts) +{ + zend_hrtime_t remaining = php_io_deadline_remaining(dl, zend_hrtime()); + ts->tv_sec = (int64_t) (remaining / ZEND_NANO_IN_SEC); + ts->tv_nsec = (long long) (remaining % ZEND_NANO_IN_SEC); +} + +/* Entries the last submits left behind are tried again */ +static int php_io_ring_flush(php_io_ring *ring) +{ + if (ring->unsubmitted == 0) { + return 0; + } + int rc = ior_submit(ring->ctx); + if (rc >= 0) { + ring->unsubmitted = rc == 0 ? 0 : ring->unsubmitted - MIN((uint32_t) rc, ring->unsubmitted); + } + return rc; +} + +/* An entry taken for a prep that failed: a nop whose completion carries no + * record, so the next submit issues nothing stale */ +static void php_io_ring_sqe_void(ior_ctx *ctx, ior_sqe *sqe) +{ + ior_prep_nop(ctx, sqe); + ior_sqe_set_data(ctx, sqe, NULL); +} + +/* Takes n entries or none; a full submission queue is submitted first */ +static bool php_io_ring_get_sqes(php_io_ring *ring, ior_sqe **sqes, uint32_t n) +{ + for (int attempt = 0; attempt < 2; attempt++) { + uint32_t got = 0; + while (got < n && (sqes[got] = ior_get_sqe(ring->ctx)) != NULL) { + got++; + } + ring->unsubmitted += got; + if (got == n) { + return true; + } + for (uint32_t i = 0; i < got; i++) { + php_io_ring_sqe_void(ring->ctx, sqes[i]); + } + php_io_ring_flush(ring); + } + return false; +} + +/* Preps and submits one op (a member of an Any included). Returns FAILURE + * with errno set: EBUSY when the ring has no room now, ENOTSUP when the op + * has no ring form. */ +static zend_result php_io_ring_submit_one(php_io_ring *ring, php_io_ring_req *req) +{ + php_io_op *op = req->op; + ior_ctx *ctx = ring->ctx; + bool link_deadline = !php_deadline_is_infinite(&req->deadline) && req->type != PHP_IO_OP_TIMER; + bool uses_caller = false; /* the backend references buf or fd */ + ior_sqe *sqes[2]; + int err = 0; + + /* What has no ring form is refused before an entry is taken */ + switch (req->type) { + case PHP_IO_OP_POLL: + /* ior has no priority event */ + if (!(ring->features & IOR_FEAT_POLL_ADD) || (req->op->u.poll.events & PHP_POLL_PRI)) { + errno = ENOTSUP; + return FAILURE; + } + break; + case PHP_IO_OP_TIMER: + if (php_deadline_is_infinite(&req->deadline)) { + /* Never fires: ior_prep_nop would end it at once */ + errno = ENOTSUP; + return FAILURE; + } + break; + case PHP_IO_OP_READ: + case PHP_IO_OP_WRITE: + case PHP_IO_OP_RECV: + case PHP_IO_OP_SEND: + case PHP_IO_OP_ACCEPT: + case PHP_IO_OP_CONNECT: + case PHP_IO_OP_GETADDRINFO: + case PHP_IO_OP_GETNAMEINFO: + case PHP_IO_OP_FSYNC: + case PHP_IO_OP_WAITPID: + case PHP_IO_OP_SIGWAIT: + break; + default: + errno = ENOTSUP; + return FAILURE; + } + + if (!php_io_ring_get_sqes(ring, sqes, link_deadline ? 2 : 1)) { + errno = EBUSY; + return FAILURE; + } + ior_sqe *sqe = sqes[0]; + + switch (req->type) { + case PHP_IO_OP_POLL: + ior_prep_poll_add(ctx, sqe, (ior_fd_t) op->fd, php_io_ring_poll_mask_to_ior(op->u.poll.events)); + break; + case PHP_IO_OP_TIMER: + php_io_ring_deadline_to_ts(&req->deadline, &req->ts); + ior_prep_timeout(ctx, sqe, &req->ts, 0, 0); + break; + case PHP_IO_OP_READ: + ior_prep_read(ctx, sqe, php_io_ring_file_fd(op), php_io_ring_io_buf(req, op), php_io_ring_io_len(op), + op->u.io.offset < 0 ? IOR_OFF_NONE : (uint64_t) op->u.io.offset); + uses_caller = true; + break; + case PHP_IO_OP_WRITE: + ior_prep_write(ctx, sqe, php_io_ring_file_fd(op), php_io_ring_io_buf(req, op), php_io_ring_io_len(op), + op->u.io.offset < 0 ? IOR_OFF_NONE : (uint64_t) op->u.io.offset); + uses_caller = true; + break; + case PHP_IO_OP_RECV: + ior_prep_recv(ctx, sqe, (ior_fd_t) op->fd, php_io_ring_io_buf(req, op), php_io_ring_io_len(op), op->u.io.flags); + uses_caller = true; + break; + case PHP_IO_OP_SEND: + ior_prep_send(ctx, sqe, (ior_fd_t) op->fd, php_io_ring_io_buf(req, op), php_io_ring_io_len(op), op->u.io.flags); + uses_caller = true; + break; + case PHP_IO_OP_ACCEPT: + ior_prep_accept(ctx, sqe, (ior_fd_t) op->fd, req->u.sock.addr, req->u.sock.addr ? &req->u.sock.addrlen : NULL, + IOR_ACCEPT_CLOEXEC | (ring->fd_nonblock ? IOR_ACCEPT_NONBLOCK : 0)); + uses_caller = true; + break; + case PHP_IO_OP_CONNECT: + ior_prep_connect(ctx, sqe, (ior_fd_t) op->fd, req->u.sock.addr, req->u.sock.addrlen); + uses_caller = true; + break; + case PHP_IO_OP_GETADDRINFO: + err = ior_prep_work(ctx, sqe, php_io_ring_work_getaddrinfo, req) < 0 ? ENOTSUP : 0; + break; + case PHP_IO_OP_GETNAMEINFO: + err = ior_prep_work(ctx, sqe, php_io_ring_work_getnameinfo, req) < 0 ? ENOTSUP : 0; + break; + case PHP_IO_OP_FSYNC: + err = ior_prep_work(ctx, sqe, php_io_ring_work_fsync, req) < 0 ? ENOTSUP : 0; + uses_caller = true; + break; + case PHP_IO_OP_WAITPID: + /* ior observes the child and leaves collecting it to the wrapper */ + err = ior_prep_waitpid(ctx, sqe, (ior_pid_t) op->u.waitpid.pid, &req->u.waitpid.status, op->u.waitpid.options) < 0 ? ENOTSUP : 0; + break; + case PHP_IO_OP_SIGWAIT: { + int rc = ior_prep_sigwait(ctx, sqe, (const ior_sigset_t *) &req->u.sigwait.set, (ior_siginfo_t *) &req->u.sigwait.info); + err = rc < 0 ? -rc : 0; + break; + } + default: + ZEND_UNREACHABLE(); + } + + if (err) { + php_io_ring_sqe_void(ctx, sqe); + if (link_deadline) { + php_io_ring_sqe_void(ctx, sqes[1]); + } + php_io_ring_flush(ring); + errno = err; + return FAILURE; + } + + if (uses_caller) { + op->in_flight = true; + } + ior_sqe_set_data(ctx, sqe, req); + + if (link_deadline) { + ior_sqe_set_flags(ctx, sqe, IOR_SQE_IO_LINK); + php_io_ring_deadline_to_ts(&req->deadline, &req->ts); + ior_prep_link_timeout(ctx, sqes[1], &req->ts, 0); + ior_sqe_set_data(ctx, sqes[1], (void *) ((uintptr_t) req | PHP_IO_RING_TAG_LT)); + req->has_lt = true; + } + + /* What a failed or short submit leaves is tried again before a wait blocks */ + php_io_ring_flush(ring); + return SUCCESS; +} + +static bool php_io_ring_submit_cancel(php_io_ring *ring, php_io_ring_req *req) +{ + ior_sqe *sqe; + if (!php_io_ring_get_sqes(ring, &sqe, 1)) { + return false; + } + ior_prep_cancel(ring->ctx, sqe, req); + ior_sqe_set_data(ring->ctx, sqe, (void *) ((uintptr_t) req | PHP_IO_RING_TAG_CANCEL)); + php_io_ring_flush(ring); + return true; +} + +/* A completion to hand out: the member fires its group, anything else is ready */ +static void php_io_ring_req_complete(php_io_ring *ring, php_io_ring_req *req) +{ + if (req->group) { + if (!req->group->group_done && !req->group->fired) { + req->group->fired = true; + php_io_ring_list_push(&ring->fired, &ring->n_fired, &ring->fired_cap, req->group); + } + return; + } + req->ready = true; + php_io_ring_list_push(&ring->ready, &ring->n_ready, &ring->ready_cap, req); +} + +/* The op completes without reaching the backend */ +static void php_io_ring_req_fail(php_io_ring *ring, php_io_ring_req *req, int err) +{ + req->main_done = true; + req->result.status = err == ENOTSUP ? PHP_IO_UNSUPPORTED : PHP_IO_DONE; + req->result.index = req->index; + req->result.res = -1; + req->result.error = err == ENOTSUP ? 0 : err; + php_io_ring_req_complete(ring, req); +} + +static void php_io_ring_req_main_cqe(php_io_ring *ring, php_io_ring_req *req, int32_t res); + +/* The multishot poll of a record: one entry, its edges come back with IOR_CQE_F_MORE */ +static bool php_io_ring_reg_arm(php_io_ring *ring, php_io_ring_reg *rec) +{ + ior_sqe *sqe; + uint32_t mask = php_io_ring_poll_mask_to_ior(rec->edge); + if (!php_io_ring_get_sqes(ring, &sqe, 1)) { + return false; + } + php_io_ring_req *req = php_io_ring_req_alloc(ring); + req->type = PHP_IO_OP_POLL; + req->multishot = true; + req->reg = rec; + php_deadline_init_infinite(&req->deadline); + ior_prep_poll_multishot(ring->ctx, sqe, (ior_fd_t) rec->fd, mask); + ior_sqe_set_data(ring->ctx, sqe, req); + rec->poll = req; + rec->poll_mask = mask; + php_io_ring_flush(ring); + return true; +} + +/* The multishot is let go: its last completion frees the entry, the record forgets it */ +static void php_io_ring_reg_disarm(php_io_ring *ring, php_io_ring_reg *rec) +{ + php_io_ring_req *req = rec->poll; + rec->poll = NULL; + req->reg = NULL; + if (php_io_ring_foreign(ring)) { + req->main_done = true; + } else if (!req->main_done && !req->cancelled) { + req->cancelled = true; + if (!php_io_ring_submit_cancel(ring, req)) { + req->cancel_pending = true; + ring->n_cancel_pending++; + } + } + req->orphaned = true; + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } +} + +/* An edge of the record: the parked waits it answers complete, the rest is kept for the next one; + * a hangup or error answers every wait from now on */ +static void php_io_ring_reg_edge(php_io_ring *ring, php_io_ring_reg *rec, uint32_t res) +{ + bool failure = (res & (IOR_POLL_ERR | IOR_POLL_HUP | IOR_POLL_NVAL)) != 0; + uint32_t consumed = 0; + if (failure) { + rec->hup = true; + } + php_io_ring_req *w = ring->waiting; + while (w) { + php_io_ring_req *next = w->w_next; + if (w->waiting == rec && (failure || (w->w_mask & res))) { + consumed |= w->w_mask; + php_io_ring_waiter_settle(ring, w); + php_io_ring_req_main_cqe(ring, w, (int32_t) res); + } + w = next; + } + rec->ready |= res & rec->poll_mask & ~consumed; +} + +/* The multishot accept of a registered listener: one entry, its connections come back with + * IOR_CQE_F_MORE and the accepted socket as the result */ +static bool php_io_ring_reg_arm_accept(php_io_ring *ring, php_io_ring_reg *rec) +{ + ior_sqe *sqe; + if (!php_io_ring_get_sqes(ring, &sqe, 1)) { + return false; + } + php_io_ring_req *req = php_io_ring_req_alloc(ring); + req->type = PHP_IO_OP_ACCEPT; + req->multishot = true; + req->reg = rec; + php_deadline_init_infinite(&req->deadline); + ior_prep_accept_multishot(ring->ctx, sqe, (ior_fd_t) rec->fd, + IOR_ACCEPT_CLOEXEC | (ring->fd_nonblock ? IOR_ACCEPT_NONBLOCK : 0)); + ior_sqe_set_data(ring->ctx, sqe, req); + rec->accept = req; + php_io_ring_flush(ring); + return true; +} + +/* Let go for the cap, keeping the record so the connections it still posts are buffered, or for + * good, when they are closed */ +static void php_io_ring_reg_disarm_accept(php_io_ring *ring, php_io_ring_reg *rec, bool keep) +{ + php_io_ring_req *req = rec->accept; + rec->accept = NULL; + if (!keep) { + req->reg = NULL; + } + if (php_io_ring_foreign(ring)) { + req->main_done = true; + } else if (!req->main_done && !req->cancelled) { + req->cancelled = true; + if (!php_io_ring_submit_cancel(ring, req)) { + req->cancel_pending = true; + ring->n_cancel_pending++; + } + } + req->orphaned = true; + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } +} + +/* An Accept completes with a connection: the peer name, which a multishot accept does not report, + * is asked of the socket when the caller wants it */ +static void php_io_ring_accept_complete(php_io_ring *ring, php_io_ring_req *req, int fd) +{ + if (req->u.sock.addr) { + req->u.sock.addrlen = req->u.sock.cap; + if (getpeername((php_socket_t) fd, req->u.sock.addr, &req->u.sock.addrlen) != 0) { + req->u.sock.addrlen = 0; + } + } + php_io_ring_req_main_cqe(ring, req, (int32_t) fd); +} + +/* The oldest buffered connection; the multishot comes back once the buffer is half empty, or at + * the first take after it ended on its own */ +static int php_io_ring_reg_take(php_io_ring *ring, php_io_ring_reg *rec) +{ + int fd = rec->fds[0]; + rec->n_fds--; + memmove(rec->fds, &rec->fds[1], rec->n_fds * sizeof(*rec->fds)); + if (!rec->accept && !rec->accept_failed && rec->n_fds < PHP_IO_RING_ACCEPT_CAP / 2) { + php_io_ring_reg_arm_accept(ring, rec); + } + return fd; +} + +/* A connection the multishot accepted: the oldest parked accept takes it, or it is buffered */ +static void php_io_ring_reg_connection(php_io_ring *ring, php_io_ring_reg *rec, int fd) +{ + php_io_ring_req *w = ring->waiting, *oldest = NULL; + while (w) { + if (w->waiting == rec && w->type == PHP_IO_OP_ACCEPT) { + oldest = w; + } + w = w->w_next; + } + if (oldest) { + php_io_ring_waiter_settle(ring, oldest); + php_io_ring_accept_complete(ring, oldest, fd); + return; + } + if (rec->n_fds == rec->fds_cap) { + rec->fds_cap = rec->fds_cap ? rec->fds_cap * 2 : 8; + rec->fds = safe_erealloc(rec->fds, rec->fds_cap, sizeof(*rec->fds), 0); + } + rec->fds[rec->n_fds++] = fd; + if (rec->accept && rec->n_fds >= PHP_IO_RING_ACCEPT_CAP) { + php_io_ring_reg_disarm_accept(ring, rec, true); + } +} + +/* An Accept on a registered listener: served from the buffer, or parked behind the multishot + * accept armed at the first one. False when the accept takes its own entry. */ +static bool php_io_ring_accept_wait(php_io_ring *ring, php_io_ring_req *req) +{ + php_io_op *op = req->op; + if (!op->registration || op->registration->trigger != PHP_IO_TRIGGER_EDGE) { + return false; + } + php_io_ring_reg *rec = php_io_ring_reg_of(ring, op->registration, op->fd); + if (!rec || !(rec->edge & PHP_POLL_READ) || rec->accept_failed) { + return false; + } + if (rec->n_fds) { + php_io_ring_accept_complete(ring, req, php_io_ring_reg_take(ring, rec)); + return true; + } + if (!rec->accept && !php_io_ring_reg_arm_accept(ring, rec)) { + return false; + } + php_io_ring_waiter_park(ring, req, rec, IOR_POLL_IN); + return true; +} + +/* A wait after a drain on an Edge pair: answered from the record, or parked on it behind the + * multishot poll armed at the first such wait. False when the wait takes its own entry. */ +static bool php_io_ring_edge_wait(php_io_ring *ring, php_io_ring_req *req) +{ + php_io_op *op = req->op; + if (op->type == PHP_IO_OP_ACCEPT) { + return php_io_ring_accept_wait(ring, req); + } + if (op->type != PHP_IO_OP_POLL || !(op->flags & PHP_IO_OP_F_AFTER_DRAIN) || !op->registration + || op->registration->trigger != PHP_IO_TRIGGER_EDGE) { + return false; + } + uint32_t events = op->u.poll.events & (PHP_POLL_READ | PHP_POLL_WRITE); + php_io_ring_reg *rec = php_io_ring_reg_of(ring, op->registration, op->fd); + if (!events || !rec || (rec->edge & events) != events) { + return false; + } + uint32_t mask = php_io_ring_poll_mask_to_ior(events); + if (rec->hup || (rec->ready & mask)) { + uint32_t res = (rec->ready & mask) | (rec->hup ? IOR_POLL_HUP : 0); + rec->ready &= ~mask; + php_io_ring_req_main_cqe(ring, req, (int32_t) res); + return true; + } + if (rec->poll && rec->poll_mask != php_io_ring_poll_mask_to_ior(rec->edge)) { + /* A direction registered since: the new poll reports what is ready now */ + php_io_ring_reg_disarm(ring, rec); + } + if (!rec->poll && !php_io_ring_reg_arm(ring, rec)) { + return false; + } + php_io_ring_waiter_park(ring, req, rec, mask); + return true; +} + +/* Submitted, or kept in the backlog until the ring has room; false when + * the op failed, errno set */ +static bool php_io_ring_start(php_io_ring *ring, php_io_ring_req *req) +{ + if (php_io_ring_edge_wait(ring, req)) { + return true; + } + if (!ring->bl_head && php_io_ring_submit_one(ring, req) == SUCCESS) { + return true; + } + if (ring->bl_head || errno == EBUSY) { + php_io_ring_backlog_push(ring, req); + return true; + } + return false; +} + +static void php_io_ring_flush_backlog(php_io_ring *ring) +{ + while (ring->bl_head) { + php_io_ring_req *req = ring->bl_head; + if (php_io_ring_submit_one(ring, req) == SUCCESS) { + php_io_ring_backlog_remove(ring, req); + continue; + } + if (errno == EBUSY) { + break; + } + int err = errno; + php_io_ring_backlog_remove(ring, req); + php_io_ring_req_fail(ring, req, err); + } +} + +PHPAPI zend_result php_io_ring_submit_op(php_io_ring *ring, php_io_op *op, void *data) +{ + if (php_io_ring_foreign(ring)) { + errno = EPERM; + return FAILURE; + } + if (op->queue_data) { + errno = EALREADY; + return FAILURE; + } + + php_io_ring_req *req = php_io_ring_req_create(ring, op, data); + op->queue_data = req; + ring->pending++; + + if (op->type == PHP_IO_OP_ANY) { + uint32_t n = op->u.any.n; + op->u.any.n_results = 0; + req->n_members = n; + req->members = n ? safe_emalloc(n, sizeof(*req->members), 0) : NULL; + req->main_done = true; /* the Any itself has no cqe */ + for (uint32_t i = 0; i < n; i++) { + php_io_ring_req *m = php_io_ring_req_create(ring, op->u.any.ops[i], NULL); + m->group = req; + m->index = i; + op->u.any.ops[i]->queue_data = m; + req->members[i] = m; + } + for (uint32_t i = 0; i < n; i++) { + php_io_ring_req *m = req->members[i]; + if (!php_io_ring_start(ring, m)) { + /* A member without a ring form completes at once */ + php_io_ring_req_fail(ring, m, errno); + } + } + if (n == 0) { + req->fired = true; + php_io_ring_list_push(&ring->fired, &ring->n_fired, &ring->fired_cap, req); + } + return SUCCESS; + } + + if (!php_io_ring_start(ring, req)) { + php_io_ring_req_fail(ring, req, errno); + } + return SUCCESS; +} + +/* Cancel every cqe-producing submission of a record; one still in the + * backlog never reached the backend and settles at once */ +static void php_io_ring_req_cancel(php_io_ring *ring, php_io_ring_req *req) +{ + if (req->backlogged) { + php_io_ring_backlog_remove(ring, req); + req->main_done = true; + return; + } + if (req->waiting) { + /* Never reached the backend: the record's multishot stays */ + php_io_ring_waiter_settle(ring, req); + req->main_done = true; + return; + } + if (!req->main_done && !req->cancelled) { + req->cancelled = true; + if (!php_io_ring_submit_cancel(ring, req)) { + req->cancel_pending = true; + ring->n_cancel_pending++; + } + } +} + +/* Cancels that found no entry, one at a time with a reap in between */ +static void php_io_ring_retry_cancels(php_io_ring *ring) +{ + while (ring->n_cancel_pending) { + php_io_ring_req *target = ring->live; + while (target && !target->cancel_pending) { + target = target->next; + } + ZEND_ASSERT(target); + if (!php_io_ring_submit_cancel(ring, target)) { + return; + } + target->cancel_pending = false; + ring->n_cancel_pending--; + php_io_ring_reap(ring); + } +} + +static void php_io_ring_read_unclaimed(php_io_ring_req *req, php_stream *stream, int32_t res) +{ + if ((req->type == PHP_IO_OP_READ || req->type == PHP_IO_OP_RECV) && req->u.io.advances) { + php_io_stream_keep_read(stream, !req->u.io.buf, res); + } +} + +/* The record is no longer wanted: drop it now if settled, or let the reap + * that settles it drop it */ +static void php_io_ring_req_release(php_io_ring *ring, php_io_ring_req *req) +{ + req->orphaned = true; + if (php_io_ring_req_settled(req)) { + /* Nothing outstanding: no freeze to keep */ + req->orphan_stream = NULL; + req->orphan_handle = NULL; + } + if (req->ready) { + php_io_ring_list_remove(ring->ready, &ring->n_ready, req); + req->ready = false; + } + if (req->op && req->op->stream && req->main_done && !req->delivered) { + /* Reaped, never delivered */ + php_io_ring_read_unclaimed(req, req->op->stream, req->main_res); + } + if (req->op) { + req->op->queue_data = NULL; + req->op->queue = NULL; + req->op->in_flight = false; + req->op = NULL; + } + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } +} + +/* In a child the record can neither be cancelled nor complete: it is + * settled here so that the release frees it */ +static void php_io_ring_req_cancel_or_forget(php_io_ring *ring, php_io_ring_req *req) +{ + if (php_io_ring_foreign(ring)) { + if (req->backlogged) { + php_io_ring_backlog_remove(ring, req); + } + if (req->waiting) { + php_io_ring_waiter_settle(ring, req); + } + req->main_done = true; + req->lt_done = true; + req->delivered = true; + req->orphan_stream = NULL; + req->orphan_handle = NULL; + return; + } + php_io_ring_req_cancel(ring, req); +} + +PHPAPI zend_result php_io_ring_cancel(php_io_ring *ring, php_io_op *op) +{ + php_io_ring_req *req = op->queue_data; + if (!req) { + errno = ENOENT; + return FAILURE; + } + if (req->group) { + errno = EINVAL; + return FAILURE; + } + + ring->pending--; + if (op->type == PHP_IO_OP_ANY) { + if (req->fired) { + php_io_ring_list_remove(ring->fired, &ring->n_fired, req); + req->fired = false; + } + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_ring_req *m = req->members[i]; + m->group = NULL; + php_io_ring_req_cancel_or_forget(ring, m); + php_io_ring_req_release(ring, m); + } + req->n_members = 0; + } else { + php_io_ring_req_cancel_or_forget(ring, req); + } + php_io_ring_req_release(ring, req); + return SUCCESS; +} + +static void php_io_ring_owner_orphan(php_io_queue *queue, php_io_op *op) +{ + if (op->stream) { + php_io_stream_orphan(op->stream, queue); + } else { + php_io_handle_orphan(op->handle, queue); + } +} + +/* An op the backend may still run after its frame: on a stream, or on a handle's descriptor */ +static zend_always_inline bool php_io_ring_op_keeps(const php_io_op *op) +{ + return op->stream || (op->handle && op->fd != SOCK_ERR); +} + +PHPAPI bool php_io_ring_orphan(php_io_ring *ring, php_io_op *op) +{ + /* The caller's frame is going away; a record still in flight keeps the + * stream frozen and finishes silently in a later wait. In a child + * nothing completes, so nothing is kept. */ + php_io_ring_req *req = op->queue_data; + bool keep = req && op->in_flight && php_io_ring_op_keeps(op) && !req->group + && op->type != PHP_IO_OP_ANY && !php_io_ring_req_settled(req) + && !php_io_ring_foreign(ring); + if (keep) { + req->orphan_stream = op->stream; + req->orphan_handle = op->stream ? NULL : op->handle; + } + php_io_ring_cancel(ring, op); + if (keep) { + op->in_flight = true; + } + return keep; +} + +/* Completion processing */ + +static void php_io_ring_result_from_cqe(php_io_ring_req *req, int32_t res) +{ + php_io_op_result *r = &req->result; + bool dns = req->type == PHP_IO_OP_GETADDRINFO || req->type == PHP_IO_OP_GETNAMEINFO; + + r->index = req->index; + r->error = 0; + r->res = res; + + if (res >= 0) { + r->status = PHP_IO_DONE; + if (req->type == PHP_IO_OP_POLL) { + r->res = php_io_ring_poll_mask_from_ior((uint32_t) res); + } else if (dns && res != 0) { + /* EAI_* codes are the work result as they are */ + r->error = res; + r->res = -1; + } + return; + } + + switch (res) { + case -ETIME: + r->status = req->type == PHP_IO_OP_TIMER ? PHP_IO_DONE : PHP_IO_TIMEOUT; + r->res = 0; + break; + case -ECANCELED: + /* A fired linked timeout cancels the guarded op */ + r->status = req->cancelled ? PHP_IO_CANCELLED : (req->has_lt ? PHP_IO_TIMEOUT : PHP_IO_CANCELLED); + r->res = -1; + break; + case -EOPNOTSUPP: + case -ENOSYS: +#if defined(ENOTSUP) && ENOTSUP != EOPNOTSUPP + case -ENOTSUP: +#endif + case -ENOTSOCK: + r->status = PHP_IO_UNSUPPORTED; + r->res = -1; + break; + default: + r->status = PHP_IO_DONE; + /* A negative EAI_* code for the DNS ops */ + r->error = dns ? res : -res; + r->res = -1; + break; + } +} + +/* The main cqe of a record arrived */ +static void php_io_ring_req_main_cqe(php_io_ring *ring, php_io_ring_req *req, int32_t res) +{ + req->main_done = true; + req->main_res = res; + if (req->cancel_pending) { + req->cancel_pending = false; + ring->n_cancel_pending--; + } + if (req->op) { + req->op->in_flight = false; + } + + if (req->orphaned) { + if (req->orphan_stream && !req->delivered) { + php_io_ring_read_unclaimed(req, req->orphan_stream, res); + } + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } + return; + } + + php_io_ring_result_from_cqe(req, res); + if (!req->early) { + php_io_ring_req_complete(ring, req); + } +} + +/* An op still running at its deadline (a work callback, a regular file on a worker) or being + * cancelled by it: it completes as Timeout now, and its main cqe only releases the record, + * unless it comes before the completion was delivered */ +static void php_io_ring_req_lt_cqe(php_io_ring *ring, php_io_ring_req *req, int32_t res) +{ + req->lt_done = true; + if (req->orphaned) { + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } + return; + } + if (req->main_done || req->cancelled || (res != -EALREADY && res != -ETIME)) { + return; + } + /* Without a queue nothing keeps the stream frozen for the backend */ + if (!ring->queue && req->op && req->op->in_flight && php_io_ring_op_keeps(req->op)) { + return; + } + req->early = true; + req->result.index = req->index; + req->result.status = PHP_IO_TIMEOUT; + req->result.res = 0; + req->result.error = 0; + php_io_ring_req_complete(ring, req); +} + +/* Poll members that are ready by now but whose cqe is not there yet (the + * thread backend posts each from its poller in turn) are reported too */ +static void php_io_ring_group_probe(php_io_ring_req *req, bool *probed) +{ + php_pollfd stack[16]; + php_pollfd *fds = stack; + uint32_t n = 0; + + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_ring_req *m = req->members[i]; + if (!m->main_done && m->type == PHP_IO_OP_POLL) { + n++; + } + } + if (n == 0) { + return; + } + if (n > sizeof(stack) / sizeof(stack[0])) { + fds = safe_emalloc(n, sizeof(*fds), 0); + } + n = 0; + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_ring_req *m = req->members[i]; + if (!m->main_done && m->type == PHP_IO_OP_POLL) { + fds[n].fd = m->op->fd; + fds[n].events = (m->op->u.poll.events & PHP_POLL_READ ? POLLIN : 0) + | (m->op->u.poll.events & PHP_POLL_WRITE ? POLLOUT : 0); + fds[n].revents = 0; + n++; + } + } + if (php_poll2(fds, n, 0) > 0) { + n = 0; + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_ring_req *m = req->members[i]; + if (!m->main_done && m->type == PHP_IO_OP_POLL) { + short revents = fds[n++].revents; + if (revents) { + uint32_t mask = (revents & POLLIN ? IOR_POLL_IN : 0) | (revents & POLLOUT ? IOR_POLL_OUT : 0) + | (revents & POLLERR ? IOR_POLL_ERR : 0) | (revents & POLLHUP ? IOR_POLL_HUP : 0) + | (revents & POLLNVAL ? IOR_POLL_NVAL : 0); + php_io_ring_result_from_cqe(m, (int32_t) mask); + probed[i] = true; + } + } + } + } + if (fds != stack) { + efree(fds); + } +} + +/* The Any completes with the members that completed by now; the rest are + * cancelled and their cqes consumed silently */ +static void php_io_ring_group_fold(php_io_ring *ring, php_io_ring_req *req) +{ + php_io_op *op = req->op; + uint32_t n_results = 0; + bool *probed = ecalloc(MAX(req->n_members, 1), sizeof(bool)); + + req->fired = false; + req->group_done = true; + php_io_ring_group_probe(req, probed); + + for (uint32_t i = 0; i < req->n_members; i++) { + php_io_ring_req *m = req->members[i]; + if (m->main_done || m->early || probed[i]) { + if (op->u.any.results) { + op->u.any.results[n_results] = m->result; + } + php_io_ring_req_output(m, m->op); + n_results++; + } + if (!m->main_done && !m->early) { + php_io_ring_req_cancel(ring, m); + } + m->group = NULL; + php_io_ring_req_release(ring, m); + } + req->n_members = 0; + op->u.any.n_results = n_results; + efree(probed); + + req->ready = true; + req->result.status = PHP_IO_DONE; + req->result.index = 0; + req->result.res = 0; + req->result.error = 0; + php_io_ring_list_push(&ring->ready, &ring->n_ready, &ring->ready_cap, req); +} + +static void php_io_ring_fold_all(php_io_ring *ring) +{ + while (ring->n_fired) { + php_io_ring_req *req = ring->fired[0]; + php_io_ring_list_remove(ring->fired, &ring->n_fired, req); + php_io_ring_group_fold(ring, req); + } +} + +/* A multishot poll's completion: an edge of its record, or its last one after a cancel, an error + * or a full completion queue, after which the record re-arms at the next wait and a possibly + * missed edge becomes at most one spurious wakeup */ +static void php_io_ring_multishot_cqe(php_io_ring *ring, php_io_ring_req *req, int32_t res, bool more) +{ + php_io_ring_reg *rec = req->reg; + if (req->type == PHP_IO_OP_ACCEPT) { + /* A connection, buffered or taken; one posted after the record let go is closed */ + if (res >= 0) { + if (rec) { + php_io_ring_reg_connection(ring, rec, (int) res); + } else { + closesocket((php_socket_t) res); + } + } + if (more) { + return; + } + if (rec) { + if (rec->accept == req) { + rec->accept = NULL; + } + req->reg = NULL; + if (res < 0 && res != -ECANCELED) { + /* Refused (an old kernel, -EMFILE): the parked accepts get the error, later + * ones take their own entry */ + rec->accept_failed = true; + php_io_ring_req *w = ring->waiting; + while (w) { + php_io_ring_req *next = w->w_next; + if (w->waiting == rec && w->type == PHP_IO_OP_ACCEPT) { + php_io_ring_waiter_settle(ring, w); + php_io_ring_req_main_cqe(ring, w, res); + } + w = next; + } + } + } + } else if (more) { + if (rec && res > 0) { + php_io_ring_reg_edge(ring, rec, (uint32_t) res); + } + return; + } else if (rec) { + rec->poll = NULL; + req->reg = NULL; + php_io_ring_reg_edge(ring, rec, res > 0 ? (uint32_t) res : rec->poll_mask); + } + req->main_done = true; + if (req->cancel_pending) { + req->cancel_pending = false; + ring->n_cancel_pending--; + } + req->orphaned = true; + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } +} + +static void php_io_ring_process_cqe(php_io_ring *ring, uintptr_t data, int32_t res, bool more) +{ + php_io_ring_req *req = (php_io_ring_req *) (data & ~PHP_IO_RING_TAG_MASK); + /* A cancel's own cqe: the target completes on its own and may be freed by + * the time it arrives, so it is not looked at */ + if (!req || (data & PHP_IO_RING_TAG_MASK) == PHP_IO_RING_TAG_CANCEL) { + return; + } + if (req->multishot && (data & PHP_IO_RING_TAG_MASK) == 0) { + php_io_ring_multishot_cqe(ring, req, res, more); + return; + } + switch (data & PHP_IO_RING_TAG_MASK) { + case PHP_IO_RING_TAG_LT: + php_io_ring_req_lt_cqe(ring, req, res); + break; + default: + php_io_ring_req_main_cqe(ring, req, res); + break; + } +} + +/* Take everything the ring has, until a peek finds nothing: the members + * of an Any that are ready by now are all reported with it. The batch is + * consumed before it is processed, so a cancel submitted meanwhile finds + * the room it needs. */ +static uint32_t php_io_ring_reap(php_io_ring *ring) +{ + uint32_t total = 0; + for (;;) { + unsigned n = ior_peek_batch_cqe(ring->ctx, ring->cqes, ring->cqes_cap); + if (n == 0) { + break; + } + for (unsigned i = 0; i < n; i++) { + ring->batch[i].data = (uintptr_t) ior_cqe_get_data(ring->ctx, ring->cqes[i]); + ring->batch[i].res = ior_cqe_get_res(ring->ctx, ring->cqes[i]); + ring->batch[i].more = (ior_cqe_get_flags(ring->ctx, ring->cqes[i]) & IOR_CQE_F_MORE) != 0; + } + ior_cq_advance(ring->ctx, n); + for (unsigned i = 0; i < n; i++) { + php_io_ring_process_cqe(ring, ring->batch[i].data, ring->batch[i].res, ring->batch[i].more); + } + total += n; + } + php_io_ring_fold_all(ring); + return total; +} + +/* Everything that needs no waiting */ +static void php_io_ring_progress(php_io_ring *ring) +{ + php_io_ring_reap(ring); + php_io_ring_retry_cancels(ring); + php_io_ring_flush_backlog(ring); + php_io_ring_flush(ring); +} + +/* Ops still in the backlog or parked on a record at their deadline time out + * here, having no entry to link a timeout to; returns the next such deadline */ +static zend_hrtime_t php_io_ring_expire(php_io_ring *ring, zend_hrtime_t now) +{ + zend_hrtime_t next = ZEND_HRTIME_T_MAX; + php_io_ring_req *r = ring->bl_head; + while (r) { + php_io_ring_req *bl_next = r->bl_next; + if (!php_deadline_is_infinite(&r->deadline)) { + if (r->deadline.hrtime <= now) { + php_io_ring_backlog_remove(ring, r); + php_io_ring_req_main_cqe(ring, r, -ETIME); + } else if (r->deadline.hrtime < next) { + next = r->deadline.hrtime; + } + } + r = bl_next; + } + r = ring->waiting; + while (r) { + php_io_ring_req *w_next = r->w_next; + if (!php_deadline_is_infinite(&r->deadline)) { + if (r->deadline.hrtime <= now) { + php_io_ring_waiter_settle(ring, r); + php_io_ring_req_main_cqe(ring, r, -ETIME); + } else if (r->deadline.hrtime < next) { + next = r->deadline.hrtime; + } + } + r = w_next; + } + php_io_ring_fold_all(ring); + return next; +} + +/* Wait until every orphaned op on the stream settled; other completions + * stay queued for delivery */ +PHPAPI void php_io_ring_drain(php_io_ring *ring, const void *owner) +{ + if (php_io_ring_foreign(ring)) { + return; + } + for (;;) { + php_io_ring_progress(ring); + bool pending = false; + for (php_io_ring_req *r = ring->live; r; r = r->next) { + if (r->orphan_stream == owner || r->orphan_handle == owner) { + pending = true; + break; + } + } + if (!pending) { + return; + } + ior_cqe *cqe; + int rc = ior_wait_cqe(ring->ctx, &cqe); + if (rc < 0 && rc != -EINTR) { + return; + } + } +} + +/* A ready record becomes the caller's completion and lets go of the op */ +static void php_io_ring_deliver_one(php_io_ring *ring, php_io_ring_req *req, php_io_queue_completion *out) +{ + if (req->type != PHP_IO_OP_ANY) { + php_io_ring_req_output(req, req->op); + if (!req->main_done && req->op->in_flight && php_io_ring_op_keeps(req->op)) { + /* The backend still uses the stream: it stays frozen until the record settled */ + req->orphan_stream = req->op->stream; + req->orphan_handle = req->op->stream ? NULL : req->op->handle; + php_io_ring_owner_orphan(ring->queue, req->op); + } + } + out->op = req->op; + out->data = req->data; + out->result = req->result; + req->ready = false; + ring->pending--; + req->op->queue_data = NULL; + req->op->queue = NULL; + req->op = NULL; + req->orphaned = true; + if (php_io_ring_req_settled(req)) { + php_io_ring_req_free(ring, req); + } +} + +static uint32_t php_io_ring_deliver(php_io_ring *ring, php_io_queue_completion *out, uint32_t max) +{ + uint32_t n = MIN(max, ring->n_ready); + + for (uint32_t i = 0; i < n; i++) { + php_io_ring_deliver_one(ring, ring->ready[i], &out[i]); + } + memmove(ring->ready, &ring->ready[n], (ring->n_ready - n) * sizeof(*ring->ready)); + ring->n_ready -= n; + return n; +} + +PHPAPI bool php_io_ring_take_inline(php_io_ring *ring, php_io_op *op, php_io_queue_completion *out) +{ + php_io_ring_req *req = op->queue_data; + + if (!req || !req->ready || op->type == PHP_IO_OP_ANY || php_io_ring_foreign(ring)) { + return false; + } + php_io_ring_list_remove(ring->ready, &ring->n_ready, req); + php_io_ring_deliver_one(ring, req, out); + return true; +} + +PHPAPI int php_io_ring_wait(php_io_ring *ring, php_io_queue_completion *out, uint32_t max, const php_deadline *dl) +{ + if (php_io_ring_foreign(ring)) { + errno = EPERM; + return -1; + } + zend_hrtime_t limit = dl ? dl->hrtime : ZEND_HRTIME_T_MAX; + /* Only orphans: once they settled there is nothing to report, as + * count_pending() told */ + bool orphans_only = ring->pending == 0 && php_io_ring_settling(ring); + + if (max == 0) { + return 0; + } + if (limit == 0) { + /* A loop woken by the notification descriptor: clear, then reap until empty */ + php_io_ring_notify_clear(ring); + } + + for (;;) { + php_io_ring_progress(ring); + zend_hrtime_t now = zend_hrtime(); + zend_hrtime_t next = php_io_ring_expire(ring, now); + if (ring->n_ready) { + return (int) php_io_ring_deliver(ring, out, max); + } + if (limit != ZEND_HRTIME_T_MAX && now >= limit) { + return 0; + } + if (ring->pending == 0) { + if (php_io_ring_settling(ring)) { + orphans_only = true; + } else if (orphans_only) { + return 0; + } else if (limit == ZEND_HRTIME_T_MAX) { + errno = EDEADLK; + return -1; + } + } + + if (ring->unsubmitted) { + int rc = php_io_ring_flush(ring); + if (rc < 0 && rc != -EAGAIN && rc != -EBUSY && rc != -EINTR) { + errno = -rc; + return -1; + } + } + + ior_cqe *cqe; + int rc; + zend_hrtime_t until = MIN(limit, next); + if (until == ZEND_HRTIME_T_MAX) { + rc = ior_wait_cqe(ring->ctx, &cqe); + } else { + now = zend_hrtime(); + zend_hrtime_t remaining = until > now ? until - now : 0; + ior_timespec ts = { .tv_sec = (int64_t) (remaining / ZEND_NANO_IN_SEC), .tv_nsec = (long long) (remaining % ZEND_NANO_IN_SEC) }; + rc = ior_wait_cqe_timeout(ring->ctx, &cqe, &ts); + } + if (rc < 0 && rc != -ETIME && (rc != -EINTR || php_io_interrupt_pending())) { + errno = -rc; + return -1; + } + /* The next pass consumes it */ + } +} + +PHPAPI void php_io_ring_destroy(php_io_ring *ring) +{ + bool foreign = php_io_ring_foreign(ring); + + /* The Edge records go first: their multishots settle like any entry */ + php_io_ring_reg *rec; + ZEND_HASH_FOREACH_PTR(&ring->regs, rec) { + if (rec->poll) { + rec->poll->reg = NULL; + } + if (rec->accept) { + rec->accept->reg = NULL; + } + /* In a forked child these are its copies */ + php_io_ring_reg_close_fds(rec); + efree(rec); + } ZEND_HASH_FOREACH_END(); + zend_hash_destroy(&ring->regs); + + /* Nobody takes a completion any more: every op forgets its record, as + * after a cancel, and every record is an orphan */ + for (php_io_ring_req *r = ring->live; r; r = r->next) { + if (r->op) { + r->op->queue = NULL; + r->op->queue_data = NULL; + r->op->in_flight = false; + r->op = NULL; + } + if (r->backlogged) { + php_io_ring_backlog_remove(ring, r); + r->main_done = true; + } + if (r->waiting) { + php_io_ring_waiter_settle(ring, r); + r->main_done = true; + } + if (foreign) { + r->main_done = true; + r->lt_done = true; + r->delivered = true; + } + r->group = NULL; + r->ready = false; + r->fired = false; + r->orphaned = true; + } + ring->n_ready = 0; + ring->n_fired = 0; + ring->pending = 0; + + /* Cancel everything in flight and drain until each has completed */ + for (;;) { + php_io_ring_req *r = ring->live; + while (r) { + php_io_ring_req *next = r->next; + if (php_io_ring_req_settled(r)) { + php_io_ring_req_free(ring, r); + } else if (!r->cancelled) { + php_io_ring_req_cancel(ring, r); + } + r = next; + } + if (!ring->live) { + break; + } + php_io_ring_retry_cancels(ring); + php_io_ring_flush(ring); + ior_cqe *cqe; + int rc = ior_wait_cqe(ring->ctx, &cqe); + if (rc < 0 && rc != -EINTR) { + /* The backend may still write into what the records own: they + * and the context are leaked rather than freed under it */ + ring->ctx = NULL; + break; + } + php_io_ring_reap(ring); + } + if (ring->ctx && !foreign) { + ior_queue_exit(ring->ctx); + } + if (ring->prev_ring) { + ring->prev_ring->next_ring = ring->next_ring; + } else { + php_io_rings = ring->next_ring; + } + if (ring->next_ring) { + ring->next_ring->prev_ring = ring->prev_ring; + } + if (ring->ready) { + efree(ring->ready); + } + if (ring->fired) { + efree(ring->fired); + } + efree(ring->cqes); + efree(ring->batch); + efree(ring); +} + +/* The ring as an operation queue */ + +typedef struct php_io_ring_queue { + php_io_queue base; + php_io_ring *ring; +} php_io_ring_queue; + +static zend_result php_io_ring_queue_submit(php_io_queue *base, php_io_op *op, void *data) +{ + php_io_ring_queue *q = (php_io_ring_queue *) base; + if (op->queue) { + errno = EALREADY; + return FAILURE; + } + op->queue = base; + if (op->type == PHP_IO_OP_ANY) { + for (uint32_t i = 0; i < op->u.any.n; i++) { + op->u.any.ops[i]->queue = base; + } + } + if (php_io_ring_submit_op(q->ring, op, data) != SUCCESS) { + op->queue = NULL; + return FAILURE; + } + return SUCCESS; +} + +static void php_io_ring_queue_orphan(php_io_queue *base, php_io_op *op) +{ + if (op->queue == base && php_io_ring_orphan(((php_io_ring_queue *) base)->ring, op)) { + php_io_ring_owner_orphan(base, op); + } +} + +static zend_result php_io_ring_queue_cancel(php_io_queue *base, php_io_op *op) +{ + php_io_ring_queue *q = (php_io_ring_queue *) base; + if (op->queue != base) { + errno = ENOENT; + return FAILURE; + } + php_io_ring_req *req = op->queue_data; + if (req && !req->group) { + /* Kept frozen as an orphan while the backend may fill the buffer */ + php_io_ring_queue_orphan(base, op); + return SUCCESS; + } + return php_io_ring_cancel(q->ring, op); +} + +static zend_result php_io_ring_queue_add(php_io_queue *base, php_io_registration *reg) +{ + return php_io_ring_add(((php_io_ring_queue *) base)->ring, reg); +} + +static void php_io_ring_queue_remove(php_io_queue *base, php_io_registration *reg) +{ + php_io_ring_remove(((php_io_ring_queue *) base)->ring, reg); +} + +static bool php_io_ring_queue_take_inline(php_io_queue *base, php_io_op *op, php_io_queue_completion *out) +{ + return php_io_ring_take_inline(((php_io_ring_queue *) base)->ring, op, out); +} + +static int php_io_ring_queue_wait(php_io_queue *base, php_io_queue_completion *out, uint32_t max, const php_deadline *dl) +{ + return php_io_ring_wait(((php_io_ring_queue *) base)->ring, out, max, dl); +} + +static void php_io_ring_queue_drain(php_io_queue *base, const void *owner) +{ + php_io_ring_drain(((php_io_ring_queue *) base)->ring, owner); +} + +static uint32_t php_io_ring_queue_count_pending(php_io_queue *base) +{ + return php_io_ring_count_pending(((php_io_ring_queue *) base)->ring); +} + +static uint32_t php_io_ring_queue_hook_flags(php_io_queue *base) +{ + return php_io_ring_hook_flags(((php_io_ring_queue *) base)->ring); +} + +static void php_io_ring_queue_destroy(php_io_queue *base) +{ + php_io_ring_queue *q = (php_io_ring_queue *) base; + php_io_ring_destroy(q->ring); + efree(q); +} + +static const php_io_queue_ops php_io_ring_queue_ops = { + .submit = php_io_ring_queue_submit, + .take_inline = php_io_ring_queue_take_inline, + .cancel = php_io_ring_queue_cancel, + .add = php_io_ring_queue_add, + .remove = php_io_ring_queue_remove, + .wait = php_io_ring_queue_wait, + .orphan = php_io_ring_queue_orphan, + .drain = php_io_ring_queue_drain, + .count_pending = php_io_ring_queue_count_pending, + .hook_flags = php_io_ring_queue_hook_flags, + .destroy = php_io_ring_queue_destroy, +}; + +PHPAPI php_io_ring *php_io_queue_ring(php_io_queue *q) +{ + ZEND_ASSERT(q->ops == &php_io_ring_queue_ops); + return ((php_io_ring_queue *) q)->ring; +} + +PHPAPI php_io_queue *php_io_queue_create_ring(uint32_t entries) +{ + /* Ops come only from the core, which makes every descriptor it submits + * non-blocking: sockets, accepted ones included, and unseekable plain + * files; regular files run to completion either way */ + php_io_ring *ring = php_io_ring_create(entries, true); + if (!ring) { + return NULL; + } + php_io_ring_queue *q = ecalloc(1, sizeof(*q)); + q->base.ops = &php_io_ring_queue_ops; + q->ring = ring; + ring->queue = &q->base; + return &q->base; +} + +#endif /* HAVE_IOR */ diff --git a/main/network.c b/main/network.c index 01e4f2e778d4..2df126fef7c1 100644 --- a/main/network.c +++ b/main/network.c @@ -64,6 +64,7 @@ #endif #include "ext/standard/file.h" +#include "php_io.h" #ifdef PHP_WIN32 # include "win32/time.h" @@ -183,7 +184,9 @@ PHPAPI int php_network_getaddresses(const char *host, int socktype, struct socka hints.ai_family = ipv6_borked ? AF_INET : AF_UNSPEC; # endif - if ((n = getaddrinfo(host, NULL, &hints, &res))) { + php_deadline deadline; + php_deadline_init_infinite(&deadline); + if ((n = php_io_getaddrinfo(host, NULL, &hints, &res, &deadline))) { # if defined(PHP_WIN32) char *gai_error = php_win32_error_to_msg(n); # elif defined(HAVE_GAI_STRERROR) @@ -233,7 +236,7 @@ PHPAPI int php_network_getaddresses(const char *host, int socktype, struct socka sap++; } while ((sai = sai->ai_next) != NULL); - freeaddrinfo(res); + php_io_freeaddrinfo(res); #else if (!inet_pton(AF_INET, host, &in)) { if(strlen(host) > MAXFQDNLEN) { @@ -326,8 +329,9 @@ static inline void php_network_set_limit_time(struct timeval *limit_time, * Optionally, the connect can be made asynchronously, which will implicitly * enable non-blocking mode on the socket. * */ -/* {{{ php_network_connect_socket */ -PHPAPI int php_network_connect_socket(php_socket_t sockfd, +/* {{{ php_network_connect_socket_stream */ +PHPAPI int php_network_connect_socket_stream(php_stream *stream, + php_socket_t sockfd, const struct sockaddr *addr, socklen_t addrlen, int asynchronous, @@ -336,98 +340,45 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, int *error_code) { php_non_blocking_flags_t orig_flags; - int n; int error = 0; - socklen_t len; int ret = 0; SET_SOCKET_BLOCKING_MODE(sockfd, orig_flags); - if ((n = connect(sockfd, addr, addrlen)) != 0) { - error = php_socket_errno(); - - if (error_code) { - *error_code = error; - } - - if (error != EINPROGRESS) { - if (error_string) { - *error_string = php_socket_error_str(error); + if (asynchronous) { + /* Started here, completed by the caller's own readiness wait */ + if (connect(sockfd, addr, addrlen) != 0) { + error = php_socket_errno(); + if (error_code) { + *error_code = error; + } + if (error != EINPROGRESS) { + if (error_string) { + *error_string = php_socket_error_str(error); + } + return -1; } - - return -1; } - if (asynchronous && error == EINPROGRESS) { - /* this is fine by us */ - return 0; + if (error_code) { + *error_code = 0; } + return 0; } - if (n == 0) { - goto ok; - } -# ifdef PHP_WIN32 - /* The documentation for connect() says in case of non-blocking connections - * the select function reports success in the writefds set and failure in - * the exceptfds set. Indeed, using PHP_POLLREADABLE results in select - * failing only due to the timeout and not immediately as would be - * expected when a connection is actively refused. This way, - * php_pollfd_for will return a mask with POLLOUT if the connection - * is successful and with POLLPRI otherwise. */ - int events = POLLOUT|POLLPRI; -#else - int events = PHP_POLLREADABLE|POLLOUT; -#endif - struct timeval working_timeout; -#ifdef HAVE_GETTIMEOFDAY - struct timeval limit_time, time_now; -#endif - if (timeout) { - memcpy(&working_timeout, timeout, sizeof(working_timeout)); -#ifdef HAVE_GETTIMEOFDAY - php_network_set_limit_time(&limit_time, &working_timeout); -#endif - } - - while (true) { - n = php_pollfd_for(sockfd, events, timeout ? &working_timeout : NULL); - if (n < 0) { - if (errno == EINTR) { -#ifdef HAVE_GETTIMEOFDAY - if (timeout) { - gettimeofday(&time_now, NULL); - - if (!timercmp(&time_now, &limit_time, <)) { - /* time limit expired; no need for another poll */ - error = PHP_TIMEOUT_ERROR_VALUE; - break; - } else { - /* work out remaining time */ - sub_times(limit_time, time_now, &working_timeout); - } - } -#endif - continue; - } - ret = -1; - } else if (n == 0) { + /* A Connect op: the connect and the wait for its outcome, on the + * provider when one is installed */ + php_deadline deadline; + deadline = php_io_deadline_from_timeval(timeout); + if (php_io_connect(stream, sockfd, addr, addrlen, &deadline) != 0) { + error = php_socket_errno(); + if (error == ETIMEDOUT) { error = PHP_TIMEOUT_ERROR_VALUE; - } else { - len = sizeof(error); - /* BSD-derived systems set errno correctly. - * Solaris returns -1 from getsockopt in case of error. */ - if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, (char*)&error, &len) != 0) { - ret = -1; - } } - break; + ret = -1; } -ok: - if (!asynchronous) { - /* back to blocking mode */ - RESTORE_SOCKET_BLOCKING_MODE(sockfd, orig_flags); - } + /* back to blocking mode */ + RESTORE_SOCKET_BLOCKING_MODE(sockfd, orig_flags); if (error_code) { *error_code = error; @@ -441,6 +392,18 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, } return ret; } + +PHPAPI int php_network_connect_socket(php_socket_t sockfd, + const struct sockaddr *addr, + socklen_t addrlen, + int asynchronous, + struct timeval *timeout, + zend_string **error_string, + int *error_code) +{ + return php_network_connect_socket_stream(NULL, sockfd, addr, addrlen, asynchronous, timeout, + error_string, error_code); +} /* }}} */ static void php_network_set_socket_buffers(php_socket_t sock, const php_sockvals *sockvals) @@ -836,7 +799,8 @@ PHPAPI int php_network_get_sock_name(php_socket_t sock, * version of the address will be emalloc'd and returned. * */ -PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, +PHPAPI php_socket_t php_network_accept_incoming_stream_ex(php_stream *stream, + php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, @@ -847,42 +811,73 @@ PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, ) { php_socket_t clisock = -1; - int error = 0, n; + int error = 0; php_sockaddr_storage sa; socklen_t sl; - n = php_pollfd_for(srvsock, PHP_POLLREADABLE, timeout); - - if (n == 0) { - error = PHP_TIMEOUT_ERROR_VALUE; - } else if (n == -1) { - error = php_socket_errno(); - } else { + if (php_io_hooks_active()) { + php_deadline deadline; + deadline = php_io_deadline_from_timeval(timeout); sl = sizeof(sa); + if (stream) { + /* A listener shared with other processes: a provider's multishot accept would + * pull connections into this one, so the pair stays unregistered and its + * accepts one-shot. Read here, under both the plain and the TLS transport. */ + zval *opt = PHP_STREAM_CONTEXT(stream) + ? php_stream_context_get_option(PHP_STREAM_CONTEXT(stream), "socket", "accept_multishot") : NULL; + if (opt && !zend_is_true(opt)) { + stream->flags |= PHP_STREAM_FLAG_NO_IO_REGISTRATION; + } else { + stream->flags &= ~PHP_STREAM_FLAG_NO_IO_REGISTRATION; + } + } + clisock = php_io_accept(stream, srvsock, (struct sockaddr*)&sa, &sl, &deadline); - clisock = accept(srvsock, (struct sockaddr*)&sa, &sl); + if (clisock == SOCK_ERR) { + error = php_socket_errno(); + if (error == ETIMEDOUT) { + error = PHP_TIMEOUT_ERROR_VALUE; + } + } + } else { + /* Without a provider a signal interrupts the wait */ + do { + error = 0; + int n = php_pollfd_for(srvsock, PHP_POLLREADABLE, timeout); + if (n == 0) { + error = PHP_TIMEOUT_ERROR_VALUE; + break; + } + if (n < 0) { + error = php_socket_errno(); + break; + } + sl = sizeof(sa); + clisock = accept(srvsock, (struct sockaddr*)&sa, &sl); + if (clisock == SOCK_ERR) { + error = php_socket_errno(); + } + } while (clisock == SOCK_ERR && PHP_IS_TRANSIENT_ERROR(error)); + } - if (clisock != SOCK_ERR) { - php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl, - textaddr, - addr, addrlen - ); + if (clisock != SOCK_ERR) { + php_network_populate_name_from_sockaddr((struct sockaddr*)&sa, sl, + textaddr, + addr, addrlen + ); #ifdef TCP_NODELAY - if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_NODELAY)) { - int tcp_nodelay = 1; - setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay)); - } + if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_NODELAY)) { + int tcp_nodelay = 1; + setsockopt(clisock, IPPROTO_TCP, TCP_NODELAY, (char*)&tcp_nodelay, sizeof(tcp_nodelay)); + } #endif #ifdef TCP_KEEPALIVE - /* MacOS does not inherit TCP_KEEPALIVE so it needs to be set */ - if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_KEEPIDLE)) { - setsockopt(clisock, IPPROTO_TCP, TCP_KEEPALIVE, - (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); - } -#endif - } else { - error = php_socket_errno(); + /* MacOS does not inherit TCP_KEEPALIVE so it needs to be set */ + if (PHP_SOCKVAL_IS_SET(sockvals, PHP_SOCKVAL_TCP_KEEPIDLE)) { + setsockopt(clisock, IPPROTO_TCP, TCP_KEEPALIVE, + (char*)&sockvals->keepalive.keepidle, sizeof(sockvals->keepalive.keepidle)); } +#endif } if (error_code) { @@ -895,7 +890,22 @@ PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, return clisock; } -PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, +PHPAPI php_socket_t php_network_accept_incoming_ex(php_socket_t srvsock, + zend_string **textaddr, + struct sockaddr **addr, + socklen_t *addrlen, + struct timeval *timeout, + zend_string **error_string, + int *error_code, + php_sockvals *sockvals + ) +{ + return php_network_accept_incoming_stream_ex(NULL, srvsock, textaddr, addr, addrlen, timeout, error_string, + error_code, sockvals); +} + +PHPAPI php_socket_t php_network_accept_incoming_stream(php_stream *stream, + php_socket_t srvsock, zend_string **textaddr, struct sockaddr **addr, socklen_t *addrlen, @@ -907,16 +917,30 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, { php_sockvals sockvals = { .mask = tcp_nodelay ? PHP_SOCKVAL_TCP_NODELAY : 0 }; - return php_network_accept_incoming_ex(srvsock, textaddr, addr, addrlen, timeout, error_string, + return php_network_accept_incoming_stream_ex(stream, srvsock, textaddr, addr, addrlen, timeout, error_string, error_code, &sockvals); } +PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, + zend_string **textaddr, + struct sockaddr **addr, + socklen_t *addrlen, + struct timeval *timeout, + zend_string **error_string, + int *error_code, + int tcp_nodelay + ) +{ + return php_network_accept_incoming_stream(NULL, srvsock, textaddr, addr, addrlen, timeout, error_string, + error_code, tcp_nodelay); +} + /* Connect to a remote host using an interruptible connect with optional timeout. * Optionally, the connect can be made asynchronously, which will implicitly * enable non-blocking mode on the socket. * Returns the connected (or connecting) socket, or -1 on failure. * */ -php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, +php_socket_t php_network_connect_socket_to_host_stream(php_stream *stream, php_socket_t *current, const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals ) @@ -1095,13 +1119,20 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh php_network_set_socket_buffers(sock, sockvals); } - n = php_network_connect_socket(sock, sa, socklen, asynchronous, + /* The stream's handle resolves its descriptor while the connect waits */ + if (current) { + *current = sock; + } + n = php_network_connect_socket_stream(stream, sock, sa, socklen, asynchronous, timeout ? &working_timeout : NULL, error_string, error_code); if (n != -1) { goto connected; } + if (current) { + *current = SOCK_ERR; + } /* adjust timeout for next attempt */ #ifdef HAVE_GETTIMEOFDAY @@ -1131,6 +1162,10 @@ php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned sh } #endif + /* The next attempt's socket must not inherit this one's records */ + if (stream && stream->io_registrations) { + php_io_unregister_all(&stream->io_registrations); + } closesocket(sock); } sock = -1; @@ -1147,10 +1182,19 @@ php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short int *error_code, const char *bindto, unsigned short bindport, long sockopts ) { - return php_network_connect_socket_to_host_ex(host, port, socktype, asynchronous, timeout, + return php_network_connect_socket_to_host_stream(NULL, NULL, host, port, socktype, asynchronous, timeout, error_string, error_code, bindto, bindport, sockopts, NULL); } +php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsigned short port, + int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, + int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals + ) +{ + return php_network_connect_socket_to_host_stream(NULL, NULL, host, port, socktype, asynchronous, timeout, + error_string, error_code, bindto, bindport, sockopts, sockvals); +} + /* {{{ php_any_addr * Fills any (wildcard) address into php_sockaddr_storage */ @@ -1335,6 +1379,7 @@ PHPAPI php_stream *_php_stream_sock_open_from_socket(php_socket_t socket, const sock->timeout.tv_sec = FG(default_socket_timeout); sock->timeout.tv_usec = 0; sock->socket = socket; + php_netstream_set_nonblocking(sock); stream = php_stream_alloc_rel(&php_stream_generic_socket_ops, sock, persistent_id, "r+"); @@ -1397,6 +1442,40 @@ PHPAPI zend_result php_set_sock_blocking(php_socket_t socketd, bool block) return ret; } +PHPAPI void php_netstream_set_nonblocking(php_netstream_data_t *sock) +{ + if (sock->socket == SOCK_ERR) { + return; + } +#ifdef PHP_WIN32 + /* The mode of a socket cannot be queried, sockets start blocking */ + if (php_set_sock_blocking(sock->socket, false) == SUCCESS) { + sock->restore_blocking = true; + } +#else + sock->restore_pid = getpid(); + int flags = fcntl(sock->socket, F_GETFL); + if (flags == -1 || (flags & O_NONBLOCK)) { + return; + } + if (fcntl(sock->socket, F_SETFL, flags | O_NONBLOCK) == 0) { + sock->restore_blocking = true; + } +#endif +} + +PHPAPI void php_netstream_restore_blocking(php_netstream_data_t *sock) +{ + if (sock->restore_blocking && sock->socket != SOCK_ERR +#ifndef PHP_WIN32 + && sock->restore_pid == getpid() +#endif + ) { + php_set_sock_blocking(sock->socket, true); + } + sock->restore_blocking = false; +} + PHPAPI void _php_emit_fd_setsize_warning(int max_fd) { diff --git a/main/php_deadline.h b/main/php_deadline.h new file mode 100644 index 000000000000..fd1403a3401b --- /dev/null +++ b/main/php_deadline.h @@ -0,0 +1,86 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + */ + +#ifndef PHP_DEADLINE_H +#define PHP_DEADLINE_H + +#include + +#include +#include +#include "Zend/zend_hrtime.h" + +typedef struct php_deadline { + zend_hrtime_t hrtime; +} php_deadline; + +static inline bool php_deadline_is_infinite(php_deadline *deadline) +{ + return deadline->hrtime == (zend_hrtime_t)-1; +} + +static inline void php_deadline_init_infinite(php_deadline *deadline) +{ + deadline->hrtime = (zend_hrtime_t)-1; +} + +static inline void php_deadline_init_nonblock(php_deadline *deadline) +{ + deadline->hrtime = 0; +} + +/* timeout is relative to now */ +static inline void php_deadline_init(php_deadline *deadline, struct timeval *timeout) +{ + if (timeout == NULL || timeout->tv_sec == -1) { + php_deadline_init_infinite(deadline); + return; + } + + ZEND_ASSERT(timeout->tv_sec >= 0); + ZEND_ASSERT(timeout->tv_usec >= 0 && timeout->tv_usec <= 999999); + + deadline->hrtime = zend_hrtime(); + + if (UNEXPECTED((zend_hrtime_t)timeout->tv_sec + >= (ZEND_HRTIME_T_MAX - deadline->hrtime) / ZEND_NANO_IN_SEC)) { + php_deadline_init_infinite(deadline); + return; + } + + deadline->hrtime += timeout->tv_sec * ZEND_NANO_IN_SEC + timeout->tv_usec * 1000; +} + +/* For poll(2): -1 when infinite or beyond an int, 0 when passed */ +static inline int php_deadline_to_timeout_ms(php_deadline *deadline) +{ + if (deadline == NULL || php_deadline_is_infinite(deadline)) { + return -1; + } + + zend_hrtime_t now = zend_hrtime(); + + if (deadline->hrtime <= now) { + return 0; + } + + zend_hrtime_t remaining = deadline->hrtime - now; + const int nano_in_milli = 1000*1000; + + if (UNEXPECTED(remaining >= (zend_hrtime_t)INT_MAX * nano_in_milli)) { + return -1; + } + + return (remaining + nano_in_milli - 1) / nano_in_milli; +} + +#endif /* PHP_DEADLINE_H */ diff --git a/main/php_io.h b/main/php_io.h index 79bc245ef754..08d12494f19f 100644 --- a/main/php_io.h +++ b/main/php_io.h @@ -19,6 +19,8 @@ #include "php.h" #include "php_network.h" +#include "php_io_hooks.h" +#include "php_io_ring.h" #define PHP_IO_COPY_ALL SIZE_MAX @@ -36,6 +38,9 @@ typedef struct php_io_fd { php_io_fd_type fd_type; struct timeval timeout; unsigned is_blocked:1; + /* Windows: the position of a file opened overlapped, which the kernel keeps none for; the + * stream owns it and the copy moves it. NULL for a descriptor with a file position. */ + zend_off_t *position; } php_io_fd; typedef zend_result (*php_io_copy_fn)(php_io_fd *src, php_io_fd *dest, size_t maxlen, size_t *copied); diff --git a/main/php_io_hooks.h b/main/php_io_hooks.h new file mode 100644 index 000000000000..9a3395bdbe17 --- /dev/null +++ b/main/php_io_hooks.h @@ -0,0 +1,461 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_HOOKS_H +#define PHP_IO_HOOKS_H + +#include "php.h" +#include "php_network.h" +#include "php_streams.h" +#include "php_deadline.h" +#include "main/php_poll.h" + +BEGIN_EXTERN_C() + +/* Operations */ + +typedef enum { + PHP_IO_OP_POLL, + PHP_IO_OP_TIMER, + PHP_IO_OP_READ, + PHP_IO_OP_WRITE, + PHP_IO_OP_RECV, + PHP_IO_OP_SEND, + PHP_IO_OP_ACCEPT, + PHP_IO_OP_CONNECT, + PHP_IO_OP_GETADDRINFO, + PHP_IO_OP_GETNAMEINFO, + PHP_IO_OP_FSYNC, + PHP_IO_OP_WAITPID, + PHP_IO_OP_SIGWAIT, + PHP_IO_OP_ANY, +} php_io_op_type; + +/* READ/RECV: buf is the stream's read buffer, valid until the stream is drained */ +#define PHP_IO_OP_F_STREAM_BUF 0x01 +/* The wait follows a drain (EAGAIN, a short write): readiness recorded for an Edge pair since + * then may answer it. Set by the wrappers; a Poll op without it checks readiness at arm time. */ +#define PHP_IO_OP_F_AFTER_DRAIN 0x02 +/* POLL: the caller checked readiness at arm time (a zero-timeout poll() found none), so a queue + * need not check a registered pair again; never answered from recorded readiness */ +#define PHP_IO_OP_F_CHECKED 0x04 + +typedef enum { + PHP_IO_DONE, + PHP_IO_READY, + PHP_IO_TIMEOUT, + PHP_IO_INTERRUPTED, + PHP_IO_CANCELLED, + PHP_IO_UNSUPPORTED, +} php_io_status; + +typedef struct _php_io_op_result { + php_io_status status; + uint32_t index; /* ANY member */ + int64_t res; /* bytes, fd or revents */ + int error; /* errno or EAI_* */ +} php_io_op_result; + +typedef struct _php_io_op php_io_op; +typedef struct _php_io_queue php_io_queue; +typedef struct _php_io_registration php_io_registration; + +/* getaddrinfo() codes Windows lacks */ +#ifndef EAI_SYSTEM +# define EAI_SYSTEM EAI_FAIL +#endif +#ifndef EAI_OVERFLOW +# define EAI_OVERFLOW EAI_FAIL +#endif + +struct _php_io_op { + php_io_op_type type; + uint32_t flags; + zend_object *handle; /* NULL for TIMER, DNS, ANY and a stream op until php_io_op_get_handle() */ + uint32_t ready_events; + php_socket_t fd; + php_deadline deadline; + union { + struct { uint32_t events; } poll; + struct { void *buf; size_t len; int64_t offset; int flags; } io; + struct { struct sockaddr *addr; socklen_t *addrlen; } accept; + struct { const struct sockaddr *addr; socklen_t addrlen; } connect; + struct { + const char *node; + const char *service; + const struct addrinfo *hints; + struct addrinfo **res; + } getaddrinfo; + struct { + const struct sockaddr *addr; + socklen_t addrlen; + int flags; + char *host; + size_t hostlen; + char *service; + size_t servicelen; + } getnameinfo; + struct { bool data_only; } fsync; + /* fd is the pidfd where available */ + struct { pid_t pid; int options; int *status; } waitpid; + /* fd is the signalfd, taken a signal a handle recorded */ + struct { const php_sigset_t *set; php_siginfo_t *info; int taken; } sigwait; + struct { + php_io_op **ops; /* caller owned */ + uint32_t n; + php_io_op_result *results; + uint32_t n_results; + } any; + } u; + php_stream *stream; /* frozen for the op */ + php_io_registration *registration; /* the pair's record for a wait on a registered pair */ + zend_object *zobj; /* Io\Operation wrapper */ + void *provider_data; + php_io_queue *queue; /* set while submitted */ + void *queue_data; + bool in_flight; /* the backend references buf or addr */ +}; + +PHPAPI void php_io_op_poll(php_io_op *op, zend_object *handle, php_socket_t fd, uint32_t events, + php_deadline dl); + +/* A read whose bytes cannot be read again */ +static zend_always_inline bool php_io_op_read_advances(const php_io_op *op) +{ + return (op->type == PHP_IO_OP_READ || op->type == PHP_IO_OP_RECV) && op->u.io.offset < 0 + && !(op->type == PHP_IO_OP_RECV && (op->u.io.flags & MSG_PEEK)); +} + +/* Bytes a read took for nobody */ +PHPAPI void php_io_stream_keep_read(php_stream *stream, bool in_buffer, ssize_t res); +PHPAPI void php_io_op_timer(php_io_op *op, php_deadline dl); +PHPAPI void php_io_op_read(php_io_op *op, zend_object *handle, php_socket_t fd, void *buf, size_t len, + int64_t off, php_deadline dl); +PHPAPI void php_io_op_write(php_io_op *op, zend_object *handle, php_socket_t fd, const void *buf, + size_t len, int64_t off, php_deadline dl); +PHPAPI void php_io_op_recv(php_io_op *op, zend_object *handle, php_socket_t fd, void *buf, size_t len, + int flags, php_deadline dl); +PHPAPI void php_io_op_send(php_io_op *op, zend_object *handle, php_socket_t fd, const void *buf, + size_t len, int flags, php_deadline dl); +PHPAPI void php_io_op_accept(php_io_op *op, zend_object *handle, php_socket_t fd, + struct sockaddr *addr, socklen_t *addrlen, php_deadline dl); +PHPAPI void php_io_op_connect(php_io_op *op, zend_object *handle, php_socket_t fd, + const struct sockaddr *addr, socklen_t addrlen, php_deadline dl); +PHPAPI void php_io_op_getaddrinfo(php_io_op *op, const char *node, const char *service, + const struct addrinfo *hints, struct addrinfo **res, php_deadline dl); +PHPAPI void php_io_op_getnameinfo(php_io_op *op, const struct sockaddr *addr, socklen_t addrlen, + int flags, char *host, size_t hostlen, char *service, size_t servicelen, php_deadline dl); +PHPAPI void php_io_op_fsync(php_io_op *op, zend_object *handle, php_socket_t fd, bool data_only); +PHPAPI void php_io_op_waitpid(php_io_op *op, zend_object *handle, pid_t pid, int options, int *status, + php_deadline dl); +PHPAPI void php_io_op_sigwait(php_io_op *op, zend_object *handle, const php_sigset_t *set, + php_siginfo_t *info, php_deadline dl); +PHPAPI void php_io_op_any(php_io_op *op, php_io_op **members, uint32_t n, php_io_op_result *results); + +/* The op's handle, created for a stream op on the first call and kept by the stream: borrowed */ +PHPAPI zend_object *php_io_op_get_handle(php_io_op *op); + +/* Registrations: a (descriptor, event) pair whose waits repeat until the pair is removed */ + +typedef enum { + PHP_IO_TRIGGER_LEVEL, /* waits need readiness current at arm time */ + PHP_IO_TRIGGER_EDGE, /* waits follow a drain; recorded readiness may answer them */ +} php_io_trigger; + +/* Owned by the core, kept on the registrant (a stream, or the handle of a registrant without one), + * stable between the provider's add() and remove(). */ +struct _php_io_registration { + php_socket_t fd; + uint32_t event; /* PHP_POLL_READ or PHP_POLL_WRITE */ + php_io_trigger trigger; + php_stream *stream; /* the registrant, for a stream; NULL otherwise */ + php_poll_handle_object *handle; /* referenced; for a stream NULL until asked */ + zend_object *zobj; /* Io\Registration wrapper, created lazily, detached at unregister */ + void *provider_data; /* never read by the core */ + void *queue_data; /* the queue's record of the pair, never read by the core */ + uint64_t queue_id; /* the queue that set queue_data, never read by the core */ + uint32_t generation; /* the provider it was added to */ + bool in_add; /* the provider's add() is running */ + bool dead; /* unregistered inside add(), freed when it returns */ + php_io_registration *next; /* the registrant's list */ + php_io_registration *gprev; /* FG(io_registrations) */ + php_io_registration *gnext; +}; + +/* Idempotent per pair: the provider's add() runs at most once per pair and provider, and only + * when its flags carry the trigger's capability; an Edge pair a provider takes as Level only is + * registered as Level. NULL when the pair is not registered. The stream form takes the descriptor + * the wait is on, since a cast may touch the buffers, and creates no handle. */ +PHPAPI php_io_registration *php_io_register_stream(php_stream *stream, php_socket_t fd, uint32_t event, + php_io_trigger trigger); +PHPAPI php_io_registration *php_io_register_handle(php_poll_handle_object *handle, uint32_t event, + php_io_trigger trigger); +PHPAPI void php_io_unregister(php_io_registration *reg); +/* Every record of a registrant's list; before the descriptor closes */ +PHPAPI void php_io_unregister_all(php_io_registration **list); +/* The registrant's handle, created for a stream on the first call; borrowed */ +PHPAPI php_poll_handle_object *php_io_registration_get_handle(php_io_registration *reg); + +/* Invalidates the Io\Registration wrapper of a record that ended */ +PHPAPI extern void (*php_io_registration_zobj_detach)(zend_object *zobj); + +/* Hooks */ + +/* Regular file ops and Fsync go to the provider */ +#define PHP_IO_HOOKS_F_FILES 0x01 +/* Read, Write, Recv, Send and Connect are submitted without trying the syscall first */ +#define PHP_IO_HOOKS_F_DIRECT_DATA 0x02 +/* Accept is submitted without trying accept() first */ +#define PHP_IO_HOOKS_F_DIRECT_ACCEPT 0x04 +/* add() and remove() for Edge and for Level registrations */ +#define PHP_IO_HOOKS_F_EDGE_REGISTRATIONS 0x08 +#define PHP_IO_HOOKS_F_LEVEL_REGISTRATIONS 0x10 + +typedef struct _php_io_hooks php_io_hooks; + +typedef struct _php_io_hooks_ops { + /* May suspend; FAILURE means EG(exception) is set */ + zend_result (*run)(php_io_hooks *hooks, php_io_op *op, php_io_op_result *result); + /* Called only with the capability for the trigger; may be NULL without either */ + void (*add)(php_io_hooks *hooks, php_io_registration *reg); + void (*remove)(php_io_hooks *hooks, php_io_registration *reg); + /* Unregistered or replaced: its registrations end without remove() */ + void (*dtor)(php_io_hooks *hooks); +} php_io_hooks_ops; + +/* Embedded in the provider's own struct, which finds itself with ZEND_CONTAINER_OF */ +struct _php_io_hooks { + const php_io_hooks_ops *ops; + uint32_t flags; /* PHP_IO_HOOKS_F_* */ +}; + +/* NULL uninstalls. Fails if a provider is installed, and while its add, remove or dtor runs. */ +PHPAPI zend_result php_io_hooks_register(php_io_hooks *hooks); +PHPAPI php_io_hooks *php_io_hooks_current(void); +PHPAPI bool php_io_hooks_active(void); +PHPAPI void php_io_hooks_request_shutdown(void); +/* No set_hooks() and no fiber switch until unlocked */ +PHPAPI void php_io_hooks_lock(void); +PHPAPI void php_io_hooks_unlock(void); + +/* Invalidates the Io\Operation wrapper of an op that ended */ +PHPAPI extern void (*php_io_op_zobj_detach)(zend_object *zobj); + +/* Set by pcntl: a signal waits for its PHP handler */ +PHPAPI extern bool (*php_io_signal_pending)(void); +/* Whether a wait woken by a signal gives up rather than restarts */ +PHPAPI bool php_io_interrupt_pending(void); + +/* Without a provider it waits itself */ +PHPAPI zend_result php_io_run(php_io_op *op, php_io_op_result *result); + +/* The wrappers return like the syscall, with errno ETIMEDOUT or ECANCELED. stream may be NULL; a + * stream is frozen for the call. On Windows php_socket_errno() is the Winsock code, see + * PHP_IO_SOCK_*. */ +#ifdef PHP_WIN32 +# define PHP_IO_SOCK_ETIMEDOUT WSAETIMEDOUT +# define PHP_IO_SOCK_ECANCELED WSAECANCELLED +# define PHP_IO_SOCK_EINTR WSAEINTR +#else +# define PHP_IO_SOCK_ETIMEDOUT ETIMEDOUT +# define PHP_IO_SOCK_ECANCELED ECANCELED +# define PHP_IO_SOCK_EINTR EINTR +#endif + +/* revents, 0 on timeout, -1 on error */ +PHPAPI int php_io_poll(php_stream *stream, php_socket_t fd, uint32_t events, php_deadline *dl); +PHPAPI ssize_t php_io_recv(php_stream *stream, php_socket_t fd, void *buf, size_t len, int flags, + php_deadline *dl); +PHPAPI ssize_t php_io_send(php_stream *stream, php_socket_t fd, const void *buf, size_t len, int flags, + php_deadline *dl); +/* The _ex forms take the handle of a registrant that is not a stream (ext/sockets' Socket): the op + * carries it and a wait registers the pair on it. A stream call passes NULL. */ +PHPAPI int php_io_poll_ex(php_stream *stream, zend_object *handle, php_socket_t fd, uint32_t events, + php_deadline *dl, uint32_t op_flags); +PHPAPI ssize_t php_io_recv_ex(php_stream *stream, zend_object *handle, php_socket_t fd, void *buf, + size_t len, int flags, php_deadline *dl); +PHPAPI ssize_t php_io_send_ex(php_stream *stream, zend_object *handle, php_socket_t fd, const void *buf, + size_t len, int flags, php_deadline *dl); +PHPAPI php_socket_t php_io_accept_ex(php_stream *stream, zend_object *handle, php_socket_t fd, + struct sockaddr *addr, socklen_t *addrlen, php_deadline *dl); +PHPAPI int php_io_connect_ex(php_stream *stream, zend_object *handle, php_socket_t fd, + const struct sockaddr *addr, socklen_t addrlen, php_deadline *dl); +PHPAPI ssize_t php_io_sendto_ex(php_stream *stream, zend_object *handle, php_socket_t fd, + const void *buf, size_t len, int flags, const struct sockaddr *addr, socklen_t addrlen, + php_deadline *dl); +PHPAPI ssize_t php_io_recvfrom_ex(php_stream *stream, zend_object *handle, php_socket_t fd, void *buf, + size_t len, int flags, struct sockaddr *addr, socklen_t *addrlen, php_deadline *dl); +PHPAPI ssize_t php_io_read(php_stream *stream, int fd, void *buf, size_t len, php_deadline *dl); +PHPAPI ssize_t php_io_write(php_stream *stream, int fd, const void *buf, size_t len, php_deadline *dl); +/* At an explicit offset, for a Windows overlapped file; -1 is the current position */ +PHPAPI ssize_t php_io_read_at(php_stream *stream, int fd, void *buf, size_t len, int64_t offset, + php_deadline *dl); +PHPAPI ssize_t php_io_write_at(php_stream *stream, int fd, const void *buf, size_t len, int64_t offset, + php_deadline *dl); +PHPAPI php_socket_t php_io_accept(php_stream *stream, php_socket_t fd, struct sockaddr *addr, + socklen_t *addrlen, php_deadline *dl); +PHPAPI int php_io_connect(php_stream *stream, php_socket_t fd, const struct sockaddr *addr, + socklen_t addrlen, php_deadline *dl); +/* Poll and retry on EAGAIN; a NULL deadline is the plain syscall, addr NULL is send() and recv() */ +PHPAPI ssize_t php_io_sendto(php_stream *stream, php_socket_t fd, const void *buf, size_t len, + int flags, const struct sockaddr *addr, socklen_t addrlen, php_deadline *dl); +PHPAPI ssize_t php_io_recvfrom(php_stream *stream, php_socket_t fd, void *buf, size_t len, + int flags, struct sockaddr *addr, socklen_t *addrlen, php_deadline *dl); +PHPAPI int php_io_fsync(php_stream *stream, int fd, bool data_only); +/* Returns the EAI_* code; the list must be freed with php_io_freeaddrinfo() */ +PHPAPI int php_io_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, + struct addrinfo **res, php_deadline *dl); +PHPAPI void php_io_freeaddrinfo(struct addrinfo *res); +/* A provider's list: each entry one malloc() block with the address behind the addrinfo */ +PHPAPI void php_io_addrinfo_register(struct addrinfo *head); +PHPAPI void php_io_addrinfo_free_list(struct addrinfo *head); +PHPAPI int php_io_getnameinfo(const struct sockaddr *addr, socklen_t addrlen, int flags, char *host, + size_t hostlen, char *service, size_t servicelen, php_deadline *dl); +/* FAILURE when cancelled; interrupted may be NULL */ +PHPAPI zend_result php_io_sleep(php_deadline dl, bool *interrupted); +/* On Windows pid names any process, options are ignored, a non-blocking deadline asks without + * waiting and status receives the exit code */ +PHPAPI pid_t php_io_waitpid(zend_object *handle, pid_t pid, int *status, int options, + php_deadline *dl); +#ifndef PHP_WIN32 +/* A timed out wait fails with EAGAIN */ +PHPAPI int php_io_sigwait(zend_object *handle, const php_sigset_t *set, php_siginfo_t *info, + php_deadline *dl); +#endif + +/* Active php_io_run() frames */ +PHPAPI uint32_t php_io_ops_in_flight(void); + +/* pid as fork() returned it: the child drops what the parent's ring had */ +PHPAPI void php_io_child_forget(pid_t pid); + +/* A stream whose in-flight op a queue kept after its frame ended */ +PHPAPI void php_io_stream_orphan(php_stream *stream, php_io_queue *queue); +PHPAPI void php_io_stream_unfreeze(php_stream *stream); +PHPAPI void php_io_stream_drain(php_stream *stream); +PHPAPI void php_io_handle_orphan(zend_object *handle, php_io_queue *queue); +PHPAPI void php_io_handle_unfreeze(zend_object *handle); +PHPAPI bool php_io_handle_busy(zend_object *handle); +PHPAPI void php_io_handle_drain(zend_object *handle); +/* Frozen by a running op, not only by an orphan */ +PHPAPI bool php_io_stream_busy(php_stream *stream); + +/* NULL or a negative tv_sec means no timeout */ + +static inline php_deadline php_io_deadline_from_timeval(const struct timeval *tv) +{ + php_deadline dl; + if (tv == NULL || tv->tv_sec < 0) { + php_deadline_init_infinite(&dl); + } else if (tv->tv_usec < 0 || tv->tv_usec > 999999) { + /* Out of range microseconds count against the seconds, never below zero */ + struct timeval norm = { + .tv_sec = tv->tv_sec + tv->tv_usec / 1000000, + .tv_usec = tv->tv_usec % 1000000 + }; + if (norm.tv_usec < 0) { + norm.tv_usec += 1000000; + norm.tv_sec--; + } + if (norm.tv_sec < 0) { + norm.tv_sec = norm.tv_usec = 0; + } + php_deadline_init(&dl, &norm); + } else { + php_deadline_init(&dl, (struct timeval *) tv); + } + return dl; +} + +static inline int php_io_poll_tv(php_stream *stream, php_socket_t fd, uint32_t events, + const struct timeval *tv) +{ + php_deadline dl = php_io_deadline_from_timeval(tv); + return php_io_poll(stream, fd, events, &dl); +} + +/* Operation queues */ + +typedef struct _php_io_queue_completion { + php_io_op *op; + void *data; + php_io_op_result result; +} php_io_queue_completion; + +typedef struct _php_io_queue_ops { + zend_result (*submit)(php_io_queue *q, php_io_op *op, void *data); + /* An op (not an Any) the queue completed at submit: taken out of the completions to deliver, + * so the caller needs no wait for it. False when the op is in flight. */ + bool (*take_inline)(php_io_queue *q, php_io_op *op, php_io_queue_completion *out); + zend_result (*cancel)(php_io_queue *q, php_io_op *op); + /* Bracket a registration; what the queue retains for the pair goes to reg->queue_data */ + zend_result (*add)(php_io_queue *q, php_io_registration *reg); + void (*remove)(php_io_queue *q, php_io_registration *reg); + /* NULL waits for good; the non-blocking deadline is one reap that never blocks */ + int (*wait)(php_io_queue *q, php_io_queue_completion *out, uint32_t max, const php_deadline *dl); + void (*orphan)(php_io_queue *q, php_io_op *op); + /* May be NULL when orphan() never keeps an op in flight */ + void (*drain)(php_io_queue *q, const void *owner); + uint32_t (*count_pending)(php_io_queue *q); + uint32_t (*hook_flags)(php_io_queue *q); + void (*destroy)(php_io_queue *q); +} php_io_queue_ops; + +struct _php_io_queue { + const php_io_queue_ops *ops; +}; + +PHPAPI php_io_queue *php_io_queue_create_poll(php_poll_backend_type backend); +/* Never repeats within a thread: a queue tells its own record on a registration from one a queue + * before it left there */ +PHPAPI uint64_t php_io_queue_new_id(void); + +static inline php_deadline php_io_deadline_from_ms(zend_long ms) +{ + php_deadline dl; + if (ms < 0) { + php_deadline_init_infinite(&dl); + } else { + struct timeval tv = { .tv_sec = ms / 1000, .tv_usec = (ms % 1000) * 1000 }; + php_deadline_init(&dl, &tv); + } + return dl; +} + +static inline php_deadline php_io_deadline_from_ns(zend_hrtime_t ns) +{ + php_deadline dl; + dl.hrtime = zend_hrtime(); + if (ns >= ZEND_HRTIME_T_MAX - dl.hrtime) { + php_deadline_init_infinite(&dl); + } else { + dl.hrtime += ns; + } + return dl; +} + +static inline php_deadline php_io_deadline_infinite(void) +{ + php_deadline dl; + php_deadline_init_infinite(&dl); + return dl; +} + +/* Only for finite deadlines */ +static inline zend_hrtime_t php_io_deadline_remaining(const php_deadline *dl, zend_hrtime_t now) +{ + return dl->hrtime > now ? dl->hrtime - now : 0; +} + +END_EXTERN_C() + +#endif /* PHP_IO_HOOKS_H */ diff --git a/main/php_io_ring.h b/main/php_io_ring.h new file mode 100644 index 000000000000..949484e6839a --- /dev/null +++ b/main/php_io_ring.h @@ -0,0 +1,83 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ +*/ + +#ifndef PHP_IO_RING_H +#define PHP_IO_RING_H + +#include "php.h" +#include "main/php_io_hooks.h" + +#ifdef HAVE_IOR + +BEGIN_EXTERN_C() + +/* A PHPAPI wrapper over ior */ + +typedef struct php_io_ring php_io_ring; + +/* The core's queue depth, and Io\Ring\Engine's for 0 */ +#define PHP_IO_RING_DEFAULT_ENTRIES 256 + +/* Keep in sync with Io\Ring\Backend */ +typedef enum php_io_ring_backend_type { + PHP_IO_RING_BACKEND_IO_URING, + PHP_IO_RING_BACKEND_IOCP, + PHP_IO_RING_BACKEND_THREADS, +} php_io_ring_backend_type; + +/* fd_nonblock: every submitted descriptor is non-blocking */ +PHPAPI php_io_ring *php_io_ring_create(uint32_t entries, bool fd_nonblock); +PHPAPI void php_io_ring_destroy(php_io_ring *ring); + +/* After it returns the op is completed or cancellable */ +PHPAPI zend_result php_io_ring_submit_op(php_io_ring *ring, php_io_op *op, void *data); +/* An op completed at submit (an accept from the buffer, a wait answered from the ready bits) */ +PHPAPI bool php_io_ring_take_inline(php_io_ring *ring, php_io_op *op, php_io_queue_completion *out); +PHPAPI zend_result php_io_ring_cancel(php_io_ring *ring, php_io_op *op); +/* Cancels the op silently; true when it stays in flight and its stream must stay frozen */ +PHPAPI bool php_io_ring_orphan(php_io_ring *ring, php_io_op *op); +PHPAPI void php_io_ring_drain(php_io_ring *ring, const void *owner); + +/* dl NULL waits for good; with the non-blocking deadline it first clears the notification + * descriptor and reaps once */ +PHPAPI int php_io_ring_wait(php_io_ring *ring, php_io_queue_completion *out, uint32_t max, + const php_deadline *dl); +PHPAPI uint32_t php_io_ring_count_pending(php_io_ring *ring); + +PHPAPI php_socket_t php_io_ring_notify_fd(php_io_ring *ring); +PHPAPI void php_io_ring_notify_clear(php_io_ring *ring); +PHPAPI uint32_t php_io_ring_features(php_io_ring *ring); +PHPAPI php_io_ring_backend_type php_io_ring_get_backend_type(php_io_ring *ring); +PHPAPI const char *php_io_ring_backend_name(php_io_ring *ring); +/* PHP_IO_HOOKS_F_* a provider on this ring should register with by default */ +PHPAPI uint32_t php_io_ring_hook_flags(php_io_ring *ring); +/* PHP_IO_HOOKS_F_* this ring can serve, for a provider that opts in */ +PHPAPI uint32_t php_io_ring_supported_hook_flags(php_io_ring *ring); + +/* Edge registrations; Level ones never reach the ring */ +PHPAPI zend_result php_io_ring_add(php_io_ring *ring, php_io_registration *reg); +PHPAPI void php_io_ring_remove(php_io_ring *ring, php_io_registration *reg); + +/* Created by another process: every operation fails */ +PHPAPI bool php_io_ring_inherited(php_io_ring *ring); +/* In a forked child: closes the descriptors of every ring of this thread */ +PHPAPI void php_io_ring_after_fork(void); + +PHPAPI php_io_queue *php_io_queue_create_ring(uint32_t entries); +/* Only for a queue created by php_io_queue_create_ring() */ +PHPAPI php_io_ring *php_io_queue_ring(php_io_queue *q); + +END_EXTERN_C() + +#endif /* HAVE_IOR */ + +#endif /* PHP_IO_RING_H */ diff --git a/main/php_network.h b/main/php_network.h index c93a519911f0..b9e4681f5a04 100644 --- a/main/php_network.h +++ b/main/php_network.h @@ -297,6 +297,14 @@ PHPAPI php_socket_t php_network_connect_socket_to_host_ex(const char *host, unsi int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals ); +/* The *_stream variants take the socket's stream, or NULL, for the IO hooks. current, if not NULL, + * holds the socket while the connect waits. */ +PHPAPI php_socket_t php_network_connect_socket_to_host_stream(php_stream *stream, php_socket_t *current, + const char *host, unsigned short port, + int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, + int *error_code, const char *bindto, unsigned short bindport, long sockopts, php_sockvals *sockvals + ); + PHPAPI php_socket_t php_network_connect_socket_to_host(const char *host, unsigned short port, int socktype, int asynchronous, struct timeval *timeout, zend_string **error_string, int *error_code, const char *bindto, unsigned short bindport, long sockopts @@ -310,6 +318,15 @@ PHPAPI int php_network_connect_socket(php_socket_t sockfd, zend_string **error_string, int *error_code); +PHPAPI int php_network_connect_socket_stream(php_stream *stream, + php_socket_t sockfd, + const struct sockaddr *addr, + socklen_t addrlen, + int asynchronous, + struct timeval *timeout, + zend_string **error_string, + int *error_code); + #define php_connect_nonb(sock, addr, addrlen, timeout) \ php_network_connect_socket((sock), (addr), (addrlen), 0, (timeout), NULL, NULL) @@ -341,6 +358,28 @@ PHPAPI php_socket_t php_network_accept_incoming(php_socket_t srvsock, int tcp_nodelay ); +PHPAPI php_socket_t php_network_accept_incoming_stream_ex(php_stream *stream, + php_socket_t srvsock, + zend_string **textaddr, + struct sockaddr **addr, + socklen_t *addrlen, + struct timeval *timeout, + zend_string **error_string, + int *error_code, + php_sockvals *sockvals + ); + +PHPAPI php_socket_t php_network_accept_incoming_stream(php_stream *stream, + php_socket_t srvsock, + zend_string **textaddr, + struct sockaddr **addr, + socklen_t *addrlen, + struct timeval *timeout, + zend_string **error_string, + int *error_code, + int tcp_nodelay + ); + PHPAPI int php_network_get_sock_name(php_socket_t sock, zend_string **textaddr, struct sockaddr **addr, @@ -363,8 +402,19 @@ struct _php_netstream_data_t { bool timeout_event; struct timeval timeout; size_t ownsize; + /* the descriptor was blocking before the stream made it non-blocking */ + bool restore_blocking; +#ifndef PHP_WIN32 + /* the process that changed the mode; a forked child leaves it alone */ + pid_t restore_pid; +#endif }; typedef struct _php_netstream_data_t php_netstream_data_t; + +BEGIN_EXTERN_C() +PHPAPI void php_netstream_set_nonblocking(php_netstream_data_t *sock); +PHPAPI void php_netstream_restore_blocking(php_netstream_data_t *sock); +END_EXTERN_C() PHPAPI extern const php_stream_ops php_stream_socket_ops; extern const php_stream_ops php_stream_generic_socket_ops; #define PHP_STREAM_IS_SOCKET (&php_stream_socket_ops) diff --git a/main/php_poll.h b/main/php_poll.h index 9b878d44bd20..e1696daae9dd 100644 --- a/main/php_poll.h +++ b/main/php_poll.h @@ -15,10 +15,17 @@ #ifndef PHP_POLL_H #define PHP_POLL_H +#include "Zend/zend_hrtime.h" + #include "php.h" #include "php_network.h" #include +#include +#include +#ifndef PHP_WIN32 +# include +#endif /* ----- Public generic API ----- */ @@ -32,6 +39,11 @@ #define PHP_POLL_RDHUP 0x10 #define PHP_POLL_ONESHOT 0x20 #define PHP_POLL_ET 0x40 /* Edge-triggered */ +#define PHP_POLL_PRI 0x80 /* Priority data, backend dependent */ +#define PHP_POLL_TIMER 0x100 +#define PHP_POLL_NOTIFY 0x200 +#define PHP_POLL_SIGNAL 0x400 +#define PHP_POLL_PROCESS 0x800 /* The child exited */ /* Poll flags */ #define PHP_POLL_FLAG_PERSISTENT 0x01 @@ -94,6 +106,10 @@ typedef struct php_poll_event php_poll_event; PHPAPI bool php_poll_is_backend_available(php_poll_backend_type backend); PHPAPI bool php_poll_backend_supports_edge_triggering(php_poll_backend_type backend); +PHPAPI bool php_poll_backend_supports_priority(php_poll_backend_type backend); +/* Whether ProcessHandle and SignalHandle have a native source */ +PHPAPI bool php_poll_backend_supports_process_handles(php_poll_backend_type backend); +PHPAPI bool php_poll_backend_supports_signal_handles(php_poll_backend_type backend); PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, uint32_t flags); PHPAPI php_poll_ctx *php_poll_create_by_name(const char *preferred_backend, uint32_t flags); @@ -109,9 +125,86 @@ PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd); PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, const struct timespec *timeout); +/* Timers are reported by php_poll_wait() as events with fd -1 and PHP_POLL_TIMER, before the + * descriptor events. Deadlines are zend_hrtime() values. A one-shot timer (period 0) stays + * registered but disarmed after it fired; a periodic one re-arms from its previous deadline. */ +typedef struct php_poll_timer php_poll_timer; + +PHPAPI php_poll_timer *php_poll_timer_add(php_poll_ctx *ctx, zend_hrtime_t deadline, + zend_hrtime_t period, void *data); +PHPAPI zend_result php_poll_timer_modify(php_poll_ctx *ctx, php_poll_timer *timer, + zend_hrtime_t deadline, zend_hrtime_t period, void *data); +PHPAPI void php_poll_timer_remove(php_poll_ctx *ctx, php_poll_timer *timer); +/* Armed timers */ +PHPAPI uint32_t php_poll_timer_count(php_poll_ctx *ctx); + +/* On Windows a set is a bit per CRT signal number, like ior_sigset_t */ +#ifdef PHP_WIN32 +typedef struct php_sigset_t { uint32_t bits; } php_sigset_t; +typedef struct php_siginfo_t { int si_signo; int si_code; } php_siginfo_t; +# define PHP_NSIG NSIG +static zend_always_inline int php_sigemptyset(php_sigset_t *set) +{ + set->bits = 0; + return 0; +} +static zend_always_inline int php_sigaddset(php_sigset_t *set, int signo) +{ + if (signo < 1 || signo >= 32) { + _set_errno(EINVAL); + return -1; + } + set->bits |= 1u << signo; + return 0; +} +static zend_always_inline int php_sigdelset(php_sigset_t *set, int signo) +{ + if (signo < 1 || signo >= 32) { + _set_errno(EINVAL); + return -1; + } + set->bits &= ~(1u << signo); + return 0; +} +static zend_always_inline int php_sigismember(const php_sigset_t *set, int signo) +{ + if (signo < 1 || signo >= 32) { + _set_errno(EINVAL); + return -1; + } + return (set->bits >> signo) & 1; +} +#else +typedef sigset_t php_sigset_t; +typedef siginfo_t php_siginfo_t; +# define PHP_NSIG NSIG +# define php_sigemptyset sigemptyset +# define php_sigaddset sigaddset +# define php_sigdelset sigdelset +# define php_sigismember sigismember +#endif + +#ifndef PHP_WIN32 +/* A descriptor readable when the child exited or a signal of the set is pending: a pidfd or a + * signalfd, or a private kqueue. -1 with ENOSYS where there is none, ESRCH for a process that is + * not a waitable child. It neither reaps the child nor consumes the signal. */ +PHPAPI bool php_poll_has_process_source(void); +PHPAPI bool php_poll_has_signal_source(void); +PHPAPI int php_poll_process_source_open(pid_t pid); +/* Whether the child exited, without collecting it: its pid with the waitpid() status, 0 while it + * runs (or is stopped), -1 with errno, ECHILD for a process that is not a waitable child */ +PHPAPI pid_t php_poll_process_exit_probe(pid_t pid, int *status); +PHPAPI int php_poll_signal_source_open(const sigset_t *set); +/* Never waits, 0 when nothing is pending; fd -1 takes from the pending set */ +PHPAPI int php_poll_signal_source_take(int fd, const sigset_t *set, siginfo_t *info); +/* Never waits, 0 when nothing is pending */ +PHPAPI int php_poll_signal_take_pending(const sigset_t *set, siginfo_t *info); +#endif + PHPAPI const char *php_poll_backend_name(php_poll_ctx *ctx); PHPAPI php_poll_backend_type php_poll_get_backend_type(php_poll_ctx *ctx); PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx); +PHPAPI bool php_poll_supports_priority(php_poll_ctx *ctx); PHPAPI php_poll_error php_poll_get_error(php_poll_ctx *ctx); /* Get suitable max_events for backend */ @@ -132,29 +225,56 @@ typedef struct php_poll_handle_object php_poll_handle_object; struct php_poll_handle_ops { /** * Get file descriptor for this handle - * @param handle The handle object * @return File descriptor or SOCK_ERR if invalid/not applicable */ php_socket_t (*get_fd)(php_poll_handle_object *handle); /** * Check if handle is still valid - * @param handle The handle object * @return true if valid, false if invalid */ int (*is_valid)(php_poll_handle_object *handle); /** * Cleanup handle-specific data - * @param handle The handle object */ void (*cleanup)(php_poll_handle_object *handle); + + /** + * The event a NOTIFY, SIGNAL or PROCESS handle reports, watched as READ on its descriptor. + * Zero for descriptor handles. + */ + uint32_t event; + + /** + * Consumes the source of such a handle once it is readable. May be NULL. + * @return whether the event is reported + */ + bool (*fired)(php_poll_handle_object *handle); + + /** + * Called when a context starts watching the handle. May be NULL. + */ + void (*added)(php_poll_handle_object *handle); + + /** + * Called when a context stops watching the handle, also when it is destroyed with the handle + * in it. May be NULL. + */ + void (*removed)(php_poll_handle_object *handle); }; +/* A script may reach the resource through the handle: set by the userland factory, or by the core + * for a stream a script holds; sticky */ +#define PHP_POLL_HANDLE_F_EXPOSED 0x01 + /* Base poll handle object structure */ struct php_poll_handle_object { php_poll_handle_ops *ops; void *handle_data; + HashTable *watching; /* context key -> watcher, not refcounted */ + uint32_t flags; /* PHP_POLL_HANDLE_F_* */ + struct _php_io_registration *registrations; /* IO hooks registrations on this handle */ zend_object std; }; diff --git a/main/php_streams.h b/main/php_streams.h index 0111a6a78413..274e0d7ac04f 100644 --- a/main/php_streams.h +++ b/main/php_streams.h @@ -191,6 +191,15 @@ struct _php_stream_wrapper { #define PHP_STREAM_FLAG_NO_IO 0x400 +/* An operation on the stream is in flight: passing it to a function throws */ +#define PHP_STREAM_FLAG_IN_USE 0x800 + +/* the IO hooks register no pair for it: every wait stays one-shot */ +#define PHP_STREAM_FLAG_NO_IO_REGISTRATION 0x1000 + +/* Set by php_stream_mark_read_lost() */ +#define PHP_STREAM_FLAG_READ_LOST 0x2000 + #define PHP_STREAM_FLAG_WAS_WRITTEN 0x80000000 struct _php_stream { @@ -207,6 +216,7 @@ struct _php_stream { uint16_t in_free:2; /* to prevent recursion during free */ uint16_t eof:1; uint16_t __exposed:1; /* non-zero if exposed as a zval somewhere */ + uint16_t userland:1; /* handed to a script as a resource (php_stream_to_zval) */ /* so we know how to clean it up correctly. This should be set to * PHP_STREAM_FCLOSE_XXX as appropriate */ @@ -249,9 +259,14 @@ struct _php_stream { struct _php_stream *enclosing_stream; /* this is a private stream owned by enclosing_stream */ + /* StreamPollWeakHandle singleton, referenced by the stream until php_stream_free() */ + zend_object *weak_poll_handle; + zend_llist *error_list; HashTable *poll_watchers; /* Io\Poll watchers notified before the stream is closed */ + + struct _php_io_registration *io_registrations; /* IO hooks registrations, ended before the close */ }; /* php_stream */ #define PHP_STREAM_CONTEXT(stream) \ @@ -275,7 +290,9 @@ END_EXTERN_C() /* use this to assign the stream to a zval and tell the stream that is * has been exported to the engine; it will expect to be closed automatically * when the resources are auto-destructed */ -#define php_stream_to_zval(stream, zval) { ZVAL_RES(zval, (stream)->res); (stream)->__exposed = 1; } +/* the stream is in a script's hands: a StreamPollWeakHandle may hand it back */ +#define php_stream_expose(stream) { (stream)->__exposed = 1; (stream)->userland = 1; } +#define php_stream_to_zval(stream, zval) { ZVAL_RES(zval, (stream)->res); php_stream_expose(stream); } #define php_stream_from_zval(xstr, pzval) do { \ if (((xstr) = (php_stream*)zend_fetch_resource2_ex((pzval), \ @@ -310,6 +327,10 @@ static zend_always_inline bool php_stream_zend_parse_arg_into_stream( * as we want to be able to specify the argument number in the type error */ if (EXPECTED(res->type == php_file_le_stream() || res->type == php_file_le_pstream())) { *destination_stream_ptr = (php_stream*)res->ptr; + if (UNEXPECTED((*destination_stream_ptr)->flags & PHP_STREAM_FLAG_IN_USE)) { + zend_throw_error(NULL, "Concurrent access to a stream"); + return false; + } return true; } else { zend_argument_type_error(arg_num, "must be an open stream resource"); @@ -373,6 +394,18 @@ PHPAPI ssize_t php_stream_write(php_stream *stream, const char *buf, size_t coun PHPAPI zend_result php_stream_fill_read_buffer(php_stream *stream, size_t size); +/* Where the next buffered read lands */ +static zend_always_inline unsigned char *php_stream_read_buffer_tail(php_stream *stream) +{ + return stream->readbuf ? stream->readbuf + stream->writepos : NULL; +} +/* Bytes a read placed at the tail; 0 is EOF */ +PHPAPI void php_stream_read_buffer_commit(php_stream *stream, size_t len); + +/* Bytes were lost: every later read fails */ +#define php_stream_mark_read_lost(stream) ((stream)->flags |= PHP_STREAM_FLAG_READ_LOST) +#define php_stream_is_read_lost(stream) (((stream)->flags & PHP_STREAM_FLAG_READ_LOST) != 0) + PHPAPI ssize_t php_stream_printf(php_stream *stream, const char *fmt, ...) PHP_ATTRIBUTE_FORMAT(printf, 2, 3); PHPAPI bool php_stream_eof(php_stream *stream); @@ -536,6 +569,8 @@ END_EXTERN_C() #define PHP_STREAM_AS_FD_FOR_SELECT 3 /* cast as fd/socket for copy purposes */ #define PHP_STREAM_AS_FD_FOR_COPY 4 +/* cast as fd/socket to watch for readiness: the descriptor and nothing else, buffers untouched */ +#define PHP_STREAM_AS_FD_FOR_POLL 5 /* try really, really hard to make sure the cast happens (avoid using this flag if possible) */ #define PHP_STREAM_CAST_TRY_HARD 0x80000000 diff --git a/main/poll/php_poll_internal.h b/main/poll/php_poll_internal.h index 21b85575409d..f5a15e612f29 100644 --- a/main/poll/php_poll_internal.h +++ b/main/poll/php_poll_internal.h @@ -57,8 +57,18 @@ typedef struct php_poll_backend_ops { /* Backend supports edge triggering natively */ bool supports_et; + + /* Backend reports priority data (POLLPRI) */ + bool supports_priority; } php_poll_backend_ops; +struct php_poll_timer { + zend_hrtime_t deadline; + zend_hrtime_t period; + void *data; + uint32_t heap_idx; /* UINT32_MAX while disarmed */ +}; + /* Main poll context */ struct php_poll_ctx { const php_poll_backend_ops *backend_ops; @@ -75,6 +85,14 @@ struct php_poll_ctx { /* Backend-specific data */ void *backend_data; + + /* A forked child must not touch the backend it shares with the parent */ + pid_t owner_pid; + + /* Deadline heap of the armed timers */ + php_poll_timer **timers; + uint32_t timer_count; + uint32_t timer_cap; }; /* Generic FD entry structure */ @@ -158,6 +176,18 @@ static inline void php_poll_set_error(php_poll_ctx *ctx, php_poll_error error) ctx->last_error = error; } +/* Nanoseconds of a relative timeout, saturating instead of wrapping */ +static inline zend_hrtime_t php_poll_timespec_to_ns(const struct timespec *timeout) +{ + if (timeout == NULL || timeout->tv_sec < 0) { + return ZEND_HRTIME_T_MAX; + } + if ((zend_hrtime_t) timeout->tv_sec >= ZEND_HRTIME_T_MAX / ZEND_NANO_IN_SEC) { + return ZEND_HRTIME_T_MAX; + } + return (zend_hrtime_t) timeout->tv_sec * ZEND_NANO_IN_SEC + (zend_hrtime_t) timeout->tv_nsec; +} + static inline int php_poll_timespec_to_ms(const struct timespec *timeout) { if (timeout == NULL) { diff --git a/main/poll/poll_backend_epoll.c b/main/poll/poll_backend_epoll.c index ef8d42435ec6..fcaf3105d7ed 100644 --- a/main/poll/poll_backend_epoll.c +++ b/main/poll/poll_backend_epoll.c @@ -54,6 +54,9 @@ static uint32_t epoll_events_to_native(uint32_t events) if (events & PHP_POLL_ET) { native |= EPOLLET; } + if (events & PHP_POLL_PRI) { + native |= EPOLLPRI; + } return native; } @@ -75,6 +78,9 @@ static uint32_t epoll_events_from_native(uint32_t native) if (native & EPOLLRDHUP) { events |= PHP_POLL_RDHUP; } + if (native & EPOLLPRI) { + events |= PHP_POLL_PRI; + } return events; } @@ -263,6 +269,7 @@ const php_poll_backend_ops php_poll_backend_epoll_ops = { .is_available = epoll_backend_is_available, .get_suitable_max_events = epoll_backend_get_suitable_max_events, .supports_et = true, + .supports_priority = true, }; #endif /* HAVE_EPOLL */ diff --git a/main/poll/poll_backend_eventport.c b/main/poll/poll_backend_eventport.c index 35e7c61326f1..c65d6a32128a 100644 --- a/main/poll/poll_backend_eventport.c +++ b/main/poll/poll_backend_eventport.c @@ -52,6 +52,9 @@ static int eventport_events_to_native(uint32_t events) if (events & PHP_POLL_RDHUP) { native |= POLLHUP; /* Map RDHUP to HUP */ } + if (events & PHP_POLL_PRI) { + native |= POLLPRI; + } return native; } @@ -74,6 +77,9 @@ static uint32_t eventport_events_from_native(int native) if (native & POLLNVAL) { events |= PHP_POLL_ERROR; } + if (native & POLLPRI) { + events |= PHP_POLL_PRI; + } return events; } @@ -223,10 +229,10 @@ static bool eventport_associate_callback(int fd, php_poll_fd_entry *entry, void { eventport_associate_ctx *assoc_ctx = (eventport_associate_ctx *) user_data; - /* Only associate if marked as needing association */ - if (entry->last_revents == EVENTPORT_NEEDS_ASSOC) { - int native_events = eventport_events_to_native(entry->events); - + /* Only associate if marked as needing association; with no events the + * port would still report hangups and errors, so it waits for a modify */ + int native_events = eventport_events_to_native(entry->events); + if (entry->last_revents == EVENTPORT_NEEDS_ASSOC && native_events != 0) { if (port_associate(assoc_ctx->backend_data->port_fd, PORT_SOURCE_FD, fd, native_events, entry->data) == -1) { @@ -397,7 +403,8 @@ const php_poll_backend_ops php_poll_backend_eventport_ops = { .wait = eventport_backend_wait, .is_available = eventport_backend_is_available, .get_suitable_max_events = eventport_backend_get_suitable_max_events, - .supports_et = false /* Event ports are level-triggered only */ + .supports_et = false, /* Event ports are level-triggered only */ + .supports_priority = true }; #endif /* HAVE_EVENT_PORTS */ diff --git a/main/poll/poll_backend_kqueue.c b/main/poll/poll_backend_kqueue.c index f2c0212f4d15..4537b171fc38 100644 --- a/main/poll/poll_backend_kqueue.c +++ b/main/poll/poll_backend_kqueue.c @@ -97,6 +97,18 @@ static void kqueue_backend_cleanup(php_poll_ctx *ctx) } } +/* kqueue has no filter for hangups and errors alone: they come as EV_EOF + * and EV_ERROR on the read filter, watched edge-triggered so that unread + * data nobody asked about does not report it over and over */ +static uint32_t kqueue_filter_events(uint32_t events) +{ + if (!(events & (PHP_POLL_READ | PHP_POLL_WRITE)) + && (events & (PHP_POLL_HUP | PHP_POLL_ERROR | PHP_POLL_RDHUP))) { + events |= PHP_POLL_READ | PHP_POLL_ET; + } + return events; +} + static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { kqueue_backend_data_t *backend_data = (kqueue_backend_data_t *) ctx->backend_data; @@ -112,6 +124,7 @@ static zend_result kqueue_backend_add(php_poll_ctx *ctx, int fd, uint32_t events struct kevent changes[2]; /* Max 2 changes: read + write */ int change_count = 0; + events = kqueue_filter_events(events); uint16_t flags = EV_ADD | EV_ENABLE; if (events & PHP_POLL_ONESHOT) { @@ -170,6 +183,7 @@ static zend_result kqueue_backend_modify(php_poll_ctx *ctx, int fd, uint32_t eve int delete_count = 0; int add_count = 0; int successful_deletes = 0; + events = kqueue_filter_events(events); uint16_t add_flags = EV_ADD | EV_ENABLE; if (events & PHP_POLL_ONESHOT) { @@ -264,6 +278,12 @@ static zend_result kqueue_backend_remove(php_poll_ctx *ctx, int fd) int result = kevent(backend_data->kqueue_fd, &change, 1, NULL, 0, NULL); if (result == 0) { successful_deletes++; + } else if (errno == EBADF) { + /* Closed while registered: close() already dropped its filters */ + if (!ctx->raw_events && zend_hash_index_del(backend_data->fd_tracking, fd) == SUCCESS) { + backend_data->fd_count--; + } + return SUCCESS; } else if (!php_poll_is_not_found_error()) { php_poll_set_current_errno_error(ctx); return FAILURE; @@ -514,7 +534,8 @@ const php_poll_backend_ops php_poll_backend_kqueue_ops = { .wait = kqueue_backend_wait, .is_available = kqueue_backend_is_available, .get_suitable_max_events = kqueue_backend_get_suitable_max_events, - .supports_et = true + .supports_et = true, + .supports_priority = false }; diff --git a/main/poll/poll_backend_poll.c b/main/poll/poll_backend_poll.c index 05a51a04d31d..c1b864b7fce7 100644 --- a/main/poll/poll_backend_poll.c +++ b/main/poll/poll_backend_poll.c @@ -37,6 +37,9 @@ static uint32_t poll_events_to_native(uint32_t events) if (events & PHP_POLL_HUP) { native |= POLLHUP; } + if (events & PHP_POLL_PRI) { + native |= POLLPRI; + } return native; } @@ -58,6 +61,9 @@ static uint32_t poll_events_from_native(uint32_t native) if (native & POLLNVAL) { events |= PHP_POLL_ERROR; /* Map invalid FD to error */ } + if (native & POLLPRI) { + events |= PHP_POLL_PRI; + } return events; } @@ -172,6 +178,11 @@ static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user { poll_build_context *ctx = (poll_build_context *) user_data; + /* A fired one-shot registration stays disarmed until it is modified */ + if (entry->events == 0) { + return true; + } + ctx->fds[ctx->index].fd = fd; ctx->fds[ctx->index].events = poll_events_to_native(entry->events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); @@ -181,6 +192,18 @@ static bool poll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *user return true; } +/* Nothing armed: wait out the timeout the way poll(2) does with an empty + * set, so a signal interrupts it and no timeout blocks like the other + * backends do */ +static int poll_backend_wait_nothing(php_poll_ctx *ctx, const struct timespec *timeout) +{ + if (poll(NULL, 0, php_poll_timespec_to_ms(timeout)) < 0) { + php_poll_set_current_errno_error(ctx); + return -1; + } + return 0; +} + static int poll_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, const struct timespec *timeout) @@ -189,10 +212,7 @@ static int poll_backend_wait( int fd_count = php_poll_fd_table_count(backend_data->fd_table); if (fd_count == 0) { - if (timeout != NULL && (timeout->tv_sec > 0 || timeout->tv_nsec > 0)) { - nanosleep(timeout, NULL); - } - return 0; + return poll_backend_wait_nothing(ctx, timeout); } /* Ensure temp_fds array is large enough */ @@ -210,6 +230,10 @@ static int poll_backend_wait( /* Build struct pollfd array from fd_table */ poll_build_context build_ctx = { .fds = backend_data->temp_fds, .index = 0 }; php_poll_fd_table_foreach(backend_data->fd_table, poll_build_fds_callback, &build_ctx); + fd_count = build_ctx.index; + if (fd_count == 0) { + return poll_backend_wait_nothing(ctx, timeout); + } /* Convert timespec to milliseconds (poll() only supports ms resolution) */ int timeout_ms = php_poll_timespec_to_ms(timeout); @@ -230,26 +254,26 @@ static int poll_backend_wait( if (pfd->revents != 0) { php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, pfd->fd); if (entry) { - /* Handle POLLNVAL by automatically removing the invalid FD */ - if (pfd->revents & POLLNVAL) { - php_poll_fd_table_remove(backend_data->fd_table, pfd->fd); - continue; /* Don't report this event */ - } - events[event_count].fd = pfd->fd; events[event_count].events = entry->events; events[event_count].revents = poll_events_from_native(pfd->revents); events[event_count].data = entry->data; event_count++; + if (pfd->revents & POLLNVAL) { + /* Not an open descriptor: reported once as an error, then + * disarmed like a fired one-shot until it is modified */ + entry->events = 0; + } } } } - /* Handle oneshot removals */ + /* A fired one-shot registration is disarmed, not dropped, so that a + * modify can re-arm it as on epoll */ for (int i = 0; i < event_count; i++) { php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, events[i].fd); if (entry && (entry->events & PHP_POLL_ONESHOT) && events[i].revents != 0) { - php_poll_fd_table_remove(backend_data->fd_table, events[i].fd); + entry->events = 0; } } @@ -285,6 +309,12 @@ const php_poll_backend_ops php_poll_backend_poll_ops = { .is_available = poll_backend_is_available, .get_suitable_max_events = poll_backend_get_suitable_max_events, .supports_et = false, +#ifdef __APPLE__ + /* Darwin's poll() never reports POLLPRI for urgent data */ + .supports_priority = false, +#else + .supports_priority = true, +#endif }; #endif diff --git a/main/poll/poll_backend_wsapoll.c b/main/poll/poll_backend_wsapoll.c index 101a7cfe0e4b..2daa835af37d 100644 --- a/main/poll/poll_backend_wsapoll.c +++ b/main/poll/poll_backend_wsapoll.c @@ -120,6 +120,14 @@ static zend_result wsapoll_backend_add(php_poll_ctx *ctx, int fd, uint32_t event return FAILURE; } + /* WSAPoll() fails as a whole on a descriptor that is not a socket */ + int type; + int type_len = sizeof(type); + if (getsockopt((SOCKET) fd, SOL_SOCKET, SO_TYPE, (char *) &type, &type_len) != 0) { + php_poll_set_error(ctx, WSAGetLastError() == WSAENOTSOCK ? PHP_POLL_ERR_NOSUPPORT : PHP_POLL_ERR_INVALID); + return FAILURE; + } + php_poll_fd_entry *entry = php_poll_fd_table_get_new(backend_data->fd_table, fd); if (!entry) { php_poll_set_error(ctx, PHP_POLL_ERR_NOMEM); @@ -177,6 +185,11 @@ static bool wsapoll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *u { wsapoll_build_context *ctx = (wsapoll_build_context *) user_data; + /* Disarmed: WSAPoll() would still report hangups and errors */ + if (entry->events == 0) { + return true; + } + ctx->fds[ctx->index].fd = (SOCKET) fd; ctx->fds[ctx->index].events = (SHORT) wsapoll_events_to_native(entry->events & ~(PHP_POLL_ET | PHP_POLL_ONESHOT)); @@ -186,6 +199,18 @@ static bool wsapoll_build_fds_callback(int fd, php_poll_fd_entry *entry, void *u return true; } +static int wsapoll_backend_wait_nothing(const struct timespec *timeout) +{ + if (timeout != NULL && (timeout->tv_sec > 0 || timeout->tv_nsec > 0)) { + /* Convert to milliseconds, rounding up */ + int sleep_ms = php_poll_timespec_to_ms(timeout); + if (sleep_ms > 0) { + Sleep(sleep_ms); + } + } + return 0; +} + static int wsapoll_backend_wait( php_poll_ctx *ctx, php_poll_event *events, int max_events, const struct timespec *timeout) @@ -194,14 +219,7 @@ static int wsapoll_backend_wait( int fd_count = php_poll_fd_table_count(backend_data->fd_table); if (fd_count == 0) { - if (timeout != NULL && (timeout->tv_sec > 0 || timeout->tv_nsec > 0)) { - /* Convert to milliseconds, rounding up */ - int sleep_ms = php_poll_timespec_to_ms(timeout); - if (sleep_ms > 0) { - Sleep(sleep_ms); - } - } - return 0; + return wsapoll_backend_wait_nothing(timeout); } /* Ensure temp_fds array is large enough */ @@ -219,6 +237,10 @@ static int wsapoll_backend_wait( /* Build WSAPOLLFD array from fd_table */ wsapoll_build_context build_ctx = { .fds = backend_data->temp_fds, .index = 0 }; php_poll_fd_table_foreach(backend_data->fd_table, wsapoll_build_fds_callback, &build_ctx); + fd_count = build_ctx.index; + if (fd_count == 0) { + return wsapoll_backend_wait_nothing(timeout); + } /* Convert timespec to milliseconds (WSAPoll only supports ms resolution, round up) */ int timeout_ms = php_poll_timespec_to_ms(timeout); @@ -233,16 +255,10 @@ static int wsapoll_backend_wait( switch (wsa_error) { case WSAENOTSOCK: - /* Special case: all sockets in array are invalid - * WSAPoll fails entirely, but we should clean up and return 0 - * This differs from Unix poll() which would report POLLNVAL per socket */ - - /* Remove all invalid sockets from fd_table */ - for (int i = 0; i < fd_count; i++) { - int fd = (int) backend_data->temp_fds[i].fd; - php_poll_fd_table_remove(backend_data->fd_table, fd); - } - return 0; + /* A registered socket was closed and its handle reused by + * something else; add() refuses what is not a socket */ + error_code = PHP_POLL_ERR_INVALID; + break; case WSAENOBUFS: error_code = PHP_POLL_ERR_NOMEM; break; @@ -271,12 +287,6 @@ static int wsapoll_backend_wait( int fd = (int) pfd->fd; php_poll_fd_entry *entry = php_poll_fd_table_find(backend_data->fd_table, fd); if (entry) { - /* WSAPoll-specific handling of POLLNVAL */ - if (pfd->revents & POLLNVAL) { - php_poll_fd_table_remove(backend_data->fd_table, fd); - continue; /* Do not report this event */ - } - /* Convert WSAPoll events to PHP poll events */ uint32_t converted_events = wsapoll_events_from_native(pfd->revents); @@ -285,6 +295,11 @@ static int wsapoll_backend_wait( events[event_count].revents = converted_events; events[event_count].data = entry->data; event_count++; + if (pfd->revents & POLLNVAL) { + /* Not an open socket: reported once as an error, then + * disarmed until it is modified */ + entry->events = 0; + } } } } @@ -330,6 +345,7 @@ const php_poll_backend_ops php_poll_backend_wsapoll_ops = { .is_available = wsapoll_backend_is_available, .get_suitable_max_events = wsapoll_backend_get_suitable_max_events, .supports_et = false, + .supports_priority = false, }; #endif /* PHP_WIN32 */ diff --git a/main/poll/poll_core.c b/main/poll/poll_core.c index f985dbcdd3eb..ed3adb07f72b 100644 --- a/main/poll/poll_core.c +++ b/main/poll/poll_core.c @@ -13,6 +13,10 @@ */ #include "php_poll_internal.h" +#include "php_io.h" +#ifndef PHP_WIN32 +# include +#endif /* Backend registry */ static const php_poll_backend_ops *registered_backends[8]; @@ -142,6 +146,9 @@ static php_poll_ctx *php_poll_create_context(uint32_t flags) } ctx->persistent = persistent; ctx->raw_events = (flags & PHP_POLL_FLAG_RAW_EVENTS) != 0; +#ifndef PHP_WIN32 + ctx->owner_pid = getpid(); +#endif return ctx; } @@ -165,6 +172,58 @@ PHPAPI php_poll_ctx *php_poll_create(php_poll_backend_type preferred_backend, ui return ctx; } +/* The process and signal sources (poll_source.c) are descriptors, a pidfd + * or signalfd on Linux and a private kqueue on kqueue platforms, so any + * backend that watches descriptors of every kind serves them */ +static bool php_poll_backend_watches_any_descriptor(php_poll_backend_type backend) +{ + if (backend == PHP_POLL_BACKEND_AUTO) { + if (num_registered_backends > 0 && registered_backends[0]) { + backend = registered_backends[0]->type; + } else { + return false; + } + } + return backend == PHP_POLL_BACKEND_EPOLL || backend == PHP_POLL_BACKEND_POLL + || backend == PHP_POLL_BACKEND_KQUEUE; +} + +PHPAPI bool php_poll_backend_supports_process_handles(php_poll_backend_type backend) +{ +#ifdef PHP_WIN32 + return false; +#else + return php_poll_has_process_source() && php_poll_backend_watches_any_descriptor(backend); +#endif +} + +PHPAPI bool php_poll_backend_supports_signal_handles(php_poll_backend_type backend) +{ +#ifdef PHP_WIN32 + return false; +#else + return php_poll_has_signal_source() && php_poll_backend_watches_any_descriptor(backend); +#endif +} + +PHPAPI bool php_poll_backend_supports_priority(php_poll_backend_type backend) +{ + if (backend == PHP_POLL_BACKEND_AUTO) { + if (num_registered_backends > 0 && registered_backends[0]) { + return registered_backends[0]->supports_priority; + } + return false; + } + + for (int i = 0; i < num_registered_backends; i++) { + if (registered_backends[i] && registered_backends[i]->type == backend) { + return registered_backends[i]->supports_priority; + } + } + + return false; +} + /* Create new poll context */ PHPAPI php_poll_ctx *php_poll_create_by_name(const char *preferred_backend, uint32_t flags) { @@ -238,18 +297,182 @@ PHPAPI void php_poll_destroy(php_poll_ctx *ctx) ctx->backend_ops->cleanup(ctx); } + /* Armed timers are owned by their creators, who must remove them first */ + ZEND_ASSERT(ctx->timer_count == 0); + if (ctx->timers) { + pefree(ctx->timers, ctx->persistent); + } + pefree(ctx, ctx->persistent); } +/* Timers */ + +#define PHP_POLL_TIMER_DISARMED UINT32_MAX + +static zend_always_inline void php_poll_timer_heap_set(php_poll_ctx *ctx, uint32_t i, php_poll_timer *t) +{ + ctx->timers[i] = t; + t->heap_idx = i; +} + +static void php_poll_timer_heap_up(php_poll_ctx *ctx, uint32_t i) +{ + php_poll_timer *t = ctx->timers[i]; + while (i > 0) { + uint32_t parent = (i - 1) / 2; + if (ctx->timers[parent]->deadline <= t->deadline) { + break; + } + php_poll_timer_heap_set(ctx, i, ctx->timers[parent]); + i = parent; + } + php_poll_timer_heap_set(ctx, i, t); +} + +static void php_poll_timer_heap_down(php_poll_ctx *ctx, uint32_t i) +{ + php_poll_timer *t = ctx->timers[i]; + for (;;) { + uint32_t left = 2 * i + 1, right = left + 1, smallest = i; + zend_hrtime_t best = t->deadline; + if (left < ctx->timer_count && ctx->timers[left]->deadline < best) { + smallest = left; + best = ctx->timers[left]->deadline; + } + if (right < ctx->timer_count && ctx->timers[right]->deadline < best) { + smallest = right; + } + if (smallest == i) { + break; + } + php_poll_timer_heap_set(ctx, i, ctx->timers[smallest]); + i = smallest; + } + php_poll_timer_heap_set(ctx, i, t); +} + +static void php_poll_timer_arm(php_poll_ctx *ctx, php_poll_timer *t) +{ + if (ctx->timer_count == ctx->timer_cap) { + ctx->timer_cap = ctx->timer_cap ? ctx->timer_cap * 2 : 16; + ctx->timers = perealloc(ctx->timers, ctx->timer_cap * sizeof(*ctx->timers), ctx->persistent); + } + php_poll_timer_heap_set(ctx, ctx->timer_count++, t); + php_poll_timer_heap_up(ctx, t->heap_idx); +} + +static void php_poll_timer_disarm(php_poll_ctx *ctx, php_poll_timer *t) +{ + uint32_t i = t->heap_idx; + if (i == PHP_POLL_TIMER_DISARMED) { + return; + } + ZEND_ASSERT(i < ctx->timer_count && ctx->timers[i] == t); + t->heap_idx = PHP_POLL_TIMER_DISARMED; + ctx->timer_count--; + if (i == ctx->timer_count) { + return; + } + php_poll_timer_heap_set(ctx, i, ctx->timers[ctx->timer_count]); + php_poll_timer_heap_down(ctx, i); + php_poll_timer_heap_up(ctx, ctx->timers[i]->heap_idx); +} + +PHPAPI php_poll_timer *php_poll_timer_add(php_poll_ctx *ctx, zend_hrtime_t deadline, zend_hrtime_t period, void *data) +{ + php_poll_timer *t = pemalloc(sizeof(*t), ctx->persistent); + t->deadline = deadline; + t->period = period; + t->data = data; + t->heap_idx = PHP_POLL_TIMER_DISARMED; + php_poll_timer_arm(ctx, t); + return t; +} + +PHPAPI zend_result php_poll_timer_modify(php_poll_ctx *ctx, php_poll_timer *t, zend_hrtime_t deadline, zend_hrtime_t period, void *data) +{ + php_poll_timer_disarm(ctx, t); + t->deadline = deadline; + t->period = period; + t->data = data; + php_poll_timer_arm(ctx, t); + return SUCCESS; +} + +PHPAPI void php_poll_timer_remove(php_poll_ctx *ctx, php_poll_timer *t) +{ + php_poll_timer_disarm(ctx, t); + pefree(t, ctx->persistent); +} + +PHPAPI uint32_t php_poll_timer_count(php_poll_ctx *ctx) +{ + return ctx->timer_count; +} + +/* Expired timers go first in the event array; what does not fit stays at + * the head of the heap and makes the next wait return at once. */ +static int php_poll_timer_report(php_poll_ctx *ctx, php_poll_event *events, int max_events, int n_fd_events) +{ + zend_hrtime_t now = zend_hrtime(); + int n_timers = 0; + + while (ctx->timer_count && ctx->timers[0]->deadline <= now && n_fd_events + n_timers < max_events) { + php_poll_timer *t = ctx->timers[0]; + void *data = t->data; + + if (t->period) { + /* Skip the missed periods at once; one past the end never fires */ + zend_hrtime_t periods = (now - t->deadline) / t->period + 1; + if (periods > (ZEND_HRTIME_T_MAX - t->deadline) / t->period) { + t->deadline = ZEND_HRTIME_T_MAX; + } else { + t->deadline += periods * t->period; + } + php_poll_timer_heap_down(ctx, 0); + } else { + php_poll_timer_disarm(ctx, t); + } + + memmove(&events[1], &events[0], (n_fd_events + n_timers) * sizeof(*events)); + events[0].fd = -1; + events[0].events = PHP_POLL_TIMER; + events[0].revents = PHP_POLL_TIMER; + events[0].data = data; + n_timers++; + } + + return n_fd_events + n_timers; +} + /* Add file descriptor */ +static zend_always_inline bool php_poll_ctx_foreign(php_poll_ctx *ctx) +{ +#ifdef PHP_WIN32 + return false; +#else + return ctx->owner_pid != getpid(); +#endif +} + PHPAPI zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { + if (php_poll_ctx_foreign(ctx)) { + ctx->last_error = PHP_POLL_ERR_PERMISSION; + return FAILURE; + } ZEND_ASSERT(ctx); if (UNEXPECTED(!ctx->initialized || fd < 0)) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); return FAILURE; } + if (UNEXPECTED((events & PHP_POLL_PRI) && !ctx->backend_ops->supports_priority)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT); + return FAILURE; + } + /* Delegate to backend - it handles all validation and tracking */ if (EXPECTED(ctx->backend_ops->add(ctx, fd, events, data) == SUCCESS)) { return SUCCESS; @@ -261,12 +484,21 @@ PHPAPI zend_result php_poll_add(php_poll_ctx *ctx, int fd, uint32_t events, void /* Modify file descriptor */ PHPAPI zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, void *data) { + if (php_poll_ctx_foreign(ctx)) { + ctx->last_error = PHP_POLL_ERR_PERMISSION; + return FAILURE; + } ZEND_ASSERT(ctx); if (UNEXPECTED(!ctx->initialized || fd < 0)) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); return FAILURE; } + if (UNEXPECTED((events & PHP_POLL_PRI) && !ctx->backend_ops->supports_priority)) { + php_poll_set_error(ctx, PHP_POLL_ERR_NOSUPPORT); + return FAILURE; + } + /* Delegate to backend - it handles validation */ if (EXPECTED(ctx->backend_ops->modify(ctx, fd, events, data) == SUCCESS)) { return SUCCESS; @@ -278,6 +510,10 @@ PHPAPI zend_result php_poll_modify(php_poll_ctx *ctx, int fd, uint32_t events, v /* Remove file descriptor */ PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd) { + if (php_poll_ctx_foreign(ctx)) { + ctx->last_error = PHP_POLL_ERR_PERMISSION; + return FAILURE; + } ZEND_ASSERT(ctx); if (UNEXPECTED(!ctx->initialized || fd < 0)) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); @@ -296,14 +532,73 @@ PHPAPI zend_result php_poll_remove(php_poll_ctx *ctx, int fd) PHPAPI int php_poll_wait(php_poll_ctx *ctx, php_poll_event *events, int max_events, const struct timespec *timeout) { + if (php_poll_ctx_foreign(ctx)) { + ctx->last_error = PHP_POLL_ERR_PERMISSION; + return -1; + } ZEND_ASSERT(ctx); if (UNEXPECTED(!ctx->initialized || !events || max_events <= 0)) { php_poll_set_error(ctx, PHP_POLL_ERR_INVALID); return -1; } - /* Delegate to backend - it handles everything including ET simulation if needed */ - int nfds = ctx->backend_ops->wait(ctx, events, max_events, timeout); + /* The nearest armed timer bounds the wait, and timers already due get + * their slots reserved so a level-triggered descriptor cannot starve them */ + struct timespec timer_ts; + int n_due = 0; + if (ctx->timer_count) { + zend_hrtime_t now = zend_hrtime(); + for (uint32_t i = 0; i < ctx->timer_count; i++) { + if (ctx->timers[i]->deadline <= now) { + n_due++; + } + } + if (n_due > max_events) { + n_due = max_events; + } + zend_hrtime_t head = ctx->timers[0]->deadline; + zend_hrtime_t remaining = head > now ? head - now : 0; + if (!timeout || remaining < php_poll_timespec_to_ns(timeout)) { + timer_ts.tv_sec = remaining / ZEND_NANO_IN_SEC; + timer_ts.tv_nsec = remaining % ZEND_NANO_IN_SEC; + timeout = &timer_ts; + } + } + + /* Delegate to backend - it handles everything including ET simulation if needed. + * An interrupted wait restarts with the remaining time unless PHP has a + * handler to run (an io_uring in the same process interrupts waits for + * its task work). */ + int nfds = 0; + if (n_due < max_events) { + zend_hrtime_t limit = ZEND_HRTIME_T_MAX; + struct timespec rest_ts; + if (timeout) { + zend_hrtime_t now = zend_hrtime(); + zend_hrtime_t rel = php_poll_timespec_to_ns(timeout); + limit = rel < ZEND_HRTIME_T_MAX - now ? now + rel : ZEND_HRTIME_T_MAX; + } + for (;;) { + nfds = ctx->backend_ops->wait(ctx, events, max_events - n_due, timeout); + if (nfds >= 0 || ctx->last_error != PHP_POLL_ERR_INTERRUPTED || php_io_interrupt_pending()) { + break; + } + if (limit != ZEND_HRTIME_T_MAX) { + zend_hrtime_t now = zend_hrtime(); + zend_hrtime_t remaining = limit > now ? limit - now : 0; + rest_ts.tv_sec = remaining / ZEND_NANO_IN_SEC; + rest_ts.tv_nsec = remaining % ZEND_NANO_IN_SEC; + timeout = &rest_ts; + } + } + if (nfds < 0) { + return nfds; + } + } + + if (ctx->timer_count) { + nfds = php_poll_timer_report(ctx, events, max_events, nfds); + } return nfds; } @@ -326,6 +621,11 @@ PHPAPI bool php_poll_supports_et(php_poll_ctx *ctx) return ctx && ctx->backend_ops && ctx->backend_ops->supports_et; } +PHPAPI bool php_poll_supports_priority(php_poll_ctx *ctx) +{ + return ctx && ctx->backend_ops && ctx->backend_ops->supports_priority; +} + /* Get suitable max_events for backend */ PHPAPI int php_poll_get_suitable_max_events(php_poll_ctx *ctx) { diff --git a/main/poll/poll_handle.c b/main/poll/poll_handle.c index 228d44e1e420..76b46380045b 100644 --- a/main/poll/poll_handle.c +++ b/main/poll/poll_handle.c @@ -66,6 +66,9 @@ PHPAPI php_poll_handle_object *php_poll_handle_object_create( intern->ops = ops ? ops : &php_poll_handle_default_ops; intern->handle_data = NULL; + intern->watching = NULL; + intern->flags = 0; + intern->registrations = NULL; return intern; } @@ -79,6 +82,17 @@ PHPAPI void php_poll_handle_object_free(zend_object *obj) intern->ops->cleanup(intern); } + if (intern->watching) { + zend_hash_destroy(intern->watching); + efree(intern->watching); + /* The cycle collector frees the objects of a cycle in any order, and + * a context freed later still unwatches through its watchers */ + intern->watching = NULL; + } + + /* Registrations hold a reference on the handle, so none can be left */ + ZEND_ASSERT(intern->registrations == NULL); + zend_object_std_dtor(&intern->std); } diff --git a/main/poll/poll_source.c b/main/poll/poll_source.c new file mode 100644 index 000000000000..5ffa0d92e8cd --- /dev/null +++ b/main/poll/poll_source.c @@ -0,0 +1,314 @@ +/* + +----------------------------------------------------------------------+ + | Copyright © The PHP Group and Contributors. | + +----------------------------------------------------------------------+ + | This source file is subject to the Modified BSD License that is | + | bundled with this package in the file LICENSE, and is available | + | through the World Wide Web at . | + | | + | SPDX-License-Identifier: BSD-3-Clause | + +----------------------------------------------------------------------+ + | Authors: Jakub Zelenka | + +----------------------------------------------------------------------+ +*/ + +/* Native sources for process and signal handles: a descriptor readable when the child exited or a + * signal of the set is pending, so every descriptor backend can watch it. + * + * Linux: a pidfd and a signalfd, which is readable while a signal of its set is pending. + * + * kqueue: a private kqueue per source with EVFILT_PROC or one EVFILT_SIGNAL per signal. Both + * filters record events, not state, so a child already waitable and a signal already pending are + * announced by a triggered EVFILT_USER note. Neither consumes: the fired handle reads the exit + * without collecting the child, and a pending signal is taken with sigwait() on it alone. */ + +#include "php_poll_internal.h" + +#ifndef PHP_WIN32 + +#include +#include +#include +#include +#include +#include +#include +#ifdef __linux__ +# include +#endif +#ifdef HAVE_SYS_SIGNALFD_H +# include +#endif +#ifdef HAVE_SYS_PIDFD_H +# include +#endif +#ifdef HAVE_KQUEUE +# include +# include +#endif + +#if defined(__linux__) && (defined(HAVE_PIDFD_OPEN) || defined(SYS_pidfd_open)) +# define PHP_POLL_PROCESS_SOURCE_PIDFD 1 +#elif defined(HAVE_KQUEUE) +# define PHP_POLL_PROCESS_SOURCE_KQUEUE 1 +#endif + +#if defined(__linux__) && defined(HAVE_SYS_SIGNALFD_H) +# define PHP_POLL_SIGNAL_SOURCE_SIGNALFD 1 +#elif defined(HAVE_KQUEUE) +# define PHP_POLL_SIGNAL_SOURCE_KQUEUE 1 +#endif + +PHPAPI bool php_poll_has_process_source(void) +{ +#if defined(PHP_POLL_PROCESS_SOURCE_PIDFD) || defined(PHP_POLL_PROCESS_SOURCE_KQUEUE) + return true; +#else + return false; +#endif +} + +PHPAPI bool php_poll_has_signal_source(void) +{ +#if defined(PHP_POLL_SIGNAL_SOURCE_SIGNALFD) || defined(PHP_POLL_SIGNAL_SOURCE_KQUEUE) + return true; +#else + return false; +#endif +} + +#ifdef HAVE_KQUEUE +/* Make the kqueue readable at once, for what the filters would not report */ +static zend_result php_poll_kqueue_kick(int kq) +{ + struct kevent kevs[2]; +#ifdef EVFILT_USER + /* Added and triggered in two steps: FreeBSD drops NOTE_TRIGGER given with + * the EV_ADD of a new note */ + EV_SET(&kevs[0], 0, EVFILT_USER, EV_ADD | EV_ONESHOT, 0, 0, NULL); + EV_SET(&kevs[1], 0, EVFILT_USER, 0, NOTE_TRIGGER, 0, NULL); + return kevent(kq, kevs, 2, NULL, 0, NULL) == 0 ? SUCCESS : FAILURE; +#else + EV_SET(&kevs[0], 0, EVFILT_TIMER, EV_ADD | EV_ONESHOT, 0, 0, NULL); + return kevent(kq, kevs, 1, NULL, 0, NULL) == 0 ? SUCCESS : FAILURE; +#endif +} + +static int php_poll_kqueue_open(void) +{ + int kq = kqueue(); + if (kq >= 0) { + fcntl(kq, F_SETFD, FD_CLOEXEC); + } + return kq; +} +#endif + +PHPAPI pid_t php_poll_process_exit_probe(pid_t pid, int *status) +{ + siginfo_t si; + int r; + /* si_pid is unspecified when nothing changed */ + memset(&si, 0, sizeof(si)); + do { + r = waitid(P_PID, (id_t) pid, &si, WEXITED | WNOHANG | WNOWAIT); + } while (r == -1 && errno == EINTR); + if (r == -1) { + return -1; + } + if (si.si_pid != pid) { + return 0; + } + /* The status waitpid() would store */ + switch (si.si_code) { + case CLD_EXITED: + *status = (si.si_status & 0xff) << 8; + return pid; + case CLD_KILLED: + *status = si.si_status & 0x7f; + return pid; + case CLD_DUMPED: + *status = (si.si_status & 0x7f) | 0x80; + return pid; + default: + /* A stop or continue: macOS reports a stopped child whatever the + * options ask, since its kernel tests WSTOPPED with a value every + * option bit overlaps */ + return 0; + } +} + +PHPAPI int php_poll_process_source_open(pid_t pid) +{ +#if defined(PHP_POLL_PROCESS_SOURCE_PIDFD) +# ifdef HAVE_PIDFD_OPEN + int fd = pidfd_open(pid, 0); +# else + int fd = (int) syscall(SYS_pidfd_open, pid, 0); +# endif + if (fd >= 0) { + fcntl(fd, F_SETFD, FD_CLOEXEC); + } + return fd; +#elif defined(PHP_POLL_PROCESS_SOURCE_KQUEUE) + int kq = php_poll_kqueue_open(); + if (kq < 0) { + return -1; + } + struct kevent kev; + EV_SET(&kev, pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL); + if (kevent(kq, &kev, 1, NULL, 0, NULL) == 0) { + return kq; + } + int err = errno; + if (err == ESRCH) { + /* Exited already: a child not collected yet is still waitable, + * and the source reports it at the first wait */ + int status; + if (php_poll_process_exit_probe(pid, &status) == pid && php_poll_kqueue_kick(kq) == SUCCESS) { + return kq; + } + err = ESRCH; + } + close(kq); + errno = err; + return -1; +#else + (void) pid; + errno = ENOSYS; + return -1; +#endif +} + +#if defined(PHP_POLL_SIGNAL_SOURCE_KQUEUE) +static bool php_poll_signal_any_pending(const sigset_t *set, int after) +{ + sigset_t pending; + if (sigpending(&pending) != 0) { + return false; + } + for (int signo = after + 1; signo < NSIG; signo++) { + if (sigismember(set, signo) == 1 && sigismember(&pending, signo) == 1) { + return true; + } + } + return false; +} +#endif + +PHPAPI int php_poll_signal_take_pending(const sigset_t *set, siginfo_t *info) +{ +#ifdef HAVE_SIGTIMEDWAIT + struct timespec zero = { 0, 0 }; + siginfo_t local; + int signo; + do { + signo = sigtimedwait(set, info ? info : &local, &zero); + } while (signo == -1 && errno == EINTR); + return signo > 0 ? signo : 0; +#else + /* Without sigtimedwait(): what sigpending() shows is taken with + * sigwait() on that one signal, which returns at once for a pending + * signal and yields no siginfo beyond the number */ + sigset_t pending; + if (sigpending(&pending) != 0) { + return 0; + } + for (int signo = 1; signo < NSIG; signo++) { + if (sigismember(set, signo) == 1 && sigismember(&pending, signo) == 1) { + sigset_t one; + int got; + sigemptyset(&one); + sigaddset(&one, signo); + if (sigwait(&one, &got) != 0) { + continue; + } + if (info) { + memset(info, 0, sizeof(*info)); + info->si_signo = got; + } + return got; + } + } + return 0; +#endif +} + +PHPAPI int php_poll_signal_source_open(const sigset_t *set) +{ +#if defined(PHP_POLL_SIGNAL_SOURCE_SIGNALFD) + return signalfd(-1, set, SFD_NONBLOCK | SFD_CLOEXEC); +#elif defined(PHP_POLL_SIGNAL_SOURCE_KQUEUE) + int kq = php_poll_kqueue_open(); + if (kq < 0) { + return -1; + } + struct kevent kevs[NSIG]; + int n = 0; + for (int signo = 1; signo < NSIG; signo++) { + if (sigismember(set, signo) == 1) { + EV_SET(&kevs[n++], signo, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); + } + } + if (n == 0 || kevent(kq, kevs, n, NULL, 0, NULL) != 0 + /* Pending already: the filter would not report it */ + || (php_poll_signal_any_pending(set, 0) && php_poll_kqueue_kick(kq) != SUCCESS)) { + int err = n == 0 ? EINVAL : errno; + close(kq); + errno = err; + return -1; + } + return kq; +#else + (void) set; + errno = ENOSYS; + return -1; +#endif +} + +PHPAPI int php_poll_signal_source_take(int fd, const sigset_t *set, siginfo_t *info) +{ +#if defined(PHP_POLL_SIGNAL_SOURCE_SIGNALFD) + if (fd >= 0) { + struct signalfd_siginfo fdsi; + ssize_t n; + do { + n = read(fd, &fdsi, sizeof(fdsi)); + } while (n == -1 && errno == EINTR); + if (n != (ssize_t) sizeof(fdsi)) { + return 0; + } + if (info) { + memset(info, 0, sizeof(*info)); + info->si_signo = (int) fdsi.ssi_signo; + info->si_errno = (int) fdsi.ssi_errno; + info->si_code = (int) fdsi.ssi_code; + info->si_pid = (pid_t) fdsi.ssi_pid; + info->si_uid = (uid_t) fdsi.ssi_uid; + info->si_status = (int) fdsi.ssi_status; + } + return (int) fdsi.ssi_signo; + } + return php_poll_signal_take_pending(set, info); +#elif defined(PHP_POLL_SIGNAL_SOURCE_KQUEUE) + if (fd >= 0) { + /* Retrieve the notes so the kqueue stops reading as readable; what + * is pending comes from the kernel, not from the notes */ + struct kevent kevs[8]; + const struct timespec zero = { 0, 0 }; + while (kevent(fd, NULL, 0, kevs, 8, &zero) == 8) { + } + } + int signo = php_poll_signal_take_pending(set, info); + if (signo > 0 && fd >= 0 && php_poll_signal_any_pending(set, 0)) { + /* Still level: more of the set is pending */ + php_poll_kqueue_kick(fd); + } + return signo; +#else + (void) fd; + return php_poll_signal_take_pending(set, info); +#endif +} + +#endif /* PHP_WIN32 */ diff --git a/main/streams/cast.c b/main/streams/cast.c index c93b9d9747f8..269bb9e21784 100644 --- a/main/streams/cast.c +++ b/main/streams/cast.c @@ -194,7 +194,8 @@ PHPAPI zend_result php_stream_cast(php_stream *stream, int castas, void **ret, i castas &= ~PHP_STREAM_CAST_MASK; /* synchronize our buffer (if possible) */ - if (ret && castas != PHP_STREAM_AS_FD_FOR_SELECT && castas != PHP_STREAM_AS_FD_FOR_COPY) { + if (ret && castas != PHP_STREAM_AS_FD_FOR_SELECT && castas != PHP_STREAM_AS_FD_FOR_COPY + && castas != PHP_STREAM_AS_FD_FOR_POLL) { php_stream_flush(stream); if (stream->ops->seek && (stream->flags & PHP_STREAM_FLAG_NO_SEEK) == 0) { zend_off_t dummy; @@ -304,7 +305,8 @@ PHPAPI zend_result php_stream_cast(php_stream *stream, int castas, void **ret, i } } - if (php_stream_is_filtered(stream) && castas != PHP_STREAM_AS_FD_FOR_SELECT) { + if (php_stream_is_filtered(stream) && castas != PHP_STREAM_AS_FD_FOR_SELECT + && castas != PHP_STREAM_AS_FD_FOR_POLL) { if (show_err) { php_stream_warn(stream, CastNotSupported, "Cannot cast a filtered stream on this system"); @@ -316,11 +318,13 @@ PHPAPI zend_result php_stream_cast(php_stream *stream, int castas, void **ret, i if (show_err) { /* these names depend on the values of the PHP_STREAM_AS_XXX defines in php_streams.h */ - static const char *cast_names[4] = { + static const char *cast_names[6] = { "STDIO FILE*", "File Descriptor", "Socket Descriptor", - "select()able descriptor" + "select()able descriptor", + "copyable descriptor", + "pollable descriptor" }; php_stream_warn(stream, CastNotSupported, diff --git a/main/streams/php_stream_errors.h b/main/streams/php_stream_errors.h index c49f518ea4d9..06417dcc1f22 100644 --- a/main/streams/php_stream_errors.h +++ b/main/streams/php_stream_errors.h @@ -81,16 +81,24 @@ typedef struct _php_stream_stored_error { struct _php_stream_stored_error *next; } php_stream_stored_error; -typedef struct { +/* Operation stack of the main flow or of one fiber */ +typedef struct _php_stream_error_stack { php_stream_error_operation *current_operation; uint32_t operation_depth; uint32_t operation_floor; uint32_t refused_operations; + uint32_t overflow_capacity; + php_stream_error_operation *overflow_operations; + struct _php_stream_error_stack *prev; + struct _php_stream_error_stack *next; + php_stream_error_operation operation_pool[PHP_STREAM_ERROR_OPERATION_POOL_SIZE]; +} php_stream_error_stack; + +typedef struct php_stream_error_state { + php_stream_error_stack main_stack; + php_stream_error_stack *fiber_stacks; php_stream_stored_error *stored_errors; uint32_t stored_count; - php_stream_error_operation operation_pool[PHP_STREAM_ERROR_OPERATION_POOL_SIZE]; - php_stream_error_operation *overflow_operations; - uint32_t overflow_capacity; } php_stream_error_state; /* Error operation management */ diff --git a/main/streams/php_stream_plain_wrapper.h b/main/streams/php_stream_plain_wrapper.h index aa74859c11e5..a36897a3ede0 100644 --- a/main/streams/php_stream_plain_wrapper.h +++ b/main/streams/php_stream_plain_wrapper.h @@ -33,6 +33,12 @@ PHPAPI php_stream *_php_stream_fopen_from_fd(int fd, const char *mode, const cha PHPAPI php_stream *_php_stream_fopen_from_pipe(FILE *file, const char *mode STREAMS_DC); #define php_stream_fopen_from_pipe(file, mode) _php_stream_fopen_from_pipe((file), (mode) STREAMS_CC) +#ifndef PHP_WIN32 +/* popen(3) keeping the child's pid, so that closing the stream waits for it as an operation */ +PHPAPI php_stream *_php_stream_popen(const char *command, const char *mode STREAMS_DC); +#define php_stream_popen(command, mode) _php_stream_popen((command), (mode) STREAMS_CC) +#endif + PHPAPI php_stream *_php_stream_fopen_tmpfile(int dummy STREAMS_DC); #define php_stream_fopen_tmpfile() _php_stream_fopen_tmpfile(0 STREAMS_CC) diff --git a/main/streams/plain_wrapper.c b/main/streams/plain_wrapper.c index eb9b81b6e2c5..2527700c4acd 100644 --- a/main/streams/plain_wrapper.c +++ b/main/streams/plain_wrapper.c @@ -17,6 +17,7 @@ #include "php_network.h" #include "php_open_temporary_file.h" #include "ext/standard/file.h" +#include "ext/standard/io_poll.h" #include "ext/standard/flock_compat.h" #include "ext/standard/php_filestat.h" #include @@ -134,13 +135,22 @@ PHPAPI int php_stream_parse_fopen_modes(const char *mode, int *open_flags) typedef struct { FILE *file; int fd; /* underlying file descriptor */ + int child_pid; /* php_stream_popen() child, else 0 */ unsigned is_process_pipe:1; /* use pclose instead of fclose */ unsigned is_pipe:1; /* stream is an actual pipe, currently Windows only*/ unsigned cached_fstat:1; /* sb is valid */ unsigned is_pipe_blocking:1; /* allow blocking read() on pipes, currently Windows only */ unsigned no_forced_fstat:1; /* Use fstat cache even if forced */ unsigned is_seekable:1; /* don't try and seek, if not set */ - unsigned _reserved:26; + unsigned is_overlapped:1; /* Windows: FILE_FLAG_OVERLAPPED, uses position */ + unsigned nonblock_ours:1; /* O_NONBLOCK set for the IO hooks */ + unsigned _reserved:24; +#ifdef PHP_WIN32 + zend_off_t position; /* of the next overlapped read or write */ + int open_flags; /* the open()-style flags, for the synchronous reopen */ +#else + pid_t nonblock_pid; /* the process that set nonblock_ours */ +#endif int lock_flag; /* stores the lock state */ zend_string *temp_name; /* if non-null, this is the path to a temporary file that @@ -162,6 +172,49 @@ typedef struct { } php_stdio_stream_data; #define PHP_STDIOP_GET_FD(anfd, data) anfd = (data)->file ? fileno((data)->file) : (data)->fd +#ifdef PHP_WIN32 +/* The first FILE* or descriptor cast turns an overlapped stream synchronous: the CRT cannot drive + * an overlapped handle, so the stream goes on with one reopened without the flag, at its position, + * and the overlapped one goes. One handle, so a lock taken through the stream and a consumer's + * dup() of the descriptor share it, and the position moves as the consumer reads or writes. */ +static int php_stdiop_make_sync(php_stdio_stream_data *data) +{ + php_ioutil_open_opts opts; + int flags = data->open_flags & ~(_O_CREAT | _O_TRUNC | _O_EXCL | _O_TEMPORARY); + if (!php_win32_ioutil_posix_to_open_opts(flags, 0, &opts)) { + return -1; + } + HANDLE reopened = ReOpenFile((HANDLE) _get_osfhandle(data->fd), opts.access, opts.share, 0); + if (reopened == INVALID_HANDLE_VALUE) { + SET_ERRNO_FROM_WIN32_CODE(GetLastError()); + return -1; + } + int fd = _open_osfhandle((intptr_t) reopened, flags & (_O_RDONLY | _O_WRONLY | _O_RDWR | _O_APPEND | _O_BINARY | _O_TEXT)); + if (fd < 0) { + CloseHandle(reopened); + return -1; + } + zend_lseek(fd, data->position, SEEK_SET); + close(data->fd); + data->fd = fd; + data->is_overlapped = 0; + return fd; +} +#endif + +/* The descriptor a cast hands out */ +static php_socket_t php_stdiop_cast_fd(php_stdio_stream_data *data) +{ + php_socket_t fd; +#ifdef PHP_WIN32 + if (data->is_overlapped && php_stdiop_make_sync(data) < 0) { + return SOCK_ERR; + } +#endif + PHP_STDIOP_GET_FD(fd, data); + return fd; +} + #ifdef PHP_WIN32 static ZEND_COLD void php_win32_stream_wrapper_warn_error( php_stream_wrapper *wrapper, @@ -384,6 +437,121 @@ PHPAPI php_stream *_php_stream_fopen_from_pipe(FILE *file, const char *mode STRE return stream; } +#ifndef PHP_WIN32 +PHPAPI php_stream *_php_stream_popen(const char *command, const char *mode STREAMS_DC) +{ + bool reading = mode[0] == 'r'; + int fds[2]; + +#ifdef HAVE_PIPE2 + if (pipe2(fds, O_CLOEXEC) != 0) { + return NULL; + } +#else + if (pipe(fds) != 0) { + return NULL; + } + fcntl(fds[0], F_SETFD, FD_CLOEXEC); + fcntl(fds[1], F_SETFD, FD_CLOEXEC); +#endif + + pid_t pid = fork(); + if (pid < 0) { + int err = errno; + close(fds[0]); + close(fds[1]); + errno = err; + return NULL; + } + if (pid == 0) { + int child_end = reading ? fds[1] : fds[0]; + int std_fd = reading ? STDOUT_FILENO : STDIN_FILENO; + if (child_end == std_fd) { + fcntl(child_end, F_SETFD, 0); + } else { + dup2(child_end, std_fd); + } +#ifdef VIRTUAL_DIR + if (CWDG(cwd).cwd_length > 0 && chdir(CWDG(cwd).cwd) != 0) { + _exit(127); + } +#endif + /* Signals blocked for a SignalHandle are the parent's business */ + sigset_t mask; + php_io_poll_signal_child_mask(&mask); + sigprocmask(SIG_SETMASK, &mask, NULL); + execl("/bin/sh", "sh", "-c", command, (char *) NULL); + _exit(127); + } + + int parent_end = reading ? fds[0] : fds[1]; + close(reading ? fds[1] : fds[0]); + FILE *fp = fdopen(parent_end, reading ? "r" : "w"); + if (fp == NULL) { + int err = errno; + close(parent_end); + while (waitpid(pid, NULL, 0) < 0 && errno == EINTR); + errno = err; + return NULL; + } + + php_stream *stream = _php_stream_fopen_from_pipe(fp, mode STREAMS_REL_CC); + if (stream) { + ((php_stdio_stream_data *) stream->abstract)->child_pid = (int) pid; + } else { + fclose(fp); + while (waitpid(pid, NULL, 0) < 0 && errno == EINTR); + } + return stream; +} +#endif + +#ifndef PHP_WIN32 +/* An unseekable descriptor becomes non-blocking once the IO hooks see it, as + * the ring expects; the deadline carries the stream's mode, a non-blocking + * one leaves it to the kernel */ +static php_deadline php_stdiop_io_deadline(php_stream *stream, php_stdio_stream_data *data) +{ + php_deadline dl; + php_deadline_init_infinite(&dl); +#ifdef O_NONBLOCK + if (!(stream->flags & PHP_STREAM_FLAG_NO_SEEK) || data->nonblock_ours) { + return dl; + } + if (!php_io_hooks_active()) { + php_deadline_init_nonblock(&dl); + return dl; + } + int flags = fcntl(data->fd, F_GETFL); + if (flags == -1) { + return dl; + } + if (flags & O_NONBLOCK) { + php_deadline_init_nonblock(&dl); + } else if (fcntl(data->fd, F_SETFL, flags | O_NONBLOCK) == 0) { + data->nonblock_ours = 1; + data->nonblock_pid = getpid(); + } +#endif + return dl; +} + +static void php_stdiop_restore_blocking(php_stdio_stream_data *data) +{ +#ifdef O_NONBLOCK + int fd; + PHP_STDIOP_GET_FD(fd, data); + if (data->nonblock_ours && fd >= 0 && data->nonblock_pid == getpid()) { + int flags = fcntl(fd, F_GETFL); + if (flags != -1 && (flags & O_NONBLOCK)) { + fcntl(fd, F_SETFL, flags & ~O_NONBLOCK); + } + } +#endif + data->nonblock_ours = 0; +} +#endif + static ssize_t php_stdiop_write(php_stream *stream, const char *buf, size_t count) { php_stdio_stream_data *data = (php_stdio_stream_data*)stream->abstract; @@ -393,9 +561,19 @@ static ssize_t php_stdiop_write(php_stream *stream, const char *buf, size_t coun if (data->fd >= 0) { #ifdef PHP_WIN32 - bytes_written = _write(data->fd, buf, PLAIN_WRAP_BUF_SIZE(count)); + if (data->is_overlapped) { + php_deadline deadline; + php_deadline_init_infinite(&deadline); + bytes_written = php_io_write_at(stream, data->fd, buf, PLAIN_WRAP_BUF_SIZE(count), data->position, &deadline); + if (bytes_written > 0) { + data->position += bytes_written; + } + } else { + bytes_written = _write(data->fd, buf, PLAIN_WRAP_BUF_SIZE(count)); + } #else - bytes_written = write(data->fd, buf, count); + php_deadline deadline = php_stdiop_io_deadline(stream, data); + bytes_written = php_io_write(stream, data->fd, buf, count, &deadline); #endif if (bytes_written < 0) { if (PHP_IS_TRANSIENT_ERROR(errno)) { @@ -468,14 +646,28 @@ static ssize_t php_stdiop_read(php_stream *stream, char *buf, size_t count) } } #endif - ret = read(data->fd, buf, PLAIN_WRAP_BUF_SIZE(count)); - - if (ret == (size_t)-1 && errno == EINTR) { - /* Read was interrupted, retry once, - If read still fails, give up with feof==0 - so script can retry if desired */ +#ifdef PHP_WIN32 + if (data->is_overlapped) { + php_deadline deadline; + php_deadline_init_infinite(&deadline); + ret = php_io_read_at(stream, data->fd, buf, PLAIN_WRAP_BUF_SIZE(count), data->position, &deadline); + if (ret > 0) { + data->position += ret; + } + } else { ret = read(data->fd, buf, PLAIN_WRAP_BUF_SIZE(count)); + + if (ret == (size_t)-1 && errno == EINTR) { + /* Read was interrupted, retry once, + If read still fails, give up with feof==0 + so script can retry if desired */ + ret = read(data->fd, buf, PLAIN_WRAP_BUF_SIZE(count)); + } } +#else + php_deadline deadline = php_stdiop_io_deadline(stream, data); + ret = php_io_read(stream, data->fd, buf, PLAIN_WRAP_BUF_SIZE(count), &deadline); +#endif if (ret < 0) { if (PHP_IS_TRANSIENT_ERROR(errno)) { @@ -543,14 +735,34 @@ static int php_stdiop_close(php_stream *stream, int close_handle) } #endif +#ifndef PHP_WIN32 + php_stdiop_restore_blocking(data); +#endif + if (close_handle) { if (data->file) { if (data->is_process_pipe) { errno = 0; - ret = pclose(data->file); +#ifndef PHP_WIN32 + if (data->child_pid > 0) { + /* The pipe closes first so the child sees EOF, then the wait is an op */ + int wstatus = 0; + pid_t wp; + php_deadline dl = php_io_deadline_infinite(); + fclose(data->file); + data->file = NULL; + do { + wp = php_io_waitpid(NULL, (pid_t) data->child_pid, &wstatus, 0, &dl); + } while (wp == -1 && errno == EINTR); + ret = wp > 0 ? wstatus : -1; + } else +#endif + { + ret = pclose(data->file); + } #ifdef HAVE_SYS_WAIT_H - if (WIFEXITED(ret)) { + if (ret != -1 && WIFEXITED(ret)) { ret = WEXITSTATUS(ret); } #endif @@ -613,17 +825,19 @@ static int php_stdiop_sync(php_stream *stream, bool dataonly) FILE *fp; int fd; +#ifdef PHP_WIN32 + if (data->is_overlapped) { + /* No FILE* over an overlapped descriptor: sync the descriptor itself */ + return php_stdiop_flush(stream) == 0 ? php_io_fsync(stream, data->fd, dataonly) : -1; + } +#endif if (php_stream_cast(stream, PHP_STREAM_AS_STDIO, (void**)&fp, REPORT_ERRORS) == FAILURE) { return -1; } if (php_stdiop_flush(stream) == 0) { PHP_STDIOP_GET_FD(fd, data); - if (dataonly) { - return fdatasync(fd); - } else { - return fsync(fd); - } + return php_io_fsync(stream, fd, dataonly); } return -1; } @@ -644,10 +858,22 @@ static int php_stdiop_seek(php_stream *stream, zend_off_t offset, int whence, ze if (data->fd >= 0) { zend_off_t result; +#ifdef PHP_WIN32 + if (data->is_overlapped && whence == SEEK_CUR) { + /* The kernel keeps no position for an overlapped handle */ + offset += data->position; + whence = SEEK_SET; + } +#endif result = zend_lseek(data->fd, offset, whence); if (result == (zend_off_t)-1) return -1; +#ifdef PHP_WIN32 + if (data->is_overlapped) { + data->position = result; + } +#endif *newoffset = result; return 0; @@ -671,6 +897,11 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret) switch (castas) { case PHP_STREAM_AS_STDIO: if (ret) { +#ifdef PHP_WIN32 + if (data->is_overlapped && php_stdiop_make_sync(data) < 0) { + return FAILURE; + } +#endif if (data->file == NULL) { /* we were opened as a plain file descriptor, so we @@ -689,7 +920,8 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret) return SUCCESS; case PHP_STREAM_AS_FD_FOR_SELECT: - PHP_STDIOP_GET_FD(fd, data); + case PHP_STREAM_AS_FD_FOR_POLL: + fd = php_stdiop_cast_fd(data); if (SOCK_ERR == fd) { return FAILURE; } @@ -699,7 +931,7 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret) return SUCCESS; case PHP_STREAM_AS_FD: - PHP_STDIOP_GET_FD(fd, data); + fd = php_stdiop_cast_fd(data); if (SOCK_ERR == fd) { return FAILURE; } @@ -727,6 +959,11 @@ static int php_stdiop_cast(php_stream *stream, int castas, void **ret) copy_fd->timeout.tv_sec = 0; copy_fd->timeout.tv_usec = 0; copy_fd->is_blocked = 0; +#ifdef PHP_WIN32 + copy_fd->position = data->is_overlapped ? &data->position : NULL; +#else + copy_fd->position = NULL; +#endif } return SUCCESS; @@ -766,8 +1003,11 @@ static int php_stdiop_set_option(php_stream *stream, int option, int value, void if (fd == -1) return -1; #ifdef O_NONBLOCK + if (value && data->nonblock_ours) { + return 1; + } flags = fcntl(fd, F_GETFL, 0); - oldval = (flags & O_NONBLOCK) ? 0 : 1; + oldval = (flags & O_NONBLOCK) && !data->nonblock_ours ? 0 : 1; if (value) flags &= ~O_NONBLOCK; else @@ -775,6 +1015,7 @@ static int php_stdiop_set_option(php_stream *stream, int option, int value, void if (-1 == fcntl(fd, F_SETFL, flags)) return -1; + data->nonblock_ours = 0; return oldval; #else return -1; /* not yet implemented */ @@ -1073,7 +1314,7 @@ static int php_stdiop_set_option(php_stream *stream, int option, int value, void flags = fcntl(fd, F_GETFL, 0); add_assoc_bool((zval*)ptrparam, "timed_out", 0); - add_assoc_bool((zval*)ptrparam, "blocked", (flags & O_NONBLOCK)? 0 : 1); + add_assoc_bool((zval*)ptrparam, "blocked", (flags & O_NONBLOCK) && !data->nonblock_ours ? 0 : 1); add_assoc_bool((zval*)ptrparam, "eof", stream->eof); return PHP_STREAM_OPTION_RETURN_OK; @@ -1182,6 +1423,38 @@ static php_stream *php_plain_files_dir_opener(php_stream_wrapper *wrapper, const } /* }}} */ +#ifdef PHP_WIN32 +/* The device namespace (named pipes, consoles, serial ports) holds no disk + * files, and opening a pipe twice to test it would take a second instance */ +/* PHP_IO_OVERLAPPED_FILES=1 in the environment opens every eligible file overlapped, hooks or + * not: the test suite run that decides whether that becomes the default */ +static bool php_stdiop_win32_overlapped_forced(void) +{ + static int forced = -1; + if (forced < 0) { + const char *env = getenv("PHP_IO_OVERLAPPED_FILES"); + forced = env != NULL && *env != '\0' && *env != '0'; + } + return forced == 1; +} + +static bool php_stdiop_win32_may_overlap(const char *path, int open_flags) +{ + if (open_flags & (O_APPEND | _O_TEXT)) { + return false; + } + if (IS_SLASH(path[0]) && IS_SLASH(path[1]) && path[2] != '\0' && IS_SLASH(path[3])) { + if (path[2] == '.') { + return false; + } + if (path[2] == '?' && strncasecmp(path + 4, "pipe", 4) == 0 && IS_SLASH(path[8])) { + return false; + } + } + return true; +} +#endif + /* {{{ php_stream_fopen */ PHPAPI php_stream *_php_stream_fopen(const char *filename, const char *mode, zend_string **opened_path, int options STREAMS_DC) { @@ -1222,7 +1495,25 @@ PHPAPI php_stream *_php_stream_fopen(const char *filename, const char *mode, zen } } #ifdef PHP_WIN32 - fd = php_win32_ioutil_open(realpath, open_flags, 0666); + /* While hooks are installed a disk file is opened overlapped, since IOCP completes nothing on + * a synchronous handle. Append mode has no position to write at and text mode needs the CRT's + * newline translation, so both keep the CRT path. */ + php_io_hooks *hooks = php_io_hooks_current(); + bool overlapped = (php_stdiop_win32_overlapped_forced() + || (hooks != NULL && (hooks->flags & PHP_IO_HOOKS_F_FILES))) + && php_stdiop_win32_may_overlap(realpath, open_flags); + fd = -1; + if (overlapped) { + fd = php_win32_ioutil_open(realpath, open_flags | PHP_WIN32_IOUTIL_O_OVERLAPPED, 0666); + if (fd != -1 && GetFileType((HANDLE) _get_osfhandle(fd)) != FILE_TYPE_DISK) { + close(fd); + fd = -1; + } + overlapped = fd != -1; + } + if (fd == -1) { + fd = php_win32_ioutil_open(realpath, open_flags, 0666); + } #else fd = open(realpath, open_flags, 0666); #endif @@ -1238,6 +1529,14 @@ PHPAPI php_stream *_php_stream_fopen(const char *filename, const char *mode, zen * O_APPEND mode) */ ret = php_stream_fopen_from_fd_rel(fd, mode, persistent_id, (open_flags & O_APPEND) == 0); } +#ifdef PHP_WIN32 + if (ret && overlapped) { + php_stdio_stream_data *self = (php_stdio_stream_data*)ret->abstract; + self->is_overlapped = 1; + self->position = 0; + self->open_flags = open_flags; + } +#endif if (EG(active)) { /* clear stat cache as mtime and ctime might got changed - phar can use stream before diff --git a/main/streams/stream_errors.c b/main/streams/stream_errors.c index 788dceff445b..1074530dae4f 100644 --- a/main/streams/stream_errors.c +++ b/main/streams/stream_errors.c @@ -19,6 +19,9 @@ #include "php_stream_errors.h" #include "zend_enum.h" #include "zend_exceptions.h" +#include "zend_extensions.h" +#include "zend_fibers.h" +#include "zend_observer.h" #include "ext/standard/file.h" #include "stream_errors_arginfo.h" @@ -29,6 +32,9 @@ static zend_class_entry *php_ce_stream_error_store; static zend_class_entry *php_ce_stream_error; static zend_class_entry *php_ce_stream_exception; +/* Fiber context reserved slot holding the fiber's operation stack */ +static int php_stream_error_fiber_slot = -1; + /* Forward declarations */ static void php_stream_error_entry_free(php_stream_error_entry *entry); @@ -189,28 +195,26 @@ static bool php_stream_has_terminating_error(const php_stream_error_entry *entry return false; } -static inline php_stream_error_operation *php_stream_get_operation_at_depth(uint32_t depth) +static inline php_stream_error_operation *php_stream_get_operation_at_depth( + php_stream_error_stack *stack, uint32_t depth) { - php_stream_error_state *state = &FG(stream_error_state); - if (depth < PHP_STREAM_ERROR_OPERATION_POOL_SIZE) { - return &state->operation_pool[depth]; + return &stack->operation_pool[depth]; } else { uint32_t overflow_index = depth - PHP_STREAM_ERROR_OPERATION_POOL_SIZE; - ZEND_ASSERT(overflow_index < state->overflow_capacity); - return &state->overflow_operations[overflow_index]; + ZEND_ASSERT(overflow_index < stack->overflow_capacity); + return &stack->overflow_operations[overflow_index]; } } -static inline php_stream_error_operation *php_stream_get_parent_operation(void) +static inline php_stream_error_operation *php_stream_get_parent_operation( + php_stream_error_stack *stack) { - const php_stream_error_state *state = &FG(stream_error_state); - - if (state->operation_depth <= state->operation_floor) { + if (stack->operation_depth <= stack->operation_floor) { return NULL; } - return php_stream_get_operation_at_depth(state->operation_depth - 1); + return php_stream_get_operation_at_depth(stack, stack->operation_depth - 1); } /* Clean up functions */ @@ -227,43 +231,101 @@ static void php_stream_error_entry_free(php_stream_error_entry *entry) } } -PHPAPI void php_stream_error_state_cleanup(void) +/* Drops the open operations and their errors without reporting them */ +static void php_stream_error_stack_clear(php_stream_error_stack *stack) { - php_stream_error_state *state = &FG(stream_error_state); - - state->operation_floor = 0; - state->refused_operations = 0; - while (state->current_operation) { - php_stream_error_operation *op = state->current_operation; - state->operation_depth--; - state->current_operation = php_stream_get_parent_operation(); - + for (uint32_t depth = 0; depth < stack->operation_depth; depth++) { + php_stream_error_operation *op = php_stream_get_operation_at_depth(stack, depth); php_stream_error_entry_free(op->first_error); - op->first_error = NULL; op->last_error = NULL; op->error_count = 0; } - php_stream_stored_error *stored = state->stored_errors; - while (stored) { - php_stream_stored_error *next = stored->next; - php_stream_error_entry_free(stored->first_error); - efree(stored); - stored = next; + stack->current_operation = NULL; + stack->operation_depth = 0; + stack->operation_floor = 0; + stack->refused_operations = 0; + + if (stack->overflow_operations) { + efree(stack->overflow_operations); + stack->overflow_operations = NULL; + stack->overflow_capacity = 0; } +} - state->stored_errors = NULL; - state->stored_count = 0; - state->operation_depth = 0; +static void php_stream_error_fiber_stack_free(php_stream_error_stack *stack) +{ + php_stream_error_state *state = &FG(stream_error_state); + + php_stream_error_stack_clear(stack); + + if (stack->prev) { + stack->prev->next = stack->next; + } else { + state->fiber_stacks = stack->next; + } + if (stack->next) { + stack->next->prev = stack->prev; + } + efree(stack); +} + +static php_stream_error_stack *php_stream_error_fiber_stack_create(void) +{ + php_stream_error_state *state = &FG(stream_error_state); + php_stream_error_stack *stack = ecalloc(1, sizeof(php_stream_error_stack)); + + stack->next = state->fiber_stacks; + if (stack->next) { + stack->next->prev = stack; + } + state->fiber_stacks = stack; + + ZEND_ASSERT(EG(current_fiber_context) != EG(main_fiber_context)); + EG(current_fiber_context)->reserved[php_stream_error_fiber_slot] = stack; + + return stack; +} + +/* NULL for a fiber without an operation yet */ +static zend_always_inline php_stream_error_stack *php_stream_error_current_stack(void) +{ + zend_fiber_context *context = EG(current_fiber_context); + + if (context == EG(main_fiber_context) || php_stream_error_fiber_slot < 0) { + return &FG(stream_error_state).main_stack; + } + return context->reserved[php_stream_error_fiber_slot]; +} + +static void php_stream_error_fiber_init(zend_fiber_context *context) +{ + context->reserved[php_stream_error_fiber_slot] = NULL; +} - if (state->overflow_operations) { - efree(state->overflow_operations); - state->overflow_operations = NULL; - state->overflow_capacity = 0; +static void php_stream_error_fiber_destroy(zend_fiber_context *context) +{ + php_stream_error_stack *stack = context->reserved[php_stream_error_fiber_slot]; + + if (stack) { + context->reserved[php_stream_error_fiber_slot] = NULL; + php_stream_error_fiber_stack_free(stack); } } +PHPAPI void php_stream_error_state_cleanup(void) +{ + php_stream_error_state *state = &FG(stream_error_state); + + while (state->fiber_stacks) { + php_stream_error_fiber_stack_free(state->fiber_stacks); + } + php_stream_error_stack_clear(&state->main_stack); + + php_stream_error_clear_stored(); +} + PHPAPI void php_stream_error_get_last(zval *return_value) { const php_stream_error_state *state = &FG(stream_error_state); @@ -296,41 +358,45 @@ PHPAPI void php_stream_error_clear_stored(void) PHPAPI php_stream_error_operation *php_stream_error_operation_begin(void) { - php_stream_error_state *state = &FG(stream_error_state); + php_stream_error_stack *stack = php_stream_error_current_stack(); - if (state->operation_depth >= PHP_STREAM_ERROR_MAX_DEPTH) { + if (UNEXPECTED(!stack)) { + stack = php_stream_error_fiber_stack_create(); + } + + if (stack->operation_depth >= PHP_STREAM_ERROR_MAX_DEPTH) { php_error_docref(NULL, E_WARNING, "Stream error operation depth exceeded (%"PRIu32"), possible infinite recursion", - state->operation_depth); - state->refused_operations++; + stack->operation_depth); + stack->refused_operations++; return NULL; } php_stream_error_operation *op; - if (state->operation_depth < PHP_STREAM_ERROR_OPERATION_POOL_SIZE) { - op = &state->operation_pool[state->operation_depth]; + if (stack->operation_depth < PHP_STREAM_ERROR_OPERATION_POOL_SIZE) { + op = &stack->operation_pool[stack->operation_depth]; } else { - uint32_t overflow_index = state->operation_depth - PHP_STREAM_ERROR_OPERATION_POOL_SIZE; + uint32_t overflow_index = stack->operation_depth - PHP_STREAM_ERROR_OPERATION_POOL_SIZE; - if (overflow_index >= state->overflow_capacity) { + if (overflow_index >= stack->overflow_capacity) { uint32_t new_capacity - = state->overflow_capacity == 0 ? 8 : state->overflow_capacity * 2; + = stack->overflow_capacity == 0 ? 8 : stack->overflow_capacity * 2; php_stream_error_operation *new_overflow = erealloc( - state->overflow_operations, sizeof(php_stream_error_operation) * new_capacity); - state->overflow_operations = new_overflow; - state->overflow_capacity = new_capacity; + stack->overflow_operations, sizeof(php_stream_error_operation) * new_capacity); + stack->overflow_operations = new_overflow; + stack->overflow_capacity = new_capacity; } - op = &state->overflow_operations[overflow_index]; + op = &stack->overflow_operations[overflow_index]; } op->first_error = NULL; op->last_error = NULL; op->error_count = 0; - state->current_operation = op; - state->operation_depth++; + stack->current_operation = op; + stack->operation_depth++; return op; } @@ -338,7 +404,8 @@ PHPAPI php_stream_error_operation *php_stream_error_operation_begin(void) static void php_stream_error_add(zend_enum_StreamErrorCode code, const char *wrapper_name, zend_string *message, const char *docref, int severity, bool terminating) { - php_stream_error_operation *op = FG(stream_error_state).current_operation; + const php_stream_error_stack *stack = php_stream_error_current_stack(); + php_stream_error_operation *op = stack ? stack->current_operation : NULL; if (!op) { zend_string_release(message); return; @@ -449,13 +516,18 @@ static void php_stream_report_errors(const php_stream_context *context, const ph PHPAPI void php_stream_error_operation_end(const php_stream_context *context) { php_stream_error_state *state = &FG(stream_error_state); - php_stream_error_operation *op = state->current_operation; + php_stream_error_stack *stack = php_stream_error_current_stack(); - if (state->refused_operations > 0) { - state->refused_operations--; + if (!stack) { return; } + if (stack->refused_operations > 0) { + stack->refused_operations--; + return; + } + + php_stream_error_operation *op = stack->current_operation; if (!op) { return; } @@ -465,8 +537,8 @@ PHPAPI void php_stream_error_operation_end(const php_stream_context *context) op->last_error = NULL; op->error_count = 0; - state->operation_depth--; - state->current_operation = php_stream_get_parent_operation(); + stack->operation_depth--; + stack->current_operation = php_stream_get_parent_operation(stack); if (!first_error) { return; @@ -485,12 +557,12 @@ PHPAPI void php_stream_error_operation_end(const php_stream_context *context) GC_ADDREF(context->res); } - uint32_t saved_floor = state->operation_floor; - state->operation_floor = state->operation_depth; - state->current_operation = NULL; + uint32_t saved_floor = stack->operation_floor; + stack->operation_floor = stack->operation_depth; + stack->current_operation = NULL; php_stream_report_errors(context, first_error, error_mode, is_terminating); - state->operation_floor = saved_floor; - state->current_operation = php_stream_get_parent_operation(); + stack->operation_floor = saved_floor; + stack->current_operation = php_stream_get_parent_operation(stack); if (context) { zend_list_delete(context->res); @@ -559,21 +631,25 @@ PHPAPI void php_stream_error_operation_end(const php_stream_context *context) PHPAPI void php_stream_error_operation_end_for_stream(const php_stream *stream) { - php_stream_error_state *state = &FG(stream_error_state); - php_stream_error_operation *op = state->current_operation; + php_stream_error_stack *stack = php_stream_error_current_stack(); - if (state->refused_operations > 0) { - state->refused_operations--; + if (!stack) { return; } + if (stack->refused_operations > 0) { + stack->refused_operations--; + return; + } + + php_stream_error_operation *op = stack->current_operation; if (!op) { return; } if (op->error_count == 0) { - state->operation_depth--; - state->current_operation = php_stream_get_parent_operation(); + stack->operation_depth--; + stack->current_operation = php_stream_get_parent_operation(stack); op->first_error = NULL; op->last_error = NULL; @@ -586,20 +662,24 @@ PHPAPI void php_stream_error_operation_end_for_stream(const php_stream *stream) PHPAPI void php_stream_error_operation_abort(void) { - php_stream_error_state *state = &FG(stream_error_state); - php_stream_error_operation *op = state->current_operation; + php_stream_error_stack *stack = php_stream_error_current_stack(); + + if (!stack) { + return; + } - if (state->refused_operations > 0) { - state->refused_operations--; + if (stack->refused_operations > 0) { + stack->refused_operations--; return; } + php_stream_error_operation *op = stack->current_operation; if (!op) { return; } - state->operation_depth--; - state->current_operation = php_stream_get_parent_operation(); + stack->operation_depth--; + stack->current_operation = php_stream_get_parent_operation(stack); php_stream_error_entry_free(op->first_error); op->first_error = NULL; @@ -613,7 +693,8 @@ static void php_stream_wrapper_error_internal(const char *wrapper_name, const ph const char *docref, int severity, bool terminating, zend_enum_StreamErrorCode code, zend_string *message) { - bool implicit_operation = (FG(stream_error_state).current_operation == NULL); + const php_stream_error_stack *stack = php_stream_error_current_stack(); + bool implicit_operation = !stack || !stack->current_operation; if (implicit_operation) { php_stream_error_operation_begin(); } @@ -875,6 +956,12 @@ PHP_MINIT_FUNCTION(stream_errors) php_ce_stream_error = register_class_StreamError(); php_ce_stream_exception = register_class_StreamException(zend_ce_exception); + php_stream_error_fiber_slot = zend_get_resource_handle("Stream errors"); + if (php_stream_error_fiber_slot >= 0) { + zend_observer_fiber_init_register(php_stream_error_fiber_init); + zend_observer_fiber_destroy_register(php_stream_error_fiber_destroy); + } + return SUCCESS; } diff --git a/main/streams/streams.c b/main/streams/streams.c index ae2d4b910627..3d95629aab45 100644 --- a/main/streams/streams.c +++ b/main/streams/streams.c @@ -249,6 +249,27 @@ static int _php_stream_free_persistent(zval *zv, void *pStream) static int php_stream_flush_ex(php_stream *stream, bool closing); +/* While the descriptor is still open: the watchers and the handle stop reporting, the + * registrations end with the provider's remove() per pair, then the stream drops its handle */ +static void php_stream_release_io(php_stream *stream) +{ + zend_object *handle_obj = stream->weak_poll_handle; + + if (stream->poll_watchers) { + php_io_poll_stream_notify_close(stream); + } + if (handle_obj) { + php_stream_poll_weak_handle_notify(handle_obj); + } + if (stream->io_registrations) { + php_io_unregister_all(&stream->io_registrations); + } + if (handle_obj) { + stream->weak_poll_handle = NULL; + OBJ_RELEASE(handle_obj); + } +} + PHPAPI int php_stream_free(php_stream *stream, int close_options) /* {{{ */ { int ret = 1; @@ -284,6 +305,13 @@ PHPAPI int php_stream_free(php_stream *stream, int close_options) /* {{{ */ #endif + /* Only the resource destructor cannot be refused; it runs with the resource cleared */ + if (UNEXPECTED(php_io_stream_busy(stream)) && !((close_options & PHP_STREAM_FREE_RSRC_DTOR) + && stream->res && stream->res->type < 0)) { + zend_throw_error(NULL, "Concurrent access to a stream"); + return EOF; + } + if (stream->in_free) { /* hopefully called recursively from the enclosing stream; the pointer was NULLed below */ if ((stream->in_free == 1) && (close_options & PHP_STREAM_FREE_IGNORE_ENCLOSING) && (stream->enclosing_stream == NULL)) { @@ -333,6 +361,12 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov (close_options & PHP_STREAM_FREE_RSRC_DTOR) == 0); #endif + /* An operation a queue kept past its frame must settle before the flush + * reuses the stream and the close frees the buffer */ + if ((close_options & PHP_STREAM_FREE_CALL_DTOR) && (stream->flags & PHP_STREAM_FLAG_IN_USE)) { + php_io_stream_drain(stream); + } + int flush_result; if (stream->flags & PHP_STREAM_FLAG_WAS_WRITTEN || stream->writefilters.head) { /* make sure everything is saved */ @@ -369,10 +403,7 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov return ret; } - /* Watchers must unregister while the fd is still open */ - if (stream->poll_watchers) { - php_io_poll_stream_notify_close(stream); - } + php_stream_release_io(stream); ret = stream->ops->close(stream, preserve_handle ? 0 : 1); if (!ret) { @@ -389,9 +420,7 @@ fprintf(stderr, "stream_free: %s:%p[%s] preserve_handle=%d release_cast=%d remov } if (close_options & PHP_STREAM_FREE_RELEASE_STREAM) { - if (stream->poll_watchers) { - php_io_poll_stream_notify_close(stream); - } + php_stream_release_io(stream); while (stream->readfilters.head) { if (stream->readfilters.head->res != NULL) { @@ -621,6 +650,16 @@ PHPAPI zend_result php_stream_fill_read_buffer(php_stream *stream, size_t size) return retval; } +PHPAPI void php_stream_read_buffer_commit(php_stream *stream, size_t len) +{ + if (len == 0) { + stream->eof = 1; + return; + } + ZEND_ASSERT(stream->readbuf && stream->writepos + len <= stream->readbuflen); + stream->writepos += len; +} + PHPAPI ssize_t php_stream_read(php_stream *stream, char *buf, size_t size) { ssize_t toread = 0, didread = 0; @@ -1684,11 +1723,17 @@ PHPAPI zend_result _php_stream_copy_to_stream_ex(php_stream *src, php_stream *de src->writepos == src->readpos && dest->writepos == dest->readpos && !php_stream_is_filtered(src) && !php_stream_is_filtered(dest) && !php_stream_has_notifier(src) && !php_stream_has_notifier(dest)) { - php_io_fd src_copy_fd, dest_copy_fd; + php_io_fd src_copy_fd = {0}, dest_copy_fd = {0}; if (php_stream_cast(src, PHP_STREAM_AS_FD_FOR_COPY, (void *) &src_copy_fd, 0) == SUCCESS && php_stream_cast(dest, PHP_STREAM_AS_FD_FOR_COPY, (void *) &dest_copy_fd, 0) == SUCCESS) { + /* Waits on a socket or pipe belong to the provider */ + if (php_io_hooks_active() + && (src_copy_fd.fd_type != PHP_IO_FD_FILE || dest_copy_fd.fd_type != PHP_IO_FD_FILE)) { + goto fallback; + } + /* copy_file_range does not work with O_APPEND */ if (src_copy_fd.fd_type == PHP_IO_FD_FILE && dest_copy_fd.fd_type == PHP_IO_FD_FILE) { int dest_flags = 0; @@ -1733,6 +1778,13 @@ PHPAPI size_t _php_stream_copy_to_stream(php_stream *src, php_stream *dest, size static void stream_resource_regular_dtor(zend_resource *rsrc) { php_stream *stream = (php_stream*)rsrc->ptr; + /* zend_list_close() while an op is suspended on the stream: the frame + * holds a reference, and the resource stays open */ + if (UNEXPECTED(php_io_stream_busy(stream)) && stream->res && GC_REFCOUNT(stream->res) > 0) { + stream->res->type = rsrc->type; + stream->res->ptr = stream; + return; + } /* set the return value for pclose */ FG(pclose_ret) = php_stream_free(stream, PHP_STREAM_FREE_CLOSE | PHP_STREAM_FREE_RSRC_DTOR); } @@ -2439,3 +2491,5 @@ PHPAPI int php_stream_scandir(const char *dirname, zend_string **namelist[], php efree(vector); return -1; } +/* }}} */ + diff --git a/main/streams/userspace.c b/main/streams/userspace.c index 9b6f283c075d..ed375fbc5eb9 100644 --- a/main/streams/userspace.c +++ b/main/streams/userspace.c @@ -1435,6 +1435,8 @@ static int php_userstreamop_cast(php_stream *stream, int castas, void **retptr) switch(castas) { case PHP_STREAM_AS_FD_FOR_SELECT: + case PHP_STREAM_AS_FD_FOR_POLL: + /* Userland knows STREAM_CAST_FOR_SELECT; the returned stream is cast as asked */ ZVAL_LONG(&args[0], PHP_STREAM_AS_FD_FOR_SELECT); break; default: diff --git a/main/streams/xp_socket.c b/main/streams/xp_socket.c index 3844414d8048..ac1b118021be 100644 --- a/main/streams/xp_socket.c +++ b/main/streams/xp_socket.c @@ -77,42 +77,27 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun else ptimeout = &sock->timeout; -retry: - didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), (sock->is_blocked && ptimeout) ? MSG_DONTWAIT : 0); + if (sock->is_blocked) { + php_deadline deadline = php_io_deadline_from_timeval(ptimeout); + sock->timeout_event = false; + didwrite = php_io_send(stream, sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0, &deadline); + } else { + didwrite = send(sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0); + } if (didwrite <= 0) { char *estr; int err = php_socket_errno(); - if (PHP_IS_TRANSIENT_ERROR(err)) { - if (sock->is_blocked) { - int retval; - - sock->timeout_event = false; - - do { - retval = php_pollfd_for(sock->socket, POLLOUT, ptimeout); - - if (retval == 0) { - sock->timeout_event = true; - break; - } - - if (retval > 0) { - /* writable now; retry */ - goto retry; - } - - err = php_socket_errno(); - } while (err == EINTR); - } else { - /* EWOULDBLOCK/EAGAIN is not an error for a non-blocking stream. - * Report zero byte write instead. */ - return 0; - } + if (err == PHP_IO_SOCK_ETIMEDOUT && sock->is_blocked) { + sock->timeout_event = true; + } else if (PHP_IS_TRANSIENT_ERROR(err)) { + /* EWOULDBLOCK/EAGAIN is not an error for a non-blocking stream. + * Report zero byte write instead. */ + return 0; } - if (!(stream->flags & PHP_STREAM_FLAG_SUPPRESS_ERRORS)) { + if (!(stream->flags & PHP_STREAM_FLAG_SUPPRESS_ERRORS) && !EG(exception)) { estr = php_socket_strerror(err, NULL, 0); php_stream_warn(stream, NetworkSendFailed, "Send of %zu bytes failed with errno=%d %s", count, err, estr); @@ -127,42 +112,6 @@ static ssize_t php_sockop_write(php_stream *stream, const char *buf, size_t coun return didwrite; } -static void php_sock_stream_wait_for_data(php_stream *stream, php_netstream_data_t *sock, bool has_buffered_data) -{ - int retval; - struct timeval *ptimeout, zero_timeout; - - if (!sock || sock->socket == -1) { - return; - } - - sock->timeout_event = false; - - if (has_buffered_data) { - /* If there is already buffered data, use no timeout. */ - zero_timeout.tv_sec = 0; - zero_timeout.tv_usec = 0; - ptimeout = &zero_timeout; - } else if (sock->timeout.tv_sec == -1) { - ptimeout = NULL; - } else { - ptimeout = &sock->timeout; - } - - while(1) { - retval = php_pollfd_for(sock->socket, PHP_POLLREADABLE, ptimeout); - - if (retval == 0) - sock->timeout_event = true; - - if (retval >= 0) - break; - - if (php_socket_errno() != EINTR) - break; - } -} - static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; @@ -171,37 +120,41 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) return -1; } - int recv_flags = 0; - /* Special handling for blocking read. */ + ssize_t nr_bytes; + int err; + + sock->timeout_event = false; + if (sock->is_blocked) { - /* Find out if there is any data buffered from the previous read. */ - bool has_buffered_data = stream->has_buffered_data; - /* No need to wait if there is any data buffered or no timeout. */ - bool dont_wait = has_buffered_data || - (sock->timeout.tv_sec == 0 && sock->timeout.tv_usec == 0); - /* Set MSG_DONTWAIT if no wait is needed or there is unlimited timeout which was - * added by fix for #41984 committed in 9343c5404. */ - if (dont_wait || sock->timeout.tv_sec != -1) { - recv_flags = MSG_DONTWAIT; + /* With data already buffered or a zero timeout, only check whether more is there */ + bool dont_wait = stream->has_buffered_data + || (sock->timeout.tv_sec == 0 && sock->timeout.tv_usec == 0); + + php_deadline deadline; + if (dont_wait) { + php_deadline_init_nonblock(&deadline); + } else { + deadline = php_io_deadline_from_timeval(&sock->timeout); } - /* If the wait is needed or it is a platform without MSG_DONTWAIT support (e.g. Windows), - * then poll for data. */ - if (!dont_wait || MSG_DONTWAIT == 0) { - php_sock_stream_wait_for_data(stream, sock, has_buffered_data); - if (sock->timeout_event) { - /* It is ok to timeout if there is any data buffered so return 0, otherwise -1. */ - return has_buffered_data ? 0 : -1; + + nr_bytes = php_io_recv(stream, sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0, &deadline); + err = php_socket_errno(); + if (nr_bytes < 0 && err == PHP_IO_SOCK_ETIMEDOUT) { + if (dont_wait) { + return 0; } + sock->timeout_event = true; + return -1; } + } else { + nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), 0); + err = php_socket_errno(); } - ssize_t nr_bytes = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(count), recv_flags); - int err = php_socket_errno(); - if (nr_bytes < 0) { if (PHP_IS_TRANSIENT_ERROR(err)) { nr_bytes = 0; - } else { + } else if (err != PHP_IO_SOCK_EINTR) { stream->eof = 1; } } else if (nr_bytes == 0) { @@ -219,20 +172,19 @@ static ssize_t php_sockop_read(php_stream *stream, char *buf, size_t count) static int php_sockop_close(php_stream *stream, int close_handle) { php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; -#ifdef PHP_WIN32 - int n; -#endif if (!sock) { return 0; } - if (close_handle) { - #ifdef PHP_WIN32 - if (sock->socket == -1) - sock->socket = SOCK_ERR; + if (sock->socket == -1) + sock->socket = SOCK_ERR; #endif + + if (!close_handle) { + php_netstream_restore_blocking(sock); + } else { if (sock->socket != SOCK_ERR) { #ifdef PHP_WIN32 /* prevent more data from coming in */ @@ -244,10 +196,11 @@ static int php_sockop_close(php_stream *stream, int close_handle) * We use a small timeout which should encourage the OS to send the data, * but at the same time avoid hanging indefinitely. * */ - do { - n = php_pollfd_for_ms(sock->socket, POLLOUT, 500); - } while (n == -1 && php_socket_errno() == EINTR); + /* A plain poll, not an op: fclose() may run outside any fiber a + * suspending provider could park it on */ + php_pollfd_for_ms(sock->socket, POLLOUT, 500); #endif + php_netstream_restore_blocking(sock); closesocket(sock->socket); sock->socket = SOCK_ERR; } @@ -279,26 +232,36 @@ static int php_sockop_stat(php_stream *stream, php_stream_statbuf *ssb) #endif } -static inline int sock_sendto(php_netstream_data_t *sock, const char *buf, size_t buflen, int flags, - struct sockaddr *addr, socklen_t addrlen +/* The descriptor is non-blocking: on a blocking stream (dl set) a call that + * would block waits for readiness as a Poll op */ +static inline int sock_sendto(php_stream *stream, php_netstream_data_t *sock, const char *buf, size_t buflen, int flags, + struct sockaddr *addr, socklen_t addrlen, php_deadline *dl ) { int ret; if (addr) { - ret = sendto(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, addr, XP_SOCK_BUF_SIZE(addrlen)); + ret = (int) php_io_sendto(stream, sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, addr, XP_SOCK_BUF_SIZE(addrlen), dl); return (ret == SOCK_CONN_ERR) ? -1 : ret; } -#ifdef PHP_WIN32 - return ((ret = send(sock->socket, buf, buflen > INT_MAX ? INT_MAX : (int)buflen, flags)) == SOCK_CONN_ERR) ? -1 : ret; -#else - return ((ret = send(sock->socket, buf, buflen, flags)) == SOCK_CONN_ERR) ? -1 : ret; -#endif + ret = (int) php_io_sendto(stream, sock->socket, buf, buflen > INT_MAX ? INT_MAX : buflen, flags, NULL, 0, dl); + return (ret == SOCK_CONN_ERR) ? -1 : ret; +} + +/* A blocking stream's transport call waits without a timeout, like the + * blocking syscall did; NULL for a non-blocking stream, which never waits */ +static inline php_deadline *sock_xport_deadline(php_netstream_data_t *sock, php_deadline *dl) +{ + if (!sock->is_blocked) { + return NULL; + } + php_deadline_init_infinite(dl); + return dl; } -static inline int sock_recvfrom(php_netstream_data_t *sock, char *buf, size_t buflen, int flags, +static inline int sock_recvfrom(php_stream *stream, php_netstream_data_t *sock, char *buf, size_t buflen, int flags, zend_string **textaddr, - struct sockaddr **addr, socklen_t *addrlen + struct sockaddr **addr, socklen_t *addrlen, php_deadline *dl ) { int ret; @@ -307,7 +270,7 @@ static inline int sock_recvfrom(php_netstream_data_t *sock, char *buf, size_t bu if (want_addr) { php_sockaddr_storage sa; socklen_t sl = sizeof(sa); - ret = recvfrom(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, (struct sockaddr*)&sa, &sl); + ret = (int) php_io_recvfrom(stream, sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, (struct sockaddr*)&sa, &sl, dl); ret = (ret == SOCK_CONN_ERR) ? -1 : ret; #ifdef PHP_WIN32 /* POSIX discards excess bytes without signalling failure; emulate this on Windows */ @@ -328,7 +291,7 @@ static inline int sock_recvfrom(php_netstream_data_t *sock, char *buf, size_t bu } } } else { - ret = recv(sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags); + ret = (int) php_io_recvfrom(stream, sock->socket, buf, XP_SOCK_BUF_SIZE(buflen), flags, NULL, NULL, dl); ret = (ret == SOCK_CONN_ERR) ? -1 : ret; } @@ -340,6 +303,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void int oldmode, flags; php_netstream_data_t *sock = (php_netstream_data_t*)stream->abstract; php_stream_xport_param *xparam; + php_deadline xport_dl; if (!sock) { return PHP_STREAM_OPTION_RETURN_NOTIMPL; @@ -372,7 +336,7 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void !(stream->flags & PHP_STREAM_FLAG_NO_IO) && ((MSG_DONTWAIT != 0) || !sock->is_blocked) ) || - php_pollfd_for(sock->socket, PHP_POLLREADABLE|POLLPRI, &tv) > 0 + php_io_poll_tv(stream, sock->socket, PHP_POLL_READ, &tv) > 0 ) { /* the poll() call was skipped if the socket is non-blocking (or MSG_DONTWAIT is available) and if the timeout is zero */ #ifdef PHP_WIN32 @@ -398,11 +362,8 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void case PHP_STREAM_OPTION_BLOCKING: oldmode = sock->is_blocked; - if (SUCCESS == php_set_sock_blocking(sock->socket, value)) { - sock->is_blocked = value; - return oldmode; - } - return PHP_STREAM_OPTION_RETURN_ERR; + sock->is_blocked = value; + return oldmode; case PHP_STREAM_OPTION_READ_TIMEOUT: sock->timeout = *(struct timeval*)ptrparam; @@ -444,11 +405,12 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void if ((xparam->inputs.flags & STREAM_OOB) == STREAM_OOB) { flags |= MSG_OOB; } - xparam->outputs.returncode = sock_sendto(sock, + xparam->outputs.returncode = sock_sendto(stream, sock, xparam->inputs.buf, xparam->inputs.buflen, flags, xparam->inputs.addr, - xparam->inputs.addrlen); + xparam->inputs.addrlen, + sock_xport_deadline(sock, &xport_dl)); if (xparam->outputs.returncode == -1) { char *err = php_socket_strerror(php_socket_errno(), NULL, 0); php_stream_warn(stream, NetworkSendFailed, "%s", err); @@ -464,13 +426,13 @@ static int php_sockop_set_option(php_stream *stream, int option, int value, void if ((xparam->inputs.flags & STREAM_PEEK) == STREAM_PEEK) { flags |= MSG_PEEK; } - xparam->outputs.returncode = sock_recvfrom(sock, + xparam->outputs.returncode = sock_recvfrom(stream, sock, xparam->inputs.buf, xparam->inputs.buflen, flags, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, - xparam->want_addr ? &xparam->outputs.addrlen : NULL - ); + xparam->want_addr ? &xparam->outputs.addrlen : NULL, + sock_xport_deadline(sock, &xport_dl)); return PHP_STREAM_OPTION_RETURN_OK; @@ -518,6 +480,7 @@ static int php_sockop_cast(php_stream *stream, int castas, void **ret) } return SUCCESS; case PHP_STREAM_AS_FD_FOR_SELECT: + case PHP_STREAM_AS_FD_FOR_POLL: case PHP_STREAM_AS_FD: case PHP_STREAM_AS_SOCKETD: if (ret) @@ -899,7 +862,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ parse_unix_address(stream, xparam, &unix_addr); - ret = php_network_connect_socket(sock->socket, + ret = php_network_connect_socket_stream(stream, sock->socket, (const struct sockaddr *)&unix_addr, (socklen_t) offsetof(struct sockaddr_un, sun_path) + xparam->inputs.namelen, xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, xparam->inputs.timeout, xparam->want_errortext ? &xparam->outputs.error_text : NULL, @@ -1005,7 +968,7 @@ static inline int php_tcp_sockop_connect(php_stream *stream, php_netstream_data_ * want the default to be TCP sockets so that the openssl extension can * re-use this code. */ - sock->socket = php_network_connect_socket_to_host_ex(host, portno, + sock->socket = php_network_connect_socket_to_host_stream(stream, &sock->socket, host, portno, PHP_STREAM_XPORT_IS_UDP(stream) ? SOCK_DGRAM : SOCK_STREAM, xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC, xparam->inputs.timeout, @@ -1060,7 +1023,7 @@ static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t } } - php_socket_t clisock = php_network_accept_incoming_ex(sock->socket, + php_socket_t clisock = php_network_accept_incoming_stream_ex(stream, sock->socket, xparam->want_textaddr ? &xparam->outputs.textaddr : NULL, xparam->want_addr ? &xparam->outputs.addr : NULL, xparam->want_addr ? &xparam->outputs.addrlen : NULL, @@ -1074,10 +1037,10 @@ static inline int php_tcp_sockop_accept(php_stream *stream, php_netstream_data_t memcpy(clisockdata, sock, sizeof(*clisockdata)); clisockdata->socket = clisock; -#ifdef __linux__ - /* O_NONBLOCK is not inherited on Linux */ clisockdata->is_blocked = true; -#endif + php_netstream_set_nonblocking(clisockdata); + /* accepted by us, non-blocking already when the ring accepted it */ + clisockdata->restore_blocking = true; xparam->outputs.client = php_stream_alloc_rel(stream->ops, clisockdata, NULL, "r+"); if (xparam->outputs.client) { @@ -1104,10 +1067,20 @@ static int php_tcp_sockop_set_option(php_stream *stream, int option, int value, case STREAM_XPORT_OP_CONNECT: case STREAM_XPORT_OP_CONNECT_ASYNC: xparam->outputs.returncode = php_tcp_sockop_connect(stream, sock, xparam); + if (xparam->outputs.returncode == 0) { + php_netstream_set_nonblocking(sock); + if (xparam->op == STREAM_XPORT_OP_CONNECT_ASYNC && sock->socket != SOCK_ERR) { + /* our own socket, left non-blocking by the async connect */ + sock->restore_blocking = true; + } + } return PHP_STREAM_OPTION_RETURN_OK; case STREAM_XPORT_OP_BIND: xparam->outputs.returncode = php_tcp_sockop_bind(stream, sock, xparam); + if (xparam->outputs.returncode == 0) { + php_netstream_set_nonblocking(sock); + } return PHP_STREAM_OPTION_RETURN_OK; diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 778a03328238..0e0510bc7372 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -5,7 +5,7 @@ COMMON_LDFLAGS = ../../.libs/libphp.a -lcmocka -lpthread -lm -ldl -lresolv -luti # Update paths in .github/workflows/unit-tests.yml when adding new test to make it run in PR when such file changes TESTS = main/test_network main/test_io_copy main/test_network_SRC = main/test_network.c -main/test_network_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=connect,--wrap=poll,--wrap=getsockopt,--wrap=gettimeofday +main/test_network_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=connect,--wrap=poll,--wrap=getsockopt main/test_io_copy_SRC = main/test_io_copy.c main/test_io_copy_LDFLAGS = $(COMMON_LDFLAGS) -Wl,--wrap=copy_file_range,--wrap=sendfile,--wrap=splice,--wrap=read,--wrap=write,--wrap=poll,--wrap=setsockopt diff --git a/tests/unit/main/test_network.c b/tests/unit/main/test_network.c index 3e0e6e37ed98..a92fcdf1fa70 100644 --- a/tests/unit/main/test_network.c +++ b/tests/unit/main/test_network.c @@ -1,7 +1,11 @@ #include "php.h" #include "php_network.h" +#include "sapi/embed/php_embed.h" #include +/* The connect waits on the core's operation queue, which needs a started + * engine; the poll(2) it ends in is mocked */ + // Mocked poll int __wrap_poll(struct pollfd *ufds, nfds_t nfds, int timeout) { @@ -10,12 +14,12 @@ int __wrap_poll(struct pollfd *ufds, nfds_t nfds, int timeout) int n = mock_type(int); if (n > 0) { - ufds->revents = 1; + ufds->revents = ufds->events; } else if (n < 0) { errno = -n; n = -1; } - + return n; } @@ -36,18 +40,6 @@ int __wrap_getsockopt(int fd, int level, int optname, void *optval, socklen_t *o return mock_type(int); } -// Mocked gettimeofday -int __wrap_gettimeofday(struct timeval *time_Info, struct timezone *timezone_Info) -{ - function_called(); - struct timeval *now = mock_ptr_type(struct timeval *); - if (now) { - time_Info->tv_sec = now->tv_sec; - time_Info->tv_usec = now->tv_usec; - } - return mock_type(int); -} - // Test successful connection static void test_php_network_connect_socket_immediate_success(void **state) { struct timeval timeout = { .tv_sec = 2, .tv_usec = 500000 }; @@ -73,14 +65,9 @@ static void test_php_network_connect_socket_progress_success(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock time setting - ignored - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, NULL); - will_return(__wrap_gettimeofday, 0); - // Mock poll to return success expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 2500); + expect_in_range(__wrap_poll, timeout, 2400, 2500); will_return(__wrap_poll, 1); // Mock no socket error @@ -94,10 +81,9 @@ static void test_php_network_connect_socket_progress_success(void **state) { assert_int_equal(error_code, 0); } -static void test_php_network_connect_socket_eintr_t1(void **state) { +// Test a poll interrupted by a signal restarting with the time left +static void test_php_network_connect_socket_eintr(void **state) { struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 500000 }; - struct timeval start_time = { .tv_sec = 1000, .tv_usec = 0 }; // Initial time - struct timeval retry_time = { .tv_sec = 1001, .tv_usec = 200000 }; // Time after EINTR php_socket_t sockfd = 12; int error_code = 0; @@ -105,24 +91,14 @@ static void test_php_network_connect_socket_eintr_t1(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock gettimeofday for initial call - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &start_time); - will_return(__wrap_gettimeofday, 0); - // Mock poll to return EINTR first expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 2500); + expect_in_range(__wrap_poll, timeout, 2400, 2500); will_return(__wrap_poll, -EINTR); - // Mock gettimeofday after EINTR - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &retry_time); - will_return(__wrap_gettimeofday, 0); - // Mock poll to succeed on retry expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 1300); + expect_in_range(__wrap_poll, timeout, 2400, 2500); will_return(__wrap_poll, 1); // Mock no socket error @@ -137,10 +113,9 @@ static void test_php_network_connect_socket_eintr_t1(void **state) { assert_int_equal(error_code, 0); } -static void test_php_network_connect_socket_eintr_t2(void **state) { +// Test microseconds beyond a second counting towards the timeout +static void test_php_network_connect_socket_usec_overflow(void **state) { struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 1500000 }; - struct timeval start_time = { .tv_sec = 1000, .tv_usec = 300000 }; // Initial time - struct timeval retry_time = { .tv_sec = 1001, .tv_usec = 200000 }; // Time after EINTR php_socket_t sockfd = 12; int error_code = 0; @@ -148,24 +123,9 @@ static void test_php_network_connect_socket_eintr_t2(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock gettimeofday for initial call - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &start_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to return EINTR first - expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 3500); - will_return(__wrap_poll, -EINTR); - - // Mock gettimeofday after EINTR - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &retry_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to succeed on retry + // Mock poll to succeed expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 2600); + expect_in_range(__wrap_poll, timeout, 3400, 3500); will_return(__wrap_poll, 1); // Mock no socket error @@ -180,10 +140,9 @@ static void test_php_network_connect_socket_eintr_t2(void **state) { assert_int_equal(error_code, 0); } -static void test_php_network_connect_socket_eintr_t3(void **state) { +// Test a connect in progress that fails +static void test_php_network_connect_socket_progress_error(void **state) { struct timeval timeout_tv = { .tv_sec = 2, .tv_usec = 500000 }; - struct timeval start_time = { .tv_sec = 1002, .tv_usec = 300000 }; // Initial time - struct timeval retry_time = { .tv_sec = 1001, .tv_usec = 2200000 }; // Time after EINTR php_socket_t sockfd = 12; int error_code = 0; @@ -191,36 +150,20 @@ static void test_php_network_connect_socket_eintr_t3(void **state) { expect_function_call(__wrap_connect); will_return(__wrap_connect, EINPROGRESS); - // Mock gettimeofday for initial call - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &start_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to return EINTR first + // Mock poll to report the outcome expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 2500); - will_return(__wrap_poll, -EINTR); - - // Mock gettimeofday after EINTR - expect_function_call(__wrap_gettimeofday); - will_return(__wrap_gettimeofday, &retry_time); - will_return(__wrap_gettimeofday, 0); - - // Mock poll to succeed on retry - expect_function_call(__wrap_poll); - expect_value(__wrap_poll, timeout, 1600); + expect_in_range(__wrap_poll, timeout, 2400, 2500); will_return(__wrap_poll, 1); - // Mock no socket error + // Mock the socket error expect_function_call(__wrap_getsockopt); - will_return(__wrap_getsockopt, 0); // optval saved result + will_return(__wrap_getsockopt, ECONNREFUSED); // optval saved result will_return(__wrap_getsockopt, 0); // actual return value int result = php_network_connect_socket(sockfd, NULL, 0, 0, &timeout_tv, NULL, &error_code); - // Ensure the function succeeds - assert_int_equal(result, 0); - assert_int_equal(error_code, 0); + assert_int_equal(result, -1); + assert_int_equal(error_code, ECONNREFUSED); } // Test connection error (ECONNREFUSED) @@ -240,15 +183,23 @@ static void test_php_network_connect_socket_connect_error(void **state) { assert_int_equal(error_code, ECONNREFUSED); } +static int setup(void **state) { + return php_embed_init(0, NULL) == SUCCESS ? 0 : -1; +} + +static int teardown(void **state) { + php_embed_shutdown(); + return 0; +} int main(void) { const struct CMUnitTest tests[] = { cmocka_unit_test(test_php_network_connect_socket_immediate_success), cmocka_unit_test(test_php_network_connect_socket_progress_success), - cmocka_unit_test(test_php_network_connect_socket_eintr_t1), - cmocka_unit_test(test_php_network_connect_socket_eintr_t2), - cmocka_unit_test(test_php_network_connect_socket_eintr_t3), + cmocka_unit_test(test_php_network_connect_socket_eintr), + cmocka_unit_test(test_php_network_connect_socket_usec_overflow), + cmocka_unit_test(test_php_network_connect_socket_progress_error), cmocka_unit_test(test_php_network_connect_socket_connect_error), }; - return cmocka_run_group_tests(tests, NULL, NULL); + return cmocka_run_group_tests(tests, setup, teardown); } diff --git a/win32/build/config.w32 b/win32/build/config.w32 index 253dab653986..ac67e7791712 100644 --- a/win32/build/config.w32 +++ b/win32/build/config.w32 @@ -298,12 +298,34 @@ AC_DEFINE('HAVE_STRNLEN', 1); AC_DEFINE('ZEND_CHECK_STACK_LIMIT', 1) -ADD_SOURCES("main/io", "php_io.c php_io_copy_windows.c"); -ADD_FLAG("CFLAGS_BD_MAIN_IO", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); - -ADD_SOURCES("main/poll", "poll_backend_wsapoll.c poll_core.c poll_fd_table.c poll_handle.c"); +ADD_SOURCES("main/poll", "poll_backend_wsapoll.c poll_core.c poll_fd_table.c poll_handle.c poll_source.c"); ADD_FLAG("CFLAGS_BD_MAIN_POLL", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); +/* Temporary, for the IO hooks proof of concept: the ring (php_io_ring.c) is + * built on the ior library, which is not bundled yet. --with-ior= + * takes an install prefix (cmake --install --prefix ) holding + * include\ior\ior.h and lib\ior.lib, a bare --with-ior looks in the + * dependencies (--with-php-build) and the usual places. The header is + * included as , the way its pkg-config file sets the include path. */ +ARG_WITH("ior", "Build the IO ring on the ior library, optionally from the given install prefix", "no"); + +var main_io_sources = "php_io.c php_io_copy_windows.c php_io_hooks.c php_io_queue_poll.c"; +if (PHP_IOR != "no") { + var ior_prefix = PHP_IOR != "yes" ? PHP_IOR : null; + var ior_include = (ior_prefix ? ior_prefix + "\\include\\ior;" : "") + PHP_PHP_BUILD + "\\include\\ior"; + if (CHECK_LIB("ior.lib", null, ior_prefix) && + CHECK_HEADER("ior.h", "CFLAGS", ior_include)) { + AC_DEFINE("HAVE_IOR", 1, "Define to 1 if the ior library is available."); + main_io_sources += " php_io_ring.c"; + } else if (ior_prefix) { + ERROR("ior not found in " + ior_prefix + ": it needs include\\ior\\ior.h and lib\\ior.lib there"); + } else { + ERROR("ior not found in " + PHP_PHP_BUILD + " (include\\ior\\ior.h, lib\\ior.lib); pass --with-ior= for another install prefix"); + } +} +ADD_SOURCES("main/io", main_io_sources); +ADD_FLAG("CFLAGS_BD_MAIN_IO", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); + ADD_SOURCES("main/streams", "streams.c stream_errors.c cast.c memory.c filter.c plain_wrapper.c \ userspace.c transports.c xp_socket.c mmap.c glob_wrapper.c"); ADD_FLAG("CFLAGS_BD_MAIN_STREAMS", "/D ZEND_ENABLE_STATIC_TSRMLS_CACHE=1"); diff --git a/win32/ioutil.c b/win32/ioutil.c index 8ac4d39d67cf..ad49698d07b4 100644 --- a/win32/ioutil.c +++ b/win32/ioutil.c @@ -336,6 +336,10 @@ PW32IO BOOL php_win32_ioutil_posix_to_open_opts(int flags, mode_t mode, php_iout opts->attributes &= ~FILE_FLAG_OVERLAPPED; }*/ + if (flags & PHP_WIN32_IOUTIL_O_OVERLAPPED) { + opts->attributes |= FILE_FLAG_OVERLAPPED; + } + /* Setting this flag makes it possible to open a directory. */ /* XXX not being done as this means a behavior change. Should be evaluated properly. */ /* opts->attributes |= FILE_FLAG_BACKUP_SEMANTICS; */ @@ -391,7 +395,7 @@ PW32IO int php_win32_ioutil_open_w(const wchar_t *path, int flags, ...) return -1; } - fd = _open_osfhandle((intptr_t) file, flags); + fd = _open_osfhandle((intptr_t) file, flags & ~PHP_WIN32_IOUTIL_O_OVERLAPPED); if (fd < 0) { DWORD error = GetLastError(); @@ -1376,3 +1380,72 @@ PW32IO ssize_t php_win32_ioutil_readlink_w(const wchar_t *path, wchar_t *buf, si return ret; }/*}}}*/ + +/* Overlapped read and write at an offset, see ioutil.h */ +static ssize_t php_win32_ioutil_overlapped_io(int fd, void *buf, size_t len, int64_t offset, bool write_op) +{/*{{{*/ + HANDLE h = (HANDLE) _get_osfhandle(fd); + if (h == INVALID_HANDLE_VALUE) { + _set_errno(EBADF); + return -1; + } + + OVERLAPPED ov; + memset(&ov, 0, sizeof(ov)); + if (offset < 0) { + /* Append, for WriteFile; ReadFile rejects it */ + ov.Offset = 0xFFFFFFFF; + ov.OffsetHigh = 0xFFFFFFFF; + } else { + ov.Offset = (DWORD) ((uint64_t) offset & 0xFFFFFFFF); + ov.OffsetHigh = (DWORD) ((uint64_t) offset >> 32); + } + + /* One event per thread, the call blocks until the operation completes; the system resets it + * as each operation starts. Kept for the thread's lifetime, it is not worth a syscall per op. */ + static __declspec(thread) HANDLE event = NULL; + if (!event) { + event = CreateEventW(NULL, TRUE, FALSE, NULL); + if (!event) { + SET_ERRNO_FROM_WIN32_CODE(GetLastError()); + return -1; + } + } + /* The low bit keeps the completion off a port the handle is bound to */ + ov.hEvent = (HANDLE) ((ULONG_PTR) event | 1); + + DWORD want = len > INT_MAX ? INT_MAX : (DWORD) len; + DWORD done = 0; + BOOL ok = write_op + ? WriteFile(h, buf, want, NULL, &ov) + : ReadFile(h, buf, want, NULL, &ov); + DWORD err = ok ? ERROR_SUCCESS : GetLastError(); + if (ok || err == ERROR_IO_PENDING) { + ok = GetOverlappedResult(h, &ov, &done, TRUE); + err = ok ? ERROR_SUCCESS : GetLastError(); + } + + if (ok) { + return (ssize_t) done; + } + if (err == ERROR_HANDLE_EOF || err == ERROR_BROKEN_PIPE) { + return 0; + } + if (err == ERROR_ACCESS_DENIED) { + /* The handle was not opened for this direction: EBADF, as the CRT reports it */ + _set_errno(EBADF); + return -1; + } + SET_ERRNO_FROM_WIN32_CODE(err); + return -1; +}/*}}}*/ + +PW32IO ssize_t php_win32_ioutil_pread(int fd, void *buf, size_t len, int64_t offset) +{/*{{{*/ + return php_win32_ioutil_overlapped_io(fd, buf, len, offset, false); +}/*}}}*/ + +PW32IO ssize_t php_win32_ioutil_pwrite(int fd, const void *buf, size_t len, int64_t offset) +{/*{{{*/ + return php_win32_ioutil_overlapped_io(fd, (void *) buf, len, offset, true); +}/*}}}*/ diff --git a/win32/ioutil.h b/win32/ioutil.h index 81dd3e6f44ef..81a17a041b6b 100644 --- a/win32/ioutil.h +++ b/win32/ioutil.h @@ -326,6 +326,21 @@ zend_always_inline static int php_win32_ioutil_access(const char *path, mode_t m return ret; }/*}}}*/ +/* An extra open flag, outside the CRT's _O_* range: open the file with + * FILE_FLAG_OVERLAPPED so an I/O completion port can complete operations + * on it. Such a descriptor keeps no file position: read and write it with + * php_win32_ioutil_pread() and php_win32_ioutil_pwrite(), never with the + * CRT read() and write(). */ +#define PHP_WIN32_IOUTIL_O_OVERLAPPED 0x00100000 + +/* ReadFile and WriteFile at an explicit offset, waiting for the result, + * on a descriptor opened with PHP_WIN32_IOUTIL_O_OVERLAPPED (a synchronous + * one works too). A negative write offset appends. 0 at end of file, -1 + * with errno on failure. The completion never reaches a completion port + * the handle is bound to. */ +PW32IO ssize_t php_win32_ioutil_pread(int fd, void *buf, size_t len, int64_t offset); +PW32IO ssize_t php_win32_ioutil_pwrite(int fd, const void *buf, size_t len, int64_t offset); + zend_always_inline static int php_win32_ioutil_open(const char *path, int flags, ...) {/*{{{*/ mode_t mode = 0;