Skip to content

Commit 61839b3

Browse files
committed
Install SIGTERM handler to clean up tempdir
1 parent 0551356 commit 61839b3

4 files changed

Lines changed: 110 additions & 1 deletion

File tree

DESCRIPTION

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Imports:
2424
R6,
2525
utils
2626
Suggests:
27-
callr (>= 3.7.0),
27+
callr (>= 3.7.3.9001),
2828
cli (>= 3.3.0),
2929
codetools,
3030
covr,
@@ -34,6 +34,8 @@ Suggests:
3434
rlang (>= 1.0.2),
3535
testthat (>= 3.0.0),
3636
withr
37+
Remotes:
38+
r-lib/callr@fix-client-so-name
3739
Encoding: UTF-8
3840
RoxygenNote: 7.2.0
3941
Roxygen: list(markdown = TRUE)

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# processx (development version)
22

3+
* On Unixes, R processes created by callr now feature a `SIGTERM`
4+
cleanup handler that cleans up the temporary directory before
5+
shutting down. To disable it, set the
6+
`PROCESSX_NO_R_SIGTERM_CLEANUP` envvar to a non-empty value.
7+
38
# processx 3.8.0
49

510
* processx error stacks are better now. They have ANSI hyperlinks for

src/client.c

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,64 @@ SEXP processx_set_stderr_to_file(SEXP file) {
234234
SEXP processx_base64_encode(SEXP array);
235235
SEXP processx_base64_decode(SEXP array);
236236

237+
238+
#ifndef _WIN32
239+
240+
#include <string.h>
241+
#include <signal.h>
242+
243+
const char* rimraf_tmpdir_cmd = NULL;
244+
245+
void term_handler(int n) {
246+
R_system(rimraf_tmpdir_cmd);
247+
248+
// We don't run finalization handlers because running R code from a
249+
// signal handler is not safe. To properly clean up a process, we'd
250+
// need R to handle SIGTERM and clean up at check-interrupt
251+
// time. We do run `atexit()` handlers though.
252+
exit(EXIT_FAILURE);
253+
}
254+
255+
void install_term_handler(void) {
256+
if (getenv("PROCESSX_NO_R_SIGTERM_CLEANUP")) {
257+
return;
258+
}
259+
260+
const char* tmp_dir = getenv("R_SESSION_TMPDIR");
261+
262+
// Should not happen but just in case
263+
if (!tmp_dir) {
264+
return;
265+
}
266+
267+
// Only install the handler if the tempdir doesn't have special
268+
// characters because we clean it through a `rm -rf` call in a
269+
// subprocess to avoid calling async-signal-unsafe functions like
270+
// `R_unlink(). Also it's faster with some filesystems, see notes in
271+
// the `R_CleanTempDir()` implementation.
272+
char *special = "'\\`$\"\n";
273+
274+
for (int i = 0; special[i] != '\0'; ++i) {
275+
if (strchr(tmp_dir, special[i])) {
276+
return;
277+
}
278+
}
279+
280+
// To make the handler as simple as we can we ignore the possibility
281+
// of the temp directory changing during the session, and create the
282+
// command string upfront. It is protected via the symbol table.
283+
SEXP rimraf_tmpdir_sym = R_ParseEvalString("as.symbol(paste0('rm -rf ', tempdir()))",
284+
R_BaseNamespace);
285+
rimraf_tmpdir_cmd = CHAR(PRINTNAME(rimraf_tmpdir_sym));
286+
287+
struct sigaction sig = {{ 0 }};
288+
sig.sa_handler = term_handler;
289+
sigaction(SIGTERM, &sig, NULL);
290+
}
291+
292+
#endif // not _WIN32
293+
294+
237295
static const R_CallMethodDef callMethods[] = {
238296
{ "processx_base64_encode", (DL_FUNC) &processx_base64_encode, 1 },
239297
{ "processx_base64_decode", (DL_FUNC) &processx_base64_decode, 1 },
@@ -250,4 +308,8 @@ void R_init_client(DllInfo *dll) {
250308
R_registerRoutines(dll, NULL, callMethods, NULL, NULL);
251309
R_useDynamicSymbols(dll, FALSE);
252310
R_forceSymbols(dll, TRUE);
311+
312+
#ifndef _WIN32
313+
install_term_handler();
314+
#endif
253315
}

tests/testthat/test-process.R

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,43 @@ test_that("working directory does not exist", {
6767
## This closes connections in finalizers
6868
gc()
6969
})
70+
71+
test_that("R process is installed with a SIGTERM cleanup handler", {
72+
# Needs POSIX signal handling
73+
skip_on_os("windows")
74+
75+
out <- tempfile()
76+
77+
fn <- function(file) {
78+
file.create(tempfile())
79+
writeLines(tempdir(), file)
80+
}
81+
82+
p <- callr::r_session$new()
83+
p$run(fn, list(file = out))
84+
85+
p_temp_dir <- readLines(out)
86+
expect_true(dir.exists(p_temp_dir))
87+
88+
p$signal(ps::signals()$SIGTERM)
89+
p$wait()
90+
expect_false(dir.exists(p_temp_dir))
91+
92+
# Disabled case
93+
withr::local_envvar(c(PROCESSX_NO_R_SIGTERM_CLEANUP = "true"))
94+
95+
# Just in case R adds tempdir cleanup on SIGTERM
96+
skip_on_cran()
97+
98+
p <- callr::r_session$new()
99+
p$run(fn, list(file = out))
100+
101+
p_temp_dir <- readLines(out)
102+
expect_true(dir.exists(p_temp_dir))
103+
104+
p$signal(ps::signals()$SIGTERM)
105+
p$wait()
106+
107+
# Was not cleaned up
108+
expect_true(dir.exists(p_temp_dir))
109+
})

0 commit comments

Comments
 (0)