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)
+})