├── .Rbuildignore
├── .gitattributes
├── .gitignore
├── .travis.yml
├── DESCRIPTION
├── LICENSE
├── NAMESPACE
├── NEWS.md
├── R
├── package.R
├── readwrite.R
├── signals.R
├── subprocess.R
├── tests.R
└── utils.R
├── README.md
├── TODO
├── appveyor.yml
├── build
└── vignette.rds
├── inst
└── doc
│ ├── intro.R
│ ├── intro.Rmd
│ └── intro.html
├── man
├── process_exists.Rd
├── readwrite.Rd
├── signal.Rd
├── signals.Rd
├── spawn_process.Rd
├── subprocess.Rd
├── terminating.Rd
└── tests.Rd
├── src
├── Makevars
├── Makevars.win
├── config-os.h
├── rapi.cc
├── rapi.h
├── registration.cpp
├── sub-linux.cc
├── sub-windows.cc
├── subprocess.cc
├── subprocess.h
└── tests.cc
├── subprocess.Rproj
├── tests
├── Makefile.test
├── testthat.R
└── testthat
│ ├── helper-mockery.R
│ ├── helper-processes.R
│ ├── signal-trap.sh
│ ├── test-package.R
│ ├── test-parameters.R
│ ├── test-readwrite.R
│ ├── test-signals.R
│ ├── test-subprocess.R
│ ├── test-termination.R
│ └── test-utf8.R
└── vignettes
└── intro.Rmd
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^.*\.Rproj$
2 | ^\.Rproj\.user$
3 | .vscode
4 | .travis.yml
5 | README.md
6 | TODO
7 | ^appveyor\.yml$
8 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | data/* binary
3 | src/* text=lf
4 | R/* text=lf
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # History files
2 | .Rhistory
3 | .Rapp.history
4 |
5 | # Session Data files
6 | .RData
7 |
8 | # Example code in package build process
9 | *-Ex.R
10 |
11 | # Output files from R CMD build
12 | /*.tar.gz
13 |
14 | # Output files from R CMD check
15 | /*.Rcheck/
16 |
17 | # RStudio files
18 | .Rproj.user/
19 |
20 | # produced vignettes
21 | vignettes/*.html
22 | vignettes/*.pdf
23 |
24 | # OAuth2 token, see https://github.com/hadley/httr/releases/tag/v0.3
25 | .httr-oauth
26 |
27 | # knitr and R markdown default cache directories
28 | /*_cache/
29 | /cache/
30 |
31 | # Temporary files created by R markdown
32 | *.utf8.md
33 | *.knit.md
34 | .Rproj.user
35 |
36 | # C build files
37 | *.o
38 | *.so
39 | *.dll
40 | *.exe
41 |
42 | # VS code
43 | .vscode
44 |
45 | # Vim
46 | *.swp
47 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: r
2 | cache: packages
3 | sudo: false
4 | compiler:
5 | - g++
6 | r_build_args: --no-build-vignettes
7 | r_check_args: --no-manual --timings
8 | matrix:
9 | include:
10 | - os: linux
11 | r: release
12 | - os: linux
13 | r: devel
14 | - os: osx
15 | osx_image: xcode8
16 | r: release
17 | r_packages:
18 | - covr
19 |
20 | # Set CXX1X for R-devel, as R-devel does not detect CXX1X support for gcc 4.6.3,
21 | # this was causing hunspell installation to fail
22 | before_install:
23 | - if [[ "$TRAVIS_R_VERSION_STRING" = 'devel' ]]; then mkdir ~/.R && echo 'CXX1X=g++ -std=c++0x -g -O2 -fPIC' > ~/.R/Makevars; fi
24 |
25 | # Only report coverage for the release version
26 | after_success:
27 | - test $TRAVIS_R_VERSION_STRING = 'release' && Rscript -e 'covr::codecov()'
28 |
29 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: subprocess
2 | Type: Package
3 | Title: Manage Sub-Processes in R
4 | Version: 0.8.4
5 | Authors@R: person("Lukasz", "Bartnik", email = "l.bartnik@gmail.com", role = c("aut", "cre"))
6 | Description: Create and handle multiple sub-processes in R, exchange
7 | data over standard input and output streams, control their life cycle.
8 | License: MIT + file LICENSE
9 | URL: https://github.com/lbartnik/subprocess
10 | BugReports: https://github.com/lbartnik/subprocess/issues
11 | Depends:
12 | R (>= 3.2.0)
13 | Suggests:
14 | mockery,
15 | testthat,
16 | knitr,
17 | rmarkdown (>= 1.0)
18 | Collate:
19 | 'package.R'
20 | 'readwrite.R'
21 | 'signals.R'
22 | 'subprocess.R'
23 | 'tests.R'
24 | 'utils.R'
25 | RoxygenNote: 6.1.1
26 | Roxygen: list(markdown = TRUE)
27 | VignetteBuilder: knitr
28 | SystemRequirements: C++11
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2016
2 | COPYRIGHT HOLDER: Lukasz A. Bartnik
3 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | S3method(print,process_handle)
4 | export(CTRL_BREAK_EVENT)
5 | export(CTRL_C_EVENT)
6 | export(C_tests_utf8)
7 | export(PIPE_BOTH)
8 | export(PIPE_STDERR)
9 | export(PIPE_STDOUT)
10 | export(SIGABRT)
11 | export(SIGALRM)
12 | export(SIGCHLD)
13 | export(SIGCONT)
14 | export(SIGFPE)
15 | export(SIGHUP)
16 | export(SIGILL)
17 | export(SIGINT)
18 | export(SIGKILL)
19 | export(SIGPIPE)
20 | export(SIGQUIT)
21 | export(SIGSEGV)
22 | export(SIGSTOP)
23 | export(SIGTERM)
24 | export(SIGTSTP)
25 | export(SIGTTIN)
26 | export(SIGTTOU)
27 | export(SIGUSR1)
28 | export(SIGUSR2)
29 | export(TERMINATION_CHILD_ONLY)
30 | export(TERMINATION_GROUP)
31 | export(TIMEOUT_IMMEDIATE)
32 | export(TIMEOUT_INFINITE)
33 | export(is_process_handle)
34 | export(process_close_input)
35 | export(process_exists)
36 | export(process_kill)
37 | export(process_read)
38 | export(process_return_code)
39 | export(process_send_signal)
40 | export(process_state)
41 | export(process_terminate)
42 | export(process_wait)
43 | export(process_write)
44 | export(signals)
45 | export(spawn_process)
46 | useDynLib(subprocess, .registration = TRUE)
47 |
--------------------------------------------------------------------------------
/NEWS.md:
--------------------------------------------------------------------------------
1 | # subprocess 0.8.4
2 |
3 | * fixes builds with Oracle compiler
4 |
5 | * documentation refreshed with Roxygen2 6.1.1
6 |
7 | # subprocess 0.8.3
8 |
9 | * fixes building under Solaris
10 |
11 | * replace `select()` with `poll()`
12 |
13 | * new API: `process_exists()`
14 |
15 | # subprocess 0.8.2
16 |
17 | * fixes in test cases for `testthat` 2.0
18 |
19 | # subprocess 0.8.1
20 |
21 | * explicitly register native symbols
22 |
23 | * Ctrl+C works in Windows
24 |
25 | * multiple fixes in test code
26 |
27 | # subprocess 0.8.0
28 |
29 | * support for Mac OS
30 |
31 | * shared read from both stdout and stderr of the child process
32 |
33 | * new `process_close_input()` call to close the write end of child's
34 | standard input pipe; this in most cases will let the child know it
35 | should exit
36 |
37 | * renamed `process_poll()` to `process_wait()`; add `process_state()`
38 |
39 | * converted shared library to C++
40 |
41 | * bugfix: group terminate in Windows
42 |
43 |
44 | # subprocess 0.7.4
45 |
46 | * initial submission to CRAN; basic API works in Linux and Windows
47 |
--------------------------------------------------------------------------------
/R/package.R:
--------------------------------------------------------------------------------
1 | #' Manage Subprocesses in R
2 | #'
3 | #' Cross-platform child process management modelled after Python's
4 | #' `subprocess` module.
5 | #'
6 | #' @details This R package extends R's capabilities of starting and
7 | #' handling child processes. It brings the capability of alternating
8 | #' read from and write to a child process, communicating via signals,
9 | #' terminating it and handling its exit status (return code).
10 | #'
11 | #' With R's standard [base::system] and [base::system2]
12 | #' functions one can start a new process and capture its output but
13 | #' cannot directly write to its standard input. Another tool, the
14 | #' [parallel::mclapply] function, is aimed at replicating
15 | #' the current session and is limited to operating systems that come
16 | #' with the `fork()` system call.
17 | #'
18 | #'
19 | #' @docType package
20 | #' @name subprocess
21 | #' @rdname subprocess
22 | #'
23 | #' @references
24 | #' http://github.com/lbartnik/subprocess
25 | #'
26 | #' http://docs.python.org/3/library/subprocess.html
27 | #'
28 | NULL
29 |
30 |
31 | .onLoad <- function (libname, pkgname)
32 | {
33 | signals <<- as.list(known_signals())
34 | envir <- asNamespace(pkgname)
35 |
36 | mapply(name = names(signals),
37 | code = as.integer(signals),
38 | function (name, code) {
39 | suppressWarnings(assign(name, code, envir = envir, inherits = FALSE))
40 | })
41 | }
42 |
--------------------------------------------------------------------------------
/R/readwrite.R:
--------------------------------------------------------------------------------
1 | #' Communicating with a Child Process
2 | #'
3 | #' `process_read()` reads data from one of the child process' streams,
4 | #' *standard output* or *standard error output*, and returns it as a
5 | #' `character` vector.
6 | #'
7 | #' If `flush=TRUE` in `process_read()` then the invocation of the
8 | #' underlying `read()` *system-call* will be repeated until the pipe
9 | #' buffer is empty.
10 | #'
11 | #' If `pipe` is set to either `PIPE_STDOUT` or `PIPE_STDERR`, the returned
12 | #' value is a single list with a single key, `stdout` or `stderr`,
13 | #' respectively. If `pipe` is set to `PIPE_BOTH` the returned `list`
14 | #' contains both keys. Values in the list are `character` vectors of 0
15 | #' or more elements, lines read from the respective output stream of the
16 | #' child process.
17 | #'
18 | #' For details on `timeout` see [terminating].
19 | #'
20 | #' @param handle Process handle obtained from `spawn_process`.
21 | #' @param pipe Output stream identifier: `PIPE_STDOUT`, `PIPE_STDERR` or
22 | #' `PIPE_BOTH`.
23 | #' @param timeout Optional timeout in milliseconds.
24 | #' @param flush If there is any data within the given `timeout`
25 | #' try again with `timeout=0` until C buffer is empty.
26 | #'
27 | #' @return `process_read` returns a `list` which contains either of or
28 | #' both keys: *stdout* and *stderr*; the value is in both cases
29 | #' a `character` vector which contains lines of child's output.
30 | #'
31 | #' @format `PIPE_STDOUT`, `PIPE_STDERR` and `PIPE_BOTH` are single
32 | #' `character` values.
33 | #'
34 | #' @rdname readwrite
35 | #' @name readwrite
36 | #' @export
37 | #'
38 | process_read <- function (handle, pipe = PIPE_BOTH, timeout = TIMEOUT_IMMEDIATE, flush = TRUE)
39 | {
40 | stopifnot(is_process_handle(handle))
41 | output <- .Call("C_process_read", handle$c_handle,
42 | as.character(pipe), as.integer(timeout))
43 |
44 | is_output <- function (x) {
45 | return(is.list(output) && all(vapply(output, is.character, logical(1))))
46 | }
47 | paste0_list <- function (x, y) {
48 | z <- lapply(names(x), function (n) paste0(x[[n]], y[[n]]))
49 | `names<-`(z, names(x))
50 | }
51 |
52 | # needs to be a list of character vectors
53 | if (!is_output(output)) return(output)
54 |
55 | # there is some output, maybe there will be more?
56 | if (isTRUE(flush)) {
57 | while (TRUE) {
58 | more <- .Call("C_process_read", handle$c_handle, as.character(pipe), TIMEOUT_IMMEDIATE)
59 | if (!is_output(more) || all(vapply(more, nchar, integer(1)) == 0))
60 | break
61 | output <- paste0_list(output, more)
62 | }
63 | }
64 |
65 | # replace funny line ending and break into multiple lines
66 | output <- lapply(output, function (single_stream) {
67 | if (!length(single_stream)) return(character())
68 | single_stream <- gsub("\r", "", single_stream, fixed = TRUE)
69 | strsplit(single_stream, "\n", fixed = TRUE)[[1]]
70 | })
71 |
72 | # if asked for only one pipe return the vector, not the list
73 | if (identical(pipe, PIPE_STDOUT) || identical(pipe, PIPE_STDERR)) {
74 | return(output[[pipe]])
75 | }
76 |
77 | # return a lits
78 | return(output)
79 | }
80 |
81 |
82 | #' @description `process_write()` writes data into child's
83 | #' *standard input* stream.
84 | #'
85 | #' @param message Input for the child process.
86 | #' @return `process_write` returns the number of characters written.
87 | #'
88 | #' @rdname readwrite
89 | #' @name readwrite
90 | #' @export
91 | #'
92 | process_write <- function (handle, message)
93 | {
94 | stopifnot(is_process_handle(handle))
95 | .Call("C_process_write", handle$c_handle, as.character(message))
96 | }
97 |
98 |
99 | #' @description `process_close_input()` closes the *write* end
100 | #' of the pipe whose *read* end is the standard input stream of the
101 | #' child process. This is a standard way to gracefully request the child
102 | #' process to exit.
103 | #'
104 | #' @rdname readwrite
105 | #' @name readwrite
106 | #' @export
107 | #'
108 | process_close_input <- function (handle)
109 | {
110 | stopifnot(is_process_handle(handle))
111 | .Call("C_process_close_input", handle$c_handle)
112 | }
113 |
114 |
115 |
116 | #' @description `PIPE_STDOUT`: read from child's standard output.
117 | #'
118 | #' @rdname readwrite
119 | #' @export
120 | PIPE_STDOUT <- "stdout"
121 |
122 |
123 | #' @description `PIPE_STDERR`: read from child's standard error
124 | #' output.
125 | #'
126 | #' @rdname readwrite
127 | #' @export
128 | PIPE_STDERR <- "stderr"
129 |
130 |
131 | #' @description `PIPE_BOTH`: read from both child's output streams:
132 | #' standard output and standard error output.
133 | #'
134 | #' @rdname readwrite
135 | #' @export
136 | PIPE_BOTH <- "both"
137 |
138 |
--------------------------------------------------------------------------------
/R/signals.R:
--------------------------------------------------------------------------------
1 | #' Sending signals to the child process.
2 | #'
3 | #' @param handle Process handle obtained from `spawn_process()`.
4 | #'
5 | #' @seealso [spawn_process()]
6 | #'
7 | #' @format An object of class `list`.
8 | #' @rdname signals
9 | #' @name signals
10 | NULL
11 |
12 |
13 | #' @description Operating-System-level signals that can be sent via
14 | #' [process_send_signal] are defined in the `subprocess::signals`` list.
15 | #' It is a list that is generated when the package is loaded and it
16 | #' contains only signals supported by the current platform (Windows or
17 | #' Linux).
18 | #'
19 | #' All signals, both supported and not supported by the current
20 | #' platform, are also exported under their names. If a given signal
21 | #' is not supported on the current platform, then its value is set to
22 | #' `NA`.
23 | #'
24 | #' Calling `process_kill()` and `process_terminate()` invokes
25 | #' the appropriate OS routine (`waitpid()` or
26 | #' `WaitForSingleObject()`, closing the process handle, etc.) that
27 | #' effectively lets the operating system clean up after the child
28 | #' process. Calling `process_send_signal()` is not accompanied by
29 | #' such clean-up and if the child process exits it needs to be followed
30 | #' by a call to [process_wait()].
31 | #'
32 | #' @details
33 | #' In Windows, signals are delivered either only to the child process or
34 | #' to the child process and all its descendants. This behavior is
35 | #' controlled by the `termination_mode` argument of the
36 | #' [subprocess::spawn_process()] function. Setting it to
37 | #' `TERMINATION_GROUP` results in signals being delivered to the
38 | #' child and its descendants.
39 | #'
40 | #' @rdname signals
41 | #' @export
42 | #'
43 | #' @examples
44 | #' \dontrun{
45 | #' # send the SIGKILL signal to bash
46 | #' h <- spawn_process('bash')
47 | #' process_signal(h, signals$SIGKILL)
48 | #' process_signal(h, SIGKILL)
49 | #'
50 | #' # is SIGABRT supported on the current platform?
51 | #' is.na(SIGABRT)
52 | #' }
53 | #'
54 | signals <- character()
55 |
56 |
57 | #' @description `process_terminate()` on Linux sends the
58 | #' `SIGTERM` signal to the process pointed to by `handle`.
59 | #' On Windows it calls `TerminateProcess()`.
60 | #'
61 | #' @rdname signals
62 | #' @export
63 | #'
64 | process_terminate <- function (handle)
65 | {
66 | stopifnot(is_process_handle(handle))
67 | .Call("C_process_terminate", handle$c_handle)
68 | }
69 |
70 |
71 | #' @description `process_kill()` on Linux sends the `SIGKILL`
72 | #' signal to `handle`. On Windows it is an alias for
73 | #' `process_terminate()`.
74 | #'
75 | #' @rdname signals
76 | #' @export
77 | #'
78 | process_kill <- function (handle)
79 | {
80 | stopifnot(is_process_handle(handle))
81 | .Call("C_process_kill", handle$c_handle)
82 | }
83 |
84 |
85 | #' @description `process_send_signal()` sends an OS-level
86 | #' `signal` to `handle`. In Linux all standard signal
87 | #' numbers are supported. On Windows supported signals are
88 | #' `SIGTERM`, `CTRL_C_EVENT` and `CTRL_BREAK_EVENT`.
89 | #' Those values will be available via the `signals` list which
90 | #' is also attached in the package namespace.
91 | #'
92 | #' @param signal Signal number, one of `names(signals)`.
93 | #'
94 | #' @rdname signals
95 | #' @export
96 | #'
97 | #' @examples
98 | #' \dontrun{
99 | #' # Windows
100 | #' process_send_signal(h, SIGTERM)
101 | #' process_send_signal(h, CTRL_C_EVENT)
102 | #' process_send_signal(h, CTRL_BREAK_EVENT)
103 | #' }
104 | #'
105 | process_send_signal <- function (handle, signal)
106 | {
107 | stopifnot(is_process_handle(handle))
108 | .Call("C_process_send_signal", handle$c_handle, as.integer(signal))
109 | }
110 |
111 |
112 | #' @export
113 | #' @rdname signals
114 | SIGABRT <- NA
115 |
116 | #' @export
117 | #' @rdname signals
118 | SIGALRM <- NA
119 |
120 | #' @export
121 | #' @rdname signals
122 | SIGCHLD <- NA
123 |
124 | #' @export
125 | #' @rdname signals
126 | SIGCONT <- NA
127 |
128 | #' @export
129 | #' @rdname signals
130 | SIGFPE <- NA
131 |
132 | #' @export
133 | #' @rdname signals
134 | SIGHUP <- NA
135 |
136 | #' @export
137 | #' @rdname signals
138 | SIGILL <- NA
139 |
140 | #' @export
141 | #' @rdname signals
142 | SIGINT <- NA
143 |
144 | #' @export
145 | #' @rdname signals
146 | SIGKILL <- NA
147 |
148 | #' @export
149 | #' @rdname signals
150 | SIGPIPE <- NA
151 |
152 | #' @export
153 | #' @rdname signals
154 | SIGQUIT <- NA
155 |
156 | #' @export
157 | #' @rdname signals
158 | SIGSEGV <- NA
159 |
160 | #' @export
161 | #' @rdname signals
162 | SIGSTOP <- NA
163 |
164 | #' @export
165 | #' @rdname signals
166 | SIGTERM <- NA
167 |
168 | #' @export
169 | #' @rdname signals
170 | SIGTSTP <- NA
171 |
172 | #' @export
173 | #' @rdname signals
174 | SIGTTIN <- NA
175 |
176 | #' @export
177 | #' @rdname signals
178 | SIGTTOU <- NA
179 |
180 | #' @export
181 | #' @rdname signals
182 | SIGUSR1 <- NA
183 |
184 | #' @export
185 | #' @rdname signals
186 | SIGUSR2 <- NA
187 |
188 | #' @export
189 | #' @rdname signals
190 | CTRL_C_EVENT <- NA
191 |
192 | #' @export
193 | #' @rdname signals
194 | CTRL_BREAK_EVENT <- NA
195 |
196 |
197 | #' A helper function used in vignette.
198 | #'
199 | #' @param signal Signal number.
200 | #' @param handler Either `"default"` or `"ignore"`.
201 | #'
202 | signal <- function (signal, handler)
203 | {
204 | .Call("C_signal", as.integer(signal), as.character(handler))
205 | }
206 |
--------------------------------------------------------------------------------
/R/subprocess.R:
--------------------------------------------------------------------------------
1 | #' @useDynLib subprocess, .registration = TRUE
2 | NULL
3 |
4 |
5 | #' Start a new child process.
6 | #'
7 | #' @description
8 | #' In Linux, the usual combination of `fork()` and `exec()`
9 | #' is used to spawn a new child process. Standard streams are redirected
10 | #' over regular unnamed `pipe`s.
11 | #'
12 | #' In Windows a new process is spawned with `CreateProcess()` and
13 | #' streams are redirected over unnamed pipes obtained with
14 | #' `CreatePipe()`. However, because non-blocking (*overlapped*
15 | #' in Windows-speak) read/write is not supported for unnamed pipes,
16 | #' two reader threads are created for each new child process. These
17 | #' threads never touch memory allocated by R and thus they will not
18 | #' interfere with R interpreter's memory management (garbage collection).
19 | #'
20 | #'
21 | #' @details
22 | #' `command` is always prepended to `arguments` so that the
23 | #' child process can correcty recognize the name of its executable
24 | #' via its `argv` vector. This is done automatically by
25 | #' `spawn_process`.
26 | #'
27 | #' `environment` can be passed as a `character` vector whose
28 | #' elements take the form `"NAME=VALUE"`, a named `character`
29 | #' vector or a named `list`.
30 | #'
31 | #' `workdir` is the path to the directory where the new process is
32 | #' ought to be started. `NULL` and `""` mean that working
33 | #' directory is inherited from the parent.
34 | #'
35 | #' @section Termination:
36 | #'
37 | #' The `termination_mode` specifies what should happen when
38 | #' `process_terminate()` or `process_kill()` is called on a
39 | #' subprocess. If it is set to `TERMINATION_GROUP`, then the
40 | #' termination signal is sent to the parent and all its descendants
41 | #' (sub-processes). If termination mode is set to
42 | #' `TERMINATION_CHILD_ONLY`, only the child process spawned
43 | #' directly from the R session receives the signal.
44 | #'
45 | #' In Windows this is implemented with the job API, namely
46 | #' `CreateJobObject()`, `AssignProcessToJobObject()` and
47 | #' `TerminateJobObject()`. In Linux, the child calls `setsid()`
48 | #' after `fork()` but before `execve()`, and `kill()` is
49 | #' called with the negate process id.
50 | #'
51 | #' @param command Path to the executable.
52 | #' @param arguments Optional arguments for the program.
53 | #' @param environment Optional environment.
54 | #' @param workdir Optional new working directory.
55 | #' @param termination_mode Either `TERMINATION_GROUP` or
56 | #' `TERMINATION_CHILD_ONLY`.
57 | #'
58 | #' @return `spawn_process()` returns an object of the
59 | #' *process handle* class.
60 | #' @rdname spawn_process
61 | #'
62 | #' @format `TERMINATION_GROUP` and `TERMINATION_CHILD_ONLY`
63 | #' are single `character` values.
64 | #'
65 | #' @export
66 | spawn_process <- function (command, arguments = character(), environment = character(),
67 | workdir = "", termination_mode = TERMINATION_GROUP)
68 | {
69 | command <- as.character(command)
70 | command <- normalizePath(command, mustWork = TRUE)
71 |
72 | # handle named environment
73 | if (!is.null(names(environment))) {
74 | if (any(names(environment) == "")) {
75 | stop("empty name(s) for environment variables", call. = FALSE)
76 | }
77 | environment <- paste(names(environment), as.character(environment), sep = '=')
78 | }
79 |
80 | if(!(is.null(workdir) || identical(workdir, ""))){
81 | workdir <- normalizePath(workdir, mustWork = TRUE)
82 | }
83 | # hand over to C
84 | handle <- .Call("C_process_spawn", command, c(command, as.character(arguments)),
85 | as.character(environment), as.character(workdir),
86 | as.character(termination_mode))
87 |
88 | structure(list(c_handle = handle, command = command, arguments = arguments),
89 | class = 'process_handle')
90 | }
91 |
92 |
93 | #' @param x Object to be printed or tested.
94 | #' @param ... Other parameters passed to the `print` method.
95 | #'
96 | #' @export
97 | #' @rdname spawn_process
98 | print.process_handle <- function (x, ...)
99 | {
100 | cat('Process Handle\n')
101 | cat('command : ', x$command, ' ', paste(x$arguments, collapse = ' '), '\n', sep = '')
102 | cat('system id : ', as.integer(x$c_handle), '\n', sep = '')
103 | cat('state : ', process_state(x), '\n', sep = '')
104 |
105 | invisible(x)
106 | }
107 |
108 |
109 | #' @description `is_process_handle()` verifies that an object is a
110 | #' valid *process handle* as returned by `spawn_process()`.
111 | #'
112 | #' @export
113 | #' @rdname spawn_process
114 | is_process_handle <- function (x)
115 | {
116 | inherits(x, 'process_handle')
117 | }
118 |
119 |
120 | #' Terminating a Child Process.
121 | #'
122 | #' @description
123 | #'
124 | #' These functions give access to the state of the child process and to
125 | #' its exit status (return code).
126 | #'
127 | #' The `timeout` parameter can take one of three values:
128 | #' \itemize{
129 | #' \item `0` which means no timeout
130 | #' \item `-1` which means "wait until there is data to read"
131 | #' \item a positive integer, which is the actual timeout in milliseconds
132 | #' }
133 | #'
134 | #' @details `process_wait()` checks the state of the child process
135 | #' by invoking the system call `waitpid()` or
136 | #' `WaitForSingleObject()`.
137 | #'
138 | #' @param handle Process handle obtained from `spawn_process`.
139 | #' @param timeout Optional timeout in milliseconds.
140 | #'
141 | #' @return `process_wait()` returns an `integer` exit code
142 | #' of the child process or `NA` if the child process has not exited
143 | #' yet. The same value can be accessed by `process_return_code()`.
144 | #'
145 | #' @name terminating
146 | #' @rdname terminating
147 | #' @export
148 | #'
149 | #' @seealso [spawn_process()], [process_read()]
150 | #' [signals()]
151 | #'
152 | process_wait <- function (handle, timeout = TIMEOUT_INFINITE)
153 | {
154 | stopifnot(is_process_handle(handle))
155 | .Call("C_process_wait", handle$c_handle, as.integer(timeout))
156 | }
157 |
158 |
159 | #' @details `process_state()` refreshes the handle by calling
160 | #' `process_wait()` with no timeout and returns one of these
161 | #' values: `"not-started"`. `"running"`, `"exited"`,
162 | #' `"terminated"`.
163 | #'
164 | #' @rdname terminating
165 | #' @export
166 | #'
167 | process_state <- function (handle)
168 | {
169 | stopifnot(is_process_handle(handle))
170 | .Call("C_process_state", handle$c_handle)
171 | }
172 |
173 |
174 | #' @details `process_return_code()` gives access to the value
175 | #' returned also by `process_wait()`. It does not invoke
176 | #' `process_wait()` behind the scenes.
177 | #'
178 | #' @rdname terminating
179 | #' @export
180 | #'
181 | process_return_code <- function (handle)
182 | {
183 | stopifnot(is_process_handle(handle))
184 | .Call("C_process_return_code", handle$c_handle)
185 | }
186 |
187 |
188 | #' Check if process with a given id exists.
189 | #'
190 | #' @param x A process handle returned by [spawn_process] or a OS-level process id.
191 | #' @return `TRUE` if process exists, `FALSE` otherwise.
192 | #'
193 | #' @export
194 | #'
195 | process_exists <- function (x)
196 | {
197 | if (is_process_handle(x)) {
198 | x <- x$c_handle
199 | }
200 |
201 | isTRUE(.Call("C_process_exists", as.integer(x)))
202 | }
203 |
204 |
205 | #' @description `TIMEOUT_INFINITE` denotes an "infinite" timeout
206 | #' (that is, wait until response is available) when waiting for an
207 | #' operation to complete.
208 | #'
209 | #' @rdname terminating
210 | #' @export
211 | TIMEOUT_INFINITE <- -1L
212 |
213 |
214 | #' @description `TIMEOUT_IMMEDIATE` denotes an "immediate" timeout
215 | #' (in other words, no timeout) when waiting for an operation to
216 | #' complete.
217 | #'
218 | #' @rdname terminating
219 | #' @export
220 | TIMEOUT_IMMEDIATE <- 0L
221 |
222 |
223 | #' @description `TERMINATION_GROUP`: `process_terminate(handle)`
224 | #' and `process_kill(handle)` deliver the signal to the child
225 | #' process pointed to by `handle` and all of its descendants.
226 | #'
227 | #' @rdname spawn_process
228 | #' @export
229 | TERMINATION_GROUP <- "group"
230 |
231 |
232 | #' @description `TERMINATION_CHILD_ONLY`:
233 | #' `process_terminate(handle)` and `process_kill(handle)`
234 | #' deliver the signal only to the child process pointed to by
235 | #' `handle` but to none of its descendants.
236 | #'
237 | #' @rdname spawn_process
238 | #' @export
239 | TERMINATION_CHILD_ONLY <- "child_only"
240 |
241 |
--------------------------------------------------------------------------------
/R/tests.R:
--------------------------------------------------------------------------------
1 |
2 | #' Run UTF8 tests implemented in C.
3 | #'
4 | #' If there is no error in those tests a simple string message
5 | #' is returned. If there is at least one error, another message
6 | #' is returned.
7 | #'
8 | #' @return A string `"All C tests passed!"` if there are no errors.
9 | #' @export
10 | #' @rdname tests
11 | #'
12 | C_tests_utf8 <- function ()
13 | {
14 | ret <- .Call("test_consume_utf8");
15 |
16 | if (ret == 0) {
17 | return("All C tests passed!")
18 | }
19 |
20 | paste0(ret, " error(s) encountered in C tests, see warnings() for details")
21 | }
22 |
23 |
--------------------------------------------------------------------------------
/R/utils.R:
--------------------------------------------------------------------------------
1 | is_windows <- function ()
2 | {
3 | identical(tolower(Sys.info()[["sysname"]]), 'windows')
4 | }
5 |
6 | is_linux <- function ()
7 | {
8 | identical(tolower(Sys.info()[["sysname"]]), 'linux')
9 | }
10 |
11 | known_signals <- function ()
12 | {
13 | .Call("C_known_signals")
14 | }
15 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | subprocess
2 | ==========================
3 |
4 | | CRAN version | Travis build status | AppVeyor | Coverage | Downloads |
5 | | :-------------: |:---------------------:|:--------:|:--------:|:---------:|
6 | | [](https://cran.r-project.org/package=subprocess) | [](https://travis-ci.org/lbartnik/subprocess) | [](https://ci.appveyor.com/project/lbartnik/subprocess) | [](https://codecov.io/gh/lbartnik/subprocess)| [](https://cranlogs.r-pkg.org/) |
7 |
8 |
9 |
10 | Run and interact with a child process in R! `subprocess` brings a new
11 | R API to create, control the life cycle and shutdown a child process
12 | in **Linux**, **Windows** and **Mac OS**. Check this out.
13 |
14 |
15 | ## Remote shell example
16 |
17 | Here's an example of running a `ssh` client child process. It connects
18 | to a ssh server using public key (thus, no password). Then we list files
19 | in the remote account and finally gracefully shutdown the child process.
20 |
21 | Load the `subprocess` package and start a new child process:
22 |
23 | ```r
24 | library(subprocess)
25 |
26 | ssh_path <- '/usr/bin/ssh'
27 | handle <- spawn_process(ssh_path, c('-T', 'test@example'))
28 | ```
29 |
30 | Here is the description of the child process:
31 |
32 | ```r
33 | print(handle)
34 | #> Process Handle
35 | #> command : /usr/bin/ssh -T test@example
36 | #> system id : 17659
37 | #> state : running
38 | ```
39 |
40 | And here is what the child process has sent so far to its output streams:
41 |
42 | ```r
43 | process_read(handle, PIPE_STDOUT, timeout = TIMEOUT_INFINITE)
44 | #> [1] "Welcome to Ubuntu 16.10 (GNU/Linux 4.8.0-27-generic x86_64)"
45 | #> [2] ""
46 | #> [3] " * Documentation: https://help.ubuntu.com"
47 | #> [4] " * Management: https://landscape.canonical.com"
48 | #> [5] " * Support: https://ubuntu.com/advantage"
49 | #> [6] ""
50 | #> [7] "0 packages can be updated."
51 | #> [8] "0 updates are security updates."
52 | #> [9] ""
53 | process_read(handle, PIPE_STDERR)
54 | #> character(0)
55 | ```
56 |
57 | Nothing in the standard error output. Good! Now we ask the remote shell
58 | to list files.
59 |
60 | ```r
61 | process_write(handle, 'ls\n')
62 | #> [1] 3
63 | process_read(handle, PIPE_STDOUT, timeout = TIMEOUT_INFINITE)
64 | #> [1] "Desktop" "Download" "examples.desktop"
65 | #> [4] "Music" "Public" "Video"
66 | process_read(handle, PIPE_STDERR)
67 | #> character(0)
68 | ```
69 |
70 | The first number in the output is the value returned by `process_write()`
71 | which is the number of characters written to standard input of the
72 | child process. The final `character(0)` is the output read from the
73 | standard error stream.
74 |
75 |
76 | We are now ready to close the connection by exiting the remote shell:
77 |
78 | ```r
79 | process_write(handle, 'exit\n')
80 | #> [1] 5
81 | process_read(handle, PIPE_STDOUT, timeout = TIMEOUT_INFINITE)
82 | #> character(0)
83 | process_read(handle, PIPE_STDERR)
84 | #> character(0)
85 | ```
86 |
87 | The last thing is making sure that the child process is no longer alive:
88 |
89 | ```r
90 | process_wait(handle, TIMEOUT_INFINITE)
91 | #> [1] 0
92 | process_status(handle)
93 | #> [1] "exited"
94 | ```
95 |
--------------------------------------------------------------------------------
/TODO:
--------------------------------------------------------------------------------
1 | * move stdout/stderr fds to pipe_writer
2 |
3 | * unify process::read between Linux and Windows; use non-blocking
4 | polling in both places and handle Ctrl+C
5 | * make Ctrl+C abandon read/write to/from a process
6 | * prevent Ctrl+C from being passed to the child process
7 |
8 | * implement a high-level popen() call/factor function with parameters:
9 | stdin, stdout, stderr; enable string-based input, output to /dev/null
10 | (Linux) or nul (Windows); redirecting stderr to stdout
11 |
12 | * make partial UTF8 handling optional in process_read(); maybe someone
13 | is reading bytes and not textual output?
14 | * handle differing new-line characters
15 |
16 | * see how tools::pskill can be used our own process_send_signal
17 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | # DO NOT CHANGE the "init" and "install" sections below
2 |
3 | # Download script file from GitHub
4 | init:
5 | ps: |
6 | $ErrorActionPreference = "Stop"
7 | Invoke-WebRequest http://raw.github.com/krlmlr/r-appveyor/master/scripts/appveyor-tool.ps1 -OutFile "..\appveyor-tool.ps1"
8 | Import-Module '..\appveyor-tool.ps1'
9 |
10 | install:
11 | ps: Bootstrap
12 |
13 | # Adapt as necessary starting from here
14 |
15 | build_script:
16 | - travis-tool.sh install_deps
17 |
18 | test_script:
19 | - travis-tool.sh run_tests
20 |
21 | on_failure:
22 | - 7z a failure.zip *.Rcheck\*
23 | - appveyor PushArtifact failure.zip
24 |
25 | artifacts:
26 | - path: '*.Rcheck\**\*.log'
27 | name: Logs
28 |
29 | - path: '*.Rcheck\**\*.out'
30 | name: Logs
31 |
32 | - path: '*.Rcheck\**\*.fail'
33 | name: Logs
34 |
35 | - path: '*.Rcheck\**\*.Rout'
36 | name: Logs
37 |
38 | - path: '\*_*.tar.gz'
39 | name: Bits
40 |
41 | - path: '\*_*.zip'
42 | name: Bits
43 |
--------------------------------------------------------------------------------
/build/vignette.rds:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lbartnik/subprocess/ecd741a14bd701b628da6a6e1978899047ee2ee8/build/vignette.rds
--------------------------------------------------------------------------------
/inst/doc/intro.R:
--------------------------------------------------------------------------------
1 | ## ----setup, include=FALSE------------------------------------------------
2 | library(subprocess)
3 | library(knitr)
4 |
5 | knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
6 |
7 | ## ----helpers-------------------------------------------------------------
8 | is_windows <- function () (tolower(.Platform$OS.type) == "windows")
9 |
10 | R_binary <- function () {
11 | R_exe <- ifelse (is_windows(), "R.exe", "R")
12 | return(file.path(R.home("bin"), R_exe))
13 | }
14 |
15 | ## ----platform------------------------------------------------------------
16 | ifelse(is_windows(), "Windows", "Linux")
17 |
18 | ## ------------------------------------------------------------------------
19 | library(subprocess)
20 |
21 | ## ----new_child-----------------------------------------------------------
22 | handle <- spawn_process(R_binary(), c('--no-save'))
23 | Sys.sleep(1)
24 |
25 | ## ------------------------------------------------------------------------
26 | print(handle)
27 |
28 | ## ----read_from_child-----------------------------------------------------
29 | process_read(handle, PIPE_STDOUT, timeout = 1000)
30 | process_read(handle, PIPE_STDERR)
31 |
32 | ## ----new_n---------------------------------------------------------------
33 | process_write(handle, 'n <- 10\n')
34 | process_read(handle, PIPE_STDOUT, timeout = 1000)
35 | process_read(handle, PIPE_STDERR)
36 |
37 | ## ----rnorn_n-------------------------------------------------------------
38 | process_write(handle, 'rnorm(n)\n')
39 | process_read(handle, PIPE_STDOUT, timeout = 1000)
40 | process_read(handle, PIPE_STDERR)
41 |
42 | ## ----quit_child----------------------------------------------------------
43 | process_write(handle, 'q(save = "no")\n')
44 | process_read(handle, PIPE_STDOUT, timeout = 1000)
45 | process_read(handle, PIPE_STDERR)
46 |
47 | ## ----verify_child_exited-------------------------------------------------
48 | process_state(handle)
49 | process_return_code(handle)
50 |
51 | ## ----spawn_shell---------------------------------------------------------
52 | shell_binary <- function () {
53 | ifelse (tolower(.Platform$OS.type) == "windows",
54 | "C:/Windows/System32/cmd.exe", "/bin/sh")
55 | }
56 |
57 | handle <- spawn_process(shell_binary())
58 | print(handle)
59 |
60 | ## ----interact_with_shell-------------------------------------------------
61 | process_write(handle, "ls\n")
62 | Sys.sleep(1)
63 | process_read(handle, PIPE_STDOUT)
64 | process_read(handle, PIPE_STDERR)
65 |
66 | ## ----signal_child--------------------------------------------------------
67 | sub_command <- "library(subprocess);subprocess:::signal(15,'ignore');Sys.sleep(1000)"
68 | handle <- spawn_process(R_binary(), c('--slave', '-e', sub_command))
69 | Sys.sleep(1)
70 |
71 | # process is hung
72 | process_wait(handle, 1000)
73 | process_state(handle)
74 |
75 | # ask nicely to exit; will be ignored in Linux but not in Windows
76 | process_terminate(handle)
77 | process_wait(handle, 1000)
78 | process_state(handle)
79 |
80 | # forced exit; in Windows the same as the previous call to process_terminate()
81 | process_kill(handle)
82 | process_wait(handle, 1000)
83 | process_state(handle)
84 |
85 | ## ----show_three_signals--------------------------------------------------
86 | length(signals)
87 | signals[1:3]
88 |
89 | ## ------------------------------------------------------------------------
90 | ls(pattern = 'SIG', envir = asNamespace('subprocess'))
91 |
92 | ## ----eval=FALSE----------------------------------------------------------
93 | # handle <- spawn_process(R_binary, '--slave')
94 | #
95 | # process_send_signal(handle, SIGUSR1)
96 |
97 |
--------------------------------------------------------------------------------
/inst/doc/intro.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Introduction to Sub-Processes in R"
3 | author: "Lukasz A. Bartnik"
4 | date: "`r Sys.Date()`"
5 | output: rmarkdown::html_vignette
6 | vignette: >
7 | %\VignetteIndexEntry{Introduction to Sub-Processes in R}
8 | %\VignetteEngine{knitr::rmarkdown}
9 | %\VignetteEncoding{UTF-8}
10 | ---
11 |
12 | ```{r setup, include=FALSE}
13 | library(subprocess)
14 | library(knitr)
15 |
16 | knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
17 | ```
18 |
19 | ## Introduction
20 |
21 | Since R is not really a systems-programming language[^systemslanguage]
22 | some facilities present in other such languages (e.g. C/C++, Python)
23 | haven't been yet brought to R. One of such features is process
24 | management which is understood here as the capability to create,
25 | interact with and control the lifetime of child processes.
26 |
27 | The R package `subprocess` aims at filling this gap by providing the few
28 | basic operations to carry out the aforementioned tasks. The
29 | `spawn_subprocess()` function starts a new child process and returns a
30 | handle which can be later used in `process_read()` and `process_write()`
31 | to send and receive data or in `process_wait()` or `process_terminate()`
32 | to stop the such a process.
33 |
34 | The R `subprocess` package has been designed after the exemplary
35 | Python package which goes by the same. Its documentation can be found
36 | [here](https://docs.python.org/3/library/subprocess.html) and numerous
37 | examples of how it can be used can be found on the Web.
38 |
39 | The R `subprocess` package has been verified to run on **Linux**,
40 | **Windows** and **MacOS**.
41 |
42 |
43 | [^systemslanguage]: "By systems programming I mean writing code that
44 | directly uses hardware resources, has serious resource constraints,
45 | or closely interacts with code that does." Bjarne Stroustrup, "The
46 | C++ Programming Language"
47 |
48 |
49 | ## Design and Implementation
50 |
51 | The main concept in the package is the __handle__ which holds process
52 | identifiers and an __external pointer__ object which in turn is a handle
53 | to a low-level data structure holding various system-level parameters
54 | of the running sub-process.
55 |
56 | A child process, once started, runs until it exits on its own or until
57 | its killed. Its current state as well as its exit status can be obtained
58 | by dedicated API.
59 |
60 | Communication with the child process can be carried out over the three
61 | standard streams: the standard input, standard output and standard error
62 | output. These streams are intercepted on the child process' side and
63 | redirected into three anonymous pipes whose other ends are held by the
64 | parent process and can be accessed through the process __handle__.
65 |
66 | In **Linux** these are regular pipes created with the `pipe()` system call
67 | and opened in the __non-blocking__ mode. All communication takes place
68 | on request and follows the usual OS rules (e.g. the sub-process will
69 | sleep if its output buffer gets filled).
70 |
71 | In **Windows** these pipes are created with the `CreatePipe()` call and
72 | opened in the __blocking__ mode. **Windows** does not support
73 | __non-blocking__ (__overlapped__ in **Windows**-speak) mode for anonymous
74 | pipes. For that reason each stream has an accompanying reader thread.
75 | Reader threads are separated from R interpreter, do not exchange memory
76 | with the R interpreter and will not break the single-thread assumption
77 | under which R operates.
78 |
79 |
80 | ## Introduction to Examples
81 |
82 | Before we move on to examples, let's define a few helper functions
83 | that abstract out the underlying operating system. We will use them
84 | throughout this vignette.
85 |
86 | ```{r helpers}
87 | is_windows <- function () (tolower(.Platform$OS.type) == "windows")
88 |
89 | R_binary <- function () {
90 | R_exe <- ifelse (is_windows(), "R.exe", "R")
91 | return(file.path(R.home("bin"), R_exe))
92 | }
93 | ```
94 |
95 | Just for the record, vignette has been built in
96 | `r ifelse(is_windows(), "Windows", "Linux")`.
97 |
98 | ```{r platform}
99 | ifelse(is_windows(), "Windows", "Linux")
100 | ```
101 |
102 | Now we can load the package and move on to the next section.
103 |
104 | ```{r}
105 | library(subprocess)
106 | ```
107 |
108 |
109 | ## Example: controlling chlid R process
110 |
111 | In this example we spawn a new R process, send a few commands to its
112 | standard input and read the responses from its standard output. First,
113 | let's spawn the child process (and give it a moment to complete the
114 | start-up sequence[^syssleep]):
115 |
116 | ```{r new_child}
117 | handle <- spawn_process(R_binary(), c('--no-save'))
118 | Sys.sleep(1)
119 | ```
120 |
121 | [^syssleep]: Depending on the system load, R can take a few seconds
122 | to start and be ready for input. This is true also for other processes.
123 | Thus, you will see `Sys.sleep()` following `spawn_process()` in almost
124 | every example in this vignette.
125 |
126 | Let's see the description of the child process:
127 | ```{r}
128 | print(handle)
129 | ```
130 |
131 | And now let's see what we can find it the child's output:
132 | ```{r read_from_child}
133 | process_read(handle, PIPE_STDOUT, timeout = 1000)
134 | process_read(handle, PIPE_STDERR)
135 | ```
136 |
137 | The first number in the output is the value returned by `process_write`
138 | which is the number of characters written to standard input of the
139 | child process. The final `character(0)` is the output read from the
140 | standard error stream.
141 |
142 | Next, we create a new variable in child's session. Please notice the
143 | new-line character at the end of the command. It triggers the child
144 | process to process its input.
145 |
146 | ```{r new_n}
147 | process_write(handle, 'n <- 10\n')
148 | process_read(handle, PIPE_STDOUT, timeout = 1000)
149 | process_read(handle, PIPE_STDERR)
150 | ```
151 |
152 | Now it's time to use this variable in a function call:
153 |
154 | ```{r rnorn_n}
155 | process_write(handle, 'rnorm(n)\n')
156 | process_read(handle, PIPE_STDOUT, timeout = 1000)
157 | process_read(handle, PIPE_STDERR)
158 | ```
159 |
160 | Finally, we exit the child process:
161 |
162 | ```{r quit_child}
163 | process_write(handle, 'q(save = "no")\n')
164 | process_read(handle, PIPE_STDOUT, timeout = 1000)
165 | process_read(handle, PIPE_STDERR)
166 | ```
167 |
168 | The last thing is making sure that the child process is no longer alive:
169 |
170 | ```{r verify_child_exited}
171 | process_state(handle)
172 | process_return_code(handle)
173 | ```
174 |
175 | Of course there is little value in running a child R process since there
176 | are multiple other tools that let you do that, like `parallel`, `Rserve`
177 | and `opencpu` to name just a few. However, it's quite easy to imagine
178 | how running a remote shell in this manner enables new ways of
179 | interacting with the environment. Consider running a local shell:
180 |
181 | ```{r spawn_shell}
182 | shell_binary <- function () {
183 | ifelse (tolower(.Platform$OS.type) == "windows",
184 | "C:/Windows/System32/cmd.exe", "/bin/sh")
185 | }
186 |
187 | handle <- spawn_process(shell_binary())
188 | print(handle)
189 | ```
190 |
191 | Now we can interact with the shell sub-process. We send a request to
192 | list the current directory, then give it a moment to process the command
193 | and produce the output (and maybe finish its start-up, too). Finally,
194 | we check its output streams.
195 |
196 | ```{r interact_with_shell}
197 | process_write(handle, "ls\n")
198 | Sys.sleep(1)
199 | process_read(handle, PIPE_STDOUT)
200 | process_read(handle, PIPE_STDERR)
201 | ```
202 |
203 |
204 | ## Advanced techniques
205 |
206 | ### Terminating a child process
207 |
208 | If the child process needs to be terminated one can choose to:
209 |
210 | - send a command on the standard input with `process_write()`
211 | - send the termination signal, `SIGTERM` (**Linux**, **Windows**)
212 | - send the kill signal, `SIGKILL` (**Linux** only)
213 |
214 | Assume the child R process is hung and there is no way to stop it
215 | gracefully. `process_wait(handle, 1000)` waits for 1 second (1000
216 | milliseconds) for the child process to exit. It then returns `NA` and
217 | `process_terminate()` gives `R` a chance to exit graceully. Finally,
218 | `process_kill()` forces it to exit.
219 |
220 |
221 | ```{r signal_child}
222 | sub_command <- "library(subprocess);subprocess:::signal(15,'ignore');Sys.sleep(1000)"
223 | handle <- spawn_process(R_binary(), c('--slave', '-e', sub_command))
224 | Sys.sleep(1)
225 |
226 | # process is hung
227 | process_wait(handle, 1000)
228 | process_state(handle)
229 |
230 | # ask nicely to exit; will be ignored in Linux but not in Windows
231 | process_terminate(handle)
232 | process_wait(handle, 1000)
233 | process_state(handle)
234 |
235 | # forced exit; in Windows the same as the previous call to process_terminate()
236 | process_kill(handle)
237 | process_wait(handle, 1000)
238 | process_state(handle)
239 | ```
240 |
241 | We see that the child process remains running until it receives the
242 | `SIGKILL` signal[^signal]. The final return code (exit status) is the
243 | number of the signal that caused the child process to exit[^status].
244 |
245 | [^termination]: In **Windows**, `process_terminate()` is an alias for
246 | `process_kill()`. They both lead to immediate termination of the child
247 | process.
248 |
249 | [^signal]: The `.Call("C_signal")` in our example is a call to a hidden
250 | C function that `subprocess` provides mainly for the purposes of this
251 | example.
252 |
253 | [^status]: See the `waitpid()` manual page, e.g. [here](https://linux.die.net/man/2/waitpid).
254 |
255 |
256 | ### Sending a signal to the child process
257 |
258 | The last topic we want to cover here is sending an arbitrary[^windowssignals]
259 | signal to the child process. Signals can be listed by looking at the
260 | `signals` variable present in the package. It is constructed
261 | automatically when the package is loaded and its value on **Linux** is
262 | different than on **Windows**. In the example below we see the first
263 | three elements of the **Linux** list of signals.
264 |
265 | ```{r show_three_signals}
266 | length(signals)
267 | signals[1:3]
268 | ```
269 |
270 |
271 | All possible signal identifiers are supported directly from the
272 | `subprocess` package. Signals not supported on the current platform
273 | are set to `NA` and the rest have their OS-specific numbers as their
274 | values.
275 |
276 | ```{r}
277 | ls(pattern = 'SIG', envir = asNamespace('subprocess'))
278 | ```
279 |
280 |
281 | Now we can create a new child process and send an arbitrary using its
282 | handle.
283 |
284 | ```{r eval=FALSE}
285 | handle <- spawn_process(R_binary, '--slave')
286 |
287 | process_send_signal(handle, SIGUSR1)
288 | ```
289 |
290 |
291 | [^windowssignals]: The list of signals supported in **Windows** is much
292 | shorter than the list of signals supported in **Linux** and contains the
293 | following three signals: `SIGTERM`, `CTRL_C_EVENT` and `CTRL_BREAK_EVENT`.
294 |
--------------------------------------------------------------------------------
/inst/doc/intro.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Since R is not really a systems-programming language1 some facilities present in other such languages (e.g. C/C++, Python) haven’t been yet brought to R. One of such features is process management which is understood here as the capability to create, interact with and control the lifetime of child processes.
80 |
The R package subprocess aims at filling this gap by providing the few basic operations to carry out the aforementioned tasks. The spawn_subprocess() function starts a new child process and returns a handle which can be later used in process_read() and process_write() to send and receive data or in process_wait() or process_terminate() to stop the such a process.
81 |
The R subprocess package has been designed after the exemplary Python package which goes by the same. Its documentation can be found here and numerous examples of how it can be used can be found on the Web.
82 |
The R subprocess package has been verified to run on Linux, Windows and MacOS.
83 |
84 |
85 |
Design and Implementation
86 |
The main concept in the package is the handle which holds process identifiers and an external pointer object which in turn is a handle to a low-level data structure holding various system-level parameters of the running sub-process.
87 |
A child process, once started, runs until it exits on its own or until its killed. Its current state as well as its exit status can be obtained by dedicated API.
88 |
Communication with the child process can be carried out over the three standard streams: the standard input, standard output and standard error output. These streams are intercepted on the child process’ side and redirected into three anonymous pipes whose other ends are held by the parent process and can be accessed through the process handle.
89 |
In Linux these are regular pipes created with the pipe() system call and opened in the non-blocking mode. All communication takes place on request and follows the usual OS rules (e.g. the sub-process will sleep if its output buffer gets filled).
90 |
In Windows these pipes are created with the CreatePipe() call and opened in the blocking mode. Windows does not support non-blocking (overlapped in Windows-speak) mode for anonymous pipes. For that reason each stream has an accompanying reader thread. Reader threads are separated from R interpreter, do not exchange memory with the R interpreter and will not break the single-thread assumption under which R operates.
91 |
92 |
93 |
Introduction to Examples
94 |
Before we move on to examples, let’s define a few helper functions that abstract out the underlying operating system. We will use them throughout this vignette.
Now we can load the package and move on to the next section.
105 |
library(subprocess)
106 |
107 |
108 |
Example: controlling chlid R process
109 |
In this example we spawn a new R process, send a few commands to its standard input and read the responses from its standard output. First, let’s spawn the child process (and give it a moment to complete the start-up sequence2):
print(handle)
114 | #> Process Handle
115 | #> command : /usr/lib/R/bin/R --no-save
116 | #> system id : 14227
117 | #> state : running
118 |
And now let’s see what we can find it the child’s output:
119 |
process_read(handle, PIPE_STDOUT, timeout =1000)
120 | #> [1] ""
121 | #> [2] "R version 3.4.1 (2017-06-30) -- \"Single Candle\""
122 | #> [3] "Copyright (C) 2017 The R Foundation for Statistical Computing"
123 | #> [4] "Platform: x86_64-pc-linux-gnu (64-bit)"
124 | #> [5] ""
125 | #> [6] "R is free software and comes with ABSOLUTELY NO WARRANTY."
126 | #> [7] "You are welcome to redistribute it under certain conditions."
127 | #> [8] "Type 'license()' or 'licence()' for distribution details."
128 | #> [9] ""
129 | #> [10] " Natural language support but running in an English locale"
130 | #> [11] ""
131 | #> [12] "R is a collaborative project with many contributors."
132 | #> [13] "Type 'contributors()' for more information and"
133 | #> [14] "'citation()' on how to cite R or R packages in publications."
134 | #> [15] ""
135 | #> [16] "Type 'demo()' for some demos, 'help()' for on-line help, or"
136 | #> [17] "'help.start()' for an HTML browser interface to help."
137 | #> [18] "Type 'q()' to quit R."
138 | #> [19] ""
139 | #> [20] "> "
140 | process_read(handle, PIPE_STDERR)
141 | #> character(0)
142 |
The first number in the output is the value returned by process_write which is the number of characters written to standard input of the child process. The final character(0) is the output read from the standard error stream.
143 |
Next, we create a new variable in child’s session. Please notice the new-line character at the end of the command. It triggers the child process to process its input.
Of course there is little value in running a child R process since there are multiple other tools that let you do that, like parallel, Rserve and opencpu to name just a few. However, it’s quite easy to imagine how running a remote shell in this manner enables new ways of interacting with the environment. Consider running a local shell:
Now we can interact with the shell sub-process. We send a request to list the current directory, then give it a moment to process the command and produce the output (and maybe finish its start-up, too). Finally, we check its output streams.
If the child process needs to be terminated one can choose to:
198 |
199 |
send a command on the standard input with process_write()
200 |
send the termination signal, SIGTERM (Linux, Windows)
201 |
send the kill signal, SIGKILL (Linux only)
202 |
203 |
Assume the child R process is hung and there is no way to stop it gracefully. process_wait(handle, 1000) waits for 1 second (1000 milliseconds) for the child process to exit. It then returns NA and process_terminate() gives R a chance to exit graceully. Finally, process_kill() forces it to exit.
204 |
sub_command <- "library(subprocess);subprocess:::signal(15,'ignore');Sys.sleep(1000)"
205 | handle <-spawn_process(R_binary(), c('--slave', '-e', sub_command))
206 | Sys.sleep(1)
207 |
208 | # process is hung
209 | process_wait(handle, 1000)
210 | #> [1] 1
211 | process_state(handle)
212 | #> [1] "exited"
213 |
214 | # ask nicely to exit; will be ignored in Linux but not in Windows
215 | process_terminate(handle)
216 | #> [1] TRUE
217 | process_wait(handle, 1000)
218 | #> [1] 1
219 | process_state(handle)
220 | #> [1] "exited"
221 |
222 | # forced exit; in Windows the same as the previous call to process_terminate()
223 | process_kill(handle)
224 | #> [1] TRUE
225 | process_wait(handle, 1000)
226 | #> [1] 1
227 | process_state(handle)
228 | #> [1] "exited"
229 |
We see that the child process remains running until it receives the SIGKILL signal3. The final return code (exit status) is the number of the signal that caused the child process to exit4.
230 |
231 |
232 |
Sending a signal to the child process
233 |
The last topic we want to cover here is sending an arbitrary5 signal to the child process. Signals can be listed by looking at the signals variable present in the package. It is constructed automatically when the package is loaded and its value on Linux is different than on Windows. In the example below we see the first three elements of the Linux list of signals.
All possible signal identifiers are supported directly from the subprocess package. Signals not supported on the current platform are set to NA and the rest have their OS-specific numbers as their values.
“By systems programming I mean writing code that directly uses hardware resources, has serious resource constraints, or closely interacts with code that does.” Bjarne Stroustrup, “The C++ Programming Language”↩
260 |
Depending on the system load, R can take a few seconds to start and be ready for input. This is true also for other processes. Thus, you will see Sys.sleep() following spawn_process() in almost every example in this vignette.↩
261 |
The .Call("C_signal") in our example is a call to a hidden C function that subprocess provides mainly for the purposes of this example.↩
The list of signals supported in Windows is much shorter than the list of signals supported in Linux and contains the following three signals: SIGTERM, CTRL_C_EVENT and CTRL_BREAK_EVENT.↩
264 |
265 |
266 |
267 |
268 |
269 |
270 |
278 |
279 |
280 |
281 |
--------------------------------------------------------------------------------
/man/process_exists.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/subprocess.R
3 | \name{process_exists}
4 | \alias{process_exists}
5 | \title{Check if process with a given id exists.}
6 | \usage{
7 | process_exists(x)
8 | }
9 | \arguments{
10 | \item{x}{A process handle returned by \link{spawn_process} or a OS-level process id.}
11 | }
12 | \value{
13 | \code{TRUE} if process exists, \code{FALSE} otherwise.
14 | }
15 | \description{
16 | Check if process with a given id exists.
17 | }
18 |
--------------------------------------------------------------------------------
/man/readwrite.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/readwrite.R
3 | \docType{data}
4 | \name{readwrite}
5 | \alias{readwrite}
6 | \alias{process_read}
7 | \alias{process_write}
8 | \alias{process_close_input}
9 | \alias{PIPE_STDOUT}
10 | \alias{PIPE_STDERR}
11 | \alias{PIPE_BOTH}
12 | \title{Communicating with a Child Process}
13 | \format{\code{PIPE_STDOUT}, \code{PIPE_STDERR} and \code{PIPE_BOTH} are single
14 | \code{character} values.}
15 | \usage{
16 | process_read(handle, pipe = PIPE_BOTH, timeout = TIMEOUT_IMMEDIATE,
17 | flush = TRUE)
18 |
19 | process_write(handle, message)
20 |
21 | process_close_input(handle)
22 |
23 | PIPE_STDOUT
24 |
25 | PIPE_STDERR
26 |
27 | PIPE_BOTH
28 | }
29 | \arguments{
30 | \item{handle}{Process handle obtained from \code{spawn_process}.}
31 |
32 | \item{pipe}{Output stream identifier: \code{PIPE_STDOUT}, \code{PIPE_STDERR} or
33 | \code{PIPE_BOTH}.}
34 |
35 | \item{timeout}{Optional timeout in milliseconds.}
36 |
37 | \item{flush}{If there is any data within the given \code{timeout}
38 | try again with \code{timeout=0} until C buffer is empty.}
39 |
40 | \item{message}{Input for the child process.}
41 | }
42 | \value{
43 | \code{process_read} returns a \code{list} which contains either of or
44 | both keys: \emph{stdout} and \emph{stderr}; the value is in both cases
45 | a \code{character} vector which contains lines of child's output.
46 |
47 | \code{process_write} returns the number of characters written.
48 | }
49 | \description{
50 | \code{process_read()} reads data from one of the child process' streams,
51 | \emph{standard output} or \emph{standard error output}, and returns it as a
52 | \code{character} vector.
53 |
54 | \code{process_write()} writes data into child's
55 | \emph{standard input} stream.
56 |
57 | \code{process_close_input()} closes the \emph{write} end
58 | of the pipe whose \emph{read} end is the standard input stream of the
59 | child process. This is a standard way to gracefully request the child
60 | process to exit.
61 |
62 | \code{PIPE_STDOUT}: read from child's standard output.
63 |
64 | \code{PIPE_STDERR}: read from child's standard error
65 | output.
66 |
67 | \code{PIPE_BOTH}: read from both child's output streams:
68 | standard output and standard error output.
69 | }
70 | \details{
71 | If \code{flush=TRUE} in \code{process_read()} then the invocation of the
72 | underlying \code{read()} \emph{system-call} will be repeated until the pipe
73 | buffer is empty.
74 |
75 | If \code{pipe} is set to either \code{PIPE_STDOUT} or \code{PIPE_STDERR}, the returned
76 | value is a single list with a single key, \code{stdout} or \code{stderr},
77 | respectively. If \code{pipe} is set to \code{PIPE_BOTH} the returned \code{list}
78 | contains both keys. Values in the list are \code{character} vectors of 0
79 | or more elements, lines read from the respective output stream of the
80 | child process.
81 |
82 | For details on \code{timeout} see \link{terminating}.
83 | }
84 | \keyword{datasets}
85 |
--------------------------------------------------------------------------------
/man/signal.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/signals.R
3 | \name{signal}
4 | \alias{signal}
5 | \title{A helper function used in vignette.}
6 | \usage{
7 | signal(signal, handler)
8 | }
9 | \arguments{
10 | \item{signal}{Signal number.}
11 |
12 | \item{handler}{Either \code{"default"} or \code{"ignore"}.}
13 | }
14 | \description{
15 | A helper function used in vignette.
16 | }
17 |
--------------------------------------------------------------------------------
/man/signals.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/signals.R
3 | \docType{data}
4 | \name{signals}
5 | \alias{signals}
6 | \alias{process_terminate}
7 | \alias{process_kill}
8 | \alias{process_send_signal}
9 | \alias{SIGABRT}
10 | \alias{SIGALRM}
11 | \alias{SIGCHLD}
12 | \alias{SIGCONT}
13 | \alias{SIGFPE}
14 | \alias{SIGHUP}
15 | \alias{SIGILL}
16 | \alias{SIGINT}
17 | \alias{SIGKILL}
18 | \alias{SIGPIPE}
19 | \alias{SIGQUIT}
20 | \alias{SIGSEGV}
21 | \alias{SIGSTOP}
22 | \alias{SIGTERM}
23 | \alias{SIGTSTP}
24 | \alias{SIGTTIN}
25 | \alias{SIGTTOU}
26 | \alias{SIGUSR1}
27 | \alias{SIGUSR2}
28 | \alias{CTRL_C_EVENT}
29 | \alias{CTRL_BREAK_EVENT}
30 | \title{Sending signals to the child process.}
31 | \format{An object of class \code{list}.}
32 | \usage{
33 | signals
34 |
35 | process_terminate(handle)
36 |
37 | process_kill(handle)
38 |
39 | process_send_signal(handle, signal)
40 |
41 | SIGABRT
42 |
43 | SIGALRM
44 |
45 | SIGCHLD
46 |
47 | SIGCONT
48 |
49 | SIGFPE
50 |
51 | SIGHUP
52 |
53 | SIGILL
54 |
55 | SIGINT
56 |
57 | SIGKILL
58 |
59 | SIGPIPE
60 |
61 | SIGQUIT
62 |
63 | SIGSEGV
64 |
65 | SIGSTOP
66 |
67 | SIGTERM
68 |
69 | SIGTSTP
70 |
71 | SIGTTIN
72 |
73 | SIGTTOU
74 |
75 | SIGUSR1
76 |
77 | SIGUSR2
78 |
79 | CTRL_C_EVENT
80 |
81 | CTRL_BREAK_EVENT
82 | }
83 | \arguments{
84 | \item{handle}{Process handle obtained from \code{spawn_process()}.}
85 |
86 | \item{signal}{Signal number, one of \code{names(signals)}.}
87 | }
88 | \description{
89 | Operating-System-level signals that can be sent via
90 | \link{process_send_signal} are defined in the `subprocess::signals`` list.
91 | It is a list that is generated when the package is loaded and it
92 | contains only signals supported by the current platform (Windows or
93 | Linux).
94 |
95 | All signals, both supported and not supported by the current
96 | platform, are also exported under their names. If a given signal
97 | is not supported on the current platform, then its value is set to
98 | \code{NA}.
99 |
100 | Calling \code{process_kill()} and \code{process_terminate()} invokes
101 | the appropriate OS routine (\code{waitpid()} or
102 | \code{WaitForSingleObject()}, closing the process handle, etc.) that
103 | effectively lets the operating system clean up after the child
104 | process. Calling \code{process_send_signal()} is not accompanied by
105 | such clean-up and if the child process exits it needs to be followed
106 | by a call to \code{\link[=process_wait]{process_wait()}}.
107 |
108 | \code{process_terminate()} on Linux sends the
109 | \code{SIGTERM} signal to the process pointed to by \code{handle}.
110 | On Windows it calls \code{TerminateProcess()}.
111 |
112 | \code{process_kill()} on Linux sends the \code{SIGKILL}
113 | signal to \code{handle}. On Windows it is an alias for
114 | \code{process_terminate()}.
115 |
116 | \code{process_send_signal()} sends an OS-level
117 | \code{signal} to \code{handle}. In Linux all standard signal
118 | numbers are supported. On Windows supported signals are
119 | \code{SIGTERM}, \code{CTRL_C_EVENT} and \code{CTRL_BREAK_EVENT}.
120 | Those values will be available via the \code{signals} list which
121 | is also attached in the package namespace.
122 | }
123 | \details{
124 | In Windows, signals are delivered either only to the child process or
125 | to the child process and all its descendants. This behavior is
126 | controlled by the \code{termination_mode} argument of the
127 | \code{\link[subprocess:spawn_process]{subprocess::spawn_process()}} function. Setting it to
128 | \code{TERMINATION_GROUP} results in signals being delivered to the
129 | child and its descendants.
130 | }
131 | \examples{
132 | \dontrun{
133 | # send the SIGKILL signal to bash
134 | h <- spawn_process('bash')
135 | process_signal(h, signals$SIGKILL)
136 | process_signal(h, SIGKILL)
137 |
138 | # is SIGABRT supported on the current platform?
139 | is.na(SIGABRT)
140 | }
141 |
142 | \dontrun{
143 | # Windows
144 | process_send_signal(h, SIGTERM)
145 | process_send_signal(h, CTRL_C_EVENT)
146 | process_send_signal(h, CTRL_BREAK_EVENT)
147 | }
148 |
149 | }
150 | \seealso{
151 | \code{\link[=spawn_process]{spawn_process()}}
152 | }
153 | \keyword{datasets}
154 |
--------------------------------------------------------------------------------
/man/spawn_process.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/subprocess.R
3 | \docType{data}
4 | \name{spawn_process}
5 | \alias{spawn_process}
6 | \alias{print.process_handle}
7 | \alias{is_process_handle}
8 | \alias{TERMINATION_GROUP}
9 | \alias{TERMINATION_CHILD_ONLY}
10 | \title{Start a new child process.}
11 | \format{\code{TERMINATION_GROUP} and \code{TERMINATION_CHILD_ONLY}
12 | are single \code{character} values.}
13 | \usage{
14 | spawn_process(command, arguments = character(),
15 | environment = character(), workdir = "",
16 | termination_mode = TERMINATION_GROUP)
17 |
18 | \method{print}{process_handle}(x, ...)
19 |
20 | is_process_handle(x)
21 |
22 | TERMINATION_GROUP
23 |
24 | TERMINATION_CHILD_ONLY
25 | }
26 | \arguments{
27 | \item{command}{Path to the executable.}
28 |
29 | \item{arguments}{Optional arguments for the program.}
30 |
31 | \item{environment}{Optional environment.}
32 |
33 | \item{workdir}{Optional new working directory.}
34 |
35 | \item{termination_mode}{Either \code{TERMINATION_GROUP} or
36 | \code{TERMINATION_CHILD_ONLY}.}
37 |
38 | \item{x}{Object to be printed or tested.}
39 |
40 | \item{...}{Other parameters passed to the \code{print} method.}
41 | }
42 | \value{
43 | \code{spawn_process()} returns an object of the
44 | \emph{process handle} class.
45 | }
46 | \description{
47 | In Linux, the usual combination of \code{fork()} and \code{exec()}
48 | is used to spawn a new child process. Standard streams are redirected
49 | over regular unnamed \code{pipe}s.
50 |
51 | In Windows a new process is spawned with \code{CreateProcess()} and
52 | streams are redirected over unnamed pipes obtained with
53 | \code{CreatePipe()}. However, because non-blocking (\emph{overlapped}
54 | in Windows-speak) read/write is not supported for unnamed pipes,
55 | two reader threads are created for each new child process. These
56 | threads never touch memory allocated by R and thus they will not
57 | interfere with R interpreter's memory management (garbage collection).
58 |
59 | \code{is_process_handle()} verifies that an object is a
60 | valid \emph{process handle} as returned by \code{spawn_process()}.
61 |
62 | \code{TERMINATION_GROUP}: \code{process_terminate(handle)}
63 | and \code{process_kill(handle)} deliver the signal to the child
64 | process pointed to by \code{handle} and all of its descendants.
65 |
66 | \code{TERMINATION_CHILD_ONLY}:
67 | \code{process_terminate(handle)} and \code{process_kill(handle)}
68 | deliver the signal only to the child process pointed to by
69 | \code{handle} but to none of its descendants.
70 | }
71 | \details{
72 | \code{command} is always prepended to \code{arguments} so that the
73 | child process can correcty recognize the name of its executable
74 | via its \code{argv} vector. This is done automatically by
75 | \code{spawn_process}.
76 |
77 | \code{environment} can be passed as a \code{character} vector whose
78 | elements take the form \code{"NAME=VALUE"}, a named \code{character}
79 | vector or a named \code{list}.
80 |
81 | \code{workdir} is the path to the directory where the new process is
82 | ought to be started. \code{NULL} and \code{""} mean that working
83 | directory is inherited from the parent.
84 | }
85 | \section{Termination}{
86 |
87 |
88 | The \code{termination_mode} specifies what should happen when
89 | \code{process_terminate()} or \code{process_kill()} is called on a
90 | subprocess. If it is set to \code{TERMINATION_GROUP}, then the
91 | termination signal is sent to the parent and all its descendants
92 | (sub-processes). If termination mode is set to
93 | \code{TERMINATION_CHILD_ONLY}, only the child process spawned
94 | directly from the R session receives the signal.
95 |
96 | In Windows this is implemented with the job API, namely
97 | \code{CreateJobObject()}, \code{AssignProcessToJobObject()} and
98 | \code{TerminateJobObject()}. In Linux, the child calls \code{setsid()}
99 | after \code{fork()} but before \code{execve()}, and \code{kill()} is
100 | called with the negate process id.
101 | }
102 |
103 | \keyword{datasets}
104 |
--------------------------------------------------------------------------------
/man/subprocess.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/package.R
3 | \docType{package}
4 | \name{subprocess}
5 | \alias{subprocess}
6 | \alias{subprocess-package}
7 | \title{Manage Subprocesses in R}
8 | \description{
9 | Cross-platform child process management modelled after Python's
10 | \code{subprocess} module.
11 | }
12 | \details{
13 | This R package extends R's capabilities of starting and
14 | handling child processes. It brings the capability of alternating
15 | read from and write to a child process, communicating via signals,
16 | terminating it and handling its exit status (return code).
17 |
18 | With R's standard \link[base:system]{base::system} and \link[base:system2]{base::system2}
19 | functions one can start a new process and capture its output but
20 | cannot directly write to its standard input. Another tool, the
21 | \link[parallel:mclapply]{parallel::mclapply} function, is aimed at replicating
22 | the current session and is limited to operating systems that come
23 | with the \code{fork()} system call.
24 | }
25 | \references{
26 | http://github.com/lbartnik/subprocess
27 |
28 | http://docs.python.org/3/library/subprocess.html
29 | }
30 |
--------------------------------------------------------------------------------
/man/terminating.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/subprocess.R
3 | \docType{data}
4 | \name{terminating}
5 | \alias{terminating}
6 | \alias{process_wait}
7 | \alias{process_state}
8 | \alias{process_return_code}
9 | \alias{TIMEOUT_INFINITE}
10 | \alias{TIMEOUT_IMMEDIATE}
11 | \title{Terminating a Child Process.}
12 | \format{An object of class \code{integer} of length 1.}
13 | \usage{
14 | process_wait(handle, timeout = TIMEOUT_INFINITE)
15 |
16 | process_state(handle)
17 |
18 | process_return_code(handle)
19 |
20 | TIMEOUT_INFINITE
21 |
22 | TIMEOUT_IMMEDIATE
23 | }
24 | \arguments{
25 | \item{handle}{Process handle obtained from \code{spawn_process}.}
26 |
27 | \item{timeout}{Optional timeout in milliseconds.}
28 | }
29 | \value{
30 | \code{process_wait()} returns an \code{integer} exit code
31 | of the child process or \code{NA} if the child process has not exited
32 | yet. The same value can be accessed by \code{process_return_code()}.
33 | }
34 | \description{
35 | These functions give access to the state of the child process and to
36 | its exit status (return code).
37 |
38 | The \code{timeout} parameter can take one of three values:
39 | \itemize{
40 | \item \code{0} which means no timeout
41 | \item \code{-1} which means "wait until there is data to read"
42 | \item a positive integer, which is the actual timeout in milliseconds
43 | }
44 |
45 | \code{TIMEOUT_INFINITE} denotes an "infinite" timeout
46 | (that is, wait until response is available) when waiting for an
47 | operation to complete.
48 |
49 | \code{TIMEOUT_IMMEDIATE} denotes an "immediate" timeout
50 | (in other words, no timeout) when waiting for an operation to
51 | complete.
52 | }
53 | \details{
54 | \code{process_wait()} checks the state of the child process
55 | by invoking the system call \code{waitpid()} or
56 | \code{WaitForSingleObject()}.
57 |
58 | \code{process_state()} refreshes the handle by calling
59 | \code{process_wait()} with no timeout and returns one of these
60 | values: \code{"not-started"}. \code{"running"}, \code{"exited"},
61 | \code{"terminated"}.
62 |
63 | \code{process_return_code()} gives access to the value
64 | returned also by \code{process_wait()}. It does not invoke
65 | \code{process_wait()} behind the scenes.
66 | }
67 | \seealso{
68 | \code{\link[=spawn_process]{spawn_process()}}, \code{\link[=process_read]{process_read()}}
69 | \code{\link[=signals]{signals()}}
70 | }
71 | \keyword{datasets}
72 |
--------------------------------------------------------------------------------
/man/tests.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/tests.R
3 | \name{C_tests_utf8}
4 | \alias{C_tests_utf8}
5 | \title{Run UTF8 tests implemented in C.}
6 | \usage{
7 | C_tests_utf8()
8 | }
9 | \value{
10 | A string \code{"All C tests passed!"} if there are no errors.
11 | }
12 | \description{
13 | If there is no error in those tests a simple string message
14 | is returned. If there is at least one error, another message
15 | is returned.
16 | }
17 |
--------------------------------------------------------------------------------
/src/Makevars:
--------------------------------------------------------------------------------
1 | OBJECTS=rapi.o subprocess.o sub-linux.o tests.o registration.o
2 |
--------------------------------------------------------------------------------
/src/Makevars.win:
--------------------------------------------------------------------------------
1 | OBJECTS=rapi.o subprocess.o sub-windows.o tests.o registration.o
2 |
--------------------------------------------------------------------------------
/src/config-os.h:
--------------------------------------------------------------------------------
1 | /*
2 | * Puts together Windows-related includes and (re)defines.
3 | */
4 |
5 | #ifndef CONFIG_WIN_H_GUARD
6 | #define CONFIG_WIN_H_GUARD
7 |
8 |
9 | #if defined WIN64 || defined WIN32 || defined _MSC_VER || __MINGW32__
10 | #define SUBPROCESS_WINDOWS
11 | #elif defined(__MACH__)
12 | #define SUBPROCESS_MACOS
13 | #else
14 | #define SUBPROCESS_LINUX
15 | #endif
16 |
17 |
18 | #ifdef SUBPROCESS_WINDOWS
19 | #define EXPORT __declspec( dllexport )
20 | #else
21 | #define EXPORT
22 | #endif
23 |
24 |
25 | /* When included in rapi.h, OS API causes compilation errors. */
26 | #ifndef NO_SYSTEM_API
27 |
28 | #ifdef SUBPROCESS_WINDOWS
29 |
30 | /* MinGW defines this by default */
31 | #ifdef _WIN32_WINNT
32 | #undef _WIN32_WINNT
33 | #endif
34 |
35 | /* enables thread synchronization API */
36 | #define _WIN32_WINNT 0x0601
37 |
38 | #include
39 | #undef ERROR // R.h already defines this
40 |
41 | #ifndef _In_
42 | #define _In_
43 | #endif
44 |
45 | #ifndef _In_opt_
46 | #define _In_opt_
47 | #endif
48 |
49 | #ifndef _Out_
50 | #define _Out_
51 | #endif
52 |
53 | #undef min
54 | #undef max
55 | #undef length
56 |
57 | typedef HANDLE process_handle_type;
58 | typedef DWORD pid_type;
59 | typedef HANDLE pipe_handle_type;
60 |
61 | constexpr pipe_handle_type HANDLE_CLOSED = nullptr;
62 |
63 | #else /* !SUBPROCESS_WINDOWS */
64 |
65 | #include
66 | typedef pid_t process_handle_type;
67 | typedef pid_t pid_type;
68 | typedef int pipe_handle_type;
69 |
70 | constexpr pipe_handle_type HANDLE_CLOSED = -1;
71 |
72 | #endif /* SUBPROCESS_WINDOWS */
73 |
74 | #endif /* NO_SYSTEM_API */
75 |
76 | #endif /* CONFIG_WIN_H_GUARD */
77 |
--------------------------------------------------------------------------------
/src/rapi.cc:
--------------------------------------------------------------------------------
1 | /** @file rapi.cc
2 | *
3 | * Implementation of functions exported to R from the shared library.
4 | *
5 | * @author Lukasz A. Bartnik
6 | */
7 |
8 | #include "rapi.h"
9 | #include "subprocess.h"
10 |
11 | #include
12 | #include
13 | #include
14 |
15 | #include
16 |
17 |
18 | /* Windows defined TRUE but it's an enum in R */
19 | #ifdef TRUE
20 | #undef TRUE
21 | #endif
22 |
23 | #include
24 | #include
25 |
26 |
27 | using namespace subprocess;
28 |
29 | /* --- library ------------------------------------------------------ */
30 |
31 | /* defined at the end of this file */
32 | static int is_nonempty_string(SEXP _obj);
33 | static int is_single_string_or_NULL(SEXP _obj);
34 | static int is_single_integer(SEXP _obj);
35 |
36 | static void C_child_process_finalizer(SEXP ptr);
37 |
38 | static char ** to_C_array (SEXP _array);
39 |
40 | static void free_C_array (char ** _array);
41 |
42 | static SEXP allocate_single_bool (bool _value);
43 |
44 | static SEXP allocate_TRUE () { return allocate_single_bool(true); }
45 | //static SEXP allocate_FALSE () { return allocate_single_bool(false); }
46 |
47 |
48 | /* --- error handling ----------------------------------------------- */
49 |
50 | /*
51 | * This is how we make sure there are no non-trivial destructors
52 | * (including the exception object itself )that need to be called
53 | * before longjmp() that Rf_error() calls.
54 | */
55 | template
56 | inline typename std::result_of::type
57 | try_run (F _f, Args ... _args)
58 | {
59 | char try_buffer[BUFFER_SIZE] = { 0 };
60 | bool exception_caught = false;
61 | auto bound = std::bind(_f, _args...);
62 |
63 | try {
64 | return bound();
65 | }
66 | catch (subprocess_exception & e) {
67 | exception_caught = true;
68 | e.store(try_buffer, sizeof(try_buffer) - 1);
69 | }
70 |
71 | // if exception has been caught here we can do a long jump
72 | if (exception_caught) {
73 | Rf_error("%s", try_buffer);
74 | }
75 |
76 | // it will never reach this line but the compiler doesn't know this
77 | return bound();
78 | }
79 |
80 |
81 | /* --- public R API ------------------------------------------------- */
82 |
83 | static process_handle_t * extract_process_handle (SEXP _handle)
84 | {
85 | SEXP ptr = getAttrib(_handle, install("handle_ptr"));
86 | if (ptr == R_NilValue) {
87 | Rf_error("`handle_ptr` attribute not found");
88 | }
89 |
90 | void * c_ptr = R_ExternalPtrAddr(ptr);
91 | if (!c_ptr) {
92 | Rf_error("external C pointer is NULL");
93 | }
94 |
95 | return (process_handle_t*)c_ptr;
96 | }
97 |
98 |
99 | SEXP C_process_spawn (SEXP _command, SEXP _arguments, SEXP _environment, SEXP _workdir, SEXP _termination_mode)
100 | {
101 | /* basic argument sanity checks */
102 | if (!is_nonempty_string(_command)) {
103 | Rf_error("`command` must be a non-empty string");
104 | }
105 | if (!isString(_arguments)) {
106 | Rf_error("invalid value for `arguments`");
107 | }
108 | if (!isString(_environment)) {
109 | Rf_error("invalid value for `environment`");
110 | }
111 | if (!is_single_string_or_NULL(_workdir)) {
112 | Rf_error("`workdir` must be a non-empty string");
113 | }
114 | if (!is_nonempty_string(_termination_mode)) {
115 | Rf_error("`termination_mode` must be a non-emptry string");
116 | }
117 |
118 | /* translate into C */
119 | const char * command = translateChar(STRING_ELT(_command, 0));
120 |
121 | char ** arguments = to_C_array(_arguments);
122 | char ** environment = to_C_array(_environment);
123 |
124 | /* if environment if empty, simply ignore it */
125 | if (!environment || !*environment) {
126 | // allocated with Calloc() but Free() is still needed
127 | Free(environment);
128 | environment = NULL;
129 | }
130 |
131 | /* if workdir is NULL or an empty string, inherit from parent */
132 | const char * workdir = NULL;
133 | if (_workdir != R_NilValue) {
134 | workdir = translateChar(STRING_ELT(_workdir, 0));
135 | if (strlen(workdir) == 0) {
136 | workdir = NULL;
137 | }
138 | }
139 |
140 | /* see if termination mode is set properly */
141 | const char * termination_mode_str = translateChar(STRING_ELT(_termination_mode, 0));
142 | process_handle_t::termination_mode_type termination_mode = process_handle_t::TERMINATION_GROUP;
143 | if (!strncmp(termination_mode_str, "child_only", 10)) {
144 | termination_mode = process_handle_t::TERMINATION_CHILD_ONLY;
145 | }
146 | else if (strncmp(termination_mode_str, "group", 5)) {
147 | Rf_error("unknown value for `termination_mode`");
148 | }
149 |
150 | /* Calloc() handles memory allocation errors internally */
151 | process_handle_t * handle = (process_handle_t*)Calloc(1, process_handle_t);
152 | handle = new (handle) process_handle_t();
153 |
154 | /* spawn the process */
155 | try_run(&process_handle_t::spawn, handle, command, arguments, environment, workdir, termination_mode);
156 |
157 | /* return an external pointer handle */
158 | SEXP ptr;
159 | PROTECT(ptr = R_MakeExternalPtr(handle, install("process_handle"), R_NilValue));
160 | R_RegisterCFinalizerEx(ptr, C_child_process_finalizer, TRUE);
161 |
162 | /* return the child process PID */
163 | SEXP ans;
164 | ans = PROTECT(allocVector(INTSXP, 1));
165 | INTEGER(ans)[0] = handle->child_id;
166 | setAttrib(ans, install("handle_ptr"), ptr);
167 |
168 | /* free temporary memory */
169 | free_C_array(arguments);
170 | free_C_array(environment);
171 |
172 | /* ptr, ans */
173 | UNPROTECT(2);
174 | return ans;
175 | }
176 |
177 |
178 | static void C_child_process_finalizer(SEXP ptr)
179 | {
180 | process_handle_t * handle = (process_handle_t*)R_ExternalPtrAddr(ptr);
181 | if (!handle) return;
182 |
183 | // it might be necessary to terminate the process first
184 | auto try_terminate = [&handle] {
185 | try {
186 | // refresh the handle and try terminating if the child
187 | // is still running
188 | handle->wait(TIMEOUT_IMMEDIATE);
189 | handle->terminate();
190 | }
191 | catch (subprocess_exception) {
192 | handle->~process_handle_t();
193 | Free(handle);
194 | throw;
195 | }
196 | };
197 |
198 | // however termination goes, close pipe handles and free memory
199 | try_run(try_terminate);
200 | handle->~process_handle_t();
201 | Free(handle);
202 |
203 | R_ClearExternalPtr(ptr); /* not really needed */
204 | }
205 |
206 |
207 |
208 | SEXP C_process_read (SEXP _handle, SEXP _pipe, SEXP _timeout)
209 | {
210 | process_handle_t * handle = extract_process_handle(_handle);
211 |
212 | if (!is_nonempty_string(_pipe)) {
213 | Rf_error("`pipe` must be a single character value");
214 | }
215 | if (!is_single_integer(_timeout)) {
216 | Rf_error("`timeout` must be a single integer value");
217 | }
218 |
219 | /* extract timeout */
220 | int timeout = INTEGER_DATA(_timeout)[0];
221 |
222 | /* determine which pipe */
223 | const char * pipe = translateChar(STRING_ELT(_pipe, 0));
224 | pipe_type which_pipe;
225 |
226 | if (!strncmp(pipe, "stdout", 6))
227 | which_pipe = PIPE_STDOUT;
228 | else if (!strncmp(pipe, "stderr", 6))
229 | which_pipe = PIPE_STDERR;
230 | else if (!strncmp(pipe, "both", 4))
231 | which_pipe = PIPE_BOTH;
232 | else {
233 | Rf_error("unrecognized `pipe` value");
234 | }
235 |
236 | try_run(&process_handle_t::read, handle, which_pipe, timeout);
237 |
238 | /* produce the result - a list of one or two elements */
239 | SEXP ans, nms;
240 | PROTECT(ans = allocVector(VECSXP, 2));
241 | PROTECT(nms = allocVector(STRSXP, 2));
242 |
243 | SET_VECTOR_ELT(ans, 0, ScalarString(mkChar(handle->stdout_.data())));
244 | SET_STRING_ELT(nms, 0, mkChar("stdout"));
245 |
246 | SET_VECTOR_ELT(ans, 1, ScalarString(mkChar(handle->stderr_.data())));
247 | SET_STRING_ELT(nms, 1, mkChar("stderr"));
248 |
249 | /* set names */
250 | setAttrib(ans, R_NamesSymbol, nms);
251 |
252 | /* ans, nms */
253 | UNPROTECT(2);
254 | return ans;
255 | }
256 |
257 |
258 | SEXP C_process_close_input (SEXP _handle)
259 | {
260 | process_handle_t * handle = extract_process_handle(_handle);
261 | try_run(&process_handle_t::close_input, handle);
262 | return allocate_TRUE();
263 | }
264 |
265 |
266 | SEXP C_process_write (SEXP _handle, SEXP _message)
267 | {
268 | process_handle_t * handle = extract_process_handle(_handle);
269 |
270 | if (!is_nonempty_string(_message)) {
271 | Rf_error("`message` must be a single character value");
272 | }
273 |
274 | const char * message = translateChar(STRING_ELT(_message, 0));
275 | size_t ret = try_run(&process_handle_t::write, handle, message, strlen(message));
276 |
277 | return allocate_single_int((int)ret);
278 | }
279 |
280 |
281 | SEXP C_process_wait (SEXP _handle, SEXP _timeout)
282 | {
283 | /* extract timeout */
284 | if (!is_single_integer(_timeout)) {
285 | Rf_error("`timeout` must be a single integer value");
286 | }
287 |
288 | int timeout = INTEGER_DATA(_timeout)[0];
289 |
290 | /* extract handle */
291 | process_handle_t * handle = extract_process_handle(_handle);
292 |
293 | /* check the process */
294 | try_run(&process_handle_t::wait, handle, timeout);
295 |
296 | return C_process_return_code(_handle);
297 | }
298 |
299 |
300 | SEXP C_process_return_code (SEXP _handle)
301 | {
302 | /* extract handle */
303 | process_handle_t * handle = extract_process_handle(_handle);
304 |
305 | if (handle->state == process_handle_t::EXITED ||
306 | handle->state == process_handle_t::TERMINATED)
307 | return allocate_single_int(handle->return_code);
308 | else
309 | return allocate_single_int(NA_INTEGER);
310 | }
311 |
312 |
313 | SEXP C_process_state (SEXP _handle)
314 | {
315 | process_handle_t * handle = extract_process_handle(_handle);
316 |
317 | /* refresh the handle */
318 | try_run(&process_handle_t::wait, handle, TIMEOUT_IMMEDIATE);
319 |
320 | /* answer */
321 | SEXP ans;
322 | PROTECT(ans = allocVector(STRSXP, 1));
323 |
324 | if (handle->state == process_handle_t::EXITED) {
325 | SET_STRING_ELT(ans, 0, mkChar("exited"));
326 | }
327 | else if (handle->state == process_handle_t::TERMINATED) {
328 | SET_STRING_ELT(ans, 0, mkChar("terminated"));
329 | }
330 | else if (handle->state == process_handle_t::RUNNING) {
331 | SET_STRING_ELT(ans, 0, mkChar("running"));
332 | }
333 | else {
334 | SET_STRING_ELT(ans, 0, mkChar("not-started"));
335 | }
336 |
337 | /* ans */
338 | UNPROTECT(1);
339 | return ans;
340 | }
341 |
342 |
343 | SEXP C_process_terminate (SEXP _handle)
344 | {
345 | process_handle_t * handle = extract_process_handle(_handle);
346 | try_run(&process_handle_t::terminate, handle);
347 | return allocate_TRUE();
348 | }
349 |
350 |
351 | SEXP C_process_kill (SEXP _handle)
352 | {
353 | process_handle_t * handle = extract_process_handle(_handle);
354 | try_run(&process_handle_t::kill, handle);
355 | return allocate_TRUE();
356 | }
357 |
358 |
359 | SEXP C_process_send_signal (SEXP _handle, SEXP _signal)
360 | {
361 | process_handle_t * handle = extract_process_handle(_handle);
362 | if (!is_single_integer(_signal)) {
363 | Rf_error("`signal` must be a single integer value");
364 | }
365 |
366 | int signal = INTEGER_DATA(_signal)[0];
367 | try_run(&process_handle_t::send_signal, handle, signal);
368 |
369 | return allocate_TRUE();
370 | }
371 |
372 |
373 | SEXP C_process_exists (SEXP _pid)
374 | {
375 | if (!is_single_integer(_pid)) {
376 | Rf_error("`pid` must be a single integer value");
377 | }
378 |
379 | int pid = INTEGER_DATA(_pid)[0];
380 | bool ret = subprocess::process_exists(static_cast(pid));
381 |
382 | return allocate_single_bool(ret);
383 | }
384 |
385 |
386 | SEXP C_known_signals ()
387 | {
388 | SEXP ans;
389 | SEXP ansnames;
390 |
391 | #define ADD_SIGNAL(i, name) do { \
392 | INTEGER_DATA(ans)[i] = name; \
393 | SET_STRING_ELT(ansnames, i, mkChar(#name)); \
394 | } while (0); \
395 |
396 |
397 | #ifdef SUBPROCESS_WINDOWS
398 | PROTECT(ans = allocVector(INTSXP, 3));
399 | PROTECT(ansnames = allocVector(STRSXP, 3));
400 |
401 | ADD_SIGNAL(0, SIGTERM);
402 | ADD_SIGNAL(1, CTRL_C_EVENT);
403 | ADD_SIGNAL(2, CTRL_BREAK_EVENT);
404 |
405 | #else /* Linux */
406 | PROTECT(ans = allocVector(INTSXP, 19));
407 | PROTECT(ansnames = allocVector(STRSXP, 19));
408 |
409 | ADD_SIGNAL(0, SIGHUP)
410 | ADD_SIGNAL(1, SIGINT)
411 | ADD_SIGNAL(2, SIGQUIT)
412 | ADD_SIGNAL(3, SIGILL)
413 | ADD_SIGNAL(4, SIGABRT)
414 | ADD_SIGNAL(5, SIGFPE)
415 | ADD_SIGNAL(6, SIGKILL)
416 | ADD_SIGNAL(7, SIGSEGV)
417 | ADD_SIGNAL(8, SIGPIPE)
418 | ADD_SIGNAL(9, SIGALRM)
419 | ADD_SIGNAL(10, SIGTERM)
420 | ADD_SIGNAL(11, SIGUSR1)
421 | ADD_SIGNAL(12, SIGUSR2)
422 | ADD_SIGNAL(13, SIGCHLD)
423 | ADD_SIGNAL(14, SIGCONT)
424 | ADD_SIGNAL(15, SIGSTOP)
425 | ADD_SIGNAL(16, SIGTSTP)
426 | ADD_SIGNAL(17, SIGTTIN)
427 | ADD_SIGNAL(18, SIGTTOU)
428 | #endif
429 |
430 | setAttrib(ans, R_NamesSymbol, ansnames);
431 |
432 | /* ans, ansnames */
433 | UNPROTECT(2);
434 | return ans;
435 | }
436 |
437 |
438 | /* --- hidden calls ------------------------------------------------- */
439 |
440 |
441 | /* this is an access interface to system call signal(); it is used
442 | * in the introduction vignette */
443 | SEXP C_signal (SEXP _signal, SEXP _handler)
444 | {
445 | if (!is_single_integer(_signal)) {
446 | error("`signal` needs to be an integer");
447 | }
448 | if (!is_nonempty_string(_handler)) {
449 | error("`handler` needs to be a single character value");
450 | }
451 |
452 | const char * handler = translateChar(STRING_ELT(_handler, 0));
453 | if (!strncmp(handler, "ignore", 6) && !strncmp(handler, "default", 7)) {
454 | error("`handler` can be either \"ignore\" or \"default\"");
455 | }
456 |
457 | int sgn = INTEGER_DATA(_signal)[0];
458 | typedef void (*sighandler_t)(int);
459 | sighandler_t hnd = (strncmp(handler, "ignore", 6) ? SIG_DFL : SIG_IGN);
460 |
461 | if (signal(sgn, hnd) == SIG_ERR) {
462 | Rf_error("error while calling signal()");
463 | }
464 |
465 | return allocate_TRUE();
466 | }
467 |
468 |
469 | /* --- library functions -------------------------------------------- */
470 |
471 | static int is_single_string (SEXP _obj)
472 | {
473 | return isString(_obj) && (LENGTH(_obj) == 1);
474 | }
475 |
476 | static int is_nonempty_string (SEXP _obj)
477 | {
478 | return is_single_string(_obj) && (strlen(translateChar(STRING_ELT(_obj, 0))) > 0);
479 | }
480 |
481 | static int is_single_string_or_NULL (SEXP _obj)
482 | {
483 | return is_single_string(_obj) || (_obj == R_NilValue);
484 | }
485 |
486 | static int is_single_integer (SEXP _obj)
487 | {
488 | return isInteger(_obj) && (LENGTH(_obj) == 1);
489 | }
490 |
491 |
492 | static char ** to_C_array (SEXP _array)
493 | {
494 | char ** ret = (char**)Calloc(LENGTH(_array) + 1, char **);
495 | for (int i=0; i
5 | */
6 |
7 | #ifndef RAPI_H_GUARD
8 | #define RAPI_H_GUARD
9 |
10 | #include
11 | #include "config-os.h"
12 |
13 |
14 | #ifdef __cplusplus
15 | extern "C" {
16 | #endif
17 |
18 |
19 | EXPORT SEXP C_process_spawn(SEXP _command, SEXP _arguments, SEXP _environment, SEXP _workdir, SEXP _termination_mode);
20 |
21 | EXPORT SEXP C_process_read(SEXP _handle, SEXP _pipe, SEXP _timeout);
22 |
23 | EXPORT SEXP C_process_close_input (SEXP _handle);
24 |
25 | EXPORT SEXP C_process_write(SEXP _handle, SEXP _message);
26 |
27 | EXPORT SEXP C_process_wait(SEXP _handle, SEXP _timeout);
28 |
29 | EXPORT SEXP C_process_return_code(SEXP _handle);
30 |
31 | EXPORT SEXP C_process_state(SEXP _handle);
32 |
33 | EXPORT SEXP C_process_terminate(SEXP _handle);
34 |
35 | EXPORT SEXP C_process_kill(SEXP _handle);
36 |
37 | EXPORT SEXP C_process_send_signal(SEXP _handle, SEXP _signal);
38 |
39 | EXPORT SEXP C_process_exists(SEXP _pid);
40 |
41 | EXPORT SEXP C_known_signals();
42 |
43 | EXPORT SEXP C_signal (SEXP _signal, SEXP _handler);
44 |
45 |
46 | SEXP allocate_single_int (int _value);
47 |
48 |
49 | #ifdef __cplusplus
50 | } /* extern "C" */
51 | #endif
52 |
53 |
54 | #endif /* RAPI_H_GUARD */
55 |
--------------------------------------------------------------------------------
/src/registration.cpp:
--------------------------------------------------------------------------------
1 | // RegisteringDynamic Symbols
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #define NO_SYSTEM_API
8 | #include "config-os.h"
9 | #include "rapi.h"
10 |
11 |
12 | static const R_CallMethodDef callMethods[] = {
13 | { "C_process_spawn", (DL_FUNC) &C_process_spawn, 5 },
14 | { "C_process_read", (DL_FUNC) &C_process_read, 3 },
15 | { "C_process_close_input", (DL_FUNC) &C_process_close_input, 1 },
16 | { "C_process_write", (DL_FUNC) &C_process_write, 2 },
17 | { "C_process_wait", (DL_FUNC) &C_process_wait, 2 },
18 | { "C_process_return_code", (DL_FUNC) &C_process_return_code, 1 },
19 | { "C_process_state", (DL_FUNC) &C_process_state, 1 },
20 | { "C_process_terminate", (DL_FUNC) &C_process_terminate, 1 },
21 | { "C_process_kill", (DL_FUNC) &C_process_kill, 1 },
22 | { "C_process_send_signal", (DL_FUNC) &C_process_send_signal, 2 },
23 | { "C_process_exists", (DL_FUNC) &C_process_exists, 1 },
24 | { "C_known_signals", (DL_FUNC) &C_known_signals, 0 },
25 | { "C_signal", (DL_FUNC) &C_signal, 2 },
26 | { NULL, NULL, 0 }
27 | };
28 |
29 |
30 | void R_init_subprocess(DllInfo* info) {
31 | R_registerRoutines(info, NULL, callMethods, NULL, NULL);
32 | R_useDynamicSymbols(info, TRUE);
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/src/sub-linux.cc:
--------------------------------------------------------------------------------
1 | //#define _GNU_SOURCE /* See feature_test_macros(7) */
2 |
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 |
19 | #include /* Obtain O_* constant definitions */
20 | #include
21 |
22 | #include "config-os.h"
23 | #include "subprocess.h"
24 |
25 |
26 | #ifdef SUBPROCESS_MACOS
27 | #include
28 | #include
29 | #endif
30 |
31 | /* for some reason environ is unaccessible via unistd.h on Solaris
32 | * this is redundant in non-Solaris builds but fixes Solaris */
33 | extern char ** environ;
34 |
35 | #ifdef TRUE
36 | #undef TRUE
37 | #endif
38 |
39 | #define TRUE 1
40 |
41 | // a way to ignore a return value even when gcc warns about it
42 | template inline void ignore_return_value (T _t) {}
43 |
44 | namespace subprocess {
45 |
46 |
47 | /*
48 | * Append system error message to user error message.
49 | */
50 | string strerror (int _code, const string & _message)
51 | {
52 | std::stringstream message;
53 |
54 | vector buffer(BUFFER_SIZE, 0);
55 | if (strerror_r(_code, buffer.data(), buffer.size()-1) == 0) {
56 | message << _message << ": " << buffer.data();
57 | }
58 | else {
59 | message << _message << ": system error message could not be fetched, errno = " << _code;
60 | }
61 |
62 | return message.str();
63 | }
64 |
65 |
66 |
67 |
68 | // MacOS implementation according to
69 | // http://stackoverflow.com/questions/5167269/clock-gettime-alternative-in-mac-os-x/6725161#6725161
70 | static time_t clock_millisec ()
71 | {
72 | struct timespec current;
73 |
74 | #ifdef SUBPROCESS_MACOS // OS X does not have clock_gettime, use clock_get_time
75 | clock_serv_t cclock;
76 | mach_timespec_t mts;
77 | host_get_clock_service(mach_host_self(), CALENDAR_CLOCK, &cclock);
78 | clock_get_time(cclock, &mts);
79 | mach_port_deallocate(mach_task_self(), cclock);
80 | current.tv_sec = mts.tv_sec;
81 | current.tv_nsec = mts.tv_nsec;
82 | #else // Linux
83 | clock_gettime(CLOCK_REALTIME, ¤t);
84 | #endif
85 |
86 | long long current_time = static_cast(current.tv_sec) * 1000L +
87 | static_cast(current.tv_nsec / 1000000L);
88 | return static_cast(current_time);
89 | }
90 |
91 |
92 |
93 |
94 | // this is to hide from CRAN that we call exit()
95 | static void exit_on_failure ()
96 | {
97 | void * process_handle = dlopen(NULL, RTLD_NOW);
98 | void * exit_handle = dlsym(process_handle, "exit");
99 |
100 | // it's hard to imagine a situation where this symbol would not be
101 | // present; regardless, we cause a SEGMENTATION error because the
102 | // child needs to die;
103 | // also, we use write because CRAN will warn about fprintf(stderr)
104 | if (!exit_handle) {
105 | const char * message = "could not dlopen() the exit() function, going to SEGFAULT\n";
106 | ssize_t ret = write(STDERR_FILENO, message, strlen(message));
107 | *(int*)exit_handle = 0;
108 | ++ret; // hide compiler warning
109 | }
110 |
111 | typedef void (* exit_t)(int);
112 | exit_t exit_fun = (exit_t)exit_handle;
113 | exit_fun(EXIT_FAILURE);
114 | }
115 |
116 |
117 | /* --- wrappers for Linux system API -------------------------------- */
118 |
119 |
120 | /*
121 | * Duplicate handle and zero the original
122 | */
123 | inline void dup2 (int _from, int _to) {
124 | if (::dup2(_from, _to) < 0) {
125 | throw subprocess_exception(errno, "duplicating descriptor failed");
126 | }
127 | }
128 |
129 | inline void close (int & _fd) {
130 | if (::close(_fd) < 0) {
131 | throw subprocess_exception(errno, "could not close descriptor");
132 | }
133 | _fd = HANDLE_CLOSED;
134 | }
135 |
136 | inline void chdir (const string & _path) {
137 | if (::chdir(_path.c_str()) < 0) {
138 | throw subprocess_exception(errno, "could not change working directory to " + _path);
139 | }
140 | }
141 |
142 | inline void setsid () {
143 | if (::setsid() == (pid_t)-1) {
144 | throw subprocess_exception(errno, "could not start a new session");
145 | }
146 | }
147 |
148 | static void set_block (int _fd) {
149 | if (fcntl(_fd, F_SETFL, fcntl(_fd, F_GETFL) & (~O_NONBLOCK)) < 0) {
150 | throw subprocess_exception(errno, "could not set pipe to non-blocking mode");
151 | }
152 | }
153 |
154 | static void set_non_block (int _fd) {
155 | if (fcntl(_fd, F_SETFL, fcntl(_fd, F_GETFL) | O_NONBLOCK) < 0) {
156 | throw subprocess_exception(errno, "could not set pipe to non-blocking mode");
157 | }
158 | }
159 |
160 |
161 | /* --- process_handle ----------------------------------------------- */
162 |
163 | process_handle_t::process_handle_t ()
164 | : child_handle(0),
165 | pipe_stdin(HANDLE_CLOSED), pipe_stdout(HANDLE_CLOSED),
166 | pipe_stderr(HANDLE_CLOSED), state(NOT_STARTED)
167 | { }
168 |
169 |
170 | /* ------------------------------------------------------------------ */
171 |
172 | /**
173 | * A helper class that simplifies error handling when opening multiple
174 | * pipes.
175 | */
176 | struct pipe_holder {
177 |
178 | enum pipe_end { READ = 0, WRITE = 1 };
179 |
180 | int fds[2];
181 |
182 | int & operator [] (pipe_end _i) { return fds[_i]; }
183 |
184 | /**
185 | * Zero the descriptor array and immediately try opening a (unnamed)
186 | * pipe().
187 | */
188 | pipe_holder () : fds{HANDLE_CLOSED, HANDLE_CLOSED} {
189 | if (pipe(fds) < 0) {
190 | throw subprocess_exception(errno, "could not create a pipe");
191 | }
192 | }
193 |
194 | /**
195 | * Will close both descriptors unless they're set to 0 from the outside.
196 | */
197 | ~pipe_holder () {
198 | if (fds[READ] != HANDLE_CLOSED) close(fds[READ]);
199 | if (fds[WRITE] != HANDLE_CLOSED) close(fds[WRITE]);
200 | }
201 | };
202 |
203 |
204 | /**
205 | * In most cases, when a negative value is returned the calling function
206 | * can consult the value of errno.
207 | *
208 | * @return 0 on success and negative on an error.
209 | */
210 | void process_handle_t::spawn (const char * _command, char *const _arguments[],
211 | char *const _environment[], const char * _workdir,
212 | termination_mode_type _termination_mode)
213 | {
214 | if (state != NOT_STARTED) {
215 | throw subprocess_exception(EALREADY, "process already started");
216 | }
217 |
218 | int rc = 0;
219 | // can be addressed with PIPE_STDIN, PIPE_STDOUT, PIPE_STDERR
220 | pipe_holder pipes[3];
221 |
222 | /* spawn a child */
223 | if ( (child_id = fork()) < 0) {
224 | throw subprocess_exception(errno, "could not spawn a process");
225 | }
226 |
227 | /* child should copy his ends of pipes and close his and parent's
228 | * ends of pipes */
229 | if (child_id == 0) {
230 | try {
231 | dup2(pipes[PIPE_STDIN][pipe_holder::READ], STDIN_FILENO);
232 | dup2(pipes[PIPE_STDOUT][pipe_holder::WRITE], STDOUT_FILENO);
233 | dup2(pipes[PIPE_STDERR][pipe_holder::WRITE], STDERR_FILENO);
234 |
235 | close(pipes[PIPE_STDIN][pipe_holder::READ]);
236 | close(pipes[PIPE_STDIN][pipe_holder::WRITE]);
237 | close(pipes[PIPE_STDOUT][pipe_holder::READ]);
238 | close(pipes[PIPE_STDOUT][pipe_holder::WRITE]);
239 | close(pipes[PIPE_STDERR][pipe_holder::READ]);
240 | close(pipes[PIPE_STDERR][pipe_holder::WRITE]);
241 |
242 | /* change directory */
243 | if (_workdir != NULL) {
244 | chdir(_workdir);
245 | }
246 |
247 | /* if termination mode is "group" start new session */
248 | if (_termination_mode == TERMINATION_GROUP) {
249 | setsid();
250 | }
251 |
252 | /* if environment is empty, use parent's environment */
253 | if (!_environment) {
254 | _environment = environ;
255 | }
256 |
257 | /* finally start the new process */
258 | execve(_command, _arguments, _environment);
259 |
260 | // TODO if we dup() STDERR_FILENO, we can print this message there
261 | // rather then into the pipe
262 | perror((string("could not run command ") + _command).c_str());
263 | }
264 | catch (subprocess_exception & e) {
265 | // we do not name stderr explicitly because CRAN doesn't like it
266 | ignore_return_value(::write(2, e.what(), strlen(e.what())));
267 | exit_on_failure();
268 | }
269 | }
270 |
271 | // child is now running
272 | state = RUNNING;
273 | termination_mode = _termination_mode;
274 |
275 | pipe_stdin = pipes[PIPE_STDIN][pipe_holder::WRITE];
276 | pipe_stdout = pipes[PIPE_STDOUT][pipe_holder::READ];
277 | pipe_stderr = pipes[PIPE_STDERR][pipe_holder::READ];
278 |
279 | // reset the NONBLOCK on stdout-read and stderr-read descriptors
280 | set_non_block(pipe_stdout);
281 | set_non_block(pipe_stderr);
282 |
283 | // the very last step: set them to zero so that the destructor
284 | // doesn't close them
285 | pipes[PIPE_STDIN][pipe_holder::WRITE] = HANDLE_CLOSED;
286 | pipes[PIPE_STDOUT][pipe_holder::READ] = HANDLE_CLOSED;
287 | pipes[PIPE_STDERR][pipe_holder::READ] = HANDLE_CLOSED;
288 |
289 | // update process state
290 | wait(TIMEOUT_IMMEDIATE);
291 | }
292 |
293 |
294 |
295 | /* --- process_handle::shutdown ------------------------------------- */
296 |
297 | void process_handle_t::shutdown ()
298 | {
299 | if (state != RUNNING) {
300 | return;
301 | }
302 | if (!child_id) {
303 | throw subprocess_exception(ECHILD, "child does not exist");
304 | }
305 |
306 | /* all we need to do is close pipes */
307 | auto close_pipe = [](pipe_handle_type _pipe) {
308 | if (_pipe != HANDLE_CLOSED) close(_pipe);
309 | _pipe = HANDLE_CLOSED;
310 | };
311 |
312 | close_pipe(pipe_stdin);
313 | close_pipe(pipe_stdout);
314 | close_pipe(pipe_stderr);
315 |
316 | /* closing pipes should let the child process exit */
317 | // TODO there might be a need to send a termination signal first
318 | wait(TIMEOUT_IMMEDIATE);
319 | kill();
320 | wait(TIMEOUT_INFINITE);
321 |
322 | state = SHUTDOWN;
323 | }
324 |
325 |
326 | /* --- process::write ----------------------------------------------- */
327 |
328 |
329 | size_t process_handle_t::write (const void * _buffer, size_t _count)
330 | {
331 | if (!child_id) {
332 | throw subprocess_exception(ECHILD, "child does not exist");
333 | }
334 |
335 | ssize_t ret = ::write(pipe_stdin, _buffer, _count);
336 | if (ret < 0) {
337 | throw subprocess_exception(errno, "could not write to child process");
338 | }
339 |
340 | return static_cast(ret);
341 | }
342 |
343 |
344 | /* --- process::read ------------------------------------------------ */
345 |
346 |
347 | struct enable_block_mode {
348 | int fd;
349 |
350 | enable_block_mode (int _fd) : fd (_fd) {
351 | set_block(fd);
352 | }
353 |
354 | ~enable_block_mode () {
355 | set_non_block(fd);
356 | }
357 | };
358 |
359 |
360 | ssize_t timed_read (process_handle_t & _handle, pipe_type _pipe, int _timeout)
361 | {
362 | struct pollfd fds[2];
363 | fds[0].fd = -1;
364 | fds[1].fd = -1;
365 |
366 | if (_pipe & PIPE_STDOUT) {
367 | fds[0].fd = _handle.pipe_stdout;
368 | fds[0].events = POLLIN;
369 | _handle.stdout_.clear();
370 | }
371 |
372 | if (_pipe & PIPE_STDERR) {
373 | fds[1].fd = _handle.pipe_stderr;
374 | fds[1].events = POLLIN;
375 | _handle.stderr_.clear();
376 | }
377 |
378 | time_t start = clock_millisec(), timediff = _timeout;
379 | ssize_t rc;
380 |
381 | do {
382 | rc = poll(fds, 2, timediff);
383 | timediff = _timeout - (clock_millisec() - start);
384 |
385 | // interrupted or kernel failed to allocate internal resources
386 | if (rc < 0 && (errno == EINTR || errno == EAGAIN)) {
387 | rc = 0;
388 | }
389 | } while (rc == 0 && timediff > 0);
390 |
391 | // nothing to read
392 | if (rc == 0) {
393 | return 0;
394 | }
395 |
396 | // TODO if an error occurs in the first read() it will be lost
397 | if (fds[0].fd != -1 && fds[0].revents == POLLIN) {
398 | rc = std::min(rc, (ssize_t)_handle.stdout_.read(_handle.pipe_stdout, mbcslocale));
399 | }
400 | if (fds[1].fd != -1 && fds[1].revents == POLLIN) {
401 | rc = std::min(rc, (ssize_t)_handle.stderr_.read(_handle.pipe_stderr, mbcslocale));
402 | }
403 |
404 | return rc;
405 | }
406 |
407 |
408 | size_t process_handle_t::read (pipe_type _pipe, int _timeout)
409 | {
410 | if (!child_id) {
411 | throw subprocess_exception(ECHILD, "child does not exist");
412 | }
413 |
414 | ssize_t rc = timed_read(*this, _pipe, _timeout);
415 |
416 | if (rc < 0) {
417 | throw subprocess_exception(errno, "could not read from child process");
418 | }
419 |
420 | return static_cast(rc);
421 | }
422 |
423 |
424 | /* --- process::close_input ----------------------------------------- */
425 |
426 | void process_handle_t::close_input ()
427 | {
428 | if (pipe_stdin == HANDLE_CLOSED) {
429 | throw subprocess_exception(EALREADY, "child's standard input already closed");
430 | }
431 |
432 | close(pipe_stdin);
433 | pipe_stdin = HANDLE_CLOSED;
434 | }
435 |
436 |
437 | /* --- process::wait ------------------------------------------------ */
438 |
439 |
440 | void process_handle_t::wait (int _timeout)
441 | {
442 | if (!child_id) {
443 | throw subprocess_exception(ECHILD, "child does not exist");
444 | }
445 | if (state != RUNNING) {
446 | return;
447 | }
448 |
449 | /* to wait or not to wait? */
450 | int options = 0;
451 | if (_timeout >= 0) {
452 | options = WNOHANG;
453 | }
454 |
455 | /* make the actual system call */
456 | int start = clock_millisec(), rc;
457 | do {
458 | rc = waitpid(child_id, &return_code, options);
459 |
460 | // there's been an error (<0)
461 | if (rc < 0) {
462 | throw subprocess_exception(errno, "waitpid() failed");
463 | }
464 |
465 | _timeout -= clock_millisec() - start;
466 | } while (rc == 0 && _timeout > 0);
467 |
468 | // the child is still running
469 | if (rc == 0) {
470 | return;
471 | }
472 |
473 | // the child has exited or has been terminated
474 | if (WIFEXITED(return_code)) {
475 | state = process_handle_t::EXITED;
476 | return_code = WEXITSTATUS(return_code);
477 | }
478 | else if (WIFSIGNALED(return_code)) {
479 | state = process_handle_t::TERMINATED;
480 | return_code = WTERMSIG(return_code);
481 | }
482 | else {
483 | throw subprocess_exception(0, "process did not exit nor was terminated");
484 | }
485 | }
486 |
487 |
488 | /* --- process::signal ---------------------------------------------- */
489 |
490 |
491 | void process_handle_t::send_signal(int _signal)
492 | {
493 | if (!child_id) {
494 | throw subprocess_exception(ECHILD, "child does not exist");
495 | }
496 | int rc = ::kill(child_id, _signal);
497 |
498 | if (rc < 0) {
499 | throw subprocess_exception(errno, "could not post signal to child process");
500 | }
501 | }
502 |
503 |
504 | /* --- process::terminate & process::kill --------------------------- */
505 |
506 |
507 | static void termination_signal (process_handle_t & _handle, int _signal, int _timeout)
508 | {
509 | if (_handle.state != process_handle_t::RUNNING)
510 | return;
511 |
512 | pid_t addressee = (_handle.termination_mode == process_handle_t::TERMINATION_CHILD_ONLY) ?
513 | (_handle.child_id) : (-_handle.child_id);
514 | if (::kill(addressee, _signal) < 0) {
515 | throw subprocess_exception(errno, "system kill() failed");
516 | }
517 |
518 | _handle.wait(_timeout);
519 | }
520 |
521 |
522 | void process_handle_t::terminate ()
523 | {
524 | termination_signal(*this, SIGTERM, 100);
525 | }
526 |
527 |
528 | void process_handle_t::kill()
529 | {
530 | // this will terminate the child for sure so we can
531 | // wait until it happens
532 | termination_signal(*this, SIGKILL, TIMEOUT_INFINITE);
533 | }
534 |
535 | /* --- process_exists ----------------------------------------------- */
536 |
537 | bool process_exists (const pid_type & _pid)
538 | {
539 | return ::kill(_pid, 0) == 0;
540 | }
541 |
542 | } /* namespace subprocess */
543 |
544 |
545 | /* --- test --------------------------------------------------------- */
546 |
547 |
548 |
549 | #ifdef LINUX_TEST
550 |
551 | using namespace subprocess;
552 |
553 | int main (int argc, char ** argv)
554 | {
555 | process_handle_t handle;
556 | char * const args[] = { NULL };
557 | char * const env[] = { NULL };
558 |
559 | handle.spawn("/bin/bash", args, env, NULL, process_handle_t::TERMINATION_GROUP);
560 |
561 | process_write(&handle, "echo A\n", 7);
562 |
563 | /* read is non-blocking so the child needs time to produce output */
564 | sleep(1);
565 | process_read(handle, PIPE_STDOUT, TIMEOUT_INFINITE);
566 |
567 | printf("stdout: %s\n", handle.stdout_.data());
568 |
569 | handle.shutdown();
570 |
571 | return 0;
572 | }
573 | #endif /* LINUX_TEST */
574 |
575 |
--------------------------------------------------------------------------------
/src/sub-windows.cc:
--------------------------------------------------------------------------------
1 | #include "subprocess.h"
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | #include
8 |
9 |
10 | /*
11 | * There are probably many places that need to be adapted to make this
12 | * package Unicode-ready. One place that for sure needs a change is
13 | * the prepare_environment() function. It currently assumes a double
14 | * NULL environment block delimiter; if UNICODE is enabled, it will
15 | * be a four-NULL block.
16 | *
17 | * See this page for more details:
18 | * https://msdn.microsoft.com/en-us/library/windows/desktop/ms682425(v=vs.85).aspx
19 | */
20 | #ifdef UNICODE
21 | #error "This package is not ready for UNICODE"
22 | #endif
23 |
24 |
25 | namespace subprocess {
26 |
27 | static char * strjoin (char *const* _array, char _sep);
28 |
29 | char * prepare_environment(char *const* _environment);
30 |
31 |
32 |
33 | string strerror (int _code, const string & _message)
34 | {
35 | vector buffer(BUFFER_SIZE, '\0');
36 | DWORD ret = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, _code,
37 | MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
38 | buffer.data(), (DWORD)buffer.size() - 1, NULL);
39 |
40 | std::stringstream message;
41 | message << _message << ": "
42 | << ((ret > 0) ? buffer.data() : "system error message could not be fetched");
43 |
44 | return message.str().substr(0, message.str().find_last_not_of("\r\n\t"));
45 | }
46 |
47 |
48 |
49 | /* --- wrappers for system API -------------------------------------- */
50 |
51 |
52 | static void DuplicateHandle(HANDLE src, HANDLE * dst)
53 | {
54 | if (::DuplicateHandle(GetCurrentProcess(), src,
55 | GetCurrentProcess(),
56 | dst, // Address of new handle.
57 | 0, FALSE, // Make it uninheritable.
58 | DUPLICATE_SAME_ACCESS)
59 | != TRUE)
60 | {
61 | throw subprocess_exception(GetLastError(), "cannot duplicate handle");
62 | }
63 | }
64 |
65 |
66 | static void CloseHandle (HANDLE & _handle)
67 | {
68 | if (_handle == HANDLE_CLOSED) return;
69 |
70 | auto rc = ::CloseHandle(_handle);
71 | _handle = HANDLE_CLOSED;
72 |
73 | if (rc == FALSE) {
74 | throw subprocess_exception(::GetLastError(), "could not close handle");
75 | }
76 | }
77 |
78 |
79 | /* ------------------------------------------------------------------ */
80 |
81 | process_handle_t::process_handle_t ()
82 | : process_job(nullptr), child_handle(nullptr),
83 | pipe_stdin(HANDLE_CLOSED), pipe_stdout(HANDLE_CLOSED), pipe_stderr(HANDLE_CLOSED),
84 | child_id(0), state(NOT_STARTED), return_code(0),
85 | termination_mode(TERMINATION_GROUP)
86 | {}
87 |
88 |
89 | /* ------------------------------------------------------------------ */
90 |
91 | struct Handle {
92 | HANDLE handle;
93 |
94 | Handle(HANDLE _handle = nullptr) : handle(_handle) { }
95 | ~Handle() { if (handle) CloseHandle(handle); }
96 |
97 | HANDLE & operator= (const HANDLE & _new) { handle = _new; return handle; }
98 | operator HANDLE () const { return handle; }
99 | PHANDLE address() { return &handle; }
100 |
101 | HANDLE move () { auto tmp = handle; handle = nullptr; return tmp; }
102 | };
103 |
104 |
105 | /*
106 | * Wrap redirections in a class to achieve RAII.
107 | */
108 | struct StartupInfo {
109 |
110 | struct pipe_holder {
111 | Handle read, write;
112 |
113 | pipe_holder(SECURITY_ATTRIBUTES & sa)
114 | : read(nullptr), write(nullptr)
115 | {
116 | if (!::CreatePipe(read.address(), write.address(), &sa, 0)) {
117 | throw subprocess_exception(GetLastError(), "could not create pipe");
118 | }
119 | }
120 | };
121 |
122 | StartupInfo (process_handle_t & _process, DWORD & _creation_flags) {
123 | memset(&info, 0, sizeof(STARTUPINFO));
124 | info.cb = sizeof(STARTUPINFO);
125 |
126 | pipe_redirection(_process);
127 | hidden_window(_creation_flags);
128 | }
129 |
130 | ~StartupInfo () {
131 | CloseHandle(info.hStdInput);
132 | CloseHandle(info.hStdOutput);
133 | CloseHandle(info.hStdError);
134 | }
135 |
136 |
137 | /*
138 | * Set standard input/output redirection via pipes.
139 | */
140 | void pipe_redirection (process_handle_t & _process)
141 | {
142 | // Set the bInheritHandle flag so pipe handles are inherited
143 | SECURITY_ATTRIBUTES sa;
144 | sa.nLength = sizeof(SECURITY_ATTRIBUTES);
145 | sa.bInheritHandle = TRUE;
146 | sa.lpSecurityDescriptor = NULL;
147 |
148 | pipe_holder in(sa), out(sa), err(sa);
149 |
150 | // Ensure the write handle to the pipe for STDIN is not inherited.
151 | if (!::SetHandleInformation(in.write, HANDLE_FLAG_INHERIT, 0)) {
152 | throw subprocess_exception(GetLastError(), "could not set handle information");
153 | }
154 |
155 | // Create new output read handle and the input write handles. Set
156 | // the Properties to FALSE. Otherwise, the child inherits the
157 | // properties and, as a result, non-closeable handles to the pipes
158 | // are created.
159 | DuplicateHandle(in.write, &_process.pipe_stdin);
160 | DuplicateHandle(out.read, &_process.pipe_stdout);
161 | DuplicateHandle(err.read, &_process.pipe_stderr);
162 |
163 | // prepare the info object
164 | info.dwFlags |= STARTF_USESTDHANDLES;
165 |
166 | // "move" means "set to null" so that destructor does not close
167 | // those handles
168 | info.hStdError = err.write.move();
169 | info.hStdOutput = out.write.move();
170 | info.hStdInput = in.read.move();
171 | }
172 |
173 | /*
174 | * Create a hidden window. This way we can still send Ctrl+C
175 | * to that process.
176 | */
177 | void hidden_window (DWORD & _creation_flags)
178 | {
179 | info.dwFlags |= STARTF_USESHOWWINDOW;
180 | info.wShowWindow = SW_HIDE;
181 |
182 | /*
183 | * Create a hidden window but do not create a new console.
184 | * This is the only combination that seems to work in Windows (in
185 | * most scenarios, but there seem to be cases when this fails; I
186 | * still don't understand how to do it propertly, and there seems
187 | * to be a great deal of confusion among people online how to do
188 | * it correctly).
189 | *
190 | * _creation_flags |= CREATE_NEW_CONSOLE;
191 | */
192 | }
193 |
194 |
195 | /*
196 | * Alternative (debug) redirection to/from files.
197 | */
198 | int file_redirection(process_handle_t * _process, STARTUPINFO * _si)
199 | {
200 | Handle input, output, error;
201 |
202 | input = CreateFile("C:/Windows/TEMP/subprocess.in", // name of the write
203 | GENERIC_READ, // open for writing
204 | 0, // do not share
205 | NULL, // default security
206 | OPEN_EXISTING, // create new file only
207 | FILE_ATTRIBUTE_NORMAL, // normal file
208 | NULL); // no attr. template
209 |
210 | if (input == INVALID_HANDLE_VALUE) {
211 | return -1;
212 | }
213 |
214 | output = CreateFile("C:/Windows/TEMP/subprocess.out", // name of the write
215 | GENERIC_WRITE, // open for writing
216 | 0, // do not share
217 | NULL, // default security
218 | CREATE_ALWAYS, // create new file only
219 | FILE_ATTRIBUTE_NORMAL, // normal file
220 | NULL); // no attr. template
221 |
222 | if (output == INVALID_HANDLE_VALUE) {
223 | return -1;
224 | }
225 |
226 | DuplicateHandle(output, &_process->pipe_stdout);
227 | DuplicateHandle(output, &_process->pipe_stderr);
228 | DuplicateHandle(output, error.address());
229 |
230 | _si->cb = sizeof(STARTUPINFO);
231 | _si->hStdError = error;
232 | _si->hStdOutput = output;
233 | _si->hStdInput = input;
234 | _si->dwFlags = STARTF_USESTDHANDLES;
235 |
236 | return 0;
237 | }
238 |
239 | /**
240 | * The actual startup info object.
241 | */
242 | STARTUPINFO info;
243 |
244 | };
245 |
246 |
247 | // see: https://blogs.msdn.microsoft.com/oldnewthing/20131209-00/?p=2433
248 | static HANDLE CreateAndAssignChildToJob (HANDLE _process)
249 | {
250 | HANDLE job_handle = ::CreateJobObject(NULL, NULL);
251 | if (!job_handle) {
252 | throw subprocess_exception(::GetLastError(), "group termination: could not create a new job");
253 | }
254 |
255 | JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
256 | ::memset(&info, 0, sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
257 |
258 | info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
259 | if (FALSE == ::SetInformationJobObject(job_handle,
260 | JobObjectExtendedLimitInformation,
261 | &info, sizeof(info)))
262 | {
263 | CloseHandle(job_handle);
264 | throw subprocess_exception(::GetLastError(), "could not set job information");
265 | }
266 |
267 | if (::AssignProcessToJobObject(job_handle, _process) == FALSE) {
268 | CloseHandle(job_handle);
269 | throw subprocess_exception(::GetLastError(), "group termination: could not assign process to a job");
270 | }
271 |
272 | return job_handle;
273 | }
274 |
275 |
276 | //
277 | // https://msdn.microsoft.com/en-us/library/windows/desktop/ms682499(v=vs.85).aspx
278 | // https://support.microsoft.com/en-us/kb/190351
279 | //
280 | void process_handle_t::spawn (const char * _command, char *const _arguments[],
281 | char *const _environment[], const char * _workdir,
282 | termination_mode_type _termination_mode)
283 | {
284 | /* if the command is part of arguments, pass NULL to CreateProcess */
285 | if (!strcmp(_arguments[0], _command)) {
286 | _command = NULL;
287 | }
288 | /* if the environment is empty (most cases) don't bother with passing
289 | * just this single NULLed element */
290 | char * environment = NULL;
291 | if (_environment != NULL && *_environment != NULL) {
292 | environment = prepare_environment(_environment);
293 | }
294 |
295 | /* put all arguments into one line */
296 | char * command_line = strjoin(_arguments, ' ');
297 |
298 | PROCESS_INFORMATION pi;
299 | memset(&pi, 0, sizeof(PROCESS_INFORMATION));
300 |
301 | // creation flags
302 | DWORD creation_flags = CREATE_NEW_PROCESS_GROUP;
303 |
304 | // update pipe handles and creation flags
305 | StartupInfo startupInfo(*this, creation_flags);
306 |
307 | // if termination is set to "group", create a job for this process;
308 | // attempt at it at the beginning and not even try to start the process
309 | // if it fails
310 | if (_termination_mode == TERMINATION_GROUP) {
311 | creation_flags |= CREATE_SUSPENDED | CREATE_BREAKAWAY_FROM_JOB;
312 | }
313 |
314 | BOOL rc = ::CreateProcess(_command, // lpApplicationName
315 | command_line, // lpCommandLine, command line
316 | NULL, // lpProcessAttributes, process security attributes
317 | NULL, // lpThreadAttributes, primary thread security attributes
318 | TRUE, // bInheritHandles, handles are inherited
319 | creation_flags, // dwCreationFlags, creation flags
320 | environment, // lpEnvironment
321 | _workdir, // lpCurrentDirectory
322 | &startupInfo.info,// lpStartupInfo
323 | &pi); // lpProcessInformation
324 |
325 | ::HeapFree(::GetProcessHeap(), 0, command_line);
326 | ::HeapFree(::GetProcessHeap(), 0, environment);
327 |
328 | /* translate from Windows to Linux; -1 means error */
329 | if (!rc) {
330 | throw subprocess_exception(::GetLastError(), "could not create process");
331 | }
332 |
333 | /* if termination mode is "group" add process to the job; see here
334 | * for more details:
335 | * https://msdn.microsoft.com/en-us/library/windows/desktop/ms684161(v=vs.85).aspx
336 | *
337 | * "After a process is associated with a job, by default any child
338 | * processes it creates using CreateProcess are also associated
339 | * with the job."
340 | */
341 | if (_termination_mode == TERMINATION_GROUP) {
342 | // if cannot create and/or assign process to a new job, terminate
343 | try {
344 | process_job = CreateAndAssignChildToJob(pi.hProcess);
345 | }
346 | catch (subprocess_exception & e) {
347 | state = RUNNING;
348 | terminate();
349 | throw;
350 | }
351 |
352 | if (::ResumeThread(pi.hThread) == (DWORD)-1) {
353 | throw subprocess_exception(::GetLastError(), "could not resume thread");
354 | }
355 | }
356 |
357 | /* close thread handle but keep the process handle */
358 | CloseHandle(pi.hThread);
359 |
360 | state = RUNNING;
361 | child_handle = pi.hProcess;
362 | child_id = pi.dwProcessId;
363 | termination_mode = _termination_mode;
364 | }
365 |
366 |
367 | /* --- process::shutdown -------------------------------------------- */
368 |
369 |
370 | void process_handle_t::shutdown ()
371 | {
372 | if (child_handle == HANDLE_CLOSED)
373 | return;
374 |
375 | // close the process handle
376 | CloseHandle(child_handle);
377 | CloseHandle(process_job);
378 |
379 | // close read & write handles
380 | CloseHandle(pipe_stdin);
381 | CloseHandle(pipe_stdout);
382 | CloseHandle(pipe_stderr);
383 | }
384 |
385 |
386 | /* --- process::write ----------------------------------------------- */
387 |
388 | size_t process_handle_t::write (const void * _buffer, size_t _count)
389 | {
390 | DWORD written = 0;
391 | if (!::WriteFile(pipe_stdin, _buffer, (DWORD)_count, &written, NULL)) {
392 | throw subprocess_exception(::GetLastError(), "could not write to child process");
393 | }
394 |
395 | return static_cast(written);
396 | }
397 |
398 |
399 | /* --- process::read ------------------------------------------------ */
400 |
401 |
402 | size_t process_handle_t::read (pipe_type _pipe, int _timeout)
403 | {
404 | stdout_.clear();
405 | stderr_.clear();
406 |
407 | ULONGLONG start = GetTickCount64();
408 | int timediff, sleep_time = 100; /* by default sleep 0.1 seconds */
409 |
410 | if (_timeout >= 0) {
411 | sleep_time = _timeout / 10;
412 | }
413 |
414 | do {
415 | size_t rc1 = 0, rc2 = 0;
416 | if (_pipe & PIPE_STDOUT) rc1 = stdout_.read(pipe_stdout);
417 | if (_pipe & PIPE_STDERR) rc2 = stderr_.read(pipe_stderr);
418 |
419 | // if anything has been read or no timeout is specified return now
420 | if (rc1 > 0 || rc2 > 0 || sleep_time == 0) {
421 | return std::max(rc1, rc2);
422 | }
423 |
424 | // sleep_time is now guaranteed to be positive
425 | Sleep(sleep_time);
426 | timediff = (int)(GetTickCount64() - start);
427 |
428 | } while (_timeout < 0 || timediff < _timeout);
429 |
430 | // out of time
431 | return 0;
432 | }
433 |
434 |
435 | /* --- process::close_input ----------------------------------------- */
436 |
437 | void process_handle_t::close_input ()
438 | {
439 | if (pipe_stdin == HANDLE_CLOSED) {
440 | throw subprocess_exception(EALREADY, "child's standard input already closed");
441 | }
442 |
443 | CloseHandle(pipe_stdin);
444 | pipe_stdin = HANDLE_CLOSED;
445 | }
446 |
447 |
448 | /* ------------------------------------------------------------------ */
449 |
450 |
451 | void process_handle_t::wait (int _timeout)
452 | {
453 | if (!child_handle || state != RUNNING)
454 | return;
455 |
456 | // to wait or not to wait?
457 | if (_timeout == TIMEOUT_INFINITE)
458 | _timeout = INFINITE;
459 |
460 | DWORD rc = ::WaitForSingleObject(child_handle, _timeout);
461 |
462 | // if already exited
463 | if (rc == WAIT_OBJECT_0) {
464 | DWORD status;
465 | if (::GetExitCodeProcess(child_handle, &status) == FALSE) {
466 | throw subprocess_exception(::GetLastError(), "could not read child exit code");
467 | }
468 |
469 | if (status == STILL_ACTIVE) {
470 | return;
471 | }
472 |
473 | return_code = (int)status;
474 | state = EXITED;
475 | }
476 | else if (rc != WAIT_TIMEOUT) {
477 | throw subprocess_exception(::GetLastError(), "wait for child process failed");
478 | }
479 | }
480 |
481 |
482 | /* --- process::terminate ------------------------------------------- */
483 |
484 |
485 | // compare with: https://github.com/andreisavu/python-process/blob/master/killableprocess.py
486 | void process_handle_t::terminate()
487 | {
488 | // first make sure it's even still running
489 | wait(TIMEOUT_IMMEDIATE);
490 | if (!child_handle || state != RUNNING)
491 | return;
492 |
493 | // first terminate the child process; if mode is "group" terminate
494 | // the whole job
495 | if (termination_mode == TERMINATION_GROUP) {
496 | BOOL rc = ::TerminateJobObject(process_job, 127);
497 | CloseHandle(process_job);
498 | if (rc == FALSE) {
499 | throw subprocess_exception(::GetLastError(), "could not terminate child job");
500 | }
501 | }
502 | else {
503 | // now terminate just the process itself
504 | HANDLE to_terminate = ::OpenProcess(PROCESS_TERMINATE, FALSE, child_id);
505 | if (!to_terminate) {
506 | throw subprocess_exception(::GetLastError(), "could open child process for termination");
507 | }
508 |
509 | BOOL rc = ::TerminateProcess(to_terminate, 127);
510 | CloseHandle(to_terminate);
511 | if (rc == FALSE) {
512 | throw subprocess_exception(::GetLastError(), "could not terminate child process");
513 | }
514 | }
515 |
516 | // clean up
517 | wait(TIMEOUT_INFINITE);
518 | state = TERMINATED;
519 |
520 | // release handles
521 | shutdown();
522 | }
523 |
524 |
525 | /* --- process::kill ------------------------------------------------ */
526 |
527 |
528 | void process_handle_t::kill ()
529 | {
530 | terminate();
531 | }
532 |
533 |
534 | /* --- process::send_signal ----------------------------------------- */
535 |
536 |
537 | /*
538 | * This is tricky. Look here for details:
539 | * http://codetitans.pl/blog/post/sending-ctrl-c-signal-to-another-application-on-windows
540 | *
541 | * WARNING! I cannot make it work. It seems that there is no way of
542 | * sending Ctrl+C to the child process without killing the parent R
543 | * at the same time.
544 | *
545 | * From:
546 | * https://msdn.microsoft.com/en-us/library/windows/desktop/ms683155(v=vs.85).aspx
547 | *
548 | * Generates a CTRL+C signal. This signal cannot be generated for process groups.
549 | * If dwProcessGroupId is nonzero, this function will succeed, but the CTRL+C
550 | * signal will not be received by processes within the specified process group.
551 | */
552 | void process_handle_t::send_signal (int _signal)
553 | {
554 | if (_signal == SIGTERM) {
555 | return terminate();
556 | }
557 |
558 | // unsupported `signal` value
559 | if (_signal != CTRL_C_EVENT && _signal != CTRL_BREAK_EVENT) {
560 | throw subprocess_exception(ERROR_INVALID_SIGNAL_NUMBER, "signal not supported");
561 | }
562 |
563 | if (::GenerateConsoleCtrlEvent(_signal, child_id) == FALSE) {
564 | throw subprocess_exception(::GetLastError(), "signal could not be sent");
565 | }
566 | }
567 |
568 |
569 | bool process_exists (const pid_type & _pid) {
570 | /*
571 | * https://stackoverflow.com/questions/12900036/benefit-of-using-waitforsingleobject-when-checking-process-id
572 | *
573 | * When a process completes, it stops running but it doesn't go out of
574 | * existence until the last handle to it is closed. The first solution
575 | * distinguishes between those two states (still running or done running).
576 | */
577 | HANDLE h = OpenProcess(SYNCHRONIZE, FALSE, _pid);
578 | DWORD ret = WaitForSingleObject(h, 0);
579 | CloseHandle(h);
580 | return (ret == WAIT_TIMEOUT);
581 | }
582 |
583 |
584 | /**
585 | * You have to Free() the buffer returned from this function
586 | * yourself - or let R do it, since we allocate it with Calloc().
587 | */
588 | static char * strjoin (char *const* _array, char _sep)
589 | {
590 | /* total length is the sum of lengths plus spaces */
591 | size_t total_length = 0;
592 | char *const* ptr;
593 |
594 | for ( ptr = _array ; *ptr != NULL; ++ptr) {
595 | total_length += strlen(*ptr) + 1; /* +1 for space */
596 | }
597 |
598 | char * buffer = (char*)HeapAlloc(GetProcessHeap(), 0, total_length + 2); /* +2 for double NULL */
599 |
600 | /* combine all parts, put spaces between them */
601 | char * tail = buffer;
602 | for ( ptr = _array ; *ptr != NULL; ++ptr, ++tail) {
603 | size_t len = strlen(*ptr);
604 | strncpy_s(tail, total_length+1, *ptr, len);
605 | tail += len;
606 | *tail = _sep;
607 | }
608 |
609 | *tail++ = 0;
610 | *tail = 0;
611 |
612 | return buffer;
613 | }
614 |
615 |
616 | int find_double_0 (const char * _str)
617 | {
618 | int length = 0;
619 | while (*_str++ || *_str) {
620 | ++length;
621 | }
622 | return length;
623 | }
624 |
625 | char * prepare_environment (char *const* _environment)
626 | {
627 | char * current_env = GetEnvironmentStrings();
628 | char * new_env = strjoin(_environment, 0);
629 |
630 | int length_1 = find_double_0(current_env);
631 | int length_2 = find_double_0(new_env);
632 |
633 | char * combined_env = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, length_1 + length_2 + 5);
634 | CopyMemory(combined_env, current_env, length_1);
635 | CopyMemory(combined_env+length_1+1, new_env, length_2);
636 |
637 | FreeEnvironmentStrings(current_env);
638 | HeapFree(GetProcessHeap(), 0, new_env);
639 | return combined_env;
640 | }
641 |
642 | } /* namespace subprocess */
643 |
644 |
645 |
646 | #ifdef WINDOWS_TEST
647 | int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
648 | {
649 | const char * command = "x";
650 | char * args[] = { "x", NULL };
651 | char * env[] = { NULL };
652 |
653 | process_handle_t handle;
654 | if (spawn_process(&handle, command, args, env) < 0) {
655 | fprintf(stderr, "error in spawn_process\n");
656 | exit(EXIT_FAILURE);
657 | }
658 |
659 | teardown_process(&handle);
660 |
661 | return 0;
662 | }
663 | #endif /* WINDOWS_TEST */
664 |
--------------------------------------------------------------------------------
/src/subprocess.cc:
--------------------------------------------------------------------------------
1 | /** @file subprocess.cc
2 | *
3 | * Process handle - platform independent code.
4 | * @author Lukasz A. Bartnik
5 | */
6 |
7 | #include "subprocess.h"
8 |
9 | #include
10 |
11 | #define MB_PARSE_ERROR ((size_t)-1)
12 | #define MB_INCOMPLETE ((size_t)-2)
13 |
14 |
15 |
16 | namespace subprocess {
17 |
18 |
19 | size_t pipe_writer::read (pipe_handle_type _fd, bool _mbcslocale) {
20 | if (_mbcslocale) {
21 | memcpy(contents.data(), left.data, left.len);
22 | }
23 | else {
24 | left.len = 0;
25 | }
26 |
27 | size_t rc = os_read(_fd);
28 |
29 | // end with 0 to make sure R can create a string out of the data block
30 | rc += left.len;
31 | contents[rc] = 0;
32 |
33 | // if there is a partial multi-byte character at the end, keep
34 | // it around for the next read attempt
35 | if (_mbcslocale) {
36 | left.len = 0;
37 |
38 | // check if all bytes are correct UTF8 content
39 | size_t consumed = consume_utf8(contents.data(), rc);
40 | if (consumed == MB_PARSE_ERROR || (rc - consumed > 4)) {
41 | throw subprocess_exception(EIO, "malformed multibyte string");
42 | }
43 | if (consumed < (size_t)rc) {
44 | left.len = rc-consumed;
45 | memcpy(left.data, contents.data()+consumed, left.len);
46 | contents[consumed] = 0;
47 | rc = consumed;
48 | }
49 | }
50 |
51 | return rc;
52 | }
53 |
54 |
55 | static int min (int a, int b) { return a 0 we can just skip that many bytes and move on
67 | while (_length > consumed) {
68 | used = mbrtowc(&wc, _input, min(MB_CUR_MAX, _length-consumed), &mb_st);
69 | // two situations when an error in encountered
70 | if (used == MB_INCOMPLETE) {
71 | return consumed;
72 | }
73 | if (used == MB_PARSE_ERROR) {
74 | return MB_PARSE_ERROR;
75 | }
76 | // correctly consumed multi-byte character
77 | if (used) {
78 | _input += used;
79 | consumed += used;
80 | }
81 | // zero bytes consumed
82 | else {
83 | break;
84 | }
85 | }
86 |
87 | return consumed;
88 | }
89 |
90 |
91 |
92 | } /* namespace subprocess */
93 |
--------------------------------------------------------------------------------
/src/subprocess.h:
--------------------------------------------------------------------------------
1 | #ifndef SUBPROCESS_H_GUARD
2 | #define SUBPROCESS_H_GUARD
3 |
4 | #include "config-os.h"
5 |
6 | // mbcslocale
7 | #include
8 | #include
9 | #include
10 |
11 |
12 | /* In Visual Studio std::vector gets messed up by definitions in "R.h" */
13 | #ifdef _MSC_VER
14 | #undef length
15 | #endif
16 |
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include
23 |
24 |
25 | namespace subprocess {
26 |
27 | constexpr int BUFFER_SIZE = 1024;
28 |
29 |
30 | using std::string;
31 | using std::runtime_error;
32 | using std::vector;
33 |
34 |
35 |
36 | enum pipe_type { PIPE_STDIN = 0, PIPE_STDOUT = 1, PIPE_STDERR = 2, PIPE_BOTH = 3 };
37 |
38 |
39 | constexpr int TIMEOUT_IMMEDIATE = 0;
40 |
41 | constexpr int TIMEOUT_INFINITE = -1;
42 |
43 |
44 | string strerror (int _code, const string & _message);
45 |
46 | size_t consume_utf8 (const char * _input, size_t _length);
47 |
48 |
49 | /**
50 | * A simple exception class.
51 | */
52 | struct subprocess_exception : runtime_error {
53 |
54 | /**
55 | * Create a new exception object.
56 | *
57 | * The local version of strerror is used in constructor to generate
58 | * the final error message and store it in the exception object.
59 | *
60 | * @param _code Operating-system-specific error code.
61 | * @param _message User-provided error message.
62 | */
63 | subprocess_exception (int _code, const string & _message)
64 | : runtime_error(strerror(_code, _message)), code(_code)
65 | { }
66 |
67 | /** Operating-system-specific error code. */
68 | const int code;
69 |
70 | /**
71 | * Store error message in a buffer.
72 | */
73 | void store (char * _buffer, size_t _length) {
74 | snprintf(_buffer, _length, "%s", what());
75 | }
76 | };
77 |
78 |
79 |
80 | /**
81 | * Buffer for a single output stream.
82 | *
83 | * This buffer comes with additional logic of handling a number of
84 | * bytes left from the previous read that did not constitute a
85 | * correct multi-byte character.
86 | */
87 | struct pipe_writer {
88 |
89 | static constexpr size_t buffer_size = 1024;
90 |
91 | struct leftover {
92 | leftover () : len(0) { }
93 | size_t len;
94 | char data[4];
95 |
96 | static_assert(sizeof(pipe_writer::leftover::data) < buffer_size,
97 | "buffer too small for multi-byte char support");
98 | };
99 |
100 | typedef vector container_type;
101 |
102 | container_type contents;
103 | leftover left;
104 |
105 | /**
106 | * Throws if buffer is too small.
107 | */
108 | pipe_writer () : contents(buffer_size, 0) { }
109 |
110 | const container_type::value_type * data () const { return contents.data(); }
111 |
112 | void clear () { contents[0] = 0; }
113 |
114 | size_t os_read (pipe_handle_type _pipe)
115 | {
116 | char * buffer = contents.data() + left.len;
117 | size_t length = contents.size() - left.len - 1;
118 |
119 | #ifdef SUBPROCESS_WINDOWS
120 | DWORD dwAvail = 0, nBytesRead;
121 |
122 | // if returns FALSE and error is "broken pipe", pipe is gone
123 | if (!::PeekNamedPipe(_pipe, NULL, 0, NULL, &dwAvail, NULL)) {
124 | if (::GetLastError() == ERROR_BROKEN_PIPE) return 0;
125 | throw subprocess_exception(::GetLastError(), "could not peek into pipe");
126 | }
127 |
128 | if (dwAvail == 0)
129 | return 0;
130 |
131 | dwAvail = std::min((size_t)dwAvail, length);
132 | if (!::ReadFile(_pipe, buffer, dwAvail, &nBytesRead, NULL)) {
133 | throw subprocess_exception(::GetLastError(), "could not read from pipe");
134 | }
135 |
136 | return static_cast(nBytesRead);
137 | #else /* SUBPROCESS_WINDOWS */
138 | int rc = ::read(_pipe, buffer, length);
139 | if (rc < 0) {
140 | throw subprocess_exception(errno, "could not read from pipe");
141 | }
142 | return static_cast(rc);
143 | #endif /* SUBPROCESS_WINDOWS */
144 | }
145 |
146 |
147 | /**
148 | * Read from pipe.
149 | *
150 | * Will accommodate for previous leftover and will keep a single
151 | * byte to store 0 at the end of the input data. That guarantees
152 | * that R string can be correctly constructed from buffer's data
153 | * (R expects a ZERO at the end).
154 | *
155 | * @param _fd Input pipe handle.
156 | * @param _mbcslocale Is this multi-byte character set? If so, verify
157 | * string integrity after a successful read.
158 | */
159 | size_t read (pipe_handle_type _fd, bool _mbcslocale = false);
160 |
161 | }; /* pipe_writer */
162 |
163 |
164 | /**
165 | * Check if process with given pid exists.
166 | *
167 | * @param _pid Process id.
168 | */
169 | bool process_exists (const pid_type & _pid);
170 |
171 |
172 |
173 | /**
174 | * Process handle.
175 | *
176 | * The main class in the package. This is where a single process state
177 | * is stored and where API to interact with that child process is
178 | * provided.
179 | */
180 | struct process_handle_t {
181 |
182 | enum process_state_type { NOT_STARTED, RUNNING, EXITED, TERMINATED, SHUTDOWN };
183 |
184 | enum termination_mode_type { TERMINATION_GROUP, TERMINATION_CHILD_ONLY };
185 |
186 | #ifdef SUBPROCESS_WINDOWS
187 | HANDLE process_job;
188 | #endif
189 |
190 | // OS-specific handles
191 | process_handle_type child_handle;
192 |
193 | pipe_handle_type pipe_stdin,
194 | pipe_stdout,
195 | pipe_stderr;
196 |
197 | // platform-independent process data
198 | int child_id;
199 | process_state_type state;
200 | int return_code;
201 |
202 | /* how should the process be terminated */
203 | termination_mode_type termination_mode;
204 |
205 | /* stdout & stderr handling */
206 | pipe_writer stdout_, stderr_;
207 |
208 | process_handle_t ();
209 |
210 | ~process_handle_t () throw ()
211 | {
212 | try {
213 | shutdown();
214 | }
215 | catch (...) {
216 | // TODO be silent or maybe show a warning?
217 | }
218 | }
219 |
220 | void spawn(const char * _command, char *const _arguments[],
221 | char *const _environment[], const char * _workdir,
222 | termination_mode_type _termination_mode);
223 |
224 | void shutdown();
225 |
226 | size_t write(const void * _buffer, size_t _count);
227 |
228 | size_t read(pipe_type _pipe, int _timeout);
229 |
230 | void close_input ();
231 |
232 | void wait(int _timeout);
233 |
234 | void terminate();
235 |
236 | void kill();
237 |
238 | void send_signal(int _signal);
239 |
240 | };
241 |
242 |
243 |
244 | } /* namespace subprocess */
245 |
246 |
247 | #endif /* SUBPROCESS_H_GUARD */
248 |
249 |
--------------------------------------------------------------------------------
/src/tests.cc:
--------------------------------------------------------------------------------
1 | /** @file tests.cc
2 | *
3 | * Native C++ unit tests.
4 | * @author Lukasz A. Bartnik
5 | */
6 |
7 | #include "subprocess.h"
8 | #include "rapi.h"
9 |
10 | #include
11 | #include
12 |
13 |
14 | #define expect_equal(a, b) \
15 | do { \
16 | if (a != b) { \
17 | Rf_warning(#a " not equal to " #b); \
18 | errors += 1; \
19 | } \
20 | } while (0); \
21 |
22 |
23 |
24 | using subprocess::consume_utf8;
25 |
26 |
27 | // ---------------------------------------------------------------------
28 |
29 | extern "C" SEXP test_consume_utf8 ()
30 | {
31 | int errors = 0;
32 | if (!mbcslocale) {
33 | return allocate_single_int(0);
34 | }
35 |
36 | // correct ASCII input
37 | expect_equal(consume_utf8("", 0), 0);
38 | expect_equal(consume_utf8("a", 1), 1);
39 | expect_equal(consume_utf8("ab", 2), 2);
40 | expect_equal(consume_utf8("abc", 3), 3);
41 | expect_equal(consume_utf8("abcd", 4), 4);
42 |
43 | // multi-byte UTF8 characters; whole and split in the middle
44 | // https://en.wikipedia.org/wiki/UTF-8#Examples
45 |
46 | // ¢ (https://en.wikipedia.org/wiki/Cent_(currency)#Symbol)
47 | expect_equal(consume_utf8("a\xC2\xA2", 3), 3);
48 | expect_equal(consume_utf8("a\xC2", 2), 1);
49 |
50 | // € (https://en.wikipedia.org/wiki/Euro_sign)
51 | expect_equal(consume_utf8("a\xE2\x82\xAC", 4), 4);
52 | expect_equal(consume_utf8("a\xE2\x82", 3), 1);
53 | expect_equal(consume_utf8("a\xE2", 2), 1);
54 |
55 | // 𐍈 https://en.wikipedia.org/wiki/Hwair
56 |
57 | expect_equal(consume_utf8("a\xF0\x90\x8D\x88", 5), 5);
58 | expect_equal(consume_utf8("a\xF0\x90\x8D", 4), 1);
59 | expect_equal(consume_utf8("a\xF0\x90", 3), 1);
60 | expect_equal(consume_utf8("a\xF0", 2), 1);
61 | expect_equal(consume_utf8("a\xF0", 2), 1);
62 |
63 | return allocate_single_int(errors);
64 | }
65 |
--------------------------------------------------------------------------------
/subprocess.Rproj:
--------------------------------------------------------------------------------
1 | Version: 1.0
2 |
3 | RestoreWorkspace: Default
4 | SaveWorkspace: Default
5 | AlwaysSaveHistory: Default
6 |
7 | EnableCodeIndexing: Yes
8 | UseSpacesForTab: Yes
9 | NumSpacesForTab: 2
10 | Encoding: UTF-8
11 |
12 | RnwWeave: knitr
13 | LaTeX: pdfLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Package
19 | PackageUseDevtools: Yes
20 | PackageInstallArgs: --no-multiarch --with-keep.source --debug
21 | PackageBuildArgs: --debug
22 | PackageBuildBinaryArgs: --debug
23 | PackageRoxygenize: rd,collate,namespace
24 |
--------------------------------------------------------------------------------
/tests/Makefile.test:
--------------------------------------------------------------------------------
1 | test-linux: ../src/sub-linux.c
2 | $(CC) -DLINUX_TEST $^ -Wall -ggdb -O0 -o $@
3 |
4 | test-windows: ../src/sub-windows.c ../src/win-reader.c
5 | $(CC) -DWIN64 -DWINDOWS_TEST $^ -Wall -g -O0 -o $@
6 |
7 | .PHONY: clean
8 | clean:
9 | rm test
10 |
--------------------------------------------------------------------------------
/tests/testthat.R:
--------------------------------------------------------------------------------
1 | library(testthat)
2 | library(subprocess)
3 |
4 | # See for more details:
5 | # https://github.com/hadley/testthat/issues/144
6 | # https://github.com/hadley/testthat/issues/86
7 | # https://github.com/luckyrandom/cmaker/commit/b85813ac2b7aef69932eca8fbb4fa0ec225e0af0
8 | #
9 | # When R CMD check runs tests, it sets R_TESTS. When the tests
10 | # themeselves run R CMD xxxx, as is the case with the tests in
11 | # devtools, having R_TESTS set causes errors because it confuses
12 | # the R subprocesses. Unsetting it here avoids those problems.
13 | #
14 | # We need this because we spawn child R processes and they get very
15 | # confused if this variable (R_TESTS) is present in their environment.
16 | Sys.setenv("R_TESTS" = "")
17 |
18 | test_check("subprocess")
19 |
--------------------------------------------------------------------------------
/tests/testthat/helper-mockery.R:
--------------------------------------------------------------------------------
1 | library(mockery)
--------------------------------------------------------------------------------
/tests/testthat/helper-processes.R:
--------------------------------------------------------------------------------
1 | is_windows <- function () subprocess:::is_windows()
2 |
3 | is_linux <- function () subprocess:::is_linux()
4 |
5 | is_mac <- function ()
6 | {
7 | identical(tolower(Sys.info()[["sysname"]]), 'darwin')
8 | }
9 |
10 | is_solaris <- function()
11 | {
12 | identical(tolower(Sys.info()[["sysname"]]), 'sunos')
13 | }
14 |
15 | # --- R child ----------------------------------------------------------
16 |
17 | R_binary <- function ()
18 | {
19 | binary <- ifelse(is_windows(), 'Rterm.exe', 'R')
20 | binary <- file.path(R.home("bin"), binary)
21 | stopifnot(file.exists(binary))
22 | binary
23 | }
24 |
25 | R_child <- function(args = '--slave', ...)
26 | {
27 | handle <- spawn_process(R_binary(), args, ...)
28 | wait_until_appears(handle)
29 | handle
30 | }
31 |
32 |
33 | # --- OS interface -----------------------------------------------------
34 |
35 | # wait_until_*
36 | #
37 | # Wait infinitey - on CRAN tests will timeout, locally we can always
38 | # tell that something is wrong. This is because some systems are simply
39 | # overloaded and it might take *minutes* for the processes to appear
40 | # or exit.
41 |
42 | # Wait until process can be found in the system.
43 | #
44 | # @param x Process handle or OS-level process id (integer).
45 | wait_until_appears <- function (x)
46 | {
47 | while (!process_exists(x)) {
48 | if (is_process_handle(x)) {
49 | process_wait(x, TIMEOUT_IMMEDIATE)
50 | if (process_state(x) %in% c("exited", "terminated"))
51 | stop('failed to start ', x$command, call. = FALSE)
52 | }
53 |
54 | Sys.sleep(.25)
55 | }
56 | return(TRUE)
57 | }
58 |
59 |
60 | wait_until_exits <- function (handle)
61 | {
62 | while (process_exists(handle)) {
63 | Sys.sleep(.25)
64 | }
65 | return(TRUE)
66 | }
67 |
68 |
69 | terminate_gracefully <- function (handle, message = "q('no')\n")
70 | {
71 | if (!process_exists(handle)) return(TRUE)
72 |
73 | if (!is.null(message)) {
74 | process_write(handle, message)
75 | }
76 |
77 | process_close_input(handle)
78 | process_wait(handle)
79 | wait_until_exits(handle)
80 | }
81 |
--------------------------------------------------------------------------------
/tests/testthat/signal-trap.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | function handler
4 | {
5 | echo "$1"
6 | }
7 |
8 | function trap_with_name
9 | {
10 | shift
11 | for sig in $*; do
12 | trap "handler $sig" "$sig"
13 | done
14 | }
15 |
16 | trap_with_name SIGHUP SIGINT SIGQUIT SIGILL SIGABRT SIGFPE\
17 | SIGKILL SIGSEGV SIGPIPE SIGALRM SIGTERM SIGUSR1\
18 | SIGUSR2 SIGCHLD SIGCONT SIGSTOP SIGTSTP SIGTTIN\
19 | SIGTTOU
20 |
21 | echo "ready"
22 |
23 | # wait until ready to exit
24 | read
25 |
--------------------------------------------------------------------------------
/tests/testthat/test-package.R:
--------------------------------------------------------------------------------
1 | context("package")
2 |
3 | test_that("onLoad is correct", {
4 | known <- subprocess:::known_signals()
5 | expect_true(is.integer(known))
6 |
7 | # do not mess with the actual namespace
8 | dotOnLoad <- subprocess:::.onLoad
9 | environment(dotOnLoad) <- new.env(parent = asNamespace("subprocess"))
10 | environment(dotOnLoad)$signals <- list()
11 |
12 | # intercept assignments
13 | assignMock <- mock(T, cycle = TRUE)
14 | mockery::stub(dotOnLoad, 'assign', assignMock)
15 | dotOnLoad('libname', 'subprocess')
16 |
17 | expect_called(assignMock, length(known))
18 | })
19 |
--------------------------------------------------------------------------------
/tests/testthat/test-parameters.R:
--------------------------------------------------------------------------------
1 | context("child process parameters")
2 |
3 |
4 | test_that("working directory can be set", {
5 | print_wd <- 'cat(normalizePath(getwd()))\n'
6 |
7 | work_dir <- normalizePath(tempdir())
8 | norm_wd <- normalizePath(getwd())
9 | expect_false(identical(work_dir, getwd()))
10 |
11 | on.exit(process_kill(handle), add = TRUE)
12 | handle <- R_child()
13 |
14 | process_write(handle, print_wd)
15 | expect_equal(normalizePath(process_read(handle, timeout = 1000)$stdout), norm_wd)
16 | expect_true(terminate_gracefully(handle))
17 |
18 | on.exit(process_kill(handle_2), add = TRUE)
19 | handle_2 <- R_child(workdir = work_dir)
20 |
21 | process_write(handle_2, print_wd)
22 | expect_equal(normalizePath(process_read(handle_2, timeout = 1000)$stdout), work_dir)
23 | expect_true(terminate_gracefully(handle_2))
24 | })
25 |
26 |
27 | # --- new environment --------------------------------------------------
28 |
29 | test_that("inherits environment from parent", {
30 | on.exit(Sys.unsetenv("PARENT_VAR"), add = TRUE)
31 | Sys.setenv(PARENT_VAR="PARENT_VAL")
32 |
33 | on.exit(process_terminate(handle), add = TRUE)
34 | handle <- R_child(c("--slave", "-e", "cat(Sys.getenv('PARENT_VAR'))"))
35 |
36 | expect_equal(process_read(handle, timeout = TIMEOUT_INFINITE)$stdout, 'PARENT_VAL')
37 | expect_equal(process_wait(handle, timeout = TIMEOUT_INFINITE), 0)
38 | expect_equal(process_state(handle), "exited")
39 | })
40 |
41 |
42 | test_that("passing new environment", {
43 | on.exit(terminate_gracefully(handle), add = TRUE)
44 | handle <- R_child(environment = "VAR=SOME_VALUE")
45 |
46 | process_write(handle, 'cat(Sys.getenv("VAR"))\n')
47 | expect_equal(process_read(handle, timeout = 1000)$stdout, 'SOME_VALUE')
48 | })
49 |
50 |
51 | test_that("new environment via named vector", {
52 | on.exit(terminate_gracefully(handle), add = TRUE)
53 | handle <- R_child(environment = c(VAR="SOME_VALUE"))
54 |
55 | process_write(handle, 'cat(Sys.getenv("VAR"))\n')
56 | expect_equal(process_read(handle, timeout = 1000)$stdout, 'SOME_VALUE')
57 | })
58 |
59 |
60 | test_that("new environment via list", {
61 | on.exit(terminate_gracefully(handle), add = TRUE)
62 | handle <- R_child(environment = list(VAR="SOME_VALUE"))
63 |
64 | process_write(handle, 'cat(Sys.getenv("VAR"))\n')
65 | expect_equal(process_read(handle, timeout = 1000)$stdout, 'SOME_VALUE')
66 | })
67 |
68 |
69 | test_that("environment error checking", {
70 | expect_error(spawn_process(R_binary(), environment = list(A="B", "C")))
71 | })
72 |
--------------------------------------------------------------------------------
/tests/testthat/test-readwrite.R:
--------------------------------------------------------------------------------
1 | context("read-write")
2 |
3 | test_that("output buffer is flushed", {
4 | lines <- 1000
5 | command <- paste0('cat(sep = "\\n", replicate(', lines,
6 | ', paste(sample(letters, 60, TRUE), collapse = "")))')
7 |
8 | on.exit(terminate_gracefully(handle))
9 | handle <- R_child()
10 | expect_true(process_exists(handle))
11 |
12 | # send the command and give the process a moment to produce the output
13 | process_write(handle, paste(command, "\n"))
14 | Sys.sleep(3)
15 |
16 | # read everything
17 | output <- process_read(handle, PIPE_STDOUT, TIMEOUT_INFINITE, flush = TRUE)
18 |
19 | expect_length(output, lines)
20 | expect_true(all(nchar(output) == 60))
21 | })
22 |
23 |
24 | test_that("exchange data", {
25 | on.exit(terminate_gracefully(handle))
26 | handle <- R_child()
27 |
28 | expect_true(process_exists(handle))
29 |
30 | process_write(handle, 'cat("A")\n')
31 | output <- process_read(handle, timeout = 1000)
32 |
33 | expect_named(output, c('stdout', 'stderr'))
34 | expect_equal(output$stdout, 'A')
35 | expect_equal(output$stderr, character())
36 | })
37 |
38 |
39 | test_that("read from standard error output", {
40 | on.exit(terminate_gracefully(handle))
41 | handle <- R_child()
42 |
43 | process_write(handle, 'cat("A", file = stderr())\n')
44 | output <- process_read(handle, PIPE_STDERR, timeout = 1000)
45 |
46 | expect_true(is.character(output))
47 | expect_equal(output, 'A')
48 | })
49 |
50 |
51 | test_that("write returns the number of characters", {
52 | on.exit(terminate_gracefully(handle))
53 | handle <- R_child()
54 |
55 | expect_equal(process_write(handle, 'cat("A")\n'), 9)
56 | })
57 |
58 |
59 | test_that("non-blocking read", {
60 | on.exit(terminate_gracefully(handle))
61 |
62 | handle <- R_child()
63 | expect_true(process_exists(handle))
64 |
65 | expect_equal(process_read(handle, PIPE_STDOUT), character(0))
66 | expect_equal(process_read(handle, PIPE_STDERR), character(0))
67 | expect_equal(process_read(handle, PIPE_BOTH), list(stdout = character(0),
68 | stderr = character(0)))
69 | })
70 |
--------------------------------------------------------------------------------
/tests/testthat/test-signals.R:
--------------------------------------------------------------------------------
1 | context("signals")
2 |
3 |
4 | test_that("sending signal in Linux/MacOS/Solaris", {
5 | skip_if_not(is_linux() || is_mac() || is_solaris())
6 |
7 | script_path <- file.path(getwd(), 'signal-trap.sh')
8 | expect_true(file.exists(script_path))
9 |
10 | bash_path <- "/bin/bash"
11 | expect_true(file.exists(bash_path))
12 |
13 | on.exit(terminate_gracefully(handle), add = TRUE)
14 | handle <- spawn_process(bash_path, c("-e", script_path))
15 | expect_true(process_exists(handle))
16 |
17 | # this is necessary to give bash time to set up the signal trap;
18 | # otherwise it is a race
19 | output <- process_read(handle, PIPE_STDOUT, TIMEOUT_INFINITE)
20 | expect_equal(output, "ready")
21 |
22 | # exclude signals to kill or stop the child
23 | skip <- c(SIGHUP, SIGKILL, SIGCHLD, SIGSTOP, if (is_solaris()) SIGQUIT)
24 |
25 | for (signal in setdiff(signals, skip)) {
26 | process_send_signal(handle, signal)
27 | output <- process_read(handle, PIPE_STDOUT, TIMEOUT_INFINITE)
28 | i <- which(signals == signal)
29 | expect_equal(output, names(signals)[[i]], info = names(signals)[[i]])
30 | }
31 | })
32 |
33 |
34 | test_that("sending signal in Windows", {
35 | skip_if_not(is_windows())
36 |
37 | spawn <- function () {
38 | spawn_process(Sys.which("cmd"), c("/C", '"sleep 60"'))
39 | }
40 |
41 |
42 | # according to:
43 | # https://msdn.microsoft.com/en-us/library/cc704588.aspx
44 | #
45 | # 0xC0000001 = STATUS_UNSUCCESSFUL
46 | # 0xC000013A = STATUS_CONTROL_C_EXIT
47 | #
48 | # However, exit code doesn't seem to be consistent between deployments
49 | # (AppVeyor vs. CRAN's win-builder vs. a local Windows system) and
50 | # return codes vary: 0, 1, -1073741510L. For that reason we do not
51 | # check the exit code in the test below.
52 |
53 | # Ctrl+C
54 | handle <- spawn()
55 | expect_true(wait_until_appears(handle))
56 |
57 | process_send_signal(handle, CTRL_C_EVENT)
58 | expect_silent(process_wait(handle, TIMEOUT_INFINITE))
59 | expect_false(process_exists(handle))
60 |
61 | # CTRL+Break
62 | handle <- spawn()
63 | expect_true(wait_until_appears(handle))
64 |
65 | process_send_signal(handle, CTRL_BREAK_EVENT)
66 | expect_silent(process_wait(handle, TIMEOUT_INFINITE))
67 | expect_false(process_exists(handle))
68 | })
69 |
70 |
--------------------------------------------------------------------------------
/tests/testthat/test-subprocess.R:
--------------------------------------------------------------------------------
1 | context("subprocess")
2 |
3 | killed_exit_code <- ifelse(is_windows(), 127, 9)
4 |
5 | test_that("helper works", {
6 | expect_true(process_exists(Sys.getpid()))
7 | expect_false(process_exists(99999999))
8 | })
9 |
10 |
11 | test_that("a subprocess can be spawned and killed", {
12 | handle <- R_child()
13 | expect_named(handle, c('c_handle', 'command', 'arguments'))
14 | expect_true('handle_ptr' %in% names(attributes(handle$c_handle)))
15 |
16 | ptr <- attr(handle$c_handle, 'handle_ptr')
17 | expect_equal(class(ptr), 'externalptr')
18 |
19 | expect_true(process_exists(handle))
20 |
21 | # we need to clean-up 'manually'
22 | process_write(handle, "cat(tempdir())\n")
23 | path <- process_read(handle, PIPE_STDOUT, TIMEOUT_INFINITE)
24 | expect_true(dir.exists(path))
25 | on.exit(unlink(path, TRUE, TRUE), add = TRUE)
26 |
27 | process_kill(handle)
28 | expect_equal(process_wait(handle, TIMEOUT_INFINITE), killed_exit_code)
29 | expect_equal(process_state(handle), "terminated")
30 | expect_true(wait_until_exits(handle))
31 | })
32 |
33 |
34 | test_that("waiting for a child to exit", {
35 | on.exit(process_terminate(handle))
36 | handle <- R_child()
37 |
38 | process_wait(handle, TIMEOUT_IMMEDIATE)
39 | expect_equal(process_state(handle), "running")
40 |
41 | # we need to clean-up 'manually'
42 | process_write(handle, "cat(tempdir())\n")
43 | path <- process_read(handle, PIPE_STDOUT, TIMEOUT_INFINITE)
44 | expect_true(dir.exists(path))
45 | on.exit(unlink(path, TRUE, TRUE), add = TRUE)
46 |
47 | process_kill(handle)
48 |
49 | expect_equal(process_wait(handle, TIMEOUT_INFINITE), killed_exit_code)
50 | expect_equal(process_state(handle), "terminated")
51 | expect_equal(process_return_code(handle), killed_exit_code)
52 | })
53 |
54 |
55 | test_that("error when no executable", {
56 | expect_error(spawn_process("xxx"))
57 | })
58 |
59 |
60 | test_that("can expand paths", {
61 | normalizePathMock <- mock('/full/path/to/local/executable')
62 | dotCallMock <- mock(1)
63 |
64 | stub(spawn_process, 'normalizePath', normalizePathMock)
65 | stub(spawn_process, '.Call', dotCallMock)
66 | handle <- spawn_process("~/local/executable")
67 |
68 | expect_called(normalizePathMock, 1)
69 | expect_called(dotCallMock, 1)
70 | })
71 |
72 |
73 | test_that("handle can be printed", {
74 | on.exit(terminate_gracefully(handle))
75 | handle <- R_child()
76 |
77 | path <- gsub("\\\\", "\\\\\\\\", normalizePath(R_binary()))
78 | expect_output(print(handle),
79 | paste0("Process Handle\n",
80 | "command : ", path, " --slave\n",
81 | "system id : [0-9]*\n",
82 | "state : running"))
83 | })
84 |
--------------------------------------------------------------------------------
/tests/testthat/test-termination.R:
--------------------------------------------------------------------------------
1 | context("termination")
2 |
3 |
4 | # Hopefully there is no other process
5 | windows_process_id <- function (command_line)
6 | {
7 | args <- c("process", "where",
8 | paste0("\"Name='cmd.exe' and CommandLine LIKE '%%",
9 | command_line, "%%' and Name='cmd.exe'\" get ProcessId"))
10 | output <- system2("wmic.exe", args, stdout = TRUE)
11 | as.integer(trimws(grep("\\d+", output, value = TRUE)))
12 | }
13 |
14 |
15 |
16 | test_that("child process is terminated in Windows", {
17 | skip_if_not(is_windows())
18 |
19 | shell_script_child <- shell_script <- tempfile(fileext = '.bat')
20 | write(file = shell_script_child, "waitfor SomethingThatIsNeverHappening /t 100 2>NUL")
21 |
22 | shell_script_parent <- tempfile(fileext = '.bat')
23 | write(file = shell_script_parent, paste('start "subprocess test child" /b',
24 | shell_script_child))
25 |
26 | # start the parent process which in turn spawns a child process
27 | parent_handle <- spawn_process("c:\\Windows\\System32\\cmd.exe",
28 | c("/k", shell_script_parent))
29 | expect_true(process_exists(parent_handle))
30 |
31 | # find the child process' id and make sure it exists now...
32 | child_id <- windows_process_id(basename(shell_script_child))
33 | expect_length(child_id, 1)
34 | expect_true(process_exists(child_id))
35 |
36 | # ... and not after we kill the parent
37 | process_kill(parent_handle)
38 | process_wait(parent_handle, TIMEOUT_INFINITE)
39 | expect_equal(process_state(parent_handle), "terminated")
40 |
41 | # give the child a moment to dissapear from OS tables
42 | expect_true(wait_until_exits(parent_handle))
43 | expect_true(wait_until_exits(child_id))
44 | })
45 |
46 |
47 |
48 | # In RStudio this test will pass even if termination_mode is set to
49 | # "child_only" when run with Ctrl+Shift+T. It's quite possible that
50 | # RStudio creates a new session when running tests and kills that
51 | # session before completing the test run.
52 | #
53 | # This test will, however, fail in plain R if termination_mode is
54 | # set to "child_only".
55 | test_that("child process is terminated in Linux/MacOS/SunOS", {
56 | skip_if_not(is_linux() || is_mac() || is_solaris())
57 |
58 | # the parent shell script will start "sleep" and print its PID
59 | shell <- Sys.getenv("SHELL", '/bin/sh')
60 | shell_script_parent <- tempfile()
61 | shell_script_child <- tempfile()
62 |
63 | write(file = shell_script_parent,
64 | paste0('#!', shell, '\n',
65 | shell, ' ', shell_script_child, ' &', '\n',
66 | 'echo $!', '\n',
67 | 'sleep 50'))
68 | write(file = shell_script_child,
69 | paste0('#!', shell, '\n',
70 | 'sleep 100'))
71 |
72 | # start the parent process which in turn spawns a child process
73 | parent_handle <- spawn_process(shell, shell_script_parent)
74 | expect_true(process_exists(parent_handle))
75 |
76 | # make sure the child exists; this sometimes fails in Linux (seen only
77 | # in runs performed by other people), I'm not really sure why but one
78 | # possibility is that the first shell script already knows its child's
79 | # PID but pgrep cannot see it just yet - that is, a RACE condition;
80 | # we address it by adding a timeout...
81 | start <- proc.time()
82 | child_id <- as.integer(process_read(parent_handle, 'stdout', 1000))
83 |
84 | # now make sure it actually has appeared
85 | expect_true(wait_until_appears(child_id))
86 |
87 | # ... and not after we kill the parent
88 | process_kill(parent_handle)
89 |
90 | expect_true(wait_until_exits(parent_handle))
91 | process_wait(parent_handle, TIMEOUT_INFINITE)
92 | expect_equal(process_state(parent_handle), "terminated")
93 |
94 | expect_true(wait_until_exits(child_id))
95 | expect_false(process_exists(parent_handle))
96 | expect_false(process_exists(child_id))
97 | })
98 |
99 |
100 | # --- closing the stdin stream -----------------------------------------
101 |
102 |
103 | test_that("child exits when stdin is closed", {
104 | on.exit(process_terminate(handle))
105 | handle <- R_child()
106 | expect_true(process_exists(handle))
107 |
108 | process_close_input(handle)
109 | expect_equal(process_wait(handle, TIMEOUT_INFINITE), 0)
110 | expect_equal(process_state(handle), "exited")
111 | })
112 |
113 |
--------------------------------------------------------------------------------
/tests/testthat/test-utf8.R:
--------------------------------------------------------------------------------
1 | context("utf8")
2 |
3 | test_that("C tests pass", {
4 | expect_equal(C_tests_utf8(), "All C tests passed!")
5 | })
6 |
7 |
8 | test_that("multi-byte can come in parts", {
9 | skip_if_not(is_linux() || is_mac() || is_solaris())
10 | skip_if_not(l10n_info()$MBCS)
11 |
12 | print_in_R <- function (handle, text) {
13 | process_write(handle, paste0("cat('", text, "')\n"))
14 | }
15 |
16 | on.exit(terminate_gracefully(handle1))
17 | handle1 <- R_child()
18 |
19 | print_in_R(handle1, "a\\xF0\\x90")
20 | expect_equal(process_read(handle1, timeout = TIMEOUT_INFINITE)$stdout, 'a')
21 |
22 | print_in_R(handle1, "\\x8D\\x88b")
23 | expect_equal(process_read(handle1, timeout = TIMEOUT_INFINITE)$stdout, '\xF0\x90\x8D\x88b')
24 | })
25 |
--------------------------------------------------------------------------------
/vignettes/intro.Rmd:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Introduction to Sub-Processes in R"
3 | author: "Lukasz A. Bartnik"
4 | date: "`r Sys.Date()`"
5 | output: rmarkdown::html_vignette
6 | vignette: >
7 | %\VignetteIndexEntry{Introduction to Sub-Processes in R}
8 | %\VignetteEngine{knitr::rmarkdown}
9 | %\VignetteEncoding{UTF-8}
10 | ---
11 |
12 | ```{r setup, include=FALSE}
13 | library(subprocess)
14 | library(knitr)
15 |
16 | knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
17 | ```
18 |
19 | ## Introduction
20 |
21 | Since R is not really a systems-programming language[^systemslanguage]
22 | some facilities present in other such languages (e.g. C/C++, Python)
23 | haven't been yet brought to R. One of such features is process
24 | management which is understood here as the capability to create,
25 | interact with and control the lifetime of child processes.
26 |
27 | The R package `subprocess` aims at filling this gap by providing the few
28 | basic operations to carry out the aforementioned tasks. The
29 | `spawn_subprocess()` function starts a new child process and returns a
30 | handle which can be later used in `process_read()` and `process_write()`
31 | to send and receive data or in `process_wait()` or `process_terminate()`
32 | to stop the such a process.
33 |
34 | The R `subprocess` package has been designed after the exemplary
35 | Python package which goes by the same. Its documentation can be found
36 | [here](https://docs.python.org/3/library/subprocess.html) and numerous
37 | examples of how it can be used can be found on the Web.
38 |
39 | The R `subprocess` package has been verified to run on **Linux**,
40 | **Windows** and **MacOS**.
41 |
42 |
43 | [^systemslanguage]: "By systems programming I mean writing code that
44 | directly uses hardware resources, has serious resource constraints,
45 | or closely interacts with code that does." Bjarne Stroustrup, "The
46 | C++ Programming Language"
47 |
48 |
49 | ## Design and Implementation
50 |
51 | The main concept in the package is the __handle__ which holds process
52 | identifiers and an __external pointer__ object which in turn is a handle
53 | to a low-level data structure holding various system-level parameters
54 | of the running sub-process.
55 |
56 | A child process, once started, runs until it exits on its own or until
57 | its killed. Its current state as well as its exit status can be obtained
58 | by dedicated API.
59 |
60 | Communication with the child process can be carried out over the three
61 | standard streams: the standard input, standard output and standard error
62 | output. These streams are intercepted on the child process' side and
63 | redirected into three anonymous pipes whose other ends are held by the
64 | parent process and can be accessed through the process __handle__.
65 |
66 | In **Linux** these are regular pipes created with the `pipe()` system call
67 | and opened in the __non-blocking__ mode. All communication takes place
68 | on request and follows the usual OS rules (e.g. the sub-process will
69 | sleep if its output buffer gets filled).
70 |
71 | In **Windows** these pipes are created with the `CreatePipe()` call and
72 | opened in the __blocking__ mode. **Windows** does not support
73 | __non-blocking__ (__overlapped__ in **Windows**-speak) mode for anonymous
74 | pipes. For that reason each stream has an accompanying reader thread.
75 | Reader threads are separated from R interpreter, do not exchange memory
76 | with the R interpreter and will not break the single-thread assumption
77 | under which R operates.
78 |
79 |
80 | ## Introduction to Examples
81 |
82 | Before we move on to examples, let's define a few helper functions
83 | that abstract out the underlying operating system. We will use them
84 | throughout this vignette.
85 |
86 | ```{r helpers}
87 | is_windows <- function () (tolower(.Platform$OS.type) == "windows")
88 |
89 | R_binary <- function () {
90 | R_exe <- ifelse (is_windows(), "R.exe", "R")
91 | return(file.path(R.home("bin"), R_exe))
92 | }
93 | ```
94 |
95 | Just for the record, vignette has been built in
96 | `r ifelse(is_windows(), "Windows", "Linux")`.
97 |
98 | ```{r platform}
99 | ifelse(is_windows(), "Windows", "Linux")
100 | ```
101 |
102 | Now we can load the package and move on to the next section.
103 |
104 | ```{r}
105 | library(subprocess)
106 | ```
107 |
108 |
109 | ## Example: controlling chlid R process
110 |
111 | In this example we spawn a new R process, send a few commands to its
112 | standard input and read the responses from its standard output. First,
113 | let's spawn the child process (and give it a moment to complete the
114 | start-up sequence[^syssleep]):
115 |
116 | ```{r new_child}
117 | handle <- spawn_process(R_binary(), c('--no-save'))
118 | Sys.sleep(1)
119 | ```
120 |
121 | [^syssleep]: Depending on the system load, R can take a few seconds
122 | to start and be ready for input. This is true also for other processes.
123 | Thus, you will see `Sys.sleep()` following `spawn_process()` in almost
124 | every example in this vignette.
125 |
126 | Let's see the description of the child process:
127 | ```{r}
128 | print(handle)
129 | ```
130 |
131 | And now let's see what we can find it the child's output:
132 | ```{r read_from_child}
133 | process_read(handle, PIPE_STDOUT, timeout = 1000)
134 | process_read(handle, PIPE_STDERR)
135 | ```
136 |
137 | The first number in the output is the value returned by `process_write`
138 | which is the number of characters written to standard input of the
139 | child process. The final `character(0)` is the output read from the
140 | standard error stream.
141 |
142 | Next, we create a new variable in child's session. Please notice the
143 | new-line character at the end of the command. It triggers the child
144 | process to process its input.
145 |
146 | ```{r new_n}
147 | process_write(handle, 'n <- 10\n')
148 | process_read(handle, PIPE_STDOUT, timeout = 1000)
149 | process_read(handle, PIPE_STDERR)
150 | ```
151 |
152 | Now it's time to use this variable in a function call:
153 |
154 | ```{r rnorn_n}
155 | process_write(handle, 'rnorm(n)\n')
156 | process_read(handle, PIPE_STDOUT, timeout = 1000)
157 | process_read(handle, PIPE_STDERR)
158 | ```
159 |
160 | Finally, we exit the child process:
161 |
162 | ```{r quit_child}
163 | process_write(handle, 'q(save = "no")\n')
164 | process_read(handle, PIPE_STDOUT, timeout = 1000)
165 | process_read(handle, PIPE_STDERR)
166 | ```
167 |
168 | The last thing is making sure that the child process is no longer alive:
169 |
170 | ```{r verify_child_exited}
171 | process_state(handle)
172 | process_return_code(handle)
173 | ```
174 |
175 | Of course there is little value in running a child R process since there
176 | are multiple other tools that let you do that, like `parallel`, `Rserve`
177 | and `opencpu` to name just a few. However, it's quite easy to imagine
178 | how running a remote shell in this manner enables new ways of
179 | interacting with the environment. Consider running a local shell:
180 |
181 | ```{r spawn_shell}
182 | shell_binary <- function () {
183 | ifelse (tolower(.Platform$OS.type) == "windows",
184 | "C:/Windows/System32/cmd.exe", "/bin/sh")
185 | }
186 |
187 | handle <- spawn_process(shell_binary())
188 | print(handle)
189 | ```
190 |
191 | Now we can interact with the shell sub-process. We send a request to
192 | list the current directory, then give it a moment to process the command
193 | and produce the output (and maybe finish its start-up, too). Finally,
194 | we check its output streams.
195 |
196 | ```{r interact_with_shell}
197 | process_write(handle, "ls\n")
198 | Sys.sleep(1)
199 | process_read(handle, PIPE_STDOUT)
200 | process_read(handle, PIPE_STDERR)
201 | ```
202 |
203 |
204 | ## Advanced techniques
205 |
206 | ### Terminating a child process
207 |
208 | If the child process needs to be terminated one can choose to:
209 |
210 | - send a command on the standard input with `process_write()`
211 | - send the termination signal, `SIGTERM` (**Linux**, **Windows**)
212 | - send the kill signal, `SIGKILL` (**Linux** only)
213 |
214 | Assume the child R process is hung and there is no way to stop it
215 | gracefully. `process_wait(handle, 1000)` waits for 1 second (1000
216 | milliseconds) for the child process to exit. It then returns `NA` and
217 | `process_terminate()` gives `R` a chance to exit graceully. Finally,
218 | `process_kill()` forces it to exit.
219 |
220 |
221 | ```{r signal_child}
222 | sub_command <- "library(subprocess);subprocess:::signal(15,'ignore');Sys.sleep(1000)"
223 | handle <- spawn_process(R_binary(), c('--slave', '-e', sub_command))
224 | Sys.sleep(1)
225 |
226 | # process is hung
227 | process_wait(handle, 1000)
228 | process_state(handle)
229 |
230 | # ask nicely to exit; will be ignored in Linux but not in Windows
231 | process_terminate(handle)
232 | process_wait(handle, 1000)
233 | process_state(handle)
234 |
235 | # forced exit; in Windows the same as the previous call to process_terminate()
236 | process_kill(handle)
237 | process_wait(handle, 1000)
238 | process_state(handle)
239 | ```
240 |
241 | We see that the child process remains running until it receives the
242 | `SIGKILL` signal[^signal]. The final return code (exit status) is the
243 | number of the signal that caused the child process to exit[^status].
244 |
245 | [^termination]: In **Windows**, `process_terminate()` is an alias for
246 | `process_kill()`. They both lead to immediate termination of the child
247 | process.
248 |
249 | [^signal]: The `.Call("C_signal")` in our example is a call to a hidden
250 | C function that `subprocess` provides mainly for the purposes of this
251 | example.
252 |
253 | [^status]: See the `waitpid()` manual page, e.g. [here](https://linux.die.net/man/2/waitpid).
254 |
255 |
256 | ### Sending a signal to the child process
257 |
258 | The last topic we want to cover here is sending an arbitrary[^windowssignals]
259 | signal to the child process. Signals can be listed by looking at the
260 | `signals` variable present in the package. It is constructed
261 | automatically when the package is loaded and its value on **Linux** is
262 | different than on **Windows**. In the example below we see the first
263 | three elements of the **Linux** list of signals.
264 |
265 | ```{r show_three_signals}
266 | length(signals)
267 | signals[1:3]
268 | ```
269 |
270 |
271 | All possible signal identifiers are supported directly from the
272 | `subprocess` package. Signals not supported on the current platform
273 | are set to `NA` and the rest have their OS-specific numbers as their
274 | values.
275 |
276 | ```{r}
277 | ls(pattern = 'SIG', envir = asNamespace('subprocess'))
278 | ```
279 |
280 |
281 | Now we can create a new child process and send an arbitrary using its
282 | handle.
283 |
284 | ```{r eval=FALSE}
285 | handle <- spawn_process(R_binary, '--slave')
286 |
287 | process_send_signal(handle, SIGUSR1)
288 | ```
289 |
290 |
291 | [^windowssignals]: The list of signals supported in **Windows** is much
292 | shorter than the list of signals supported in **Linux** and contains the
293 | following three signals: `SIGTERM`, `CTRL_C_EVENT` and `CTRL_BREAK_EVENT`.
294 |
--------------------------------------------------------------------------------