diff --git a/NEWS.md b/NEWS.md index 12558f43..31cc218b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,8 @@ # processx (development version) +* New `process$get_end_time()` method returns the time when the process + exited as a `POSIXct`, or `NULL` if it is still running (#218). + * `process$new()` and `run()` now support `pty = TRUE` on Windows 10 version 1809 and later, in addition to Unix. The Windows implementation uses the ConPTY API (`CreatePseudoConsole`). The API is loaded dynamically so diff --git a/R/process.R b/R/process.R index 4fe63a58..ce0171c1 100644 --- a/R/process.R +++ b/R/process.R @@ -381,6 +381,17 @@ process <- R6::R6Class( get_start_time = function() process_get_start_time(self, private), + #' @description + #' `$get_end_time()` returns the time when the process finished, + #' or `NULL` if it is still running. + #' On Unix the timestamp is recorded when R first notices the exit + #' (via the `SIGCHLD` handler or a call to `$is_alive()`, + #' `$get_exit_status()`, or `$wait()`), so it may be slightly later + #' than the actual kernel exit time. + #' On Windows the exact kernel exit time is used. + + get_end_time = function() process_get_end_time(self, private), + #' @description #' `$is_supervised()` returns whether the process is being tracked by #' supervisor process. @@ -726,6 +737,7 @@ process <- R6::R6Class( cleanfiles = NULL, # which temp stdout/stderr file(s) to clean up wd = NULL, # working directory (or NULL for current) starttime = NULL, # timestamp of start + endtime = NULL, # timestamp of exit, or 0 if not yet exited echo_cmd = NULL, # whether to echo the command windows_verbatim_args = NULL, windows_hide_window = NULL, @@ -848,6 +860,18 @@ process_get_start_time <- function(self, private) { format_unix_time(private$starttime) } +process_get_end_time <- function(self, private) { + if (!is.null(private$endtime)) { + return(private$endtime) + } + et <- chain_call(c_processx__proc_end_time, private$status) + if (is.null(et)) { + return(NULL) + } + private$endtime <- format_unix_time(et) + private$endtime +} + process_get_pid <- function(self, private) { chain_call(c_processx_get_pid, private$status) } diff --git a/man/process.Rd b/man/process.Rd index 815e4807..a670186c 100644 --- a/man/process.Rd +++ b/man/process.Rd @@ -119,6 +119,7 @@ p$is_alive() \item \href{#method-process-format}{\code{process$format()}} \item \href{#method-process-print}{\code{process$print()}} \item \href{#method-process-get_start_time}{\code{process$get_start_time()}} + \item \href{#method-process-get_end_time}{\code{process$get_end_time()}} \item \href{#method-process-is_supervised}{\code{process$is_supervised()}} \item \href{#method-process-supervise}{\code{process$supervise()}} \item \href{#method-process-read_output}{\code{process$read_output()}} @@ -539,6 +540,24 @@ started. } } +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-process-get_end_time}{}}} +\subsection{\code{process$get_end_time()}}{ + \verb{$get_end_time()} returns the time when the process finished, +or \code{NULL} if it is still running. +On Unix the timestamp is recorded when R first notices the exit +(via the \code{SIGCHLD} handler or a call to \verb{$is_alive()}, +\verb{$get_exit_status()}, or \verb{$wait()}), so it may be slightly later +than the actual kernel exit time. +On Windows the exact kernel exit time is used. + \subsection{Usage}{ + \if{html}{\out{
}} + \preformatted{process$get_end_time()} + \if{html}{\out{
}} + } +} + \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-is_supervised}{}}} diff --git a/src/create-time.c b/src/create-time.c index a7b0b654..8b782ce9 100644 --- a/src/create-time.c +++ b/src/create-time.c @@ -261,3 +261,17 @@ SEXP processx__proc_start_time(SEXP status) { return ScalarReal(handle->create_time); } + +SEXP processx__proc_end_time(SEXP status) { + processx_handle_t *handle = R_ExternalPtrAddr(status); + + if (!handle) { + R_THROW_ERROR("Internal processx error, handle already removed"); + } + + if (handle->end_time == 0.0) { + return R_NilValue; + } else { + return ScalarReal(handle->end_time); + } +} diff --git a/src/init.c b/src/init.c index 5af3cee6..3a941d99 100644 --- a/src/init.c +++ b/src/init.c @@ -80,6 +80,7 @@ static const R_CallMethodDef callMethods[] = { { "processx_create_named_pipe", (DL_FUNC) &processx_create_named_pipe, 2 }, { "processx_write_named_pipe", (DL_FUNC) &processx_write_named_pipe, 2 }, { "processx__proc_start_time", (DL_FUNC) &processx__proc_start_time, 1 }, + { "processx__proc_end_time", (DL_FUNC) &processx__proc_end_time, 1 }, { "processx__set_boot_time", (DL_FUNC) &processx__set_boot_time, 1 }, { "processx_connection_create", (DL_FUNC) &processx_connection_create, 2 }, diff --git a/src/processx.h b/src/processx.h index 72f5eae4..c41cc0be 100644 --- a/src/processx.h +++ b/src/processx.h @@ -61,6 +61,7 @@ SEXP processx_poll(SEXP statuses, SEXP conn, SEXP ms); SEXP processx__process_exists(SEXP pid); SEXP processx__proc_start_time(SEXP status); +SEXP processx__proc_end_time(SEXP status); SEXP processx__unload_cleanup(void); SEXP processx_is_named_pipe_open(SEXP pipe_ext); diff --git a/src/unix/processx-unix.h b/src/unix/processx-unix.h index f7b63e5d..3bb3adc7 100644 --- a/src/unix/processx-unix.h +++ b/src/unix/processx-unix.h @@ -24,6 +24,7 @@ typedef struct processx_handle_s { int waitpipe[2]; /* use it for wait() with timeout */ int cleanup; double create_time; + double end_time; /* 0.0 until the process exits */ processx_connection_t *pipes[3]; int ptyfd; int pty_child_fd; /* parent holds child PTY fd open to prevent data loss on macOS */ diff --git a/src/unix/processx.c b/src/unix/processx.c index 38739d84..e90d3c07 100644 --- a/src/unix/processx.c +++ b/src/unix/processx.c @@ -8,6 +8,7 @@ #include #include #include +#include #include "../processx.h" #include "../cleancall.h" @@ -652,6 +653,15 @@ void processx__collect_exit_status(SEXP status, int retval, int wstat) { handle->exitcode = - WTERMSIG(wstat); } + { + struct timespec _et; + if (clock_gettime(CLOCK_REALTIME, &_et) == 0) { + handle->end_time = (double)_et.tv_sec + (double)_et.tv_nsec * 1e-9; + } else { + handle->end_time = NA_REAL; + } + } + handle->collected = 1; /* Now that the child has exited, release our hold on the PTY child fd. diff --git a/src/win/processx-win.h b/src/win/processx-win.h index 5b0896db..b533c9be 100644 --- a/src/win/processx-win.h +++ b/src/win/processx-win.h @@ -14,6 +14,7 @@ typedef struct processx_handle_s { processx_connection_t *pipes[3]; int cleanup; double create_time; + double end_time; /* 0.0 until the process exits */ void *ptycon; /* ConPTY handle (HPCON), NULL if not using PTY */ } processx_handle_t; diff --git a/src/win/processx.c b/src/win/processx.c index bade696c..bbb7e619 100644 --- a/src/win/processx.c +++ b/src/win/processx.c @@ -1418,6 +1418,18 @@ void processx__collect_exit_status(SEXP status, DWORD exitcode) { processx_handle_t *handle = R_ExternalPtrAddr(status); handle->exitcode = exitcode; handle->collected = 1; + { + FILETIME ftCreate, ftExit, ftKernel, ftUser; + if (GetProcessTimes(handle->hProcess, + &ftCreate, &ftExit, &ftKernel, &ftUser)) { + long long ll = ((LONGLONG)ftExit.dwHighDateTime) << 32; + ll += ftExit.dwLowDateTime - 116444736000000000LL; + handle->end_time = (double)(ll / 10000000) + + (double)(ll % 10000000) / 10000000.0; + } else { + handle->end_time = 0.0; + } + } /* Do NOT call ClosePseudoConsole() here. ConPTY processes the child's writes asynchronously: by the time GetExitCodeProcess() reports that the child has exited, conhost may not have finished writing the child's diff --git a/tests/testthat/_snaps/Darwin/process.md b/tests/testthat/_snaps/Darwin/process.md index 623f68a1..3d834bdb 100644 --- a/tests/testthat/_snaps/Darwin/process.md +++ b/tests/testthat/_snaps/Darwin/process.md @@ -6,7 +6,7 @@ Error: ! ! Native call to `processx_exec` failed Caused by error in `chain_call(c_processx_exec, command, c(command, args), pty, pty_options, ...` at initialize.R::: - ! cannot start processx process '/' (system error 2, No such file or directory) @unix/processx.c:628 (processx_exec) + ! cannot start processx process '/' (system error 2, No such file or directory) @unix/processx.c:629 (processx_exec) # working directory does not exist @@ -16,5 +16,5 @@ Error: ! ! Native call to `processx_exec` failed Caused by error in `chain_call(c_processx_exec, command, c(command, args), pty, pty_options, ...` at initialize.R::: - ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:628 (processx_exec) + ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:629 (processx_exec) diff --git a/tests/testthat/_snaps/Darwin/run.md b/tests/testthat/_snaps/Darwin/run.md index b0e068bc..75b93f2c 100644 --- a/tests/testthat/_snaps/Darwin/run.md +++ b/tests/testthat/_snaps/Darwin/run.md @@ -6,5 +6,5 @@ Error: ! ! Native call to `processx_exec` failed Caused by error in `chain_call(c_processx_exec, command, c(command, args), pty, pty_options, ...` at initialize.R::: - ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:628 (processx_exec) + ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:629 (processx_exec) diff --git a/tests/testthat/_snaps/Linux/process.md b/tests/testthat/_snaps/Linux/process.md index 623f68a1..3d834bdb 100644 --- a/tests/testthat/_snaps/Linux/process.md +++ b/tests/testthat/_snaps/Linux/process.md @@ -6,7 +6,7 @@ Error: ! ! Native call to `processx_exec` failed Caused by error in `chain_call(c_processx_exec, command, c(command, args), pty, pty_options, ...` at initialize.R::: - ! cannot start processx process '/' (system error 2, No such file or directory) @unix/processx.c:628 (processx_exec) + ! cannot start processx process '/' (system error 2, No such file or directory) @unix/processx.c:629 (processx_exec) # working directory does not exist @@ -16,5 +16,5 @@ Error: ! ! Native call to `processx_exec` failed Caused by error in `chain_call(c_processx_exec, command, c(command, args), pty, pty_options, ...` at initialize.R::: - ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:628 (processx_exec) + ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:629 (processx_exec) diff --git a/tests/testthat/_snaps/Linux/run.md b/tests/testthat/_snaps/Linux/run.md index b0e068bc..75b93f2c 100644 --- a/tests/testthat/_snaps/Linux/run.md +++ b/tests/testthat/_snaps/Linux/run.md @@ -6,5 +6,5 @@ Error: ! ! Native call to `processx_exec` failed Caused by error in `chain_call(c_processx_exec, command, c(command, args), pty, pty_options, ...` at initialize.R::: - ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:628 (processx_exec) + ! cannot start processx process '/px' (system error 2, No such file or directory) @unix/processx.c:629 (processx_exec) diff --git a/tests/testthat/test-process.R b/tests/testthat/test-process.R index 4af0b132..b93620fe 100644 --- a/tests/testthat/test-process.R +++ b/tests/testthat/test-process.R @@ -131,3 +131,24 @@ test_that("R process is installed with a SIGTERM cleanup handler", { # Was not cleaned up expect_true(dir.exists(p_temp_dir)) }) + +test_that("get_end_time", { + px <- get_tool("px") + + p <- process$new(px, c("sleep", "1")) + on.exit(p$kill(), add = TRUE) + + before <- Sys.time() + expect_null(p$get_end_time()) + + p$wait() + after <- Sys.time() + + et <- p$get_end_time() + expect_s3_class(et, "POSIXct") + expect_gte(as.double(et), as.double(before)) + expect_gte(as.double(et), as.double(p$get_start_time())) + + # cached: second call returns the same value + expect_equal(p$get_end_time(), et) +})