Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5ac96f6
Extract `c_processx_wait()`
lionel- Apr 19, 2023
42699d4
Implement `kill(grace = )`
lionel- Apr 19, 2023
dcb71f5
Add `cleanup_grace` argument to `run()`
lionel- Apr 19, 2023
11d37ca
Add `cleanup_grace` argument to process ctor
lionel- Apr 19, 2023
e83a48c
Use `cleanup_grace` in finaliser
lionel- Apr 27, 2023
b586b12
Poll for completion of cleanup process in unit test
lionel- Apr 27, 2023
b693822
Rbuildignore test library
lionel- Apr 27, 2023
6f1ee01
Merge commit 'df0579681e7953e52174614fba8836b9a9c0afbc'
gaborcsardi Apr 26, 2025
86528ec
Code formatting
gaborcsardi Apr 26, 2025
200d27c
Adjust Linux test snapshots
gaborcsardi Apr 26, 2025
72a8de6
Merge branch 'lionel--safe-cleanup' into kill-grace
gaborcsardi Apr 23, 2026
3291027
Explicit cast to int for cleanup grace
gaborcsardi Apr 23, 2026
56b4592
Remove unused field from internal struct
gaborcsardi Apr 23, 2026
13ea845
Correct argument name in .h file
gaborcsardi Apr 23, 2026
21dcefd
NEWS.md for cleanup_grace
gaborcsardi Apr 23, 2026
4814e2c
Better validation for cleanup grace
gaborcsardi Apr 23, 2026
7030018
Adjust required callr version in tests
gaborcsardi Apr 23, 2026
f81cef9
Make a cleanup test more robust
gaborcsardi Apr 23, 2026
0b9f843
Fix compilation on Windows
gaborcsardi Apr 23, 2026
5d7776d
Update Windows test snapshots
gaborcsardi Apr 23, 2026
60e68c6
Skip grace tests on Windows
gaborcsardi Apr 23, 2026
cfe5edd
Do not install test list into libs/test
gaborcsardi Apr 23, 2026
937a6c0
Gracefull kill test improments
gaborcsardi Apr 23, 2026
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
2 changes: 2 additions & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
^src/processx\.dll$
^src/client\.so$
^src/client\.dll$
^src/test/sigtermignore\.so$
^src/test/sigtermignore\.dll$
^src/.*\.o$
^src/tools/px$
^src/tools/px.exe$
Expand Down
1 change: 1 addition & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Suggests:
curl,
debugme,
parallel,
pkgload,
rlang (>= 1.0.2),
testthat (>= 3.0.0),
webfakes,
Expand Down
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# processx (development version)

* The `grace` argument of the `kill()` method is now active on Unix
platforms. processx first tries to kill with `SIGTERM` with a
timeout of `grace` seconds. After the timeout, `SIGKILL` is sent as
before.

* New `cleanup_grace` argument to `process$new()` and `run()`. When the
process is cleaned up (on GC or on exit), processx first tries
`SIGTERM` with a timeout of `cleanup_grace` seconds before falling
back to `SIGKILL`. On Unix only.

# processx 3.9.0

* New experimental `pipeline` R6 class for running two or more processes
Expand Down
8 changes: 8 additions & 0 deletions R/assertions.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ on_failure(is_flag) <- function(call, env) {
paste0(deparse(call$x), " is not a flag (length 1 logical)")
}

is_nonneg_numeric_scalar <- function(x) {
is.numeric(x) && length(x) == 1 && !is.na(x) && x >= 0
}

on_failure(is_nonneg_numeric_scalar) <- function(call, env) {
paste0(deparse(call$x), " is not a non-negative length 1 number")
}

is_integerish_scalar <- function(x) {
is.numeric(x) && length(x) == 1 && !is.na(x) && round(x) == x
}
Expand Down
4 changes: 4 additions & 0 deletions R/initialize.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ process_initialize <- function(
env,
cleanup,
cleanup_tree,
cleanup_grace,
wd,
echo_cmd,
supervise,
Expand All @@ -62,6 +63,7 @@ process_initialize <- function(
is.null(env) || is_env_vector(env),
is_flag(cleanup),
is_flag(cleanup_tree),
is_nonneg_numeric_scalar(cleanup_grace),
is_string_or_null(wd),
is_flag(echo_cmd),
is_flag(windows_verbatim_args),
Expand Down Expand Up @@ -119,6 +121,7 @@ process_initialize <- function(
private$args <- args
private$cleanup <- cleanup
private$cleanup_tree <- cleanup_tree
private$cleanup_grace <- cleanup_grace
private$wd <- wd
private$pstdin <- stdin
private$pstdout <- stdout
Expand Down Expand Up @@ -190,6 +193,7 @@ process_initialize <- function(
windows_detached_process,
private,
cleanup,
cleanup_grace,
wd,
encoding,
paste0("PROCESSX_", private$tree_id, "=YES"),
Expand Down
9 changes: 8 additions & 1 deletion R/process.R
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,10 @@ process <- R6::R6Class(
#' object is garbage collected.
#' @param cleanup_tree Whether to kill the process and its child
#' process tree when the `process` object is garbage collected.
#' @param cleanup_grace Grace period between `SIGTERM` and `SIGKILL`.
#' Only has an effect on Unix platforms. Set to 0 to terminate abruptly
#' with `SIGKILL` only. Currently defaults to 0 until we implement
#' a better approach on session quit.
#' @param wd Working directory of the process. It must exist.
#' If `NULL`, then the current working directory is used.
#' @param echo_cmd Whether to print the command to the screen before
Expand Down Expand Up @@ -275,6 +279,7 @@ process <- R6::R6Class(
env = NULL,
cleanup = TRUE,
cleanup_tree = FALSE,
cleanup_grace = 0.0,
wd = NULL,
echo_cmd = FALSE,
supervise = FALSE,
Expand All @@ -300,6 +305,7 @@ process <- R6::R6Class(
env,
cleanup,
cleanup_tree,
cleanup_grace,
wd,
echo_cmd,
supervise,
Expand Down Expand Up @@ -758,6 +764,7 @@ process <- R6::R6Class(
args = NULL, # Save 'args' argument here
cleanup = NULL, # cleanup argument
cleanup_tree = NULL, # cleanup_tree argument
cleanup_grace = NULL, # cleanup_grace argument
stdin = NULL, # stdin argument or stream
stdout = NULL, # stdout argument or stream
stderr = NULL, # stderr argument or stream
Expand Down Expand Up @@ -862,7 +869,7 @@ process_interrupt <- function(self, private) {

process_kill <- function(self, private, grace, close_connections) {
"!DEBUG process_kill '`private$get_short_name()`', pid `self$get_pid()`"
ret <- chain_call(
ret <- chain_clean_call(
c_processx_kill,
private$status,
as.numeric(grace),
Expand Down
9 changes: 6 additions & 3 deletions R/run.R
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@
#' strings. Line callbacks are not supported in binary mode.
#' @param cleanup_tree Whether to clean up the child process tree after
#' the process has finished.
#' @param cleanup_grace Passed to `kill()` or `kill_tree()` on cleanup.
#' @param stdin What to do with the standard input. By default it is
#' ignored (`NULL`). It can be a file name, to redirect the contents of
#' a file to the standard input. When `pty = TRUE`, `stdin` can only be
Expand Down Expand Up @@ -195,6 +196,7 @@ run <- function(
windows_hide_window = FALSE,
encoding = "",
cleanup_tree = FALSE,
cleanup_grace = 0.1,
pty = FALSE,
pty_options = list(),
...
Expand All @@ -215,6 +217,7 @@ run <- function(
assert_that(is.null(stdout_callback) || is.function(stdout_callback))
assert_that(is.null(stderr_callback) || is.function(stderr_callback))
assert_that(is_flag(cleanup_tree))
assert_that(is_nonneg_numeric_scalar(cleanup_grace))
assert_that(is_flag(stderr_to_stdout))
if (encoding == "binary") {
if (!is.null(stdout_line_callback)) {
Expand Down Expand Up @@ -299,9 +302,9 @@ run <- function(

## We make sure that the process is eliminated
if (cleanup_tree) {
on.exit(pr$kill_tree(), add = TRUE)
defer(pr$kill_tree(grace = cleanup_grace))
} else {
on.exit(pr$kill(), add = TRUE)
defer(pr$kill(grace = cleanup_grace))
}

## If echo, then we need to create our own callbacks.
Expand Down Expand Up @@ -365,7 +368,7 @@ run <- function(
}
resenv$errbuf$read()
}
tryCatch(pr$kill(), error = function(e) NULL)
tryCatch(pr$kill(grace = cleanup_grace), error = function(e) NULL)
signalCondition(new_process_interrupt_cond(
list(
interrupt = TRUE,
Expand Down
11 changes: 11 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -352,3 +352,14 @@ ends_with <- function(x, post) {
substr(x, nchar(x) - l + 1, nchar(x)) == post
}

defer <- function(expr, frame = parent.frame(), after = FALSE) {
thunk <- as.call(list(function() expr))
do.call(on.exit, list(thunk, add = TRUE, after = after), envir = frame)
}

rimraf <- function(...) {
x <- file.path(...)
if ("~" %in% x) stop("Cowardly refusing to delete `~`")
unlink(x, recursive = TRUE, force = TRUE)
}

6 changes: 6 additions & 0 deletions man/process.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/process_initialize.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions man/run.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/init.c
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ SEXP is_valgrind_(void)

static const R_CallMethodDef callMethods[] = {
CLEANCALL_METHOD_RECORD,
{ "processx_exec", (DL_FUNC) &processx_exec, 15 },
{ "processx_exec", (DL_FUNC) &processx_exec, 16 },
{ "processx_wait", (DL_FUNC) &processx_wait, 3 },
{ "processx_is_alive", (DL_FUNC) &processx_is_alive, 2 },
{ "processx_pty_close", (DL_FUNC) &processx_pty_close, 2 },
Expand Down
4 changes: 2 additions & 2 deletions src/processx.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ extern "C" {
SEXP processx_exec(SEXP command, SEXP args, SEXP pty, SEXP pty_options,
SEXP connections, SEXP env, SEXP windows_verbatim_args,
SEXP windows_hide_window, SEXP windows_detached_process,
SEXP private_, SEXP cleanup, SEXP wd, SEXP encoding,
SEXP tree_id, SEXP linux_pdeathsig);
SEXP private_, SEXP cleanup, SEXP cleanup_grace,
SEXP wd, SEXP encoding, SEXP tree_id, SEXP linux_pdeathsig);
SEXP processx_wait(SEXP status, SEXP timeout, SEXP name);
SEXP processx_is_alive(SEXP status, SEXP name);
SEXP processx_pty_close(SEXP status, SEXP name);
Expand Down
54 changes: 54 additions & 0 deletions src/tools/px.c
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ void usage(void) {
"write raw bytes (hex pairs) to stdout\n");
fprintf(stderr, " rawerr <hexstring> -- "
"write raw bytes (hex pairs) to stderr\n");
fprintf(stderr, " sigterm ignore -- "
"ignore SIGTERM\n");
fprintf(stderr, " sigterm sleep <seconds> -- "
"sleep a number of seconds on SIGTERM, then quit\n");
}

void cat2(int f, const char *s) {
Expand Down Expand Up @@ -112,6 +116,35 @@ int echo_from_fd(int fd1, int fd2, int nbytes) {
return 0;
}

#ifdef WIN32
void sigterm_ignore(void) { }
void sigterm_sleep(double fnum) { }
#else
#include <signal.h>

double sig_sleep_secs;

void sigterm_ignore(void) {
signal(SIGTERM, SIG_IGN);
}

void sig_handler_sleep(int sig, siginfo_t *info, void *ucontext) {
double fnum = sig_sleep_secs;
int num = (int) fnum;
sleep(num);
fnum = fnum - num;
if (fnum > 0) usleep((useconds_t)(fnum * 1000.0 * 1000.0));
}

void sigterm_sleep(double fnum) {
sig_sleep_secs = fnum;
struct sigaction sig = {{ 0 }};
sig.sa_flags = SA_SIGINFO;
sig.sa_sigaction = &sig_handler_sleep;
sigaction(SIGTERM, &sig, NULL);
}
#endif

int main(int argc, const char **argv) {

int num, idx, ret, fd, fd2, nbytes;
Expand Down Expand Up @@ -258,6 +291,27 @@ int main(int argc, const char **argv) {
}
}

} else if (!strcmp("sigterm", cmd)) {
idx++;
if (!strcmp("ignore", argv[idx])) {
sigterm_ignore();
} else if (!strcmp("sleep", argv[idx])) {
if (idx + 1 >= argc) {
fprintf(stderr, "Missing argument for 'sigterm sleep'\n");
return 17;
}
idx++;
ret = sscanf(argv[idx], "%lf", &fnum);
if (ret != 1) {
fprintf(stderr, "Invalid seconds for px sigterm sleep: '%s'\n", argv[idx]);
return 18;
}
sigterm_sleep(fnum);
} else {
fprintf(stderr, "Invalid 'sigterm' subcommand\n");
return 19;
}

} else {
fprintf(stderr, "Unknown px command: '%s'\n", cmd);
return 2;
Expand Down
6 changes: 6 additions & 0 deletions src/unix/processx-unix.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ typedef struct processx_handle_s {
int fd2; /* readable */
int waitpipe[2]; /* use it for wait() with timeout */
int cleanup;
double cleanup_grace;
double create_time;
double end_time; /* 0.0 until the process exits */
processx_connection_t *pipes[3];
Expand All @@ -38,7 +39,12 @@ void processx__sigchld_callback(int sig, siginfo_t *info, void *ctx);
void processx__setup_sigchld(void);
void processx__remove_sigchld(void);
void processx__block_sigchld(void);
void processx__block_sigchld_save(sigset_t *old);
void processx__unblock_sigchld(void);
void processx__procmask_set(sigset_t *set);

int c_processx_wait(processx_handle_t *handle, int timeout, const char *name);
int c_processx_kill(SEXP status, double grace, SEXP name);

void processx__finalizer(SEXP status);

Expand Down
Loading
Loading