diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 6e923d20ecbd..4f8161fd1f80 100644 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -1113,18 +1113,56 @@ PHP_FUNCTION(flush) /* {{{ Delay for a given number of seconds */ PHP_FUNCTION(sleep) { - zend_long num; + zval* num; ZEND_PARSE_PARAMETERS_START(1, 1) - Z_PARAM_LONG(num) + Z_PARAM_NUMBER(num) ZEND_PARSE_PARAMETERS_END(); - - if (num < 0) { + if (Z_TYPE_P(num) == IS_DOUBLE) { + const double seconds = Z_DVAL_P(num); + if (UNEXPECTED(seconds < 0)) { + zend_argument_value_error(1, "must be greater than or equal to 0"); + RETURN_THROWS(); + } +#ifdef HAVE_NANOSLEEP + time_t seconds_long = (time_t)seconds; + zend_long fraction_nanoseconds = (zend_long)((seconds - seconds_long) * 1000000000); + if (fraction_nanoseconds > 999999999) { + // for this to happen, you have to request to sleep for longer than + // 0.999999999 seconds and yet less than 1 second.. + // nanosleep() has a documented limit of 999999999 nanoseconds, so let's just round it up to 1 second. + // that means we'll be off by <=0.9 nanoseconds in this edge-case, probably close enough. + fraction_nanoseconds = 0; + seconds_long += 1; + } + struct timespec php_req, php_rem; + php_req.tv_sec = (time_t)seconds_long; + php_req.tv_nsec = fraction_nanoseconds; + const int result = nanosleep(&php_req, &php_rem); + if (UNEXPECTED(result == -1)) { + ZEND_ASSERT(errno != EINVAL); // this should be impossible, we carefully checked the input above + // it's probably EINTR + RETURN_DOUBLE(php_rem.tv_sec + (((double)php_rem.tv_nsec) / 1000000000.0)); + } + RETURN_LONG(0); +#elif defined(HAVE_USLEEP) + const unsigned int fraction_microseconds = (unsigned int)((seconds - (unsigned int)seconds) * 1000000); + if (fraction_microseconds > 0) { + usleep(fraction_microseconds); + } + RETURN_LONG(php_sleep((unsigned int)seconds)); +#else + // avoid -Werror=unreachable-code + RETURN_LONG(php_sleep((unsigned int)seconds)); +#endif + } + ZEND_ASSERT(Z_TYPE_P(num) == IS_LONG); // Z_PARAM_NUMBER(num) above guarantee that it's double or float or throw :) + zend_long seconds = Z_LVAL_P(num); + if (UNEXPECTED(seconds < 0)) { zend_argument_value_error(1, "must be greater than or equal to 0"); RETURN_THROWS(); } - - RETURN_LONG(php_sleep((unsigned int)num)); + RETURN_LONG(php_sleep((unsigned int)seconds)); } /* }}} */ diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index c3adaceff439..170ba0f424d6 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1966,7 +1966,7 @@ function getopt(string $short_options, array $long_options = [], &$rest_index = function flush(): void {} -function sleep(int $seconds): int {} +function sleep(int|float $seconds): int|float {} function usleep(int $microseconds): void {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index c420e5c29e0f..1fe783da3808 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -405,8 +405,8 @@ ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_flush, 0, 0, IS_VOID, 0) ZEND_END_ARG_INFO() -ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_sleep, 0, 1, IS_LONG, 0) - ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_MASK_EX(arginfo_sleep, 0, 1, MAY_BE_LONG|MAY_BE_DOUBLE) + ZEND_ARG_TYPE_MASK(0, seconds, MAY_BE_LONG|MAY_BE_DOUBLE, NULL) ZEND_END_ARG_INFO() ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_usleep, 0, 1, IS_VOID, 0) diff --git a/ext/standard/tests/general_functions/sleep_basic.phpt b/ext/standard/tests/general_functions/sleep_basic.phpt index 98402ad8b21f..276ecde59659 100644 --- a/ext/standard/tests/general_functions/sleep_basic.phpt +++ b/ext/standard/tests/general_functions/sleep_basic.phpt @@ -8,6 +8,16 @@ if (getenv("SKIP_SLOW_TESTS")) die("skip slow test"); = 0.009; // 0.009 is 9ms, we asked usleep to sleep for 10ms +} + + $sleeptime = 1; // sleep for 1 seconds set_time_limit(20); @@ -31,9 +41,31 @@ if ($time >= $sleeplow) { } else { echo "TEST FAILED - time is {$time} secs and sleep was {$sleeptime} secs\n"; } +if (!have_usleep()) { + // ¯\_(ツ)_/¯ + echo "Fractional sleep return value: 0\n"; + echo "FRACTIONAL SLEEP TEST PASSED\n"; +} else { + $time = microtime(true); + $result = sleep(0.1); + $time = microtime(true) - $time; + echo "Fractional sleep return value: " . $result . "\n"; + if ($time >= 0.09) { + echo "FRACTIONAL SLEEP TEST PASSED\n"; + } else { + var_dump([ + 'time' => $time, + 'result' => $result, + 'expected_time' => 0.1 + ]); + echo "FRACTIONAL SLEEP TEST FAILED\n"; + } +} ?> --EXPECTF-- *** Testing sleep() : basic functionality *** Thread slept for %f seconds Return value: 0 TEST PASSED +Fractional sleep return value: 0 +FRACTIONAL SLEEP TEST PASSED