From 6c5b17f3dd741423a531c2ea84e5a52bff274d8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Mon, 20 Apr 2026 13:49:04 +0200 Subject: [PATCH 01/13] More accurate $get_start_time() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). Closes #394, closes #402. --- DESCRIPTION | 3 ++- NEWS.md | 8 ++++++++ R/initialize.R | 17 +++++++++++++---- R/on-load.R | 10 ++++++++-- R/process.R | 29 +++++++++++++++-------------- src/create-time.c | 37 ++++++++++++++++++++++++++++++------- 6 files changed, 76 insertions(+), 28 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index b78af65a..047507b1 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -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: @@ -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 diff --git a/NEWS.md b/NEWS.md index 32a3233a..25070816 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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. diff --git a/R/initialize.R b/R/initialize.R index d70dd11b..1d2f47b1 100644 --- a/R/initialize.R +++ b/R/initialize.R @@ -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//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, @@ -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 diff --git a/R/on-load.R b/R/on-load.R index 08ad562a..72825388 100644 --- a/R/on-load.R +++ b/R/on-load.R @@ -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() diff --git a/R/process.R b/R/process.R index 3003feeb..4090d3f2 100644 --- a/R/process.R +++ b/R/process.R @@ -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( @@ -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, @@ -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) { diff --git a/src/create-time.c b/src/create-time.c index 8b782ce9..ff879232 100644 --- a/src/create-time.c +++ b/src/create-time.c @@ -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 +#include #include int processx__read_file(const char *path, char **buffer, size_t buffer_size) { @@ -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//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 From 81e2f098309b938803274dead1024b11f9d06ddd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 09:38:37 +0200 Subject: [PATCH 02/13] First pipeline implementation --- DESCRIPTION | 2 +- NAMESPACE | 2 + R/connections.R | 20 +- R/pipeline.R | 281 +++++ man/curl_fds.Rd | 2 +- man/pipeline.Rd | 375 ++++++ man/process.Rd | 1077 ++++++++--------- man/processx-package.Rd | 1 - man/processx_connections.Rd | 14 +- man/processx_fifos.Rd | 2 +- man/processx_sockets.Rd | 2 +- src/init.c | 2 + src/processx-connection.c | 49 + src/processx-connection.h | 1 + .../_snaps/Darwin/unix-sockets.new.md | 34 + tests/testthat/_snaps/unix-sockets.new.md | 40 + tests/testthat/test-pipeline.R | 137 +++ 17 files changed, 1452 insertions(+), 589 deletions(-) create mode 100644 R/pipeline.R create mode 100644 man/pipeline.Rd create mode 100644 tests/testthat/_snaps/Darwin/unix-sockets.new.md create mode 100644 tests/testthat/_snaps/unix-sockets.new.md create mode 100644 tests/testthat/test-pipeline.R diff --git a/DESCRIPTION b/DESCRIPTION index 047507b1..d2625c09 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -43,4 +43,4 @@ Config/testthat/edition: 3 Config/usethis/last-upkeep: 2025-04-25 Encoding: UTF-8 Roxygen: list(markdown = TRUE) -Config/roxygen2/version: 7.3.3.9000 +RoxygenNote: 7.3.3 diff --git a/NAMESPACE b/NAMESPACE index 40042131..898cafa3 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -23,6 +23,7 @@ export(conn_create_fd) export(conn_create_fifo) export(conn_create_file) export(conn_create_pipepair) +export(conn_create_proc_pipepair) export(conn_create_unix_socket) export(conn_disable_inheritance) export(conn_file_name) @@ -38,6 +39,7 @@ export(conn_write) export(curl_fds) export(default_pty_options) export(is_valid_fd) +export(pipeline) export(poll) export(process) export(processx_conn_close) diff --git a/R/connections.R b/R/connections.R index c0e84562..18df7636 100644 --- a/R/connections.R +++ b/R/connections.R @@ -274,6 +274,22 @@ conn_create_pipepair <- function(encoding = "", nonblocking = c(TRUE, FALSE)) { chain_call(c_processx_connection_create_pipepair, encoding, nonblocking) } +#' @details +#' `conn_create_proc_pipepair()` creates a unidirectional pipe suitable for +#' connecting two child processes: the first element is the write end (pass as +#' `stdout` to the writing process) and the second is the read end (pass as +#' `stdin` to the reading process). Unlike `conn_create_pipepair()`, both ends +#' are synchronous (blocking), which is required for child-process stdin/stdout +#' on Windows. +#' +#' @rdname processx_connections +#' @export + +conn_create_proc_pipepair <- function(encoding = "") { + assert_that(is_string(encoding)) + chain_call(c_processx_connection_create_proc_pipepair, encoding) +} + #' @details #' `conn_read_chars()` reads UTF-8 characters from the connections. If the #' connection itself is not UTF-8 encoded, it re-encodes it. @@ -427,9 +443,9 @@ processx_conn_write <- function(con, str, sep = "\n", encoding = "") { #' @details #' `conn_create_file()` creates a connection to a file. #' -#' @param filename File name. For `conn_create_pipe()` on Windows, a +#' @param filename File name. For `conn_create_fifo()` on Windows, a #' `\\?\pipe` prefix is added to this, if it does not have such a prefix. -#' For `conn_create_pipe()` it can also be `NULL`, in which case a random +#' For `conn_create_fifo()` it can also be `NULL`, in which case a random #' file name is used via `tempfile()`. #' @param read Whether the connection is readable. #' @param write Whethe the connection is writeable. diff --git a/R/pipeline.R b/R/pipeline.R new file mode 100644 index 00000000..38ffe6c2 --- /dev/null +++ b/R/pipeline.R @@ -0,0 +1,281 @@ +#' Pipeline of processes connected with pipes +#' +#' @description +#' A `pipeline` object represents a sequence of processes whose standard +#' input and output streams are connected with pipes, like a Unix pipeline +#' (`cmd1 | cmd2 | cmd3`). Data flows directly between the child processes +#' via kernel-level pipes — the parent R process sees only the output of the +#' final command (when `stdout = "|"`). +#' +#' @param cmds A non-empty list of character vectors. Each vector is one +#' command: the first element is the executable and the rest are its +#' arguments. Example: `list(c("sort"), c("uniq", "-c"))`. +#' @param stdin Standard input for the *first* process. `NULL` to discard, +#' `"|"` so the parent R process can write to it via `$write_input()`, or +#' a file path. +#' @param stdout Standard output of the *last* process. `"|"` (the default) +#' so the parent R process can read from it, `NULL` to discard, or a file +#' path. +#' @param stderr Standard error for *all* processes. `NULL` (the default) to +#' discard, `"|"` to create a separate readable pipe per process, `"2>&1"` +#' to merge into stdout, or a file path. When `"|"`, use +#' `$read_error()` to read from the last process; use `$get_processes()` +#' to access individual process objects for other processes. +#' @param env Environment variables for all processes, or `NULL` to inherit +#' the parent environment. +#' @param encoding Assumed encoding for stdin/stdout/stderr streams. +#' @param wd Working directory for all processes, or `NULL` for the current +#' directory. +#' @param cleanup Whether to kill the processes on garbage collection. +#' @param cleanup_tree Whether to kill the full process trees on garbage +#' collection. +#' @param n Number of characters or lines to read. -1 means all available. +#' @param str String to write to the process stdin. +#' @param sep Separator to add after `str`. +#' @param timeout Timeout in milliseconds. -1 means no timeout. +#' @param grace Grace period in seconds before sending SIGKILL (Unix) or +#' terminating forcefully (Windows). Currently not used. +#' @param close_connections Whether to close connections after killing. +#' +#' @section Methods: +#' `pipeline$new(cmds, stdin, stdout, stderr, env, encoding, wd, +#' cleanup, cleanup_tree)` +#' +#' `$read_output(n = -1)`, `$read_output_lines(n = -1)`, +#' `$read_all_output()`, `$read_all_output_lines()` — read from the last +#' process (only meaningful when `stdout = "|"`). +#' +#' `$read_error(n = -1)`, `$read_error_lines(n = -1)`, +#' `$read_all_error()`, `$read_all_error_lines()` — read stderr of the +#' last process (only meaningful when `stderr = "|"`). +#' +#' `$write_input(str, sep = "\n")` — write to first process stdin +#' (only meaningful when `stdin = "|"`). +#' +#' `$close_input()` — close the first process stdin, signalling EOF. +#' +#' `$wait(timeout = -1)` — wait for all processes to finish. +#' +#' `$kill(grace = 0.1, close_connections = TRUE)` — kill all processes. +#' +#' `$kill_tree(grace = 0.1, close_connections = TRUE)` — kill all +#' process trees. +#' +#' `$is_alive()` — returns `TRUE` if any process is still running. +#' +#' `$get_exit_statuses()` — list of exit codes (one per process; `NULL` +#' if still running). +#' +#' `$get_pids()` — integer vector of process IDs. +#' +#' `$get_processes()` — list of [process] objects, one per command. +#' +#' @examples +#' \dontrun{ +#' # sort | uniq, reading from / writing to R +#' pl <- pipeline$new( +#' list(c("sort"), c("uniq")), +#' stdin = "|", stdout = "|" +#' ) +#' pl$write_input("b\na\nb\na\n") +#' pl$close_input() +#' pl$read_all_output_lines() +#' pl$wait() +#' pl$get_exit_statuses() +#' } +#' +#' @export +pipeline <- R6::R6Class( + "pipeline", + cloneable = FALSE, + + public = list( + + #' @description Create a new pipeline. + initialize = function( + cmds, + stdin = NULL, + stdout = "|", + stderr = NULL, + env = NULL, + encoding = "utf-8", + wd = NULL, + cleanup = TRUE, + cleanup_tree = FALSE + ) { + if (!is.list(cmds) || length(cmds) == 0L) { + throw(new_error("`cmds` must be a non-empty list of character vectors")) + } + for (i in seq_along(cmds)) { + if (!is.character(cmds[[i]]) || length(cmds[[i]]) == 0L) { + throw(new_error(paste0( + "`cmds[[", i, "]]` must be a non-empty character vector" + ))) + } + } + + n <- length(cmds) + + ## Create n-1 kernel pipes connecting consecutive processes + pipes <- if (n > 1L) { + lapply(seq_len(n - 1L), function(i) conn_create_proc_pipepair()) + } else { + list() + } + + ## Spawn all processes + procs <- vector("list", n) + for (i in seq_len(n)) { + cmd <- cmds[[i]] + proc_stdin <- if (i == 1L) stdin else pipes[[i - 1L]][[2L]] + proc_stdout <- if (i < n) pipes[[i]][[1L]] else stdout + ## Disable poll_connection for intermediate processes: their stdout + ## is a connection (not "|"), so the default formula would create an + ## unnecessary extra pipe. + proc_poll <- if (i < n) FALSE else NULL + + procs[[i]] <- process$new( + cmd[[1L]], + cmd[-1L], + stdin = proc_stdin, + stdout = proc_stdout, + stderr = stderr, + env = env, + encoding = encoding, + wd = wd, + cleanup = cleanup, + cleanup_tree = cleanup_tree, + poll_connection = proc_poll + ) + } + + ## Close the parent's copies of all intermediate pipe ends so that the + ## kernel can signal EOF to the downstream process when the upstream + ## process exits and closes its end. + for (p in pipes) { + close(p[[1L]]) + close(p[[2L]]) + } + + private$procs <- procs + invisible(self) + }, + + ## ------------------------------------------------------------------ ## + ## Output (last process) ## + ## ------------------------------------------------------------------ ## + + #' @description Read output of the last process. + read_output = function(n = -1) { + private$last()$read_output(n) + }, + + #' @description Read output lines of the last process. + read_output_lines = function(n = -1) { + private$last()$read_output_lines(n) + }, + + #' @description Read all output of the last process. + read_all_output = function() { + private$last()$read_all_output() + }, + + #' @description Read all output lines of the last process. + read_all_output_lines = function() { + private$last()$read_all_output_lines() + }, + + ## ------------------------------------------------------------------ ## + ## Error (last process) ## + ## ------------------------------------------------------------------ ## + + #' @description Read stderr of the last process. + read_error = function(n = -1) { + private$last()$read_error(n) + }, + + #' @description Read stderr lines of the last process. + read_error_lines = function(n = -1) { + private$last()$read_error_lines(n) + }, + + #' @description Read all stderr of the last process. + read_all_error = function() { + private$last()$read_all_error() + }, + + #' @description Read all stderr lines of the last process. + read_all_error_lines = function() { + private$last()$read_all_error_lines() + }, + + ## ------------------------------------------------------------------ ## + ## Input (first process) ## + ## ------------------------------------------------------------------ ## + + #' @description Write to the first process stdin. + write_input = function(str, sep = "\n") { + private$procs[[1L]]$write_input(str, sep) + }, + + #' @description Close the first process stdin (signals EOF to the process). + close_input = function() { + close(private$procs[[1L]]$get_input_connection()) + }, + + ## ------------------------------------------------------------------ ## + ## Lifecycle ## + ## ------------------------------------------------------------------ ## + + #' @description Wait for all processes to finish. + wait = function(timeout = -1) { + ## Wait for the last process first: it consumes the pipeline output. + ## Then wait for the rest in reverse order. + for (p in rev(private$procs)) { + p$wait(timeout) + } + invisible(self) + }, + + #' @description Kill all processes. + kill = function(grace = 0.1, close_connections = TRUE) { + for (p in private$procs) p$kill(grace, close_connections) + invisible(self) + }, + + #' @description Kill all process trees. + kill_tree = function(grace = 0.1, close_connections = TRUE) { + for (p in private$procs) p$kill_tree(grace, close_connections) + invisible(self) + }, + + #' @description Check if any process is still alive. + is_alive = function() { + any(vapply(private$procs, function(p) p$is_alive(), logical(1L))) + }, + + ## ------------------------------------------------------------------ ## + ## Status / accessors ## + ## ------------------------------------------------------------------ ## + + #' @description Return exit codes for all processes. + get_exit_statuses = function() { + lapply(private$procs, function(p) p$get_exit_status()) + }, + + #' @description Return PIDs for all processes. + get_pids = function() { + vapply(private$procs, function(p) p$get_pid(), integer(1L)) + }, + + #' @description Return the list of process objects. + get_processes = function() { + private$procs + } + ), + + private = list( + procs = NULL, + last = function() private$procs[[length(private$procs)]] + ) +) diff --git a/man/curl_fds.Rd b/man/curl_fds.Rd index 3d522a34..febe9b09 100644 --- a/man/curl_fds.Rd +++ b/man/curl_fds.Rd @@ -8,7 +8,7 @@ curl_fds(fds) } \arguments{ \item{fds}{A list of file descriptors, as returned by -\code{\link[curl:multi_fdset]{curl::multi_fdset()}}.} +\code{\link[curl:multi]{curl::multi_fdset()}}.} } \value{ Pollable object, that be used with \code{\link[=poll]{poll()}} directly. diff --git a/man/pipeline.Rd b/man/pipeline.Rd new file mode 100644 index 00000000..0e37023b --- /dev/null +++ b/man/pipeline.Rd @@ -0,0 +1,375 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/pipeline.R +\name{pipeline} +\alias{pipeline} +\title{Pipeline of processes connected with pipes} +\description{ +A \code{pipeline} object represents a sequence of processes whose standard +input and output streams are connected with pipes, like a Unix pipeline +(\code{cmd1 | cmd2 | cmd3}). Data flows directly between the child processes +via kernel-level pipes — the parent R process sees only the output of the +final command (when \code{stdout = "|"}). +} +\section{Methods}{ + +\code{pipeline$new(cmds, stdin, stdout, stderr, env, encoding, wd, cleanup, cleanup_tree)} + +\verb{$read_output(n = -1)}, \verb{$read_output_lines(n = -1)}, +\verb{$read_all_output()}, \verb{$read_all_output_lines()} — read from the last +process (only meaningful when \code{stdout = "|"}). + +\verb{$read_error(n = -1)}, \verb{$read_error_lines(n = -1)}, +\verb{$read_all_error()}, \verb{$read_all_error_lines()} — read stderr of the +last process (only meaningful when \code{stderr = "|"}). + +\verb{$write_input(str, sep = "\\n")} — write to first process stdin +(only meaningful when \code{stdin = "|"}). + +\verb{$close_input()} — close the first process stdin, signalling EOF. + +\verb{$wait(timeout = -1)} — wait for all processes to finish. + +\verb{$kill(grace = 0.1, close_connections = TRUE)} — kill all processes. + +\verb{$kill_tree(grace = 0.1, close_connections = TRUE)} — kill all +process trees. + +\verb{$is_alive()} — returns \code{TRUE} if any process is still running. + +\verb{$get_exit_statuses()} — list of exit codes (one per process; \code{NULL} +if still running). + +\verb{$get_pids()} — integer vector of process IDs. + +\verb{$get_processes()} — list of \link{process} objects, one per command. +} + +\examples{ +\dontrun{ +# sort | uniq, reading from / writing to R +pl <- pipeline$new( + list(c("sort"), c("uniq")), + stdin = "|", stdout = "|" +) +pl$write_input("b\na\nb\na\n") +pl$close_input() +pl$read_all_output_lines() +pl$wait() +pl$get_exit_statuses() +} + +} +\section{Methods}{ +\subsection{Public methods}{ +\itemize{ +\item \href{#method-pipeline-new}{\code{pipeline$new()}} +\item \href{#method-pipeline-read_output}{\code{pipeline$read_output()}} +\item \href{#method-pipeline-read_output_lines}{\code{pipeline$read_output_lines()}} +\item \href{#method-pipeline-read_all_output}{\code{pipeline$read_all_output()}} +\item \href{#method-pipeline-read_all_output_lines}{\code{pipeline$read_all_output_lines()}} +\item \href{#method-pipeline-read_error}{\code{pipeline$read_error()}} +\item \href{#method-pipeline-read_error_lines}{\code{pipeline$read_error_lines()}} +\item \href{#method-pipeline-read_all_error}{\code{pipeline$read_all_error()}} +\item \href{#method-pipeline-read_all_error_lines}{\code{pipeline$read_all_error_lines()}} +\item \href{#method-pipeline-write_input}{\code{pipeline$write_input()}} +\item \href{#method-pipeline-close_input}{\code{pipeline$close_input()}} +\item \href{#method-pipeline-wait}{\code{pipeline$wait()}} +\item \href{#method-pipeline-kill}{\code{pipeline$kill()}} +\item \href{#method-pipeline-kill_tree}{\code{pipeline$kill_tree()}} +\item \href{#method-pipeline-is_alive}{\code{pipeline$is_alive()}} +\item \href{#method-pipeline-get_exit_statuses}{\code{pipeline$get_exit_statuses()}} +\item \href{#method-pipeline-get_pids}{\code{pipeline$get_pids()}} +\item \href{#method-pipeline-get_processes}{\code{pipeline$get_processes()}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-new}{}}} +\subsection{Method \code{new()}}{ +Create a new pipeline. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$new( + cmds, + stdin = NULL, + stdout = "|", + stderr = NULL, + env = NULL, + encoding = "utf-8", + wd = NULL, + cleanup = TRUE, + cleanup_tree = FALSE +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{cmds}}{A non-empty list of character vectors. Each vector is one +command: the first element is the executable and the rest are its +arguments. Example: \code{list(c("sort"), c("uniq", "-c"))}.} + +\item{\code{stdin}}{Standard input for the \emph{first} process. \code{NULL} to discard, +\code{"|"} so the parent R process can write to it via \verb{$write_input()}, or +a file path.} + +\item{\code{stdout}}{Standard output of the \emph{last} process. \code{"|"} (the default) +so the parent R process can read from it, \code{NULL} to discard, or a file +path.} + +\item{\code{stderr}}{Standard error for \emph{all} processes. \code{NULL} (the default) to +discard, \code{"|"} to create a separate readable pipe per process, \code{"2>&1"} +to merge into stdout, or a file path. When \code{"|"}, use +\verb{$read_error()} to read from the last process; use \verb{$get_processes()} +to access individual process objects for other processes.} + +\item{\code{env}}{Environment variables for all processes, or \code{NULL} to inherit +the parent environment.} + +\item{\code{encoding}}{Assumed encoding for stdin/stdout/stderr streams.} + +\item{\code{wd}}{Working directory for all processes, or \code{NULL} for the current +directory.} + +\item{\code{cleanup}}{Whether to kill the processes on garbage collection.} + +\item{\code{cleanup_tree}}{Whether to kill the full process trees on garbage +collection.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_output}{}}} +\subsection{Method \code{read_output()}}{ +Read output of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_output(n = -1)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read. -1 means all available.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_output_lines}{}}} +\subsection{Method \code{read_output_lines()}}{ +Read output lines of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_output_lines(n = -1)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read. -1 means all available.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_all_output}{}}} +\subsection{Method \code{read_all_output()}}{ +Read all output of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_all_output()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_all_output_lines}{}}} +\subsection{Method \code{read_all_output_lines()}}{ +Read all output lines of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_all_output_lines()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_error}{}}} +\subsection{Method \code{read_error()}}{ +Read stderr of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_error(n = -1)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read. -1 means all available.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_error_lines}{}}} +\subsection{Method \code{read_error_lines()}}{ +Read stderr lines of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_error_lines(n = -1)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read. -1 means all available.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_all_error}{}}} +\subsection{Method \code{read_all_error()}}{ +Read all stderr of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_all_error()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-read_all_error_lines}{}}} +\subsection{Method \code{read_all_error_lines()}}{ +Read all stderr lines of the last process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$read_all_error_lines()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-write_input}{}}} +\subsection{Method \code{write_input()}}{ +Write to the first process stdin. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$write_input(str, sep = "\\n")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{str}}{String to write to the process stdin.} + +\item{\code{sep}}{Separator to add after \code{str}.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-close_input}{}}} +\subsection{Method \code{close_input()}}{ +Close the first process stdin (signals EOF to the process). +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$close_input()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-wait}{}}} +\subsection{Method \code{wait()}}{ +Wait for all processes to finish. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$wait(timeout = -1)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{timeout}}{Timeout in milliseconds. -1 means no timeout.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-kill}{}}} +\subsection{Method \code{kill()}}{ +Kill all processes. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$kill(grace = 0.1, close_connections = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{grace}}{Grace period in seconds before sending SIGKILL (Unix) or +terminating forcefully (Windows). Currently not used.} + +\item{\code{close_connections}}{Whether to close connections after killing.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-kill_tree}{}}} +\subsection{Method \code{kill_tree()}}{ +Kill all process trees. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$kill_tree(grace = 0.1, close_connections = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{grace}}{Grace period in seconds before sending SIGKILL (Unix) or +terminating forcefully (Windows). Currently not used.} + +\item{\code{close_connections}}{Whether to close connections after killing.} +} +\if{html}{\out{
}} +} +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-is_alive}{}}} +\subsection{Method \code{is_alive()}}{ +Check if any process is still alive. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$is_alive()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-get_exit_statuses}{}}} +\subsection{Method \code{get_exit_statuses()}}{ +Return exit codes for all processes. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$get_exit_statuses()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-get_pids}{}}} +\subsection{Method \code{get_pids()}}{ +Return PIDs for all processes. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$get_pids()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-get_processes}{}}} +\subsection{Method \code{get_processes()}}{ +Return the list of process objects. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$get_processes()}\if{html}{\out{
}} +} + +} +} diff --git a/man/process.Rd b/man/process.Rd index 2a25e41a..cdb614a2 100644 --- a/man/process.Rd +++ b/man/process.Rd @@ -131,70 +131,69 @@ p$is_alive() } \section{Methods}{ \subsection{Public methods}{ - \itemize{ - \item \href{#method-process-initialize}{\code{process$new()}} - \item \href{#method-process-kill}{\code{process$kill()}} - \item \href{#method-process-kill_tree}{\code{process$kill_tree()}} - \item \href{#method-process-signal}{\code{process$signal()}} - \item \href{#method-process-interrupt}{\code{process$interrupt()}} - \item \href{#method-process-get_pid}{\code{process$get_pid()}} - \item \href{#method-process-is_alive}{\code{process$is_alive()}} - \item \href{#method-process-wait}{\code{process$wait()}} - \item \href{#method-process-get_exit_status}{\code{process$get_exit_status()}} - \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()}} - \item \href{#method-process-read_error}{\code{process$read_error()}} - \item \href{#method-process-read_output_bytes}{\code{process$read_output_bytes()}} - \item \href{#method-process-read_error_bytes}{\code{process$read_error_bytes()}} - \item \href{#method-process-read_output_lines}{\code{process$read_output_lines()}} - \item \href{#method-process-read_error_lines}{\code{process$read_error_lines()}} - \item \href{#method-process-is_incomplete_output}{\code{process$is_incomplete_output()}} - \item \href{#method-process-is_incomplete_error}{\code{process$is_incomplete_error()}} - \item \href{#method-process-has_input_connection}{\code{process$has_input_connection()}} - \item \href{#method-process-has_output_connection}{\code{process$has_output_connection()}} - \item \href{#method-process-has_error_connection}{\code{process$has_error_connection()}} - \item \href{#method-process-has_poll_connection}{\code{process$has_poll_connection()}} - \item \href{#method-process-get_input_connection}{\code{process$get_input_connection()}} - \item \href{#method-process-get_output_connection}{\code{process$get_output_connection()}} - \item \href{#method-process-get_error_connection}{\code{process$get_error_connection()}} - \item \href{#method-process-read_all_output}{\code{process$read_all_output()}} - \item \href{#method-process-read_all_error}{\code{process$read_all_error()}} - \item \href{#method-process-read_all_output_lines}{\code{process$read_all_output_lines()}} - \item \href{#method-process-read_all_error_lines}{\code{process$read_all_error_lines()}} - \item \href{#method-process-write_input}{\code{process$write_input()}} - \item \href{#method-process-get_input_file}{\code{process$get_input_file()}} - \item \href{#method-process-get_output_file}{\code{process$get_output_file()}} - \item \href{#method-process-get_error_file}{\code{process$get_error_file()}} - \item \href{#method-process-poll_io}{\code{process$poll_io()}} - \item \href{#method-process-get_poll_connection}{\code{process$get_poll_connection()}} - \item \href{#method-process-get_result}{\code{process$get_result()}} - \item \href{#method-process-as_ps_handle}{\code{process$as_ps_handle()}} - \item \href{#method-process-get_name}{\code{process$get_name()}} - \item \href{#method-process-get_exe}{\code{process$get_exe()}} - \item \href{#method-process-get_cmdline}{\code{process$get_cmdline()}} - \item \href{#method-process-get_status}{\code{process$get_status()}} - \item \href{#method-process-get_username}{\code{process$get_username()}} - \item \href{#method-process-get_wd}{\code{process$get_wd()}} - \item \href{#method-process-get_cpu_times}{\code{process$get_cpu_times()}} - \item \href{#method-process-get_memory_info}{\code{process$get_memory_info()}} - \item \href{#method-process-suspend}{\code{process$suspend()}} - \item \href{#method-process-resume}{\code{process$resume()}} - \item \href{#method-process-clone}{\code{process$clone()}} - } +\itemize{ +\item \href{#method-process-new}{\code{process$new()}} +\item \href{#method-process-kill}{\code{process$kill()}} +\item \href{#method-process-kill_tree}{\code{process$kill_tree()}} +\item \href{#method-process-signal}{\code{process$signal()}} +\item \href{#method-process-interrupt}{\code{process$interrupt()}} +\item \href{#method-process-get_pid}{\code{process$get_pid()}} +\item \href{#method-process-is_alive}{\code{process$is_alive()}} +\item \href{#method-process-wait}{\code{process$wait()}} +\item \href{#method-process-get_exit_status}{\code{process$get_exit_status()}} +\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()}} +\item \href{#method-process-read_error}{\code{process$read_error()}} +\item \href{#method-process-read_output_bytes}{\code{process$read_output_bytes()}} +\item \href{#method-process-read_error_bytes}{\code{process$read_error_bytes()}} +\item \href{#method-process-read_output_lines}{\code{process$read_output_lines()}} +\item \href{#method-process-read_error_lines}{\code{process$read_error_lines()}} +\item \href{#method-process-is_incomplete_output}{\code{process$is_incomplete_output()}} +\item \href{#method-process-is_incomplete_error}{\code{process$is_incomplete_error()}} +\item \href{#method-process-has_input_connection}{\code{process$has_input_connection()}} +\item \href{#method-process-has_output_connection}{\code{process$has_output_connection()}} +\item \href{#method-process-has_error_connection}{\code{process$has_error_connection()}} +\item \href{#method-process-has_poll_connection}{\code{process$has_poll_connection()}} +\item \href{#method-process-get_input_connection}{\code{process$get_input_connection()}} +\item \href{#method-process-get_output_connection}{\code{process$get_output_connection()}} +\item \href{#method-process-get_error_connection}{\code{process$get_error_connection()}} +\item \href{#method-process-read_all_output}{\code{process$read_all_output()}} +\item \href{#method-process-read_all_error}{\code{process$read_all_error()}} +\item \href{#method-process-read_all_output_lines}{\code{process$read_all_output_lines()}} +\item \href{#method-process-read_all_error_lines}{\code{process$read_all_error_lines()}} +\item \href{#method-process-write_input}{\code{process$write_input()}} +\item \href{#method-process-get_input_file}{\code{process$get_input_file()}} +\item \href{#method-process-get_output_file}{\code{process$get_output_file()}} +\item \href{#method-process-get_error_file}{\code{process$get_error_file()}} +\item \href{#method-process-poll_io}{\code{process$poll_io()}} +\item \href{#method-process-get_poll_connection}{\code{process$get_poll_connection()}} +\item \href{#method-process-get_result}{\code{process$get_result()}} +\item \href{#method-process-as_ps_handle}{\code{process$as_ps_handle()}} +\item \href{#method-process-get_name}{\code{process$get_name()}} +\item \href{#method-process-get_exe}{\code{process$get_exe()}} +\item \href{#method-process-get_cmdline}{\code{process$get_cmdline()}} +\item \href{#method-process-get_status}{\code{process$get_status()}} +\item \href{#method-process-get_username}{\code{process$get_username()}} +\item \href{#method-process-get_wd}{\code{process$get_wd()}} +\item \href{#method-process-get_cpu_times}{\code{process$get_cpu_times()}} +\item \href{#method-process-get_memory_info}{\code{process$get_memory_info()}} +\item \href{#method-process-suspend}{\code{process$suspend()}} +\item \href{#method-process-resume}{\code{process$resume()}} +\item \href{#method-process-clone}{\code{process$clone()}} +} } \if{html}{\out{
}} -\if{html}{\out{}} -\if{latex}{\out{\hypertarget{method-process-initialize}{}}} -\subsection{\code{process$new()}}{ - Start a new process in the background, and then return immediately. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$new( +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-process-new}{}}} +\subsection{Method \code{new()}}{ +Start a new process in the background, and then return immediately. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$new( command = NULL, args = character(), stdin = NULL, @@ -216,23 +215,25 @@ p$is_alive() encoding = "", post_process = NULL, linux_pdeathsig = FALSE -)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{command}}{Character scalar, the command to run. +)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{command}}{Character scalar, the command to run. Note that this argument is not passed to a shell, so no tilde-expansion or variable substitution is performed on it. It should not be quoted with \code{\link[base:shQuote]{base::shQuote()}}. See \code{\link[base:normalizePath]{base::normalizePath()}} for tilde-expansion. If you want to run \code{.bat} or \code{.cmd} files on Windows, make sure you read the 'Batch files' section above.} - \item{\code{args}}{Character vector, arguments to the command. They will be + +\item{\code{args}}{Character vector, arguments to the command. They will be passed to the process as is, without a shell transforming them, They don't need to be escaped.} - \item{\code{stdin}}{What to do with the standard input. Possible values: + +\item{\code{stdin}}{What to do with the standard input. Possible values: \itemize{ \item \code{NULL}: set to the \emph{null device}, i.e. no standard input is provided; @@ -242,7 +243,8 @@ provided; main R process does not have a standard input stream, e.g. in RGui on Windows, then an error is thrown. }} - \item{\code{stdout}}{What to do with the standard output. Possible values: + +\item{\code{stdout}}{What to do with the standard output. Possible values: \itemize{ \item \code{NULL}: discard it; \item A string starting with \code{">>"}, e.g. \code{">>output.txt"}: append it @@ -257,7 +259,8 @@ directory in the \code{wd} argument. (See issue 324.) main R process does not have a standard output stream, e.g. in RGui on Windows, then an error is thrown. }} - \item{\code{stderr}}{What to do with the standard error. Possible values: + +\item{\code{stderr}}{What to do with the standard error. Possible values: \itemize{ \item \code{NULL}: discard it. \item A string starting with \code{">>"}, e.g. \code{">>error.txt"}: append it @@ -275,7 +278,8 @@ correctly interleaved. main R process does not have a standard error stream, e.g. in RGui on Windows, then an error is thrown. }} - \item{\code{pty}}{Whether to create a pseudo terminal (pty) for the + +\item{\code{pty}}{Whether to create a pseudo terminal (pty) for the background process. This is currently only supported on Unix systems, but not supported on Solaris. If it is \code{TRUE}, then the \code{stdin}, \code{stdout} and \code{stderr} arguments @@ -289,11 +293,14 @@ the \verb{$read_output()} method. Also, because \verb{$read_output_lines()} could still block if no complete line is available, this function always fails if the process has a pty. Use \verb{$read_output()} to read from ptys.} - \item{\code{pty_options}}{Unix pseudo terminal options, a named list. see + +\item{\code{pty_options}}{Unix pseudo terminal options, a named list. see \code{\link[=default_pty_options]{default_pty_options()}} for details and defaults.} - \item{\code{connections}}{A list of processx connections to pass to the + +\item{\code{connections}}{A list of processx connections to pass to the child process. This is an experimental feature currently.} - \item{\code{poll_connection}}{Whether to create an extra connection to the + +\item{\code{poll_connection}}{Whether to create an extra connection to the process that allows polling, even if the standard input and standard output are not pipes. If this is \code{NULL} (the default), then this connection will be only created if standard output and @@ -303,7 +310,8 @@ If the poll connection is created, you can query it via to \code{p$poll_io()} and \code{\link[=poll]{poll()}}. The numeric file descriptor of the poll connection comes right after \code{stderr} (2), and the connections listed in \code{connections}.} - \item{\code{env}}{Environment variables of the child process. If \code{NULL}, + +\item{\code{env}}{Environment variables of the child process. If \code{NULL}, the parent's environment is inherited. On Windows, many programs cannot function correctly if some environment variables are not set, so we always set \code{HOMEDRIVE}, \code{HOMEPATH}, \code{LOGONSERVER}, @@ -312,26 +320,35 @@ set, so we always set \code{HOMEDRIVE}, \code{HOMEPATH}, \code{LOGONSERVER}, variables to the ones set in the current process, specify \code{"current"} in \code{env}, without a name, and the appended ones with names. The appended ones can overwrite the current ones.} - \item{\code{cleanup}}{Whether to kill the process when the \code{process} + +\item{\code{cleanup}}{Whether to kill the process when the \code{process} object is garbage collected.} - \item{\code{cleanup_tree}}{Whether to kill the process and its child + +\item{\code{cleanup_tree}}{Whether to kill the process and its child process tree when the \code{process} object is garbage collected.} - \item{\code{wd}}{Working directory of the process. It must exist. + +\item{\code{wd}}{Working directory of the process. It must exist. If \code{NULL}, then the current working directory is used.} - \item{\code{echo_cmd}}{Whether to print the command to the screen before + +\item{\code{echo_cmd}}{Whether to print the command to the screen before running it.} - \item{\code{supervise}}{Whether to register the process with a supervisor. + +\item{\code{supervise}}{Whether to register the process with a supervisor. If \code{TRUE}, the supervisor will ensure that the process is killed when the R process exits.} - \item{\code{windows_verbatim_args}}{Whether to omit quoting the arguments + +\item{\code{windows_verbatim_args}}{Whether to omit quoting the arguments on Windows. It is ignored on other platforms.} - \item{\code{windows_hide_window}}{Whether to hide the application's window + +\item{\code{windows_hide_window}}{Whether to hide the application's window on Windows. It is ignored on other platforms.} - \item{\code{windows_detached_process}}{Whether to use the + +\item{\code{windows_detached_process}}{Whether to use the \code{DETACHED_PROCESS} flag on Windows. If this is \code{TRUE}, then the child process will have no attached console, even if the parent had one.} - \item{\code{encoding}}{The encoding to assume for \code{stdin}, \code{stdout} and + +\item{\code{encoding}}{The encoding to assume for \code{stdin}, \code{stdout} and \code{stderr}. By default the encoding of the current locale is used. Note that \code{processx} always reencodes the output of the \code{stdout} and \code{stderr} streams in UTF-8 currently. @@ -340,53 +357,53 @@ specify \code{"UTF-8"} as encoding. Use \code{"binary"} to disable text conversion entirely: \verb{$read_output()} and \verb{$read_error()} will return raw vectors instead of character strings, preserving all bytes including null bytes and non-UTF-8 byte sequences.} - \item{\code{post_process}}{An optional function to run when the process has + +\item{\code{post_process}}{An optional function to run when the process has finished. Currently it only runs if \verb{$get_result()} is called. It is only run once.} - \item{\code{linux_pdeathsig}}{On Linux, send this signal to the child process + +\item{\code{linux_pdeathsig}}{On Linux, send this signal to the child process when the parent R process exits. \code{FALSE} (the default) disables this. \code{TRUE} sends \code{SIGTERM}. An integer signal number, e.g. \code{tools::SIGTERM} or \code{tools::SIGKILL}, sends that signal. Ignored on non-Linux platforms.} - } - \if{html}{\out{
}} - } - \subsection{Returns}{ - R6 object representing the process. - } } - +\if{html}{\out{
}} +} +\subsection{Returns}{ +R6 object representing the process. +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-kill}{}}} -\subsection{\code{process$kill()}}{ - Terminate the process. It also terminate all of its child +\subsection{Method \code{kill()}}{ +Terminate the process. It also terminate all of its child processes, except if they have created a new process group (on Unix), or job object (on Windows). It returns \code{TRUE} if the process was terminated, and \code{FALSE} if it was not (because it was already finished/dead when \code{processx} tried to terminate it). - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$kill(grace = 0.1, close_connections = TRUE)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{grace}}{Currently not used.} - \item{\code{close_connections}}{Whether to close standard input, standard +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$kill(grace = 0.1, close_connections = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{grace}}{Currently not used.} + +\item{\code{close_connections}}{Whether to close standard input, standard output, standard error connections and the poll connection, after killing the process.} - } - \if{html}{\out{
}} - } } - +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-kill_tree}{}}} -\subsection{\code{process$kill_tree()}}{ - Process tree cleanup. It terminates the process +\subsection{Method \code{kill_tree()}}{ +Process tree cleanup. It terminates the process (if still alive), together with any child (or grandchild, etc.) processes. It uses the \emph{ps} package, so that needs to be installed, and \emph{ps} needs to support the current platform as well. Process tree @@ -397,98 +414,89 @@ to the root of the tree cleanup in the process tree any more. \verb{$kill_tree()} returns a named integer vector of the process ids that were killed, the names are the names of the processes (e.g. \code{"sleep"}, \code{"notepad.exe"}, \code{"Rterm.exe"}, etc.). - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$kill_tree(grace = 0.1, close_connections = TRUE)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{grace}}{Currently not used.} - \item{\code{close_connections}}{Whether to close standard input, standard +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$kill_tree(grace = 0.1, close_connections = TRUE)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{grace}}{Currently not used.} + +\item{\code{close_connections}}{Whether to close standard input, standard output, standard error connections and the poll connection, after killing the process.} - } - \if{html}{\out{
}} - } } - +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-signal}{}}} -\subsection{\code{process$signal()}}{ - Send a signal to the process. On Windows only the +\subsection{Method \code{signal()}}{ +Send a signal to the process. On Windows only the \code{SIGINT}, \code{SIGTERM} and \code{SIGKILL} signals are interpreted, and the special 0 signal. The first three all kill the process. The 0 signal returns \code{TRUE} if the process is alive, and \code{FALSE} otherwise. On Unix all signals are supported that the OS supports, and the 0 signal as well. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$signal(signal)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{signal}}{An integer scalar, the id of the signal to send to -the process. See \code{\link[tools:pskill]{tools::pskill()}} for the list of signals.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$signal(signal)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{signal}}{An integer scalar, the id of the signal to send to +the process. See \code{\link[tools:pskill]{tools::pskill()}} for the list of signals.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-interrupt}{}}} -\subsection{\code{process$interrupt()}}{ - Send an interrupt to the process. On Unix this is a +\subsection{Method \code{interrupt()}}{ +Send an interrupt to the process. On Unix this is a \code{SIGINT} signal, and it is usually equivalent to pressing CTRL+C at the terminal prompt. On Windows, it is a CTRL+BREAK keypress. Applications may catch these events. By default they will quit. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$interrupt()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$interrupt()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_pid}{}}} -\subsection{\code{process$get_pid()}}{ - Query the process id. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_pid()} - \if{html}{\out{
}} - } - \subsection{Returns}{ - Integer scalar, the process id of the process. - } +\subsection{Method \code{get_pid()}}{ +Query the process id. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_pid()}\if{html}{\out{
}} } +\subsection{Returns}{ +Integer scalar, the process id of the process. +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-is_alive}{}}} -\subsection{\code{process$is_alive()}}{ - Check if the process is alive. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$is_alive()} - \if{html}{\out{
}} - } - \subsection{Returns}{ - Logical scalar. - } +\subsection{Method \code{is_alive()}}{ +Check if the process is alive. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$is_alive()}\if{html}{\out{
}} } +\subsection{Returns}{ +Logical scalar. +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-wait}{}}} -\subsection{\code{process$wait()}}{ - Wait until the process finishes, or a timeout happens. +\subsection{Method \code{wait()}}{ +Wait until the process finishes, or a timeout happens. Note that if the process never finishes, and the timeout is infinite (the default), then R will never regain control. In some rare cases, \verb{$wait()} might take a bit longer than specified to time out. This @@ -496,141 +504,125 @@ happens on Unix, when another package overwrites the processx \code{SIGCHLD} signal handler, after the processx process has started. One such package is parallel, if used with fork clusters, e.g. through \code{parallel::mcparallel()}. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$wait(timeout = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{timeout}}{Timeout in milliseconds, for the wait or the I/O -polling.} - } - \if{html}{\out{
}} - } - \subsection{Returns}{ - It returns the process itself, invisibly. - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$wait(timeout = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{timeout}}{Timeout in milliseconds, for the wait or the I/O +polling.} +} +\if{html}{\out{
}} +} +\subsection{Returns}{ +It returns the process itself, invisibly. +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_exit_status}{}}} -\subsection{\code{process$get_exit_status()}}{ - \verb{$get_exit_status} returns the exit code of the process if it has +\subsection{Method \code{get_exit_status()}}{ +\verb{$get_exit_status} returns the exit code of the process if it has finished and \code{NULL} otherwise. On Unix, in some rare cases, the exit status might be \code{NA}. This happens if another package (or R itself) overwrites the processx \code{SIGCHLD} handler, after the processx process has started. In these cases processx cannot determine the real exit status of the process. One such package is parallel, if used with fork clusters, e.g. through the \code{parallel::mcparallel()} function. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_exit_status()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_exit_status()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-format}{}}} -\subsection{\code{process$format()}}{ - \code{format(p)} or \code{p$format()} creates a string representation of the +\subsection{Method \code{format()}}{ +\code{format(p)} or \code{p$format()} creates a string representation of the process, usually for printing. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$format()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$format()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-print}{}}} -\subsection{\code{process$print()}}{ - \code{print(p)} or \code{p$print()} shows some information about the +\subsection{Method \code{print()}}{ +\code{print(p)} or \code{p$print()} shows some information about the process on the screen, whether it is running and it's process id, etc. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$print()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$print()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_start_time}{}}} -\subsection{\code{process$get_start_time()}}{ - \verb{$get_start_time()} returns the time when the process was +\subsection{Method \code{get_start_time()}}{ +\verb{$get_start_time()} returns the time when the process was started. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_start_time()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_start_time()}\if{html}{\out{
}} } +} \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, +\subsection{Method \code{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{
}} - } +\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}{}}} -\subsection{\code{process$is_supervised()}}{ - \verb{$is_supervised()} returns whether the process is being tracked by +\subsection{Method \code{is_supervised()}}{ +\verb{$is_supervised()} returns whether the process is being tracked by supervisor process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$is_supervised()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$is_supervised()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-supervise}{}}} -\subsection{\code{process$supervise()}}{ - \verb{$supervise()} if passed \code{TRUE}, tells the supervisor to start +\subsection{Method \code{supervise()}}{ +\verb{$supervise()} if passed \code{TRUE}, tells the supervisor to start tracking the process. If \code{FALSE}, tells the supervisor to stop tracking the process. Note that even if the supervisor is disabled for a process, if it was started with \code{cleanup = TRUE}, the process will still be killed when the object is garbage collected. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$supervise(status)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{status}}{Whether to turn on of off the supervisor for this -process.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$supervise(status)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{status}}{Whether to turn on of off the supervisor for this +process.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_output}{}}} -\subsection{\code{process$read_output()}}{ - \verb{$read_output()} reads from the standard output connection of the +\subsection{Method \code{read_output()}}{ +\verb{$read_output()} reads from the standard output connection of the process. If the standard output connection was not requested, then then it returns an error. It uses a non-blocking text connection. This will work only if \code{stdout="|"} was used. Otherwise, it will throw an @@ -643,90 +635,82 @@ finite, and \verb{$read_output()} only returns what is already buffered. Always call \verb{$poll_io()} (or \code{\link[=poll]{poll()}}) before reading to avoid deadlocking when the child fills the pipe buffer (see the \emph{Polling} section for details). To read \emph{all} output call \verb{$read_all_output()}. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_output(n = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{n}}{Number of characters or lines to read.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_output(n = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_error}{}}} -\subsection{\code{process$read_error()}}{ - \verb{$read_error()} is similar to \verb{$read_output()}, but reads from the +\subsection{Method \code{read_error()}}{ +\verb{$read_error()} is similar to \verb{$read_output()}, but reads from the standard error stream. Returns a raw vector when \code{encoding = "binary"} was used. The same polling requirement applies as for \verb{$read_output()} (see the \emph{Polling} section). - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_error(n = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{n}}{Number of characters or lines to read.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_error(n = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_output_bytes}{}}} -\subsection{\code{process$read_output_bytes()}}{ - \verb{$read_output_bytes()} reads from the standard output connection of +\subsection{Method \code{read_output_bytes()}}{ +\verb{$read_output_bytes()} reads from the standard output connection of the process and returns the result as a raw vector, preserving all bytes including null bytes and other binary data. Switches the underlying connection to raw mode; do not mix with \verb{$read_output()}. This will work only if \code{stdout="|"} was used. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_output_bytes(n = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{n}}{Number of characters or lines to read.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_output_bytes(n = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_error_bytes}{}}} -\subsection{\code{process$read_error_bytes()}}{ - \verb{$read_error_bytes()} is similar to \verb{$read_output_bytes()}, but reads +\subsection{Method \code{read_error_bytes()}}{ +\verb{$read_error_bytes()} is similar to \verb{$read_output_bytes()}, but reads from the standard error stream. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_error_bytes(n = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{n}}{Number of characters or lines to read.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_error_bytes(n = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_output_lines}{}}} -\subsection{\code{process$read_output_lines()}}{ - \verb{$read_output_lines()} reads lines from standard output connection +\subsection{Method \code{read_output_lines()}}{ +\verb{$read_output_lines()} reads lines from standard output connection of the process. If the standard output connection was not requested, then it returns an error. It uses a non-blocking text connection. This will work only if \code{stdout="|"} was used. Otherwise, it will @@ -739,236 +723,206 @@ on Linux/macOS, ~76KB on Windows) or when the line is not yet terminated. Always call \verb{$poll_io()} before reading to avoid deadlocking (see the \emph{Polling} section), and use \verb{$read_output()} when lines may be very long. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_output_lines(n = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{n}}{Number of characters or lines to read.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_output_lines(n = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_error_lines}{}}} -\subsection{\code{process$read_error_lines()}}{ - \verb{$read_error_lines()} is similar to \verb{$read_output_lines}, but +\subsection{Method \code{read_error_lines()}}{ +\verb{$read_error_lines()} is similar to \verb{$read_output_lines}, but it reads from the standard error stream. The same polling requirement applies (see the \emph{Polling} section). - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_error_lines(n = -1)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{n}}{Number of characters or lines to read.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_error_lines(n = -1)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{n}}{Number of characters or lines to read.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-is_incomplete_output}{}}} -\subsection{\code{process$is_incomplete_output()}}{ - \verb{$is_incomplete_output()} return \code{FALSE} if the other end of +\subsection{Method \code{is_incomplete_output()}}{ +\verb{$is_incomplete_output()} return \code{FALSE} if the other end of the standard output connection was closed (most probably because the process exited). It return \code{TRUE} otherwise. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$is_incomplete_output()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$is_incomplete_output()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-is_incomplete_error}{}}} -\subsection{\code{process$is_incomplete_error()}}{ - \verb{$is_incomplete_error()} return \code{FALSE} if the other end of +\subsection{Method \code{is_incomplete_error()}}{ +\verb{$is_incomplete_error()} return \code{FALSE} if the other end of the standard error connection was closed (most probably because the process exited). It return \code{TRUE} otherwise. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$is_incomplete_error()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$is_incomplete_error()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-has_input_connection}{}}} -\subsection{\code{process$has_input_connection()}}{ - \verb{$has_input_connection()} return \code{TRUE} if there is a connection +\subsection{Method \code{has_input_connection()}}{ +\verb{$has_input_connection()} return \code{TRUE} if there is a connection object for standard input; in other words, if \code{stdout="|"}. It returns \code{FALSE} otherwise. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$has_input_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$has_input_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-has_output_connection}{}}} -\subsection{\code{process$has_output_connection()}}{ - \verb{$has_output_connection()} returns \code{TRUE} if there is a connection +\subsection{Method \code{has_output_connection()}}{ +\verb{$has_output_connection()} returns \code{TRUE} if there is a connection object for standard output; in other words, if \code{stdout="|"}. It returns \code{FALSE} otherwise. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$has_output_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$has_output_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-has_error_connection}{}}} -\subsection{\code{process$has_error_connection()}}{ - \verb{$has_error_connection()} returns \code{TRUE} if there is a connection +\subsection{Method \code{has_error_connection()}}{ +\verb{$has_error_connection()} returns \code{TRUE} if there is a connection object for standard error; in other words, if \code{stderr="|"}. It returns \code{FALSE} otherwise. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$has_error_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$has_error_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-has_poll_connection}{}}} -\subsection{\code{process$has_poll_connection()}}{ - \verb{$has_poll_connection()} return \code{TRUE} if there is a poll connection, +\subsection{Method \code{has_poll_connection()}}{ +\verb{$has_poll_connection()} return \code{TRUE} if there is a poll connection, \code{FALSE} otherwise. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$has_poll_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$has_poll_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_input_connection}{}}} -\subsection{\code{process$get_input_connection()}}{ - \verb{$get_input_connection()} returns a connection object, to the +\subsection{Method \code{get_input_connection()}}{ +\verb{$get_input_connection()} returns a connection object, to the standard input stream of the process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_input_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_input_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_output_connection}{}}} -\subsection{\code{process$get_output_connection()}}{ - \verb{$get_output_connection()} returns a connection object, to the +\subsection{Method \code{get_output_connection()}}{ +\verb{$get_output_connection()} returns a connection object, to the standard output stream of the process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_output_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_output_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_error_connection}{}}} -\subsection{\code{process$get_error_connection()}}{ - \verb{$get_error_conneciton()} returns a connection object, to the +\subsection{Method \code{get_error_connection()}}{ +\verb{$get_error_conneciton()} returns a connection object, to the standard error stream of the process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_error_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_error_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_all_output}{}}} -\subsection{\code{process$read_all_output()}}{ - \verb{$read_all_output()} waits for all standard output from the process. +\subsection{Method \code{read_all_output()}}{ +\verb{$read_all_output()} waits for all standard output from the process. It does not return until the process has finished. Note that this process involves waiting for the process to finish, polling for I/O and potentially several \code{readLines()} calls. It returns a character scalar. This will return content only if \code{stdout="|"} was used. Otherwise, it will throw an error. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_all_output()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_all_output()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_all_error}{}}} -\subsection{\code{process$read_all_error()}}{ - \verb{$read_all_error()} waits for all standard error from the process. +\subsection{Method \code{read_all_error()}}{ +\verb{$read_all_error()} waits for all standard error from the process. It does not return until the process has finished. Note that this process involves waiting for the process to finish, polling for I/O and potentially several \code{readLines()} calls. It returns a character scalar. This will return content only if \code{stderr="|"} was used. Otherwise, it will throw an error. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_all_error()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_all_error()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_all_output_lines}{}}} -\subsection{\code{process$read_all_output_lines()}}{ - \verb{$read_all_output_lines()} waits for all standard output lines +\subsection{Method \code{read_all_output_lines()}}{ +\verb{$read_all_output_lines()} waits for all standard output lines from a process. It does not return until the process has finished. Note that this process involves waiting for the process to finish, polling for I/O and potentially several \code{readLines()} calls. It returns a character vector. This will return content only if \code{stdout="|"} was used. Otherwise, it will throw an error. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_all_output_lines()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_all_output_lines()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-read_all_error_lines}{}}} -\subsection{\code{process$read_all_error_lines()}}{ - \verb{$read_all_error_lines()} waits for all standard error lines from +\subsection{Method \code{read_all_error_lines()}}{ +\verb{$read_all_error_lines()} waits for all standard error lines from a process. It does not return until the process has finished. Note that this process involves waiting for the process to finish, polling for I/O and potentially several \code{readLines()} calls. It returns a character vector. This will return content only if \code{stderr="|"} was used. Otherwise, it will throw an error. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$read_all_error_lines()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$read_all_error_lines()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-write_input}{}}} -\subsection{\code{process$write_input()}}{ - \verb{$write_input()} writes the character vector (separated by \code{sep}) to +\subsection{Method \code{write_input()}}{ +\verb{$write_input()} writes the character vector (separated by \code{sep}) to the standard input of the process. It will be converted to the specified encoding. This operation is non-blocking, and it will return, even if the write fails (because the write buffer is full), or if it suceeds @@ -976,268 +930,231 @@ partially (i.e. not the full string is written). It returns with a raw vector, that contains the bytes that were not written. You can supply this raw vector to \verb{$write_input()} again, until it is fully written, and then the return value will be \code{raw(0)} (invisibly). - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$write_input(str, sep = "\\n")} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{str}}{Character or raw vector to write to the standard input +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$write_input(str, sep = "\\n")}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{str}}{Character or raw vector to write to the standard input of the process. If a character vector with a marked encoding, it will be converted to \code{encoding}.} - \item{\code{sep}}{Separator to add between \code{str} elements if it is a + +\item{\code{sep}}{Separator to add between \code{str} elements if it is a character vector. It is ignored if \code{str} is a raw vector.} - } - \if{html}{\out{
}} - } - \subsection{Returns}{ - Leftover text (as a raw vector), that was not written. - } } - +\if{html}{\out{
}} +} +\subsection{Returns}{ +Leftover text (as a raw vector), that was not written. +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_input_file}{}}} -\subsection{\code{process$get_input_file()}}{ - \verb{$get_input_file()} if the \code{stdin} argument was a filename, +\subsection{Method \code{get_input_file()}}{ +\verb{$get_input_file()} if the \code{stdin} argument was a filename, this returns the absolute path to the file. If \code{stdin} was \code{"|"} or \code{NULL}, this simply returns that value. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_input_file()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_input_file()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_output_file}{}}} -\subsection{\code{process$get_output_file()}}{ - \verb{$get_output_file()} if the \code{stdout} argument was a filename, +\subsection{Method \code{get_output_file()}}{ +\verb{$get_output_file()} if the \code{stdout} argument was a filename, this returns the absolute path to the file. If \code{stdout} was \code{"|"} or \code{NULL}, this simply returns that value. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_output_file()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_output_file()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_error_file}{}}} -\subsection{\code{process$get_error_file()}}{ - \verb{$get_error_file()} if the \code{stderr} argument was a filename, +\subsection{Method \code{get_error_file()}}{ +\verb{$get_error_file()} if the \code{stderr} argument was a filename, this returns the absolute path to the file. If \code{stderr} was \code{"|"} or \code{NULL}, this simply returns that value. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_error_file()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_error_file()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-poll_io}{}}} -\subsection{\code{process$poll_io()}}{ - \verb{$poll_io()} polls the process's connections for I/O. See more in +\subsection{Method \code{poll_io()}}{ +\verb{$poll_io()} polls the process's connections for I/O. See more in the \emph{Polling} section, and see also the \code{\link[=poll]{poll()}} function to poll on multiple processes. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$poll_io(timeout)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{timeout}}{Timeout in milliseconds, for the wait or the I/O -polling.} - } - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$poll_io(timeout)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{timeout}}{Timeout in milliseconds, for the wait or the I/O +polling.} +} +\if{html}{\out{
}} +} +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_poll_connection}{}}} -\subsection{\code{process$get_poll_connection()}}{ - \verb{$get_poll_connetion()} returns the poll connection, if the process has +\subsection{Method \code{get_poll_connection()}}{ +\verb{$get_poll_connetion()} returns the poll connection, if the process has one. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_poll_connection()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_poll_connection()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_result}{}}} -\subsection{\code{process$get_result()}}{ - \verb{$get_result()} returns the result of the post processesing function. +\subsection{Method \code{get_result()}}{ +\verb{$get_result()} returns the result of the post processesing function. It can only be called once the process has finished. If the process has no post-processing function, then \code{NULL} is returned. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_result()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_result()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-as_ps_handle}{}}} -\subsection{\code{process$as_ps_handle()}}{ - \verb{$as_ps_handle()} returns a \link[ps:ps_handle]{ps::ps_handle} object, corresponding to +\subsection{Method \code{as_ps_handle()}}{ +\verb{$as_ps_handle()} returns a \link[ps:ps_handle]{ps::ps_handle} object, corresponding to the process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$as_ps_handle()} - \if{html}{\out{
}} - } +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$as_ps_handle()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_name}{}}} -\subsection{\code{process$get_name()}}{ - Calls \code{\link[ps:ps_name]{ps::ps_name()}} to get the process name. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_name()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_name()}}{ +Calls \code{\link[ps:ps_name]{ps::ps_name()}} to get the process name. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_name()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_exe}{}}} -\subsection{\code{process$get_exe()}}{ - Calls \code{\link[ps:ps_exe]{ps::ps_exe()}} to get the path of the executable. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_exe()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_exe()}}{ +Calls \code{\link[ps:ps_exe]{ps::ps_exe()}} to get the path of the executable. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_exe()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_cmdline}{}}} -\subsection{\code{process$get_cmdline()}}{ - Calls \code{\link[ps:ps_cmdline]{ps::ps_cmdline()}} to get the command line. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_cmdline()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_cmdline()}}{ +Calls \code{\link[ps:ps_cmdline]{ps::ps_cmdline()}} to get the command line. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_cmdline()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_status}{}}} -\subsection{\code{process$get_status()}}{ - Calls \code{\link[ps:ps_status]{ps::ps_status()}} to get the process status. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_status()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_status()}}{ +Calls \code{\link[ps:ps_status]{ps::ps_status()}} to get the process status. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_status()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_username}{}}} -\subsection{\code{process$get_username()}}{ - calls \code{\link[ps:ps_username]{ps::ps_username()}} to get the username. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_username()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_username()}}{ +calls \code{\link[ps:ps_username]{ps::ps_username()}} to get the username. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_username()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_wd}{}}} -\subsection{\code{process$get_wd()}}{ - Calls \code{\link[ps:ps_cwd]{ps::ps_cwd()}} to get the current working directory. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_wd()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_wd()}}{ +Calls \code{\link[ps:ps_cwd]{ps::ps_cwd()}} to get the current working directory. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_wd()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_cpu_times}{}}} -\subsection{\code{process$get_cpu_times()}}{ - Calls \code{\link[ps:ps_cpu_times]{ps::ps_cpu_times()}} to get CPU usage data. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_cpu_times()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_cpu_times()}}{ +Calls \code{\link[ps:ps_cpu_times]{ps::ps_cpu_times()}} to get CPU usage data. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_cpu_times()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-get_memory_info}{}}} -\subsection{\code{process$get_memory_info()}}{ - Calls \code{\link[ps:ps_memory_info]{ps::ps_memory_info()}} to get memory data. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$get_memory_info()} - \if{html}{\out{
}} - } +\subsection{Method \code{get_memory_info()}}{ +Calls \code{\link[ps:ps_memory_info]{ps::ps_memory_info()}} to get memory data. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$get_memory_info()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-suspend}{}}} -\subsection{\code{process$suspend()}}{ - Calls \code{\link[ps:ps_suspend]{ps::ps_suspend()}} to suspend the process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$suspend()} - \if{html}{\out{
}} - } +\subsection{Method \code{suspend()}}{ +Calls \code{\link[ps:ps_suspend]{ps::ps_suspend()}} to suspend the process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$suspend()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-resume}{}}} -\subsection{\code{process$resume()}}{ - Calls \code{\link[ps:ps_resume]{ps::ps_resume()}} to resume a suspended process. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$resume()} - \if{html}{\out{
}} - } +\subsection{Method \code{resume()}}{ +Calls \code{\link[ps:ps_resume]{ps::ps_resume()}} to resume a suspended process. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$resume()}\if{html}{\out{
}} } +} \if{html}{\out{
}} \if{html}{\out{}} \if{latex}{\out{\hypertarget{method-process-clone}{}}} -\subsection{\code{process$clone()}}{ - The objects of this class are cloneable with this method. - \subsection{Usage}{ - \if{html}{\out{
}} - \preformatted{process$clone(deep = FALSE)} - \if{html}{\out{
}} - } - \subsection{Arguments}{ - \if{html}{\out{
}} - \describe{ - \item{\code{deep}}{Whether to make a deep clone.} - } - \if{html}{\out{
}} - } +\subsection{Method \code{clone()}}{ +The objects of this class are cloneable with this method. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{process$clone(deep = FALSE)}\if{html}{\out{
}} } +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{deep}}{Whether to make a deep clone.} +} +\if{html}{\out{
}} +} +} } diff --git a/man/processx-package.Rd b/man/processx-package.Rd index 48a2c775..f672b9f3 100644 --- a/man/processx-package.Rd +++ b/man/processx-package.Rd @@ -22,7 +22,6 @@ Useful links: Authors: \itemize{ - \item Gábor Csárdi \email{csardi.gabor@gmail.com} (\href{https://orcid.org/0000-0001-7098-9676}{ORCID}) [copyright holder] \item Winston Chang } diff --git a/man/processx_connections.Rd b/man/processx_connections.Rd index 0337f1fa..9dc96a70 100644 --- a/man/processx_connections.Rd +++ b/man/processx_connections.Rd @@ -4,6 +4,7 @@ \alias{conn_create_fd} \alias{conn_file_name} \alias{conn_create_pipepair} +\alias{conn_create_proc_pipepair} \alias{conn_read_chars} \alias{conn_read_chars.processx_connection} \alias{processx_conn_read_chars} @@ -35,6 +36,8 @@ conn_file_name(con) conn_create_pipepair(encoding = "", nonblocking = c(TRUE, FALSE)) +conn_create_proc_pipepair(encoding = "") + conn_read_chars(con, n = -1) \method{conn_read_chars}{processx_connection}(con, n = -1) @@ -105,9 +108,9 @@ characters or lines.} \item{sep}{Separator to use if \code{str} is a character vector. Ignored if \code{str} is a raw vector.} -\item{filename}{File name. For \code{conn_create_pipe()} on Windows, a +\item{filename}{File name. For \code{conn_create_fifo()} on Windows, a \verb{\\\\?\\pipe} prefix is added to this, if it does not have such a prefix. -For \code{conn_create_pipe()} it can also be \code{NULL}, in which case a random +For \code{conn_create_fifo()} it can also be \code{NULL}, in which case a random file name is used via \code{tempfile()}.} \item{read}{Whether the connection is readable.} @@ -136,6 +139,13 @@ where it returns the full name of the pipe. \code{conn_create_pipepair()} creates a pair of connected connections, the first one is writeable, the second one is readable. +\code{conn_create_proc_pipepair()} creates a unidirectional pipe suitable for +connecting two child processes: the first element is the write end (pass as +\code{stdout} to the writing process) and the second is the read end (pass as +\code{stdin} to the reading process). Unlike \code{conn_create_pipepair()}, both ends +are synchronous (blocking), which is required for child-process stdin/stdout +on Windows. + \code{conn_read_chars()} reads UTF-8 characters from the connections. If the connection itself is not UTF-8 encoded, it re-encodes it. diff --git a/man/processx_fifos.Rd b/man/processx_fifos.Rd index 71586b60..1bc52777 100644 --- a/man/processx_fifos.Rd +++ b/man/processx_fifos.Rd @@ -25,7 +25,7 @@ conn_connect_fifo( \item{filename}{File name of the FIFO. On Windows it the name of the pipe within the \verb{\\\\?\\pipe\\} namespace, either the full name, or the part after that prefix. If \code{NULL}, then a random name -is used, on Unix in the R temporary directory: \code{\link[base:tempdir]{base::tempdir()}}.} +is used, on Unix in the R temporary directory: \code{\link[base:tempfile]{base::tempdir()}}.} \item{read}{If \code{TRUE} then connect to the read end of the FIFO. Exactly one of \code{read} and \code{write} must be set to \code{TRUE}.} diff --git a/man/processx_sockets.Rd b/man/processx_sockets.Rd index f4c33add..a606da42 100644 --- a/man/processx_sockets.Rd +++ b/man/processx_sockets.Rd @@ -19,7 +19,7 @@ conn_unix_socket_state(con) \item{filename}{File name of the socket. On Windows it the name of the pipe within the \verb{\\\\?\\pipe\\} namespace, either the full name, or the part after that prefix. If \code{NULL}, then a random name -is used, on Unix in the R temporary directory: \code{\link[base:tempdir]{base::tempdir()}}.} +is used, on Unix in the R temporary directory: \code{\link[base:tempfile]{base::tempdir()}}.} \item{encoding}{Encoding to assume when reading from the socket.} diff --git a/src/init.c b/src/init.c index a4b5f571..b1ae202d 100644 --- a/src/init.c +++ b/src/init.c @@ -142,6 +142,8 @@ static const R_CallMethodDef callMethods[] = { (DL_FUNC) processx_connection_socket_state, 1 }, { "processx_connection_create_pipepair", (DL_FUNC) processx_connection_create_pipepair, 2 }, + { "processx_connection_create_proc_pipepair", + (DL_FUNC) processx_connection_create_proc_pipepair, 1 }, { "processx_connection_create_fd", (DL_FUNC) &processx_connection_create_fd, 3 }, { "processx_connection_create_file", (DL_FUNC) &processx_connection_create_file, 3 }, diff --git a/src/processx-connection.c b/src/processx-connection.c index 01fccace..d6c6ddbc 100644 --- a/src/processx-connection.c +++ b/src/processx-connection.c @@ -660,6 +660,55 @@ SEXP processx_connection_create_pipepair(SEXP encoding, SEXP nonblocking) { return result; } +SEXP processx_connection_create_proc_pipepair(SEXP encoding) { + const char *c_encoding = CHAR(STRING_ELT(encoding, 0)); + SEXP result, con1, con2; + +#ifdef _WIN32 + HANDLE hRead = INVALID_HANDLE_VALUE; + HANDLE hWrite = INVALID_HANDLE_VALUE; + SECURITY_ATTRIBUTES sa; + sa.nLength = sizeof(sa); + sa.lpSecurityDescriptor = NULL; + sa.bInheritHandle = TRUE; + + if (!CreatePipe(&hRead, &hWrite, &sa, 0)) { + R_THROW_SYSTEM_ERROR("Cannot create pipe"); + } + + /* con1 = write end [[1]], con2 = read end [[2]] */ + processx_c_connection_create(hWrite, PROCESSX_FILE_TYPE_PIPE, + c_encoding, NULL, &con1); + PROTECT(con1); + processx_c_connection_create(hRead, PROCESSX_FILE_TYPE_PIPE, + c_encoding, NULL, &con2); + PROTECT(con2); + +#else + int fds[2]; + if (pipe(fds) == -1) { + R_THROW_SYSTEM_ERROR("Cannot create pipe"); + } + processx__cloexec_fcntl(fds[0], 1); + processx__cloexec_fcntl(fds[1], 1); + + /* fds[1] = write end [[1]], fds[0] = read end [[2]], both blocking */ + processx_c_connection_create(fds[1], PROCESSX_FILE_TYPE_PIPE, + c_encoding, NULL, &con1); + PROTECT(con1); + processx_c_connection_create(fds[0], PROCESSX_FILE_TYPE_PIPE, + c_encoding, NULL, &con2); + PROTECT(con2); +#endif + + result = PROTECT(allocVector(VECSXP, 2)); + SET_VECTOR_ELT(result, 0, con1); /* [[1]] = write end */ + SET_VECTOR_ELT(result, 1, con2); /* [[2]] = read end */ + + UNPROTECT(3); + return result; +} + SEXP processx__connection_set_std(SEXP con, int which, int drop) { processx_connection_t *ccon = R_ExternalPtrAddr(con); if (!ccon) R_THROW_ERROR("Invalid connection object"); diff --git a/src/processx-connection.h b/src/processx-connection.h index aed4d48e..40a004ed 100644 --- a/src/processx-connection.h +++ b/src/processx-connection.h @@ -184,6 +184,7 @@ SEXP processx_connection_poll(SEXP pollables, SEXP timeout); /* Functions for connection inheritance */ SEXP processx_connection_create_pipepair(SEXP encoding, SEXP nonblocking); +SEXP processx_connection_create_proc_pipepair(SEXP encoding); SEXP processx_connection_set_stdout(SEXP con, SEXP drop); diff --git a/tests/testthat/_snaps/Darwin/unix-sockets.new.md b/tests/testthat/_snaps/Darwin/unix-sockets.new.md new file mode 100644 index 00000000..30dd74c6 --- /dev/null +++ b/tests/testthat/_snaps/Darwin/unix-sockets.new.md @@ -0,0 +1,34 @@ +# reading unaccepted server socket is error + + Code + conn_read_chars(sock1) + Condition + Error in `processx_conn_read_chars()`: + ! ! Native call to `processx_connection_read_chars` failed + Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:318:: + ! Cannot read from processx connection (system error 57, Socket is not connected) @processx-connection.c:1940 (processx__connection_read) + +# errors + + Code + conn_create_unix_socket(sock) + Condition + Error in `conn_create_unix_socket()`: + ! ! Native call to `processx_connection_create_socket` failed + Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: + ! Server socket path too long: / + Code + conn_create_unix_socket("/dev/null") + Condition + Error in `conn_create_unix_socket()`: + ! ! Native call to `processx_connection_create_socket` failed + Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: + ! Cannot bind to socket (system error 48, Address already in use) @processx-connection.c:479 (processx_connection_create_socket) + Code + conn_connect_unix_socket("/dev/null") + Condition + Error in `conn_connect_unix_socket()`: + ! ! Native call to `processx_connection_connect_socket` failed + Caused by error in `chain_call(c_processx_connection_connect_socket, filename, encoding)` at connections.R:656:: + ! Cannot connect to socket (system error 38, Socket operation on non-socket) @processx-connection.c:550 (processx_connection_connect_socket) + diff --git a/tests/testthat/_snaps/unix-sockets.new.md b/tests/testthat/_snaps/unix-sockets.new.md new file mode 100644 index 00000000..ac90e7ed --- /dev/null +++ b/tests/testthat/_snaps/unix-sockets.new.md @@ -0,0 +1,40 @@ +# CRUD + + Code + conn_accept_unix_socket(sock1) + Condition + Error in `conn_accept_unix_socket()`: + ! ! Native call to `processx_connection_accept_socket` failed + Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:669:: + ! Socket is not listening @processx-connection.c:577 (processx_connection_accept_socket) + +# writing unaccepted server socket is error + + Code + conn_write(sock1, "Hello\n") + Condition + Error in `processx_conn_write()`: + ! ! Native call to `processx_connection_write_bytes` failed + Caused by error in `chain_call(c_processx_connection_write_bytes, con, str)` at connections.R:440:: + ! Cannot write to an un-accepted socket connection @processx-connection.c:1058 (processx_c_connection_write_bytes) + +# errors + + Code + conn_accept_unix_socket(ff) + Condition + Error in `conn_accept_unix_socket()`: + ! ! Native call to `processx_connection_accept_socket` failed + Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:669:: + ! Not a socket connection @processx-connection.c:573 (processx_connection_accept_socket) + +--- + + Code + conn_unix_socket_state(ff) + Condition + Error in `conn_unix_socket_state()`: + ! ! Native call to `processx_connection_socket_state` failed + Caused by error in `chain_call(c_processx_connection_socket_state, con)` at connections.R:681:: + ! Not a socket connection @processx-connection.c:622 (processx_connection_socket_state) + diff --git a/tests/testthat/test-pipeline.R b/tests/testthat/test-pipeline.R new file mode 100644 index 00000000..9eb182c2 --- /dev/null +++ b/tests/testthat/test-pipeline.R @@ -0,0 +1,137 @@ +test_that("2-process pipeline: sort | uniq", { + skip_on_cran() + + pl <- pipeline$new( + list(c("sort"), c("uniq")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + pl$write_input("b\na\nb\na\n") + pl$close_input() + out <- pl$read_all_output_lines() + pl$wait() + + expect_equal(out, c("a", "b")) + expect_equal(pl$get_exit_statuses(), list(0L, 0L)) +}) + +test_that("3-process pipeline: cat | sort | uniq", { + skip_on_cran() + + pl <- pipeline$new( + list(c("cat"), c("sort"), c("uniq")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + pl$write_input("b\na\nb\na\n") + pl$close_input() + out <- pl$read_all_output_lines() + pl$wait() + + expect_equal(out, c("a", "b")) + expect_equal(pl$get_exit_statuses(), list(0L, 0L, 0L)) +}) + +test_that("single-process pipeline is equivalent to process$new()", { + skip_on_cran() + + pl <- pipeline$new(list(c("echo", "hello")), stdout = "|") + on.exit(pl$kill(), add = TRUE) + + pl$wait() + out <- trimws(pl$read_all_output()) + expect_equal(out, "hello") + expect_equal(pl$get_exit_statuses(), list(0L)) +}) + +test_that("pipeline is_alive() and get_pids()", { + skip_on_cran() + + pl <- pipeline$new( + list(c("sort"), c("cat")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + expect_true(pl$is_alive()) + pids <- pl$get_pids() + expect_length(pids, 2L) + expect_true(all(pids > 0L)) + + pl$close_input() + pl$wait() + expect_false(pl$is_alive()) +}) + +test_that("pipeline get_processes() returns process objects", { + skip_on_cran() + + pl <- pipeline$new( + list(c("sort"), c("uniq")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + procs <- pl$get_processes() + expect_length(procs, 2L) + expect_true(all(vapply(procs, inherits, logical(1L), "process"))) + + pl$close_input() + pl$wait() +}) + +test_that("pipeline kill() stops all processes", { + skip_on_cran() + + pl <- pipeline$new( + list(c("cat"), c("cat")), + stdin = "|", + stdout = "|" + ) + + expect_true(pl$is_alive()) + pl$kill() + Sys.sleep(0.1) + expect_false(pl$is_alive()) +}) + +test_that("pipeline stdout to file", { + skip_on_cran() + + tmp <- tempfile() + on.exit(unlink(tmp), add = TRUE) + + pl <- pipeline$new( + list(c("sort"), c("uniq")), + stdin = "|", + stdout = tmp + ) + on.exit(pl$kill(), add = TRUE) + + pl$write_input("b\na\nb\na\n") + pl$close_input() + pl$wait() + + expect_equal(readLines(tmp), c("a", "b")) + expect_equal(pl$get_exit_statuses(), list(0L, 0L)) +}) + +test_that("conn_create_proc_pipepair() returns write/read ends", { + skip_on_cran() + + pipe <- conn_create_proc_pipepair() + on.exit({ + try(close(pipe[[1]]), silent = TRUE) + try(close(pipe[[2]]), silent = TRUE) + }, add = TRUE) + + expect_length(pipe, 2L) + expect_true(is_connection(pipe[[1]])) + expect_true(is_connection(pipe[[2]])) +}) From e9dce2f390e0da75ce13a30256300bb2e8e2d101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 09:39:04 +0200 Subject: [PATCH 03/13] Fix URL to cleanup article Until the release we need to refer to the dev docs. --- R/process.R | 2 +- man/process.Rd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/R/process.R b/R/process.R index 4090d3f2..a492a0ad 100644 --- a/R/process.R +++ b/R/process.R @@ -71,7 +71,7 @@ dummy_r6 <- function() R6::R6Class #' #' @section Cleaning up background processes: #' processx provides several mechanisms to clean up background processes. -#' See the [Process cleanup](https://processx.r-lib.org/articles/cleanup.html) +#' See the [Process cleanup](https://processx.r-lib.org/dev/articles/cleanup.html) #' article for a full discussion. A brief summary: #' #' * **Explicit cleanup** (most reliable): call `$kill()` or `$kill_tree()` diff --git a/man/process.Rd b/man/process.Rd index cdb614a2..1a6ce99a 100644 --- a/man/process.Rd +++ b/man/process.Rd @@ -64,7 +64,7 @@ to collect everything. The \verb{$read_all_output()} and \section{Cleaning up background processes}{ processx provides several mechanisms to clean up background processes. -See the \href{https://processx.r-lib.org/articles/cleanup.html}{Process cleanup} +See the \href{https://processx.r-lib.org/dev/articles/cleanup.html}{Process cleanup} article for a full discussion. A brief summary: \itemize{ \item \strong{Explicit cleanup} (most reliable): call \verb{$kill()} or \verb{$kill_tree()} From a3ca564727948128a8fb07a956ad75fb126089b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 09:59:27 +0200 Subject: [PATCH 04/13] Improve pipeline tests - Mark unix specific tests as such. - Add some platform independent tests using px. --- src/tools/px.c | 2 +- tests/testthat/test-pipeline.R | 123 +++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/tools/px.c b/src/tools/px.c index 89b1f6e3..46f71365 100644 --- a/src/tools/px.c +++ b/src/tools/px.c @@ -35,7 +35,7 @@ void usage(void) { fprintf(stderr, " errflush -- " "flush stderr stream\n"); fprintf(stderr, " cat -- " - "print file to stdout\n"); + "print file to stdout (use '' for standard input)\n"); fprintf(stderr, " return -- " "return with exitcode\n"); fprintf(stderr, " writefile -- " diff --git a/tests/testthat/test-pipeline.R b/tests/testthat/test-pipeline.R index 9eb182c2..b7c177eb 100644 --- a/tests/testthat/test-pipeline.R +++ b/tests/testthat/test-pipeline.R @@ -1,5 +1,6 @@ test_that("2-process pipeline: sort | uniq", { skip_on_cran() + skip_on_os("windows") pl <- pipeline$new( list(c("sort"), c("uniq")), @@ -19,6 +20,7 @@ test_that("2-process pipeline: sort | uniq", { test_that("3-process pipeline: cat | sort | uniq", { skip_on_cran() + skip_on_os("windows") pl <- pipeline$new( list(c("cat"), c("sort"), c("uniq")), @@ -38,6 +40,7 @@ test_that("3-process pipeline: cat | sort | uniq", { test_that("single-process pipeline is equivalent to process$new()", { skip_on_cran() + skip_on_os("windows") pl <- pipeline$new(list(c("echo", "hello")), stdout = "|") on.exit(pl$kill(), add = TRUE) @@ -50,6 +53,7 @@ test_that("single-process pipeline is equivalent to process$new()", { test_that("pipeline is_alive() and get_pids()", { skip_on_cran() + skip_on_os("windows") pl <- pipeline$new( list(c("sort"), c("cat")), @@ -70,6 +74,7 @@ test_that("pipeline is_alive() and get_pids()", { test_that("pipeline get_processes() returns process objects", { skip_on_cran() + skip_on_os("windows") pl <- pipeline$new( list(c("sort"), c("uniq")), @@ -88,6 +93,7 @@ test_that("pipeline get_processes() returns process objects", { test_that("pipeline kill() stops all processes", { skip_on_cran() + skip_on_os("windows") pl <- pipeline$new( list(c("cat"), c("cat")), @@ -103,6 +109,7 @@ test_that("pipeline kill() stops all processes", { test_that("pipeline stdout to file", { skip_on_cran() + skip_on_os("windows") tmp <- tempfile() on.exit(unlink(tmp), add = TRUE) @@ -135,3 +142,119 @@ test_that("conn_create_proc_pipepair() returns write/read ends", { expect_true(is_connection(pipe[[1]])) expect_true(is_connection(pipe[[2]])) }) + +test_that("px single-process pipeline", { + skip_on_cran() + px <- get_tool("px") + + pl <- pipeline$new(list(c(px, "outln", "hello")), stdout = "|") + on.exit(pl$kill(), add = TRUE) + + pl$wait() + out <- trimws(pl$read_all_output()) + expect_equal(out, "hello") + expect_equal(pl$get_exit_statuses(), list(0L)) +}) + +test_that("px 2-process pipeline: passthrough", { + skip_on_cran() + px <- get_tool("px") + + pl <- pipeline$new( + list(c(px, "cat", ""), c(px, "cat", "")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + pl$write_input("hello\nworld") + pl$close_input() + out <- pl$read_all_output_lines() + pl$wait() + + expect_equal(out, c("hello", "world")) + expect_equal(pl$get_exit_statuses(), list(0L, 0L)) +}) + +test_that("px 3-process pipeline: passthrough", { + skip_on_cran() + px <- get_tool("px") + + pl <- pipeline$new( + list( + c(px, "cat", ""), + c(px, "cat", ""), + c(px, "cat", "") + ), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + pl$write_input("hello\nworld") + pl$close_input() + out <- pl$read_all_output_lines() + pl$wait() + + expect_equal(out, c("hello", "world")) + expect_equal(pl$get_exit_statuses(), list(0L, 0L, 0L)) +}) + +test_that("px pipeline is_alive() and get_pids()", { + skip_on_cran() + px <- get_tool("px") + + pl <- pipeline$new( + list(c(px, "cat", ""), c(px, "cat", "")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + expect_true(pl$is_alive()) + pids <- pl$get_pids() + expect_length(pids, 2L) + expect_true(all(pids > 0L)) + + pl$close_input() + pl$wait() + expect_false(pl$is_alive()) +}) + +test_that("px pipeline kill() stops all processes", { + skip_on_cran() + px <- get_tool("px") + + pl <- pipeline$new( + list(c(px, "cat", ""), c(px, "cat", "")), + stdin = "|", + stdout = "|" + ) + + expect_true(pl$is_alive()) + pl$kill() + pl$wait() + expect_false(pl$is_alive()) +}) + +test_that("px pipeline stdout to file", { + skip_on_cran() + px <- get_tool("px") + + tmp <- tempfile() + on.exit(unlink(tmp), add = TRUE) + + pl <- pipeline$new( + list(c(px, "cat", ""), c(px, "cat", "")), + stdin = "|", + stdout = tmp + ) + on.exit(pl$kill(), add = TRUE) + + pl$write_input("hello\nworld\n") + pl$close_input() + pl$wait() + + expect_equal(readLines(tmp), c("hello", "world")) + expect_equal(pl$get_exit_statuses(), list(0L, 0L)) +}) From 03e026786225c7ef8217d833d948984b3058045c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 10:07:21 +0200 Subject: [PATCH 05/13] format()/print() methods for pipelines --- R/pipeline.R | 13 ++++++++++++- R/print.R | 12 ++++++++++++ man/pipeline.Rd | 33 +++++++++++++++++++++++++++++++++ tests/testthat/test-print.R | 19 +++++++++++++++++++ 4 files changed, 76 insertions(+), 1 deletion(-) diff --git a/R/pipeline.R b/R/pipeline.R index 38ffe6c2..6a810078 100644 --- a/R/pipeline.R +++ b/R/pipeline.R @@ -36,6 +36,7 @@ #' @param grace Grace period in seconds before sending SIGKILL (Unix) or #' terminating forcefully (Windows). Currently not used. #' @param close_connections Whether to close connections after killing. +#' @param ... Not used, for compatibility with the generic. #' #' @section Methods: #' `pipeline$new(cmds, stdin, stdout, stderr, env, encoding, wd, @@ -70,6 +71,10 @@ #' #' `$get_processes()` — list of [process] objects, one per command. #' +#' `$format()` — string representation of the pipeline. +#' +#' `$print()` — print the pipeline to the screen. +#' #' @examples #' \dontrun{ #' # sort | uniq, reading from / writing to R @@ -271,7 +276,13 @@ pipeline <- R6::R6Class( #' @description Return the list of process objects. get_processes = function() { private$procs - } + }, + + #' @description Format the pipeline as a string. + format = function() pipeline_format(self, private), + + #' @description Print the pipeline to the screen. + print = function(...) pipeline_print(self, private) ), private = list( diff --git a/R/print.R b/R/print.R index 42039ac8..a7bf0362 100644 --- a/R/print.R +++ b/R/print.R @@ -24,3 +24,15 @@ process_print <- function(self, private) { process_get_short_name <- function(self, private) { basename(private$command) } + +pipeline_format <- function(self, private) { + lines <- vapply(private$procs, function(p) { + sub("^PROCESS ", "| ", p$format()) + }, character(1L)) + paste0(c("PIPELINE\n", lines), collapse = "") +} + +pipeline_print <- function(self, private) { + cat(pipeline_format(self, private)) + invisible(self) +} diff --git a/man/pipeline.Rd b/man/pipeline.Rd index 0e37023b..afcfe542 100644 --- a/man/pipeline.Rd +++ b/man/pipeline.Rd @@ -42,6 +42,10 @@ if still running). \verb{$get_pids()} — integer vector of process IDs. \verb{$get_processes()} — list of \link{process} objects, one per command. + +\verb{$format()} — string representation of the pipeline. + +\verb{$print()} — print the pipeline to the screen. } \examples{ @@ -80,6 +84,8 @@ pl$get_exit_statuses() \item \href{#method-pipeline-get_exit_statuses}{\code{pipeline$get_exit_statuses()}} \item \href{#method-pipeline-get_pids}{\code{pipeline$get_pids()}} \item \href{#method-pipeline-get_processes}{\code{pipeline$get_processes()}} +\item \href{#method-pipeline-format}{\code{pipeline$format()}} +\item \href{#method-pipeline-print}{\code{pipeline$print()}} } } \if{html}{\out{
}} @@ -371,5 +377,32 @@ Return the list of process objects. \if{html}{\out{
}}\preformatted{pipeline$get_processes()}\if{html}{\out{
}} } +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-format}{}}} +\subsection{Method \code{format()}}{ +Format the pipeline as a string. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$format()}\if{html}{\out{
}} +} + +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-print}{}}} +\subsection{Method \code{print()}}{ +Print the pipeline to the screen. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$print(...)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{...}}{Not used, for compatibility with the generic.} +} +\if{html}{\out{
}} +} } } diff --git a/tests/testthat/test-print.R b/tests/testthat/test-print.R index 72001ba1..960ab249 100644 --- a/tests/testthat/test-print.R +++ b/tests/testthat/test-print.R @@ -13,3 +13,22 @@ test_that("print", { "PROCESS .* finished" ) }) + +test_that("pipeline print", { + skip_on_cran() + px <- get_tool("px") + + pl <- pipeline$new( + list(c(px, "cat", ""), c(px, "cat", "")), + stdin = "|", + stdout = "|" + ) + on.exit(pl$kill(), add = TRUE) + + expect_output(print(pl), "^PIPELINE") + expect_output(print(pl), "\\| .* running, pid") + + pl$close_input() + pl$wait() + expect_output(print(pl), "\\| .* finished") +}) From b51752f06dc2a7997932d0c9c7261f2585e7d764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 10:12:39 +0200 Subject: [PATCH 06/13] Mark pipeline as experimental --- R/pipeline.R | 2 ++ man/pipeline.Rd | 2 ++ 2 files changed, 4 insertions(+) diff --git a/R/pipeline.R b/R/pipeline.R index 6a810078..2a35e688 100644 --- a/R/pipeline.R +++ b/R/pipeline.R @@ -1,6 +1,8 @@ #' Pipeline of processes connected with pipes #' #' @description +#' `r lifecycle::badge("experimental")` +#' #' A `pipeline` object represents a sequence of processes whose standard #' input and output streams are connected with pipes, like a Unix pipeline #' (`cmd1 | cmd2 | cmd3`). Data flows directly between the child processes diff --git a/man/pipeline.Rd b/man/pipeline.Rd index afcfe542..eb164aa9 100644 --- a/man/pipeline.Rd +++ b/man/pipeline.Rd @@ -4,6 +4,8 @@ \alias{pipeline} \title{Pipeline of processes connected with pipes} \description{ +\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} + A \code{pipeline} object represents a sequence of processes whose standard input and output streams are connected with pipes, like a Unix pipeline (\code{cmd1 | cmd2 | cmd3}). Data flows directly between the child processes From 340bcf85d5081f8ddb323684d91abcac19e6b032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 10:12:56 +0200 Subject: [PATCH 07/13] Add NEWS for pipeline --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index 25070816..2f08ec81 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # processx (development version) +* New experimental `pipeline` R6 class for running two or more processes + connected by kernel-level pipes, like a Unix shell pipeline + (`cmd1 | cmd2 | cmd3`). Data flows directly between child processes + without passing through R. Works on Unix and Windows (#280). + * New "Process cleanup" article. * New `linux_pdeathsig` argument to `process$new()`: on Linux, the child From e2408daf4d10ea460c5af98f5486d874913b0e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 10:37:36 +0200 Subject: [PATCH 08/13] Add pipeline to pkgdown reference --- _pkgdown.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 0526a06c..a4033164 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -27,6 +27,10 @@ reference: contents: - process +- title: Pipelines + contents: + - pipeline + - title: Polling contents: - poll From 8db9e28bb7621b2002f078fe0bcbd41ff3a3bb29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 10:38:20 +0200 Subject: [PATCH 09/13] Update snapshot tests --- tests/testthat/_snaps/Darwin/unix-sockets.md | 10 ++--- .../_snaps/Darwin/unix-sockets.new.md | 34 ---------------- tests/testthat/_snaps/Linux/unix-sockets.md | 10 ++--- tests/testthat/_snaps/unix-sockets.md | 10 ++--- tests/testthat/_snaps/unix-sockets.new.md | 40 ------------------- 5 files changed, 15 insertions(+), 89 deletions(-) delete mode 100644 tests/testthat/_snaps/Darwin/unix-sockets.new.md delete mode 100644 tests/testthat/_snaps/unix-sockets.new.md diff --git a/tests/testthat/_snaps/Darwin/unix-sockets.md b/tests/testthat/_snaps/Darwin/unix-sockets.md index a923fcc0..30dd74c6 100644 --- a/tests/testthat/_snaps/Darwin/unix-sockets.md +++ b/tests/testthat/_snaps/Darwin/unix-sockets.md @@ -5,8 +5,8 @@ Condition Error in `processx_conn_read_chars()`: ! ! Native call to `processx_connection_read_chars` failed - Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:302:: - ! Cannot read from processx connection (system error 57, Socket is not connected) @processx-connection.c:1891 (processx__connection_read) + Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:318:: + ! Cannot read from processx connection (system error 57, Socket is not connected) @processx-connection.c:1940 (processx__connection_read) # errors @@ -15,20 +15,20 @@ Condition Error in `conn_create_unix_socket()`: ! ! Native call to `processx_connection_create_socket` failed - Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:618:: + Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: ! Server socket path too long: / Code conn_create_unix_socket("/dev/null") Condition Error in `conn_create_unix_socket()`: ! ! Native call to `processx_connection_create_socket` failed - Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:618:: + Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: ! Cannot bind to socket (system error 48, Address already in use) @processx-connection.c:479 (processx_connection_create_socket) Code conn_connect_unix_socket("/dev/null") Condition Error in `conn_connect_unix_socket()`: ! ! Native call to `processx_connection_connect_socket` failed - Caused by error in `chain_call(c_processx_connection_connect_socket, filename, encoding)` at connections.R:640:: + Caused by error in `chain_call(c_processx_connection_connect_socket, filename, encoding)` at connections.R:656:: ! Cannot connect to socket (system error 38, Socket operation on non-socket) @processx-connection.c:550 (processx_connection_connect_socket) diff --git a/tests/testthat/_snaps/Darwin/unix-sockets.new.md b/tests/testthat/_snaps/Darwin/unix-sockets.new.md deleted file mode 100644 index 30dd74c6..00000000 --- a/tests/testthat/_snaps/Darwin/unix-sockets.new.md +++ /dev/null @@ -1,34 +0,0 @@ -# reading unaccepted server socket is error - - Code - conn_read_chars(sock1) - Condition - Error in `processx_conn_read_chars()`: - ! ! Native call to `processx_connection_read_chars` failed - Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:318:: - ! Cannot read from processx connection (system error 57, Socket is not connected) @processx-connection.c:1940 (processx__connection_read) - -# errors - - Code - conn_create_unix_socket(sock) - Condition - Error in `conn_create_unix_socket()`: - ! ! Native call to `processx_connection_create_socket` failed - Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: - ! Server socket path too long: / - Code - conn_create_unix_socket("/dev/null") - Condition - Error in `conn_create_unix_socket()`: - ! ! Native call to `processx_connection_create_socket` failed - Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: - ! Cannot bind to socket (system error 48, Address already in use) @processx-connection.c:479 (processx_connection_create_socket) - Code - conn_connect_unix_socket("/dev/null") - Condition - Error in `conn_connect_unix_socket()`: - ! ! Native call to `processx_connection_connect_socket` failed - Caused by error in `chain_call(c_processx_connection_connect_socket, filename, encoding)` at connections.R:656:: - ! Cannot connect to socket (system error 38, Socket operation on non-socket) @processx-connection.c:550 (processx_connection_connect_socket) - diff --git a/tests/testthat/_snaps/Linux/unix-sockets.md b/tests/testthat/_snaps/Linux/unix-sockets.md index 4312c943..9fc076f0 100644 --- a/tests/testthat/_snaps/Linux/unix-sockets.md +++ b/tests/testthat/_snaps/Linux/unix-sockets.md @@ -5,8 +5,8 @@ Condition Error in `processx_conn_read_chars()`: ! ! Native call to `processx_connection_read_chars` failed - Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:302:: - ! Cannot read from processx connection (system error 22, Invalid argument) @processx-connection.c:1891 (processx__connection_read) + Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:318:: + ! Cannot read from processx connection (system error 22, Invalid argument) @processx-connection.c:1940 (processx__connection_read) # errors @@ -15,20 +15,20 @@ Condition Error in `conn_create_unix_socket()`: ! ! Native call to `processx_connection_create_socket` failed - Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:618:: + Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: ! Server socket path too long: / Code conn_create_unix_socket("/dev/null") Condition Error in `conn_create_unix_socket()`: ! ! Native call to `processx_connection_create_socket` failed - Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:618:: + Caused by error in `chain_call(c_processx_connection_create_socket, filename, encoding)` at connections.R:634:: ! Cannot bind to socket (system error 98, Address already in use) @processx-connection.c:479 (processx_connection_create_socket) Code conn_connect_unix_socket("/dev/null") Condition Error in `conn_connect_unix_socket()`: ! ! Native call to `processx_connection_connect_socket` failed - Caused by error in `chain_call(c_processx_connection_connect_socket, filename, encoding)` at connections.R:640:: + Caused by error in `chain_call(c_processx_connection_connect_socket, filename, encoding)` at connections.R:656:: ! Cannot connect to socket (system error 111, Connection refused) @processx-connection.c:550 (processx_connection_connect_socket) diff --git a/tests/testthat/_snaps/unix-sockets.md b/tests/testthat/_snaps/unix-sockets.md index 5fd6d1bb..ac90e7ed 100644 --- a/tests/testthat/_snaps/unix-sockets.md +++ b/tests/testthat/_snaps/unix-sockets.md @@ -5,7 +5,7 @@ Condition Error in `conn_accept_unix_socket()`: ! ! Native call to `processx_connection_accept_socket` failed - Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:653:: + Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:669:: ! Socket is not listening @processx-connection.c:577 (processx_connection_accept_socket) # writing unaccepted server socket is error @@ -15,8 +15,8 @@ Condition Error in `processx_conn_write()`: ! ! Native call to `processx_connection_write_bytes` failed - Caused by error in `chain_call(c_processx_connection_write_bytes, con, str)` at connections.R:424:: - ! Cannot write to an un-accepted socket connection @processx-connection.c:1009 (processx_c_connection_write_bytes) + Caused by error in `chain_call(c_processx_connection_write_bytes, con, str)` at connections.R:440:: + ! Cannot write to an un-accepted socket connection @processx-connection.c:1058 (processx_c_connection_write_bytes) # errors @@ -25,7 +25,7 @@ Condition Error in `conn_accept_unix_socket()`: ! ! Native call to `processx_connection_accept_socket` failed - Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:653:: + Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:669:: ! Not a socket connection @processx-connection.c:573 (processx_connection_accept_socket) --- @@ -35,6 +35,6 @@ Condition Error in `conn_unix_socket_state()`: ! ! Native call to `processx_connection_socket_state` failed - Caused by error in `chain_call(c_processx_connection_socket_state, con)` at connections.R:665:: + Caused by error in `chain_call(c_processx_connection_socket_state, con)` at connections.R:681:: ! Not a socket connection @processx-connection.c:622 (processx_connection_socket_state) diff --git a/tests/testthat/_snaps/unix-sockets.new.md b/tests/testthat/_snaps/unix-sockets.new.md deleted file mode 100644 index ac90e7ed..00000000 --- a/tests/testthat/_snaps/unix-sockets.new.md +++ /dev/null @@ -1,40 +0,0 @@ -# CRUD - - Code - conn_accept_unix_socket(sock1) - Condition - Error in `conn_accept_unix_socket()`: - ! ! Native call to `processx_connection_accept_socket` failed - Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:669:: - ! Socket is not listening @processx-connection.c:577 (processx_connection_accept_socket) - -# writing unaccepted server socket is error - - Code - conn_write(sock1, "Hello\n") - Condition - Error in `processx_conn_write()`: - ! ! Native call to `processx_connection_write_bytes` failed - Caused by error in `chain_call(c_processx_connection_write_bytes, con, str)` at connections.R:440:: - ! Cannot write to an un-accepted socket connection @processx-connection.c:1058 (processx_c_connection_write_bytes) - -# errors - - Code - conn_accept_unix_socket(ff) - Condition - Error in `conn_accept_unix_socket()`: - ! ! Native call to `processx_connection_accept_socket` failed - Caused by error in `chain_call(c_processx_connection_accept_socket, con)` at connections.R:669:: - ! Not a socket connection @processx-connection.c:573 (processx_connection_accept_socket) - ---- - - Code - conn_unix_socket_state(ff) - Condition - Error in `conn_unix_socket_state()`: - ! ! Native call to `processx_connection_socket_state` failed - Caused by error in `chain_call(c_processx_connection_socket_state, con)` at connections.R:681:: - ! Not a socket connection @processx-connection.c:622 (processx_connection_socket_state) - From af65ac5cf8ad3fb746e44f3fef7176154b818a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 10:57:59 +0200 Subject: [PATCH 10/13] Trying to fix pipelines on Windows --- R/pipeline.R | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/R/pipeline.R b/R/pipeline.R index 2a35e688..76374051 100644 --- a/R/pipeline.R +++ b/R/pipeline.R @@ -154,14 +154,18 @@ pipeline <- R6::R6Class( cleanup_tree = cleanup_tree, poll_connection = proc_poll ) - } - ## Close the parent's copies of all intermediate pipe ends so that the - ## kernel can signal EOF to the downstream process when the upstream - ## process exits and closes its end. - for (p in pipes) { - close(p[[1L]]) - close(p[[2L]]) + ## Close the parent's copy of each pipe end immediately after the + ## process that needed it has been spawned. On Windows, every + ## inheritable handle is silently duplicated into each child created + ## with bInheritHandles = TRUE. Keeping the write-end of an + ## inter-process pipe open in the parent while spawning the next child + ## would cause that child to inherit the write-end of its own stdin + ## pipe — so the write-end is never fully closed and stdin never + ## reaches EOF. On Unix, O_CLOEXEC prevents inheritance anyway, but + ## closing early is still correct. + if (i < n) close(pipes[[i]][[1L]]) ## write end → stdout of process i + if (i > 1L) close(pipes[[i - 1L]][[2L]]) ## read end → stdin of process i } private$procs <- procs From fa303255b3cf223a43c37de1bc0e99e4bb9df00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 11:04:14 +0200 Subject: [PATCH 11/13] Better fd inheritance on Windows Create pipes and processes iteratively, so the first process will not inherit the pipes of other processes, etc. Does not matter on Unix, because of CLOEXEC, but it does matter on Windows. --- R/pipeline.R | 57 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/R/pipeline.R b/R/pipeline.R index 76374051..49589876 100644 --- a/R/pipeline.R +++ b/R/pipeline.R @@ -123,23 +123,36 @@ pipeline <- R6::R6Class( n <- length(cmds) - ## Create n-1 kernel pipes connecting consecutive processes - pipes <- if (n > 1L) { - lapply(seq_len(n - 1L), function(i) conn_create_proc_pipepair()) - } else { - list() - } + ## Spawn all processes, creating one inter-process pipe per iteration + ## so that later children cannot inherit handles from pipes that do not + ## concern them. On Windows, CreateProcess with bInheritHandles=TRUE + ## passes every inheritable handle to the child. Creating each pipe + ## just before the two processes that need it are spawned — and closing + ## both ends in the parent immediately after — ensures no child ever + ## holds a stray write-end that would prevent EOF from propagating. + ## On Unix, O_CLOEXEC already prevents inheritance, but the same + ## iterative pattern keeps the logic consistent. + procs <- vector("list", n) + prev_read <- NULL ## read end of the previous inter-process pipe - ## Spawn all processes - procs <- vector("list", n) for (i in seq_len(n)) { cmd <- cmds[[i]] - proc_stdin <- if (i == 1L) stdin else pipes[[i - 1L]][[2L]] - proc_stdout <- if (i < n) pipes[[i]][[1L]] else stdout - ## Disable poll_connection for intermediate processes: their stdout - ## is a connection (not "|"), so the default formula would create an + + ## Create the pipe connecting process i's stdout to process i+1's + ## stdin, unless this is the last process. + if (i < n) { + next_pipe <- conn_create_proc_pipepair() + proc_stdout <- next_pipe[[1L]] ## write end → child's stdout + } else { + next_pipe <- NULL + proc_stdout <- stdout + } + + proc_stdin <- if (i == 1L) stdin else prev_read + ## Disable poll_connection for intermediate processes: their stdout is + ## a connection (not "|"), so the default formula would create an ## unnecessary extra pipe. - proc_poll <- if (i < n) FALSE else NULL + proc_poll <- if (i < n) FALSE else NULL procs[[i]] <- process$new( cmd[[1L]], @@ -155,17 +168,13 @@ pipeline <- R6::R6Class( poll_connection = proc_poll ) - ## Close the parent's copy of each pipe end immediately after the - ## process that needed it has been spawned. On Windows, every - ## inheritable handle is silently duplicated into each child created - ## with bInheritHandles = TRUE. Keeping the write-end of an - ## inter-process pipe open in the parent while spawning the next child - ## would cause that child to inherit the write-end of its own stdin - ## pipe — so the write-end is never fully closed and stdin never - ## reaches EOF. On Unix, O_CLOEXEC prevents inheritance anyway, but - ## closing early is still correct. - if (i < n) close(pipes[[i]][[1L]]) ## write end → stdout of process i - if (i > 1L) close(pipes[[i - 1L]][[2L]]) ## read end → stdin of process i + ## Close parent's copies immediately: the child now owns these + ## handles, and closing here prevents the next child from inheriting + ## the write-end of a pipe it should only read from. + if (!is.null(next_pipe)) close(next_pipe[[1L]]) ## write end → stdout of process i + if (!is.null(prev_read)) close(prev_read) ## read end → stdin of process i + + prev_read <- if (!is.null(next_pipe)) next_pipe[[2L]] else NULL } private$procs <- procs From 200c85cec012d8fd25728e2ec6f113da3af41592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 11:06:51 +0200 Subject: [PATCH 12/13] Update Windows test snapshots --- tests/testthat/_snaps/Windows/unix-sockets.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/_snaps/Windows/unix-sockets.md b/tests/testthat/_snaps/Windows/unix-sockets.md index 1315e639..d0ce4761 100644 --- a/tests/testthat/_snaps/Windows/unix-sockets.md +++ b/tests/testthat/_snaps/Windows/unix-sockets.md @@ -5,6 +5,6 @@ Condition Error in `processx_conn_read_chars()`: ! ! Native call to `processx_connection_read_chars` failed - Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:302:: - ! Cannot read from an un-accepted socket connection @processx-connection.c:1780 (processx__connection_read) + Caused by error in `chain_call(c_processx_connection_read_chars, con, n)` at connections.R:318:: + ! Cannot read from an un-accepted socket connection @processx-connection.c:1829 (processx__connection_read) From 369721e7aebab07410991d75271d0a0d3844f29d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Cs=C3=A1rdi?= Date: Wed, 22 Apr 2026 11:10:55 +0200 Subject: [PATCH 13/13] Add pipeline$poll_io() method --- R/pipeline.R | 7 +++++++ man/pipeline.Rd | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/R/pipeline.R b/R/pipeline.R index 49589876..bf2db85f 100644 --- a/R/pipeline.R +++ b/R/pipeline.R @@ -48,6 +48,8 @@ #' `$read_all_output()`, `$read_all_output_lines()` — read from the last #' process (only meaningful when `stdout = "|"`). #' +#' `$poll_io(timeout)` — poll the last process's connections for I/O. +#' #' `$read_error(n = -1)`, `$read_error_lines(n = -1)`, #' `$read_all_error()`, `$read_all_error_lines()` — read stderr of the #' last process (only meaningful when `stderr = "|"`). @@ -205,6 +207,11 @@ pipeline <- R6::R6Class( private$last()$read_all_output_lines() }, + #' @description Poll the connections of the last process for I/O. + poll_io = function(timeout) { + private$last()$poll_io(timeout) + }, + ## ------------------------------------------------------------------ ## ## Error (last process) ## ## ------------------------------------------------------------------ ## diff --git a/man/pipeline.Rd b/man/pipeline.Rd index eb164aa9..a904459b 100644 --- a/man/pipeline.Rd +++ b/man/pipeline.Rd @@ -20,6 +20,8 @@ final command (when \code{stdout = "|"}). \verb{$read_all_output()}, \verb{$read_all_output_lines()} — read from the last process (only meaningful when \code{stdout = "|"}). +\verb{$poll_io(timeout)} — poll the last process's connections for I/O. + \verb{$read_error(n = -1)}, \verb{$read_error_lines(n = -1)}, \verb{$read_all_error()}, \verb{$read_all_error_lines()} — read stderr of the last process (only meaningful when \code{stderr = "|"}). @@ -73,6 +75,7 @@ pl$get_exit_statuses() \item \href{#method-pipeline-read_output_lines}{\code{pipeline$read_output_lines()}} \item \href{#method-pipeline-read_all_output}{\code{pipeline$read_all_output()}} \item \href{#method-pipeline-read_all_output_lines}{\code{pipeline$read_all_output_lines()}} +\item \href{#method-pipeline-poll_io}{\code{pipeline$poll_io()}} \item \href{#method-pipeline-read_error}{\code{pipeline$read_error()}} \item \href{#method-pipeline-read_error_lines}{\code{pipeline$read_error_lines()}} \item \href{#method-pipeline-read_all_error}{\code{pipeline$read_all_error()}} @@ -199,6 +202,23 @@ Read all output lines of the last process. \if{html}{\out{
}}\preformatted{pipeline$read_all_output_lines()}\if{html}{\out{
}} } +} +\if{html}{\out{
}} +\if{html}{\out{}} +\if{latex}{\out{\hypertarget{method-pipeline-poll_io}{}}} +\subsection{Method \code{poll_io()}}{ +Poll the connections of the last process for I/O. +\subsection{Usage}{ +\if{html}{\out{
}}\preformatted{pipeline$poll_io(timeout)}\if{html}{\out{
}} +} + +\subsection{Arguments}{ +\if{html}{\out{
}} +\describe{ +\item{\code{timeout}}{Timeout in milliseconds. -1 means no timeout.} +} +\if{html}{\out{
}} +} } \if{html}{\out{
}} \if{html}{\out{}}