diff --git a/DESCRIPTION b/DESCRIPTION index b78af65a..047507b1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -22,7 +22,7 @@ BugReports: https://github.com/r-lib/processx/issues Depends: R (>= 3.4.0) Imports: - ps (>= 1.2.0), + ps (>= 1.9.2.9001), R6, utils Suggests: @@ -37,6 +37,7 @@ Suggests: testthat (>= 3.0.0), webfakes, withr +Remotes: r-lib/ps Config/Needs/website: tidyverse/tidytemplate Config/testthat/edition: 3 Config/usethis/last-upkeep: 2025-04-25 diff --git a/NEWS.md b/NEWS.md index 32a3233a..25070816 100644 --- a/NEWS.md +++ b/NEWS.md @@ -42,6 +42,14 @@ and `conn_read_bytes()` function for reading raw bytes from a processx connection directly (#406). +* On Linux, `process$get_start_time()` now returns the correct wall-clock + start time. Previously it was systematically ~0.3–0.5 s too early because + the boot time was read from `/proc/stat btime`, which is truncated to whole + seconds. processx now derives the boot time from + `CLOCK_REALTIME − CLOCK_MONOTONIC`, which has nanosecond precision. The + ps package is updated in tandem to accept handles created by either the old + or the new method, so new ps + old processx continues to work (#394, #402). + # processx 3.8.7 No changes. diff --git a/R/initialize.R b/R/initialize.R index d70dd11b..1d2f47b1 100644 --- a/R/initialize.R +++ b/R/initialize.R @@ -169,6 +169,14 @@ process_initialize <- function( connections <- c(list(stdin, stdout, stderr), connections) "!DEBUG process_initialize exec()" + ## Capture time just before the fork so we have a lower bound for the + ## child's start time. /proc//stat starttime has only 10ms resolution + ## (100 Hz clock ticks), so it can appear to slightly predate this point. + ## We take max(kernel_start_time, before_start) so the reported start time + ## ($get_start_time()) is never earlier than when process$new() was called. + ## private$starttime_raw holds the unmodified kernel time and is used for + ## ps::ps_handle() validation (which has a 1-tick tolerance). + before_start <- as.numeric(Sys.time()) private$status <- chain_call( c_processx_exec, command, @@ -188,17 +196,18 @@ process_initialize <- function( linux_pdeathsig ) - ## We try the query the start time according to the OS, because we can + ## We try to query the start time according to the OS, because we can ## use the (pid, start time) pair as an id when performing operations on ## the process, e.g. sending signals. This is only implemented on Linux, ## macOS and Windows and on other OSes it returns 0.0, so we just use the ## current time instead. (In the C process handle, there will be 0, ## still.) - private$starttime <- + private$starttime_raw <- chain_call(c_processx__proc_start_time, private$status) - if (private$starttime == 0) { - private$starttime <- Sys.time() + if (private$starttime_raw == 0) { + private$starttime_raw <- as.numeric(Sys.time()) } + private$starttime <- max(private$starttime_raw, before_start) ## Need to close this, otherwise the child's end of the pipe ## will not be closed when the child exits, and then we cannot diff --git a/R/on-load.R b/R/on-load.R index 08ad562a..72825388 100644 --- a/R/on-load.R +++ b/R/on-load.R @@ -9,8 +9,14 @@ ## https://github.com/r-lib/processx/pull/401 if (is_linux() && ps::ps_is_supported()) { ps::ps_handle() - bt <- ps::ps_boot_time() - .Call(c_processx__set_boot_time, bt) + if (utils::packageVersion("ps") >= "1.9.2.9001") { + ## Pass NULL to enable CLOCK_REALTIME-CLOCK_MONOTONIC precise boot time, + ## which requires ps >= 1.9.2.9001 for compatible handle validation. + .Call(c_processx__set_boot_time, NULL) + } else { + bt <- ps::ps_boot_time() + .Call(c_processx__set_boot_time, bt) + } } supervisor_reset() diff --git a/R/process.R b/R/process.R index 3003feeb..4090d3f2 100644 --- a/R/process.R +++ b/R/process.R @@ -705,52 +705,52 @@ process <- R6::R6Class( #' @description #' Calls [ps::ps_name()] to get the process name. - get_name = function() ps_method(ps::ps_name, self), + get_name = function() ps_method(ps::ps_name, self, private), #' @description #' Calls [ps::ps_exe()] to get the path of the executable. - get_exe = function() ps_method(ps::ps_exe, self), + get_exe = function() ps_method(ps::ps_exe, self, private), #' @description #' Calls [ps::ps_cmdline()] to get the command line. - get_cmdline = function() ps_method(ps::ps_cmdline, self), + get_cmdline = function() ps_method(ps::ps_cmdline, self, private), #' @description #' Calls [ps::ps_status()] to get the process status. - get_status = function() ps_method(ps::ps_status, self), + get_status = function() ps_method(ps::ps_status, self, private), #' @description #' calls [ps::ps_username()] to get the username. - get_username = function() ps_method(ps::ps_username, self), + get_username = function() ps_method(ps::ps_username, self, private), #' @description #' Calls [ps::ps_cwd()] to get the current working directory. - get_wd = function() ps_method(ps::ps_cwd, self), + get_wd = function() ps_method(ps::ps_cwd, self, private), #' @description #' Calls [ps::ps_cpu_times()] to get CPU usage data. - get_cpu_times = function() ps_method(ps::ps_cpu_times, self), + get_cpu_times = function() ps_method(ps::ps_cpu_times, self, private), #' @description #' Calls [ps::ps_memory_info()] to get memory data. - get_memory_info = function() ps_method(ps::ps_memory_info, self), + get_memory_info = function() ps_method(ps::ps_memory_info, self, private), #' @description #' Calls [ps::ps_suspend()] to suspend the process. - suspend = function() ps_method(ps::ps_suspend, self), + suspend = function() ps_method(ps::ps_suspend, self, private), #' @description #' Calls [ps::ps_resume()] to resume a suspended process. - resume = function() ps_method(ps::ps_resume, self) + resume = function() ps_method(ps::ps_resume, self, private) ), private = list( @@ -768,7 +768,8 @@ process <- R6::R6Class( pstderr = NULL, # the original stderr argument cleanfiles = NULL, # which temp stdout/stderr file(s) to clean up wd = NULL, # working directory (or NULL for current) - starttime = NULL, # timestamp of start + starttime = NULL, # timestamp of start (display; >= starttime_raw) + starttime_raw = NULL, # timestamp of start as reported by OS (for ps compat) endtime = NULL, # timestamp of exit, or 0 if not yet exited echo_cmd = NULL, # whether to echo the command windows_verbatim_args = NULL, @@ -934,11 +935,11 @@ process_get_result <- function(self, private) { } process_as_ps_handle <- function(self, private) { - ps::ps_handle(self$get_pid(), self$get_start_time()) + ps::ps_handle(self$get_pid(), format_unix_time(private$starttime_raw)) } -ps_method <- function(fun, self) { - fun(ps::ps_handle(self$get_pid(), self$get_start_time())) +ps_method <- function(fun, self, private) { + fun(ps::ps_handle(self$get_pid(), format_unix_time(private$starttime_raw))) } process_close_connections <- function(self, private) { diff --git a/src/create-time.c b/src/create-time.c index 8b782ce9..ff879232 100644 --- a/src/create-time.c +++ b/src/create-time.c @@ -43,15 +43,25 @@ double processx__create_time(HANDLE process) { * on Linux. */ static double processx__linux_boot_time = 0.0; +static int processx__use_precise_boot_time = 0; +/* Pass NULL to enable the CLOCK_REALTIME-CLOCK_MONOTONIC implementation + (requires ps >= 1.9.2.9001). Pass a numeric boot time to use the legacy + /proc/stat btime implementation (for older ps). */ SEXP processx__set_boot_time(SEXP bt) { - processx__linux_boot_time = REAL(bt)[0]; + if (isNull(bt)) { + processx__use_precise_boot_time = 1; + } else { + processx__linux_boot_time = REAL(bt)[0]; + processx__use_precise_boot_time = 0; + } return R_NilValue; } #ifdef __linux__ #include +#include #include int processx__read_file(const char *path, char **buffer, size_t buffer_size) { @@ -177,23 +187,36 @@ static double processx__linux_clock_period = 0.0; double processx__create_time(long pid) { double ct; - double bt; double clock; ct = processx__create_time_since_boot(pid); if (ct == 0) return 0.0; - bt = processx__boot_time(); - if (bt == 0) return 0.0; - - /* Query if not yet queried */ + /* Query clock tick rate if not yet queried */ if (processx__linux_clock_period == 0) { clock = sysconf(_SC_CLK_TCK); if (clock == -1) return 0.0; processx__linux_clock_period = 1.0 / clock; } - return bt + ct * processx__linux_clock_period; + if (processx__use_precise_boot_time) { + /* Precise boot time: CLOCK_REALTIME - CLOCK_MONOTONIC. + CLOCK_MONOTONIC uses the same boot reference as /proc//stat + starttime, avoiding the whole-second truncation error of /proc/stat + btime. Requires ps >= 1.9.2.9001 for compatible handle validation. + See https://github.com/r-lib/processx/issues/394 */ + struct timespec real_time, mono_time; + if (clock_gettime(CLOCK_REALTIME, &real_time) == -1) return 0.0; + if (clock_gettime(CLOCK_MONOTONIC, &mono_time) == -1) return 0.0; + double real_secs = real_time.tv_sec + real_time.tv_nsec * 1e-9; + double mono_secs = mono_time.tv_sec + mono_time.tv_nsec * 1e-9; + return (real_secs - mono_secs) + ct * processx__linux_clock_period; + } else { + /* Legacy: cached boot time from /proc/stat btime (integer seconds). */ + double bt = processx__boot_time(); + if (bt == 0) return 0.0; + return bt + ct * processx__linux_clock_period; + } } #endif