diff --git a/NEWS b/NEWS index aa752e0af386..b5fc9501d343 100644 --- a/NEWS +++ b/NEWS @@ -91,6 +91,8 @@ PHP NEWS file fails after it was already truncated. (Ilia Alshanetsky) - Standard: + . Fixed bug GH-24399 (usleep() and time_nanosleep() ignore queued APCs on + Windows). (Nicolas Grekas) . Fixed chown() and lchown() failing to resolve user names in ZTS builds when getpwnam_r() needs a larger buffer. (Ilia Alshanetsky) . Fixed password_get_info() and password_needs_rehash() accepting malformed diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index ae1a1ef8666f..f9ab71cf6d70 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -36,6 +36,7 @@ #include "zend_attributes.h" #include "zend_enum.h" #include "zend_ini.h" +#include "zend_hrtime.h" #include "zend_operators.h" #include "ext/standard/php_dns.h" #include "ext/standard/php_uuencode.h" @@ -1200,8 +1201,9 @@ PHP_FUNCTION(time_sleep_until) { double target_secs; struct timeval tm; - struct timespec php_req, php_rem; + struct timespec php_req; uint64_t current_ns, target_ns, diff_ns; + zend_hrtime_t start; const uint64_t ns_per_sec = 1000000000; const double top_target_sec = (double)(UINT64_MAX / ns_per_sec); @@ -1212,6 +1214,7 @@ PHP_FUNCTION(time_sleep_until) if (gettimeofday((struct timeval *) &tm, NULL) != 0) { RETURN_FALSE; } + start = zend_hrtime(); if (UNEXPECTED(!(target_secs >= 0 && target_secs <= top_target_sec))) { zend_argument_value_error(1, "must be between 0 and %" PRIu64, (uint64_t)top_target_sec); @@ -1229,13 +1232,17 @@ PHP_FUNCTION(time_sleep_until) 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 { + while (nanosleep(&php_req, NULL)) { + if (errno != EINTR) { RETURN_FALSE; } + zend_hrtime_t elapsed = zend_hrtime() - start; + if (elapsed >= diff_ns) { + break; + } + uint64_t remaining_ns = diff_ns - elapsed; + php_req.tv_sec = (time_t) (remaining_ns / ns_per_sec); + php_req.tv_nsec = (long) (remaining_ns % ns_per_sec); } RETURN_TRUE; diff --git a/ext/standard/tests/general_functions/sleep_alertable_win32.phpt b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt new file mode 100644 index 000000000000..55da154ccf75 --- /dev/null +++ b/ext/standard/tests/general_functions/sleep_alertable_win32.phpt @@ -0,0 +1,78 @@ +--TEST-- +Sleep functions process queued APCs on Windows +--EXTENSIONS-- +ffi +--SKIPIF-- + +--INI-- +ffi.enable=1 +--FILE-- +cast('GetCurrentThread_t', $kernel32->GetCurrentThread); +$QueueUserAPC = $ffi->cast('QueueUserAPC_t', $kernel32->QueueUserAPC); +$SleepEx = $ffi->cast('SleepEx_t', $kernel32->SleepEx); + +$called = 0; +$callback = static function (int $data) use (&$called): void { + $called = $data; + // The time spent in APCs is not time slept + usleep(300000); +}; + +echo "usleep()\n"; +var_dump($QueueUserAPC($callback, $GetCurrentThread(), 1) !== 0); +var_dump(usleep(1000000)); +var_dump($called === 1); + +echo "time_nanosleep()\n"; +var_dump($QueueUserAPC($callback, $GetCurrentThread(), 2) !== 0); +var_dump(time_nanosleep(2, 0)); +var_dump($called === 2); + +echo "time_sleep_until()\n"; +var_dump($QueueUserAPC($callback, $GetCurrentThread(), 3) !== 0); +$deadline = microtime(true) + 0.5; +var_dump(time_sleep_until($deadline)); +var_dump($called === 3); +$now = microtime(true); +var_dump($now + 0.002 >= $deadline); +var_dump($now < $deadline + 0.25); + +$SleepEx(0, 1); +?> +--EXPECT-- +usleep() +bool(true) +NULL +bool(true) +time_nanosleep() +bool(true) +array(2) { + ["seconds"]=> + int(2) + ["nanoseconds"]=> + int(0) +} +bool(true) +time_sleep_until() +bool(true) +bool(true) +bool(true) +bool(true) +bool(true) diff --git a/win32/time.c b/win32/time.c index ac1abbc9e9c7..192d2fa34019 100644 --- a/win32/time.c +++ b/win32/time.c @@ -22,6 +22,7 @@ #include #include #include "php_win32_globals.h" +#include "zend_hrtime.h" static zend_always_inline void getfilesystemtime(struct timeval *tv) {/*{{{*/ @@ -63,17 +64,44 @@ PHPAPI int gettimeofday(struct timeval *time_Info, struct timezone *timezone_Inf return 0; }/*}}}*/ +static DWORD wait_timer_alertable(HANDLE timer) +{/*{{{*/ + DWORD result; + +#ifdef ZTS + /* Threads that PHP does not manage have no executor globals, and their APCs cannot run PHP code */ + if (!tsrm_is_managed_thread()) { + return WaitForSingleObjectEx(timer, INFINITE, TRUE); + } +#endif + + /* APCs can run PHP code that bails out. Close the timer before propagating the bailout. */ + zend_try { + result = WaitForSingleObjectEx(timer, INFINITE, TRUE); + } zend_catch { + CloseHandle(timer); + zend_bailout(); + } zend_end_try(); + + return result; +}/*}}}*/ + PHPAPI int usleep(unsigned int useconds) {/*{{{*/ HANDLE timer; LARGE_INTEGER due; + DWORD result; due.QuadPart = -(10 * (__int64)useconds); timer = CreateWaitableTimer(NULL, TRUE, NULL); SetWaitableTimer(timer, &due, 0, NULL, NULL, 0); - WaitForSingleObject(timer, INFINITE); + result = wait_timer_alertable(timer); CloseHandle(timer); + if (result == WAIT_IO_COMPLETION) { + errno = EINTR; + return -1; + } return 0; }/*}}}*/ @@ -84,5 +112,30 @@ PHPAPI int nanosleep( const struct timespec * rqtp, struct timespec * rmtp ) errno = EINVAL; return -1; } - return usleep( rqtp->tv_sec * 1000000 + rqtp->tv_nsec / 1000 ); + /* Run already queued APCs first, so that their runtime doesn't count as time slept */ + if (SleepEx(0, TRUE) == WAIT_IO_COMPLETION) { + if (rmtp != NULL) { + *rmtp = *rqtp; + } + errno = EINTR; + return -1; + } + zend_hrtime_t start = zend_hrtime(); + int result = usleep(rqtp->tv_sec * 1000000 + rqtp->tv_nsec / 1000); + if (result == -1 && errno == EINTR && rmtp != NULL) { + zend_hrtime_t elapsed = zend_hrtime() - start; + time_t seconds = rqtp->tv_sec - (time_t) (elapsed / ZEND_NANO_IN_SEC); + long nanoseconds = rqtp->tv_nsec - (long) (elapsed % ZEND_NANO_IN_SEC); + if (nanoseconds < 0) { + seconds--; + nanoseconds += (long) ZEND_NANO_IN_SEC; + } + if (seconds < 0) { + seconds = 0; + nanoseconds = 0; + } + rmtp->tv_sec = seconds; + rmtp->tv_nsec = nanoseconds; + } + return result; }/*}}}*/