Skip to content
Merged
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
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 13 additions & 4 deletions R/initialize.R
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pid>/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,
Expand All @@ -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
Expand Down
10 changes: 8 additions & 2 deletions R/on-load.R
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
29 changes: 15 additions & 14 deletions R/process.R
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 30 additions & 7 deletions src/create-time.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sys/sysinfo.h>
#include <time.h>
#include <unistd.h>

int processx__read_file(const char *path, char **buffer, size_t buffer_size) {
Expand Down Expand Up @@ -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/<pid>/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
Expand Down
Loading