Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 13 additions & 6 deletions ext/standard/basic_functions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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);

Expand All @@ -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);
Expand All @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions ext/standard/tests/general_functions/sleep_alertable_win32.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
--TEST--
Sleep functions process queued APCs on Windows
--EXTENSIONS--
ffi
--SKIPIF--
<?php
if (PHP_OS_FAMILY !== 'Windows') die('skip Windows only');
?>
--INI--
ffi.enable=1
--FILE--
<?php
$callconv = PHP_INT_SIZE === 4 ? '__stdcall' : '';
// kernel32.dll exports undecorated names: resolve them with the default
// calling convention, then cast them to their actual calling convention
$kernel32 = FFI::cdef(<<<C
void *GetCurrentThread(void);
unsigned long QueueUserAPC(void *callback, void *thread, uintptr_t data);
unsigned long SleepEx(unsigned long milliseconds, int alertable);
C, 'kernel32.dll');
$ffi = FFI::cdef(<<<C
typedef void ($callconv *PAPCFUNC)(uintptr_t);
typedef void *($callconv *GetCurrentThread_t)(void);
typedef unsigned long ($callconv *QueueUserAPC_t)(PAPCFUNC callback, void *thread, uintptr_t data);
typedef unsigned long ($callconv *SleepEx_t)(unsigned long milliseconds, int alertable);
C);
$GetCurrentThread = $ffi->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)
57 changes: 55 additions & 2 deletions win32/time.c
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <mmsystem.h>
#include <errno.h>
#include "php_win32_globals.h"
#include "zend_hrtime.h"

static zend_always_inline void getfilesystemtime(struct timeval *tv)
{/*{{{*/
Expand Down Expand Up @@ -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;
}/*}}}*/

Expand All @@ -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);
Comment on lines +126 to +128

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A queued APC with a 1s callback makes time_nanosleep(2, 0) report ~1s remaining although no sleep occurred. Please exclude callback time from the remainder while preserving time_sleep_until() deadline.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done for APCs that are already queued when the sleep starts. nanosleep() now runs them first and reports the full duration as remaining.

An APC that arrives during the sleep runs before the wait returns. I don't see a way to separate its runtime from the time slept.

time_sleep_until() now resumes against its deadline instead of using the remainder.

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;
}/*}}}*/
Loading