├── .Rbuildignore
├── .Rprofile
├── .covrignore
├── .github
├── .gitignore
├── CODE_OF_CONDUCT.md
└── workflows
│ ├── R-CMD-check.yaml
│ ├── pkgdown.yaml
│ ├── pr-commands.yaml
│ └── test-coverage.yaml
├── .gitignore
├── .vscode
├── extensions.json
└── settings.json
├── DESCRIPTION
├── LICENSE
├── LICENSE.md
├── NAMESPACE
├── NEWS.md
├── R
├── aaa-rstudio-detect.R
├── callr-package.R
├── check.R
├── error.R
├── eval-bg.R
├── eval.R
├── hook.R
├── load-client.R
├── options.R
├── package.R
├── presets.R
├── processx-forward.R
├── r-process.R
├── r-session.R
├── rcmd-bg.R
├── rcmd-process.R
├── rcmd.R
├── result.R
├── rscript.R
├── run.R
├── script.R
├── setup.R
├── standalone-errors.R
├── test-helpers.R
├── utils.R
└── x-client.R
├── README.Rmd
├── README.md
├── _pkgdown.yml
├── air.toml
├── callr.Rproj
├── codecov.yml
├── inst
├── COPYRIGHTS
├── WORDLIST
├── developer-notes.md
└── header.md
├── man
├── add_hook.Rd
├── callr-package.Rd
├── convert_and_check_my_args.Rd
├── default_repos.Rd
├── figures
│ ├── bg-dark.svg
│ ├── bg-methods-dark.svg
│ ├── bg-methods.svg
│ ├── bg.svg
│ ├── error1-dark.svg
│ ├── error1.svg
│ ├── error2-2-dark.svg
│ ├── error2-2.svg
│ ├── io-2-dark.svg
│ ├── io-2.svg
│ ├── io-dark.svg
│ ├── io.svg
│ ├── packages-dark.svg
│ ├── packages.svg
│ ├── passargsfail-dark.svg
│ ├── passargsfail.svg
│ ├── passargsok-dark.svg
│ ├── passargsok.svg
│ ├── poll-2-dark.svg
│ ├── poll-2.svg
│ ├── poll-3-dark.svg
│ ├── poll-3.svg
│ ├── poll-4-dark.svg
│ ├── poll-4.svg
│ ├── poll-dark.svg
│ ├── poll.svg
│ ├── rcmd-dark.svg
│ ├── rcmd.svg
│ ├── rsession-4-dark.svg
│ ├── rsession-4.svg
│ ├── rsession-5-dark.svg
│ ├── rsession-5.svg
│ ├── rsession-dark.svg
│ ├── rsession.svg
│ ├── rsession2-2-dark.svg
│ ├── rsession2-2.svg
│ ├── rsession2-dark.svg
│ ├── rsession2.svg
│ ├── simple-dark.svg
│ └── simple.svg
├── get_result.Rd
├── new_callr_crash_error.Rd
├── r.Rd
├── r_bg.Rd
├── r_copycat.Rd
├── r_process.Rd
├── r_process_options.Rd
├── r_session.Rd
├── r_session_debug.Rd
├── r_session_options.Rd
├── r_vanilla.Rd
├── rcmd.Rd
├── rcmd_bg.Rd
├── rcmd_copycat.Rd
├── rcmd_process.Rd
├── rcmd_process_options.Rd
├── rcmd_safe_env.Rd
├── reexports.Rd
├── roxygen
│ └── meta.R
├── rscript.Rd
├── rscript_process.Rd
├── rscript_process_options.Rd
└── supported_archs.Rd
├── tests
├── testthat.R
└── testthat
│ ├── _snaps
│ ├── error.md
│ ├── eval.md
│ ├── load-client.md
│ ├── options.md
│ ├── r-bg.md
│ └── r-session.md
│ ├── fixtures
│ ├── D1
│ ├── csomag
│ │ ├── DESCRIPTION
│ │ ├── NAMESPACE
│ │ └── R
│ │ │ └── libpath.R
│ ├── script.R
│ ├── script2.R
│ └── simple.txt
│ ├── helper-mock.R
│ ├── helper.R
│ ├── test-archs.R
│ ├── test-bugs.R
│ ├── test-callback.R
│ ├── test-clean-subprocess.R
│ ├── test-error.R
│ ├── test-eval.R
│ ├── test-function-env.R
│ ├── test-hook.R
│ ├── test-libpath.R
│ ├── test-load-client.R
│ ├── test-messages.R
│ ├── test-options.R
│ ├── test-package.R
│ ├── test-presets.R
│ ├── test-quit.R
│ ├── test-r-bg.R
│ ├── test-r-process.R
│ ├── test-r-session-messages.R
│ ├── test-r-session.R
│ ├── test-rcmd-bg.R
│ ├── test-rcmd-process.R
│ ├── test-rcmd.R
│ ├── test-rscript.R
│ ├── test-spelling.R
│ ├── test-timeout.R
│ └── test-utils.R
└── vignettes
└── r-session.Rmd
/.Rbuildignore:
--------------------------------------------------------------------------------
1 | ^.*\.Rproj$
2 | ^\.Rproj\.user$
3 | ^Makefile$
4 | ^README.Rmd$
5 | ^revdep$
6 | ^README.markdown$
7 | ^src/callr\.so$
8 | ^src/.*\.o$
9 | ^src/tools/px$
10 | ^src/tools/px.exe$
11 | ^docs$
12 | ^_pkgdown\.yml$
13 | ^README\.Rmd$
14 | ^inst/header\.md$
15 | ^README\.html$
16 | ^doc$
17 | ^Meta$
18 | ^r-packages$
19 | ^\.Rprofile$
20 | ^\.github$
21 | ^dev-lib$
22 | ^codecov\.yml$
23 | ^README_files$
24 | ^[.]covrignore$
25 | ^vignettes$
26 | ^man/_cache$
27 | ^README_cache$
28 | ^LICENSE\.md$
29 | ^[\.]?air\.toml$
30 | ^\.vscode$
31 |
--------------------------------------------------------------------------------
/.Rprofile:
--------------------------------------------------------------------------------
1 | # This file is created automatically by `pak:::proj_create()`
2 | # Please do not edit this file
3 | .libPaths(unique(c(file.path('r-packages', R.version$platform, getRversion()[,1:2]),.libPaths())))
4 | if (file.exists('~/.Rprofile')) source('~/.Rprofile')
5 |
--------------------------------------------------------------------------------
/.covrignore:
--------------------------------------------------------------------------------
1 | R/errors.R
2 |
--------------------------------------------------------------------------------
/.github/.gitignore:
--------------------------------------------------------------------------------
1 | *.html
2 |
--------------------------------------------------------------------------------
/.github/workflows/R-CMD-check.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | #
4 | # NOTE: This workflow is overkill for most R packages and
5 | # check-standard.yaml is likely a better choice.
6 | # usethis::use_github_action("check-standard") will install it.
7 | on:
8 | push:
9 | branches: [main, master]
10 | pull_request:
11 | workflow_dispatch:
12 |
13 | name: R-CMD-check.yaml
14 |
15 | permissions: read-all
16 |
17 | jobs:
18 | R-CMD-check:
19 | runs-on: ${{ matrix.config.os }}
20 |
21 | name: ${{ matrix.config.os }} (${{ matrix.config.r }})
22 |
23 | strategy:
24 | fail-fast: false
25 | matrix:
26 | config:
27 | - {os: macos-latest, r: 'release'}
28 |
29 | - {os: windows-latest, r: 'release'}
30 | # use 4.0 or 4.1 to check with rtools40's older compiler
31 | - {os: windows-latest, r: 'oldrel-4'}
32 |
33 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'}
34 | - {os: ubuntu-latest, r: 'release'}
35 | - {os: ubuntu-latest, r: 'oldrel-1'}
36 | - {os: ubuntu-latest, r: 'oldrel-2'}
37 | - {os: ubuntu-latest, r: 'oldrel-3'}
38 | - {os: ubuntu-latest, r: 'oldrel-4'}
39 |
40 | env:
41 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
42 | R_KEEP_PKG_SOURCE: yes
43 |
44 | steps:
45 | - uses: actions/checkout@v4
46 |
47 | - uses: r-lib/actions/setup-pandoc@v2
48 |
49 | - uses: r-lib/actions/setup-r@v2
50 | with:
51 | r-version: ${{ matrix.config.r }}
52 | http-user-agent: ${{ matrix.config.http-user-agent }}
53 | use-public-rspm: true
54 |
55 | - uses: r-lib/actions/setup-r-dependencies@v2
56 | with:
57 | extra-packages: any::rcmdcheck
58 | needs: check
59 |
60 | - uses: r-hub/actions/debug-shell@v1
61 |
62 | - uses: r-lib/actions/check-r-package@v2
63 | with:
64 | upload-snapshots: true
65 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")'
66 |
--------------------------------------------------------------------------------
/.github/workflows/pkgdown.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 | release:
8 | types: [published]
9 | workflow_dispatch:
10 |
11 | name: pkgdown.yaml
12 |
13 | permissions: read-all
14 |
15 | jobs:
16 | pkgdown:
17 | runs-on: ubuntu-latest
18 | # Only restrict concurrency for non-PR jobs
19 | concurrency:
20 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }}
21 | env:
22 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
23 | R_CLI_NUM_COLORS: 256
24 | permissions:
25 | contents: write
26 | steps:
27 | - uses: actions/checkout@v4
28 |
29 | - uses: r-lib/actions/setup-pandoc@v2
30 |
31 | - uses: r-lib/actions/setup-r@v2
32 | with:
33 | use-public-rspm: true
34 |
35 | - uses: r-lib/actions/setup-r-dependencies@v2
36 | with:
37 | extra-packages: any::pkgdown, local::.
38 | needs: website
39 |
40 | - name: Build site
41 | run: |
42 | rmarkdown::render("README.Rmd")
43 | pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE)
44 | shell: Rscript {0}
45 | env:
46 | IN_PKGDOWN: true
47 |
48 | - name: Deploy to GitHub pages 🚀
49 | if: github.event_name != 'pull_request'
50 | uses: JamesIves/github-pages-deploy-action@v4.5.0
51 | with:
52 | clean: false
53 | branch: gh-pages
54 | folder: docs
55 |
--------------------------------------------------------------------------------
/.github/workflows/pr-commands.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | name: pr-commands.yaml
8 |
9 | permissions: read-all
10 |
11 | jobs:
12 | document:
13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }}
14 | name: document
15 | runs-on: ubuntu-latest
16 | env:
17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
18 | permissions:
19 | contents: write
20 | steps:
21 | - uses: actions/checkout@v4
22 |
23 | - uses: r-lib/actions/pr-fetch@v2
24 | with:
25 | repo-token: ${{ secrets.GITHUB_TOKEN }}
26 |
27 | - uses: r-lib/actions/setup-r@v2
28 | with:
29 | use-public-rspm: true
30 |
31 | - uses: r-lib/actions/setup-r-dependencies@v2
32 | with:
33 | extra-packages: any::roxygen2
34 | needs: pr-document
35 |
36 | - name: Document
37 | run: roxygen2::roxygenise()
38 | shell: Rscript {0}
39 |
40 | - name: commit
41 | run: |
42 | git config --local user.name "$GITHUB_ACTOR"
43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
44 | git add man/\* NAMESPACE
45 | git commit -m 'Document'
46 |
47 | - uses: r-lib/actions/pr-push@v2
48 | with:
49 | repo-token: ${{ secrets.GITHUB_TOKEN }}
50 |
51 | style:
52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }}
53 | name: style
54 | runs-on: ubuntu-latest
55 | env:
56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
57 | permissions:
58 | contents: write
59 | steps:
60 | - uses: actions/checkout@v4
61 |
62 | - uses: r-lib/actions/pr-fetch@v2
63 | with:
64 | repo-token: ${{ secrets.GITHUB_TOKEN }}
65 |
66 | - uses: r-lib/actions/setup-r@v2
67 |
68 | - name: Install dependencies
69 | run: install.packages("styler")
70 | shell: Rscript {0}
71 |
72 | - name: Style
73 | run: styler::style_pkg()
74 | shell: Rscript {0}
75 |
76 | - name: commit
77 | run: |
78 | git config --local user.name "$GITHUB_ACTOR"
79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com"
80 | git add \*.R
81 | git commit -m 'Style'
82 |
83 | - uses: r-lib/actions/pr-push@v2
84 | with:
85 | repo-token: ${{ secrets.GITHUB_TOKEN }}
86 |
--------------------------------------------------------------------------------
/.github/workflows/test-coverage.yaml:
--------------------------------------------------------------------------------
1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples
2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help
3 | on:
4 | push:
5 | branches: [main, master]
6 | pull_request:
7 |
8 | name: test-coverage.yaml
9 |
10 | permissions: read-all
11 |
12 | jobs:
13 | test-coverage:
14 | runs-on: ubuntu-latest
15 | env:
16 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
17 |
18 | steps:
19 | - uses: actions/checkout@v4
20 |
21 | - uses: r-lib/actions/setup-r@v2
22 | with:
23 | use-public-rspm: true
24 |
25 | - uses: r-lib/actions/setup-r-dependencies@v2
26 | with:
27 | extra-packages: any::covr, any::xml2
28 | needs: coverage
29 |
30 | - name: Test coverage
31 | run: |
32 | cov <- covr::package_coverage(
33 | quiet = FALSE,
34 | clean = FALSE,
35 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package")
36 | )
37 | print(cov)
38 | covr::to_cobertura(cov)
39 | shell: Rscript {0}
40 |
41 | - uses: codecov/codecov-action@v5
42 | with:
43 | # Fail if error if not on PR, or if on PR and token is given
44 | fail_ci_if_error: ${{ github.event_name != 'pull_request' || secrets.CODECOV_TOKEN }}
45 | files: ./cobertura.xml
46 | plugins: noop
47 | disable_search: true
48 | token: ${{ secrets.CODECOV_TOKEN }}
49 |
50 | - name: Show testthat output
51 | if: always()
52 | run: |
53 | ## --------------------------------------------------------------------
54 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true
55 | shell: bash
56 |
57 | - name: Upload test results
58 | if: failure()
59 | uses: actions/upload-artifact@v4
60 | with:
61 | name: coverage-test-failures
62 | path: ${{ runner.temp }}/package
63 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .Rproj.user
2 | .Rhistory
3 | .RData
4 | /revdep
5 | /src/*.o
6 | /src/unix/*.o
7 | /src/win/*.o
8 | /src/supervisor/supervisor
9 | /src/supervisor/supervisor.exe
10 | /src/supervisor/supervisor.dSYM
11 | /src/tools/px
12 | /src/tools/px.exe
13 | /src/tools/px.dSYM
14 | /src/callr.so
15 | /README.html
16 | Meta
17 | /r-packages
18 | /docs
19 | /dev-lib
20 | /README_files
21 | /vignettes/*.md
22 | /vignettes/*.html
23 | /vignettes/*_files
24 | /man/_cache
25 | /README_CACHE
26 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "Posit.air-vscode"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "[r]": {
3 | "editor.formatOnSave": true,
4 | "editor.defaultFormatter": "Posit.air-vscode"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/DESCRIPTION:
--------------------------------------------------------------------------------
1 | Package: callr
2 | Title: Call R from R
3 | Version: 3.7.6.9000
4 | Authors@R: c(
5 | person("Gábor", "Csárdi", , "csardi.gabor@gmail.com", role = c("aut", "cre", "cph"),
6 | comment = c(ORCID = "0000-0001-7098-9676")),
7 | person("Winston", "Chang", role = "aut"),
8 | person("Posit Software, PBC", role = c("cph", "fnd"),
9 | comment = c(ROR = "03wc8by49")),
10 | person("Ascent Digital Services", role = c("cph", "fnd"))
11 | )
12 | Description: It is sometimes useful to perform a computation in a separate
13 | R process, without affecting the current R process at all. This
14 | packages does exactly that.
15 | License: MIT + file LICENSE
16 | URL: https://callr.r-lib.org, https://github.com/r-lib/callr
17 | BugReports: https://github.com/r-lib/callr/issues
18 | Depends:
19 | R (>= 3.4)
20 | Imports:
21 | processx (>= 3.6.1),
22 | R6,
23 | utils
24 | Suggests:
25 | asciicast (>= 2.3.1),
26 | cli (>= 1.1.0),
27 | ps,
28 | rprojroot,
29 | spelling,
30 | testthat (>= 3.2.0),
31 | withr (>= 2.3.0)
32 | Config/Needs/website:
33 | r-lib/asciicast,
34 | glue,
35 | htmlwidgets,
36 | igraph,
37 | tibble,
38 | tidyverse/tidytemplate
39 | Config/testthat/edition: 3
40 | Config/usethis/last-upkeep: 2025-04-28
41 | Encoding: UTF-8
42 | Language: en-US
43 | Roxygen: list(markdown = TRUE)
44 | RoxygenNote: 7.3.2
45 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | YEAR: 2025
2 | COPYRIGHT HOLDER: callr authors, see COPYRIGHTS file
3 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | # MIT License
2 |
3 | Copyright (c) 2025 callr authors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/NAMESPACE:
--------------------------------------------------------------------------------
1 | # Generated by roxygen2: do not edit by hand
2 |
3 | S3method(format,callr_status_error)
4 | S3method(print,callr_status_error)
5 | export(add_hook)
6 | export(default_repos)
7 | export(poll)
8 | export(process)
9 | export(r)
10 | export(r_bg)
11 | export(r_copycat)
12 | export(r_process)
13 | export(r_process_options)
14 | export(r_safe)
15 | export(r_session)
16 | export(r_session_options)
17 | export(r_vanilla)
18 | export(rcmd)
19 | export(rcmd_bg)
20 | export(rcmd_copycat)
21 | export(rcmd_process)
22 | export(rcmd_process_options)
23 | export(rcmd_safe)
24 | export(rcmd_safe_env)
25 | export(rscript)
26 | export(rscript_process)
27 | export(rscript_process_options)
28 | export(run)
29 | export(supported_archs)
30 | importFrom(processx,poll)
31 | importFrom(processx,process)
32 | importFrom(processx,run)
33 |
--------------------------------------------------------------------------------
/R/callr-package.R:
--------------------------------------------------------------------------------
1 | #' Call R from R
2 | #'
3 | #' @description
4 | #' It is sometimes useful to perform a computation in a separate
5 | #' R process, without affecting the current R process at all. This
6 | #' packages does exactly that.
7 | #'
8 | #' # callr
9 | #'
10 | #' ```{r man-readme, child = "README.Rmd"}
11 | #' ```
12 | #'
13 | #' @keywords internal
14 | "_PACKAGE"
15 |
16 | ## usethis namespace: start
17 | ## usethis namespace: end
18 | NULL
19 |
--------------------------------------------------------------------------------
/R/check.R:
--------------------------------------------------------------------------------
1 | #' Convert and check function arguments
2 | #'
3 | #' This function is used for all variants of `r` and `rcmd`. An argument
4 | #' name is only used to refer to one kind of object, to make this possible.
5 | #'
6 | #' The benefit of having a single `options` object is to avoid passing
7 | #' around a lot of arguments all the time.
8 | #'
9 | #' The benefit of making this object internal (i.e. that the `r`, etc.
10 | #' functions have multiple arguments instead of a single `options` list),
11 | #' is that documentation and usage is more user friendly (e.g. command-
12 | #' completion works in the editor.
13 | #'
14 | #' @param options List of options.
15 | #'
16 | #' @keywords internal
17 |
18 | convert_and_check_my_args <- function(options) {
19 | has <- function(x) x %in% names(options)
20 | no <- function(x) !has(x)
21 |
22 | ## Conversions
23 | options <- within(options, {
24 | if (has("libpath")) libpath <- as.character(libpath)
25 | if (has("repos")) repos <- repos
26 | if (has("stdout") && !is.null(stdout)) {
27 | stdout <- as.character(stdout)
28 | }
29 | if (has("stderr") && !is.null(stderr)) {
30 | stderr <- as.character(stderr)
31 | }
32 | if (has("error")) error <- error[1]
33 | if (has("cmdargs")) cmdargs <- as.character(cmdargs)
34 | if (has("timeout") && !inherits(timeout, "difftime")) {
35 | timeout <- as.difftime(
36 | as.double(timeout),
37 | units = "secs"
38 | )
39 | }
40 | if (no("wd")) wd <- "."
41 | if (no("echo")) echo <- FALSE
42 | if (no("fail_on_status")) fail_on_status <- FALSE
43 | if (no("tmp_files")) tmp_files <- character()
44 | if (no("package")) package <- FALSE
45 | if (no("arch")) arch <- "same"
46 | })
47 |
48 | ## Checks
49 | with(
50 | options,
51 | stopifnot(
52 | no("func") || is.function(func),
53 | no("func") || is.list(args),
54 | is.character(libpath),
55 | no("stdout") || is.null(stdout) || is_string(stdout),
56 | no("stderr") || is.null(stderr) || is_string(stderr),
57 | no("error") || is_string(error),
58 | is.character(cmdargs),
59 | no("echo") || is_flag(echo),
60 | no("show") || is_flag(show),
61 | no("callback") || is.null(callback) || is.function(callback),
62 | no("block_callback") ||
63 | is.null(block_callback) ||
64 | is.function(block_callback),
65 | no("spinner") || is_flag(spinner),
66 | is_flag(system_profile),
67 | is_flag(user_profile) || identical(user_profile, "project"),
68 | is.character(env),
69 | no("timeout") || (length(timeout) == 1 && !is.na(timeout)),
70 | no("wd") || is_string(wd),
71 | no("fail_on_status") || is_flag(fail_on_status),
72 | is_string(package) || is_flag(package),
73 | is_string(arch)
74 | )
75 | )
76 |
77 | options
78 | }
79 |
--------------------------------------------------------------------------------
/R/error.R:
--------------------------------------------------------------------------------
1 | #' Create an error object
2 | #'
3 | #' There are two kinds of errors, both have class `callr_error`:
4 | #' 1. the first one is thrown after a timeout: `callr_timeout_error`.
5 | #' 2. the second one is thrown after an R error (in the other session):
6 | #' `callr_status_error`.
7 | #'
8 | #' @param out The object returned by [run()].
9 | #' @param msg An extra message to add to the error message.
10 | #' @keywords internal
11 |
12 | new_callr_crash_error <- function(out, msg = NULL) {
13 | error_msg <- paste0(
14 | if (out$timeout) "callr timed out" else "callr subprocess failed",
15 | if (!is.null(msg)) paste0(": ", msg) else if (!out$timeout) ":"
16 | )
17 |
18 | cond <- new_error(paste(error_msg))
19 |
20 | class(cond) <- c(
21 | if (out$timeout) "callr_timeout_error" else "callr_status_error",
22 | "callr_error",
23 | class(cond)
24 | )
25 |
26 | cond$status <- out$status
27 | cond$stdout <- out$stdout
28 | cond$stderr <- out$stderr
29 |
30 | cond
31 | }
32 |
33 | callr_remote_error <- function(remerr, out) {
34 | if (inherits(remerr[[3]], "interrupt")) {
35 | err <- new_error("interrupt sugnal in callr subprocess", call. = FALSE)
36 | class(err) <- c("callr_timeout_error", "callr_error", class(err))
37 | err$message <- "callr subprocess interrupted"
38 | } else {
39 | err <- new_error("in callr subprocess.", call. = FALSE)
40 | class(err) <- c("callr_status_error", "callr_error", class(err))
41 | }
42 |
43 | err$status <- out$status
44 | err$stdout <- out$stdout
45 | err$stderr <- out$stderr
46 |
47 | err$parent_trace <- remerr[[2]]$trace
48 | err
49 | }
50 |
51 | callr_remote_error_with_stack <- function(remerr, out) {
52 | err <- new_error("in callr subprocess.", call. = FALSE)
53 | class(err) <- c("callr_status_error", "callr_error", class(err))
54 |
55 | err$status <- out$status
56 | err$stdout <- out$stdout
57 | err$stderr <- out$stderr
58 |
59 | err$stack <- clean_stack(remerr[[3]])
60 | err
61 | }
62 |
63 | #' @export
64 |
65 | format.callr_status_error <- function(
66 | x,
67 | trace = FALSE,
68 | class = FALSE,
69 | advice = !trace,
70 | ...
71 | ) {
72 | class(x) <- setdiff(class(x), "callr_status_error")
73 |
74 | lines <- NextMethod(
75 | object = x,
76 | trace = FALSE,
77 | class = class,
78 | advice = FALSE,
79 | ...
80 | )
81 |
82 | info <- if (err$.internal$has_cli()) {
83 | cli::col_cyan(cli::symbol$info)
84 | } else {
85 | "i" # nocov
86 | }
87 |
88 | if (!is.null(x$stack)) {
89 | lines <- c(
90 | lines,
91 | paste0(
92 | info,
93 | " With remote `$stack`, use `utils::debugger()` to debug it."
94 | )
95 | )
96 | }
97 |
98 | notempty <- function(x) !is.null(x) && sum(nchar(x)) > 0
99 | hasout <- notempty(x$stdout)
100 | haserr <- notempty(x$stderr)
101 | if (hasout || haserr) {
102 | if (err$.internal$is_interactive()) {
103 | lines <- c(
104 | lines,
105 | if (hasout && haserr) {
106 | paste0(
107 | info,
108 | " See `$stdout` and `$stderr` for standard output and error."
109 | )
110 | } else if (hasout) {
111 | paste0(info, " See `$stdout` for standard output.")
112 | } else {
113 | paste0(info, " See `$stderr` for standard error.")
114 | }
115 | )
116 | } else {
117 | lines <- c(
118 | lines,
119 | if (hasout) {
120 | c(
121 | "---",
122 | "Standard output:",
123 | trimws(x$stdout)
124 | )
125 | },
126 | if (haserr) {
127 | c("---", "Standard error:", trimws(x$stderr))
128 | }
129 | )
130 | }
131 | }
132 |
133 | lines <- c(
134 | lines,
135 | if (advice) err$format$advice(),
136 | if (trace && !is.null(x$trace)) {
137 | c(
138 | "---",
139 | "Backtrace:",
140 | err$format$trace(x$trace)
141 | )
142 | }
143 | )
144 |
145 | cond <- x
146 | while (trace && !is.null(cond$parent_trace)) {
147 | lines <- c(
148 | lines,
149 | c("---", "Subprocess backtrace:", format(cond$parent_trace))
150 | )
151 | cond <- cond$parent
152 | }
153 |
154 | lines
155 | }
156 |
157 | #' @export
158 |
159 | print.callr_status_error <- function(
160 | x,
161 | trace = TRUE,
162 | class = TRUE,
163 | advice = !trace,
164 | ...
165 | ) {
166 | writeLines(format(x, trace = trace, class = class, advice = advice, ...))
167 | }
168 |
--------------------------------------------------------------------------------
/R/eval-bg.R:
--------------------------------------------------------------------------------
1 | #' Evaluate an expression in another R session, in the background
2 | #'
3 | #' Starts evaluating an R function call in a background R process, and
4 | #' returns immediately.
5 | #' Use `p$get_result()` to collect the result or to throw an error
6 | #' if the background computation failed.
7 | #'
8 | #' @inheritSection r Security considerations
9 | #' @inheritParams r
10 | #' @param supervise Whether to register the process with a supervisor. If \code{TRUE},
11 | #' the supervisor will ensure that the process is killed when the R process
12 | #' exits.
13 | #' @param ... Extra arguments are passed to the [processx::process]
14 | #' constructor.
15 | #' @return An `r_process` object, which inherits from [process],
16 | #' so all `process` methods can be called on it, and in addition it also
17 | #' has a `get_result()` method to collect the result.
18 | #'
19 | #' @export
20 | #' @examplesIf FALSE
21 | #' rx <- r_bg(function() 1 + 2)
22 | #'
23 | #' # wait until it is done
24 | #' rx$wait()
25 | #' rx$is_alive()
26 | #' rx$get_result()
27 |
28 | r_bg <- function(
29 | func,
30 | args = list(),
31 | libpath = .libPaths(),
32 | repos = default_repos(),
33 | stdout = "|",
34 | stderr = "|",
35 | poll_connection = TRUE,
36 | error = getOption("callr.error", "error"),
37 | cmdargs = c("--slave", "--no-save", "--no-restore"),
38 | system_profile = FALSE,
39 | user_profile = "project",
40 | env = rcmd_safe_env(),
41 | supervise = FALSE,
42 | package = FALSE,
43 | arch = "same",
44 | ...
45 | ) {
46 | options <- as.list(environment())
47 | options$extra <- list(...)
48 | options$load_hook <- default_load_hook()
49 | r_process$new(options = options)
50 | }
51 |
--------------------------------------------------------------------------------
/R/hook.R:
--------------------------------------------------------------------------------
1 | common_hook <- function() {
2 | substitute(
3 | {
4 | # This should not happen in a new R session, but just to be safe
5 | while ("tools:callr" %in% search()) detach("tools:callr")
6 | env <- readRDS(`__envfile__`)
7 | do.call("attach", list(env, pos = length(search()), name = "tools:callr"))
8 | data <- env$`__callr_data__`
9 | data$pxlib <- data$load_client_lib(
10 | data$sofile[[paste0("arch-", .Platform$r_arch)]],
11 | data$pxdir
12 | )
13 | options(error = function() invokeRestart("abort"))
14 | rm(list = c("data", "env"))
15 |
16 | lapply(
17 | c(
18 | "R_ENVIRON",
19 | "R_ENVIRON_USER",
20 | "R_PROFILE",
21 | "R_PROFILE_USER",
22 | "R_LIBS",
23 | "R_LIBS_USER",
24 | "R_LIBS_SITE"
25 | ),
26 | function(var) {
27 | bakvar <- paste0("CALLR_", var, "_BAK")
28 | val <- Sys.getenv(bakvar, NA_character_)
29 | if (!is.na(val)) {
30 | do.call("Sys.setenv", structure(list(val), names = var))
31 | } else {
32 | Sys.unsetenv(var)
33 | }
34 | Sys.unsetenv(bakvar)
35 | }
36 | )
37 |
38 | Sys.unsetenv("CALLR_CHILD_R_LIBS")
39 | Sys.unsetenv("CALLR_CHILD_R_LIBS_SITE")
40 | Sys.unsetenv("CALLR_CHILD_R_LIBS_USER")
41 | },
42 | list("__envfile__" = env_file)
43 | )
44 | }
45 |
46 | default_load_hook <- function(user_hook = NULL) {
47 | prepare_client_files()
48 | hook <- common_hook()
49 |
50 | if (!is.null(user_hook)) {
51 | hook <- substitute(
52 | {
53 | d
54 | u
55 | },
56 | list(d = hook, u = user_hook)
57 | )
58 | }
59 | paste0(deparse(hook), "\n")
60 | }
61 |
62 | session_load_hook <- function(user_hook = NULL) {
63 | chook <- common_hook()
64 | ehook <- substitute({
65 | data <- as.environment("tools:callr")$`__callr_data__`
66 | data$pxlib$disable_fd_inheritance()
67 | rm(data)
68 | })
69 |
70 | hook <- substitute(
71 | {
72 | c
73 | e
74 | },
75 | list(c = chook, e = ehook)
76 | )
77 |
78 | if (!is.null(user_hook)) {
79 | hook <- substitute(
80 | {
81 | d
82 | u
83 | },
84 | list(d = hook, u = user_hook)
85 | )
86 | }
87 |
88 | hook <- substitute(
89 | {
90 | err_ <- TRUE
91 | callr_startup_hook <- function() {
92 | on.exit(if (err_) quit("no", 1, TRUE))
93 | {
94 | `_hook_`
95 | }
96 | err_ <<- FALSE
97 | }
98 | callr_startup_hook()
99 | rm(err_, callr_startup_hook)
100 | },
101 | list("_hook_" = hook)
102 | )
103 |
104 | paste0(deparse(hook), "\n")
105 | }
106 |
107 | user_hooks_env <- new.env(parent = emptyenv())
108 |
109 | #' Add a user hook to be executed before launching an R subprocess
110 | #'
111 | #' This function allows users of `callr` to specify functions that get invoked
112 | #' whenever an R session is launched. The function can modify the environment
113 | #' variables and command line arguments.
114 | #'
115 | #' The prototype of the hook function is `function (options)`, and it is
116 | #' expected to return the modified `options`.
117 | #' @param ... Named argument specifying a hook function to add, or `NULL` to
118 | #' delete the named hook.
119 | #' @return `add_hook` is called for its side-effects.
120 | #' @export
121 |
122 | add_hook <- function(...) {
123 | args <- list(...)
124 | if (length(args) != 1L) {
125 | stop("More than one argument passed to `add_hook`")
126 | }
127 |
128 | name <- names(args)
129 | if (is.null(name)) {
130 | stop("Argument passed to `add_hook` must be named")
131 | }
132 |
133 | hook <- args[[1]]
134 | if (is.null(hook)) {
135 | rm(list = name, envir = user_hooks_env)
136 | } else {
137 | assign(name, match.fun(hook), envir = user_hooks_env)
138 | }
139 |
140 | invisible()
141 | }
142 |
143 | call_user_hooks <- function(options) {
144 | for (name in names(user_hooks_env)) {
145 | hook <- user_hooks_env[[name]]
146 | options <- tryCatch(hook(options), error = function(e) options)
147 | }
148 | options
149 | }
150 |
--------------------------------------------------------------------------------
/R/load-client.R:
--------------------------------------------------------------------------------
1 | load_client_lib <- function(sofile = NULL, pxdir = NULL) {
2 | ext <- .Platform$dynlib.ext
3 | sofile_in_processx <- function() {
4 | arch <- .Platform$r_arch
5 | if (!is.null(pxdir)) {
6 | sofile <- file.path(pxdir, "libs", arch, paste0("client", ext))
7 | if (file.exists(sofile)) return(sofile)
8 | }
9 |
10 | sofile <- system.file(
11 | "libs",
12 | arch,
13 | paste0("client", ext),
14 | package = "processx"
15 | )
16 | if (sofile != "" && file.exists(sofile)) return(sofile)
17 |
18 | # Try this as well, this is for devtools/pkgload
19 | sofile <- system.file(
20 | "src",
21 | paste0("client", ext),
22 | package = "processx"
23 | )
24 | if (sofile != "" && file.exists(sofile)) return(sofile) # nocov
25 |
26 | # stop() here and not throw(), because this function should be standalone
27 | stop("Cannot find client file")
28 | }
29 |
30 | # We set this to `FALSE` if we load the library from the processx
31 | # install path since that library might be shared (e.g. in tests)
32 | need_cleanup <- TRUE
33 |
34 | if (is.null(sofile) || Sys.getenv("CALLR_NO_TEMP_DLLS", "false") == "true") {
35 | sofile <- sofile_in_processx()
36 | lib <- dyn.load(sofile)
37 | need_cleanup <- FALSE
38 | } else {
39 | # This is the usual case, first we try loading it from the
40 | # temporary directory. If that fails (e.g. noexec), then
41 | # from processx. We saved the location of processx when we
42 | # loaded callr, just in case the used changes the lib path.
43 | lib <- tryCatch(dyn.load(sofile), error = function(err) err)
44 | if (inherits(lib, "error")) {
45 | sofile <- sofile_in_processx()
46 | tryCatch(
47 | expr = {
48 | lib <- dyn.load(sofile)
49 | need_cleanup <- FALSE
50 | },
51 | error = function(err2) {
52 | err2$message <- err2$message <- paste0(" after ", lib$message)
53 | stop(err2)
54 | }
55 | )
56 | }
57 | }
58 |
59 | # cleanup if setup fails
60 | if (need_cleanup) {
61 | on.exit(try(dyn.unload(sofile), silent = TRUE), add = TRUE)
62 | }
63 |
64 | sym_encode <- getNativeSymbolInfo("processx_base64_encode", lib)
65 | sym_decode <- getNativeSymbolInfo("processx_base64_decode", lib)
66 | sym_disinh <- getNativeSymbolInfo("processx_disable_inheritance", lib)
67 | sym_write <- getNativeSymbolInfo("processx_write", lib)
68 | sym_setout <- getNativeSymbolInfo("processx_set_stdout", lib)
69 | sym_seterr <- getNativeSymbolInfo("processx_set_stderr", lib)
70 | sym_setoutf <- getNativeSymbolInfo("processx_set_stdout_to_file", lib)
71 | sym_seterrf <- getNativeSymbolInfo("processx_set_stderr_to_file", lib)
72 |
73 | env <- new.env(parent = emptyenv())
74 | env$.path <- sofile
75 | env$.lib <- lib
76 |
77 | mycall <- .Call
78 |
79 | env$base64_encode <- function(x) rawToChar(mycall(sym_encode, x))
80 | env$base64_decode <- function(x) {
81 | if (is.character(x)) {
82 | x <- charToRaw(paste(gsub("\\s+", "", x), collapse = ""))
83 | }
84 | mycall(sym_decode, x)
85 | }
86 |
87 | env$disable_fd_inheritance <- function() mycall(sym_disinh)
88 |
89 | env$write_fd <- function(fd, data) {
90 | if (is.character(data)) data <- charToRaw(paste0(data, collapse = ""))
91 | len <- length(data)
92 | repeat {
93 | written <- mycall(sym_write, fd, data)
94 | len <- len - written
95 | if (len == 0) break
96 | if (written) data <- data[-(1:written)]
97 | Sys.sleep(.1)
98 | }
99 | }
100 |
101 | env$set_stdout <- function(fd, drop = TRUE) {
102 | mycall(sym_setout, as.integer(fd), as.logical(drop))
103 | }
104 |
105 | env$set_stderr <- function(fd, drop = TRUE) {
106 | mycall(sym_seterr, as.integer(fd), as.logical(drop))
107 | }
108 |
109 | env$set_stdout_file <- function(path) {
110 | mycall(sym_setoutf, as.character(path)[1])
111 | }
112 |
113 | env$set_stderr_file <- function(path) {
114 | mycall(sym_seterrf, as.character(path)[1])
115 | }
116 |
117 | env$.finalize <- function() {
118 | if (need_cleanup) {
119 | dyn.unload(env$.path)
120 | }
121 | rm(list = ls(env, all.names = TRUE), envir = env)
122 | }
123 |
124 | penv <- environment()
125 | parent.env(penv) <- baseenv()
126 |
127 | reg.finalizer(
128 | env,
129 | function(e) if (".finalize" %in% names(e)) e$.finalize(),
130 | onexit = TRUE
131 | )
132 |
133 | ## Clear the cleanup method
134 | on.exit(NULL)
135 | env
136 | }
137 |
--------------------------------------------------------------------------------
/R/options.R:
--------------------------------------------------------------------------------
1 | #' Create options for an [r_process] object
2 | #'
3 | #' @param ... Options to override, named arguments.
4 | #' @return A list of options.
5 | #'
6 | #' `r_process_options()` creates a set of options to initialize a new
7 | #' object from the `r_process` class. Its arguments must be named, the
8 | #' names are used as option names. The options correspond to (some of)
9 | #' the arguments of the [r()] function. At least the `func` option must be
10 | #' specified, this is the R function to run in the background.
11 | #'
12 | #' @export
13 | #' @examples
14 | #' ## List all options and their default values:
15 | #' r_process_options()
16 |
17 | r_process_options <- function(...) {
18 | update_options(r_process_options_default(), ...)
19 | }
20 |
21 | #' Create options for an [rcmd_process] object
22 | #'
23 | #' @param ... Options to override, named arguments.
24 | #' @return A list of options.
25 | #'
26 | #' `rcmd_process_options()` creates a set of options to initialize a new
27 | #' object from the `rcmd_process` class. Its arguments must be named, the
28 | #' names are used as option names. The options correspond to (some of)
29 | #' the arguments of the [rcmd()] function. At least the `cmd` option must
30 | #' be specified, to select the `R CMD` subcommand to run. Typically
31 | #' `cmdargs` is specified as well, to supply more arguments to `R CMD`.
32 | #'
33 | #' @export
34 | #' @examples
35 | #' ## List all options and their default values:
36 | #' rcmd_process_options()
37 |
38 | rcmd_process_options <- function(...) {
39 | update_options(rcmd_process_options_default(), ...)
40 | }
41 |
42 | #' Create options for an [rscript_process] object
43 | #'
44 | #' @param ... Options to override, named arguments.
45 | #' @return A list of options.
46 | #'
47 | #' `rscript_process_options()` creates a set of options to initialize a new
48 | #' object from the `rscript_process` class. Its arguments must be named,
49 | #' the names are used as option names. The options correspond to (some of)
50 | #' the arguments of the [rscript()] function. At least the `script` option
51 | #' must be specified, the script file to run.
52 | #'
53 | #' @export
54 | #' @examples
55 | #' ## List all options and their default values:
56 | #' rscript_process_options()
57 |
58 | rscript_process_options <- function(...) {
59 | update_options(rscript_process_options_default(), ...)
60 | }
61 |
62 | r_process_options_default <- function() {
63 | list(
64 | func = NULL,
65 | args = list(),
66 | libpath = .libPaths(),
67 | repos = default_repos(),
68 | stdout = "|",
69 | stderr = "|",
70 | poll_connection = TRUE,
71 | error = getOption("callr.error", "error"),
72 | cmdargs = c("--slave", "--no-save", "--no-restore"),
73 | system_profile = FALSE,
74 | user_profile = "project",
75 | env = character(),
76 | supervise = FALSE,
77 | load_hook = default_load_hook(),
78 | extra = list(),
79 | package = FALSE,
80 | arch = "same"
81 | )
82 | }
83 |
84 | rcmd_process_options_default <- function() {
85 | list(
86 | cmd = NULL,
87 | cmdargs = character(),
88 | libpath = .libPaths(),
89 | stdout = "|",
90 | stderr = "|",
91 | poll_connection = TRUE,
92 | repos = default_repos(),
93 | system_profile = FALSE,
94 | user_profile = "project",
95 | env = rcmd_safe_env(),
96 | wd = ".",
97 | supervise = FALSE,
98 | extra = list(),
99 | arch = "same"
100 | )
101 | }
102 |
103 | rscript_process_options_default <- function() {
104 | list(
105 | script = NULL,
106 | cmdargs = character(),
107 | libpath = .libPaths(),
108 | stdout = "|",
109 | stderr = "|",
110 | poll_connection = TRUE,
111 | repos = default_repos(),
112 | system_profile = FALSE,
113 | user_profile = "project",
114 | env = rcmd_safe_env(),
115 | wd = ".",
116 | color = FALSE,
117 | extra = list(),
118 | arch = "same"
119 | )
120 | }
121 |
122 | update_options <- function(old_opts, ...) {
123 | new_opts <- list(...)
124 | stopifnot(is.named(new_opts))
125 | check_for_option_names(old_opts, new_opts)
126 | utils::modifyList(old_opts, new_opts)
127 | }
128 |
129 | check_for_option_names <- function(old, new) {
130 | if (length(miss <- setdiff(names(new), names(old)))) {
131 | throw(new_error(
132 | "Unknown option",
133 | if (length(miss) > 1) "s",
134 | ":",
135 | enumerate(sQuote(miss))
136 | ))
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/R/package.R:
--------------------------------------------------------------------------------
1 | ## R CMD check workaround
2 | dummy_r6 <- function() R6::R6Class
3 |
4 | clients <- NULL
5 | sofiles <- NULL
6 | env_file <- NULL
7 |
8 | ## We save this as an RDS, so it can be loaded quickly
9 | .onLoad <- function(libname, pkgname) {
10 | err$onload_hook()
11 | env_file <<- tempfile("callr-env-")
12 | clients <<- asNamespace("processx")$client
13 | sofiles <<- get_client_files()
14 | client_env$`__callr_data__`$sofile <- sofiles
15 | client_env$`__callr_data__`$pxdir <- system.file(package = "processx")
16 | }
17 |
18 | prepare_client_files <- function() {
19 | for (aa in names(client_env$`__callr_data__`$sofile)) {
20 | fn <- client_env$`__callr_data__`$sofile[[aa]]
21 | if (!file.exists(fn)) {
22 | dir.create(dirname(fn), recursive = TRUE)
23 | writeBin(clients[[aa]]$bytes, fn)
24 | }
25 | }
26 |
27 | if (!file.exists(env_file)) {
28 | saveRDS(client_env, file = env_file, version = 2, compress = FALSE)
29 | }
30 | invisible()
31 | }
32 |
33 | get_client_files <- function() {
34 | archs <- ls(clients)
35 | vapply(
36 | archs,
37 | function(aa) {
38 | hash <- substr(clients[[aa]]$md5, 1, 7)
39 |
40 | # Filename must be `client.ext` so that `dyn.load()` can find
41 | # the init function
42 | file.path(
43 | tempdir(),
44 | "callr",
45 | sub("arch-", "", aa), # Might be empty
46 | hash,
47 | paste0("client", .Platform$dynlib.ext)
48 | )
49 | },
50 | character(1)
51 | )
52 | }
53 |
54 | .onUnload <- function(libpath) {
55 | unlink(
56 | normalizePath(c(sofiles, env_file), mustWork = FALSE),
57 | recursive = TRUE,
58 | force = TRUE
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/R/presets.R:
--------------------------------------------------------------------------------
1 | #' Run an R child process, with no configuration
2 | #'
3 | #' It tries to mimic a fresh R installation. In particular:
4 | #' * No library path setting.
5 | #' * No CRAN(-like) repository is set.
6 | #' * The system and user profiles are not run.
7 | #'
8 | #' @param ... Additional arguments are passed to [r()].
9 | #' @inheritParams r
10 | #' @inheritSection r Security considerations
11 | #'
12 | #' @family callr functions
13 | #' @export
14 | #' @examplesIf FALSE
15 | #' # Compare to r()
16 | #' r(function() .libPaths())
17 | #' r_vanilla(function() .libPaths())
18 | #'
19 | #' r(function() getOption("repos"))
20 | #' r_vanilla(function() getOption("repos"))
21 |
22 | r_vanilla <- function(
23 | func,
24 | args = list(),
25 | libpath = character(),
26 | repos = c(CRAN = "@CRAN@"),
27 | cmdargs = "--slave",
28 | system_profile = FALSE,
29 | user_profile = FALSE,
30 | env = character(),
31 | ...
32 | ) {
33 | r(
34 | func,
35 | args = args,
36 | libpath = libpath,
37 | repos = repos,
38 | cmdargs = cmdargs,
39 | system_profile = system_profile,
40 | user_profile = user_profile,
41 | env = env,
42 | ...
43 | )
44 | }
45 |
46 | #' @rdname r
47 | #' @export
48 |
49 | r_safe <- r
50 |
51 | #' Run an R process that mimics the current R process
52 | #'
53 | #' Differences to [r()]:
54 | #' * No extra repositories are set up.
55 | #' * The `--no-save`, `--no-restore`
56 | #' command line arguments are not used. (But `--slave` still is.)
57 | #' * The system profile and the user profile are loaded.
58 | #' * No extra environment variables are set up.
59 | #'
60 | #' @inheritSection r Security considerations
61 | #' @inheritParams r
62 | #' @param ... Additional arguments are passed to [r()].
63 | #'
64 | #' @family callr functions
65 | #' @export
66 |
67 | r_copycat <- function(
68 | func,
69 | args = list(),
70 | libpath = .libPaths(),
71 | repos = getOption("repos"),
72 | cmdargs = "--slave",
73 | system_profile = TRUE,
74 | user_profile = TRUE,
75 | env = character(),
76 | ...
77 | ) {
78 | r(
79 | func,
80 | args = args,
81 | libpath = libpath,
82 | repos = repos,
83 | cmdargs = cmdargs,
84 | system_profile = system_profile,
85 | user_profile = user_profile,
86 | env = env,
87 | ...
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/R/processx-forward.R:
--------------------------------------------------------------------------------
1 | #' @importFrom processx run
2 | #' @export
3 | processx::run
4 |
5 | #' @importFrom processx process
6 | #' @export
7 | processx::process
8 |
9 | #' @importFrom processx poll
10 | #' @export
11 | processx::poll
12 |
--------------------------------------------------------------------------------
/R/r-process.R:
--------------------------------------------------------------------------------
1 | #' External R Process
2 | #'
3 | #' @description
4 | #' An R process that runs in the background. This is an R6 class that
5 | #' extends the [processx::process] class. The process starts in the
6 | #' background, evaluates an R function call, and then quits.
7 | #'
8 | #' @examplesIf FALSE
9 | #' ## List all options and their default values:
10 | #' r_process_options()
11 | #'
12 | #' ## Start an R process in the background, wait for it, get result
13 | #' opts <- r_process_options(func = function() 1 + 1)
14 | #' rp <- r_process$new(opts)
15 | #' rp$wait()
16 | #' rp$get_result()
17 | #' @export
18 |
19 | r_process <- R6::R6Class(
20 | "r_process",
21 | inherit = processx::process,
22 | public = list(
23 | #' @description
24 | #' Start a new R process in the background.
25 | #' @param options A list of options created via [r_process_options()].
26 | #' @return A new `r_process` object.
27 | initialize = function(options) rp_init(self, private, super, options),
28 |
29 | #' @description
30 | #' Return the result, an R object, from a finished
31 | #' background R process. If the process has not finished yet, it throws
32 | #' an error. (You can use `wait()` method (see [processx::process]) to
33 | #' wait for the process to finish, optionally with a timeout.) You can
34 | #' also use [processx::poll()] to wait for the end of the process,
35 | #' together with other processes or events.
36 | #'
37 | #' @return The return value of the R expression evaluated in the R
38 | #' process.
39 | get_result = function() rp_get_result(self, private)
40 | ),
41 | private = list(
42 | options = NULL,
43 | finalize = function() {
44 | unlink(private$options$tmp_files, recursive = TRUE)
45 | if ("finalize" %in% ls(super)) super$finalize()
46 | }
47 | )
48 | )
49 |
50 | rp_init <- function(self, private, super, options) {
51 | ## This contains the context that we set up in steps
52 | options <- convert_and_check_my_args(options)
53 |
54 | options <- setup_script_files(options)
55 | options <- setup_context(options)
56 | options <- setup_r_binary_and_args(options)
57 |
58 | private$options <- options
59 |
60 | with_envvar(
61 | options$env,
62 | do.call(
63 | super$initialize,
64 | c(
65 | list(
66 | options$bin,
67 | options$real_cmdargs,
68 | stdout = options$stdout,
69 | stderr = options$stderr,
70 | poll_connection = options$poll_connection,
71 | supervise = options$supervise
72 | ),
73 | options$extra
74 | )
75 | )
76 | )
77 |
78 | invisible(self)
79 | }
80 |
81 | rp_get_result <- function(self, private) {
82 | if (self$is_alive()) throw(new_error("Still alive"))
83 |
84 | ## This is artificial...
85 | out <- list(
86 | status = self$get_exit_status(),
87 | stdout = "",
88 | stderr = "",
89 | timeout = FALSE
90 | )
91 |
92 | get_result(out, private$options)
93 | }
94 |
--------------------------------------------------------------------------------
/R/rcmd-bg.R:
--------------------------------------------------------------------------------
1 | #' Run an `R CMD` command in the background
2 | #'
3 | #' The child process is started in the background, and the function
4 | #' return immediately.
5 | #'
6 | #' @inheritSection r Security considerations
7 | #' @inheritParams rcmd
8 | #' @param supervise Whether to register the process with a supervisor. If \code{TRUE},
9 | #' the supervisor will ensure that the process is killed when the R process
10 | #' exits.
11 | #' @param ... Extra arguments are passed to the [processx::process]
12 | #' constructor.
13 | #' @return It returns a [process] object.
14 | #'
15 | #' @family R CMD commands
16 | #' @export
17 |
18 | rcmd_bg <- function(
19 | cmd,
20 | cmdargs = character(),
21 | libpath = .libPaths(),
22 | stdout = "|",
23 | stderr = "|",
24 | poll_connection = TRUE,
25 | repos = default_repos(),
26 | system_profile = FALSE,
27 | user_profile = "project",
28 | env = rcmd_safe_env(),
29 | wd = ".",
30 | supervise = FALSE,
31 | ...
32 | ) {
33 | options <- as.list(environment())
34 | options$extra <- list(...)
35 | rcmd_process$new(options = options)
36 | }
37 |
--------------------------------------------------------------------------------
/R/rcmd-process.R:
--------------------------------------------------------------------------------
1 | #' External `R CMD` Process
2 | #'
3 | #' @description
4 | #' An `R CMD *` command that runs in the background. This is an R6 class
5 | #' that extends the [processx::process] class.
6 | #'
7 | #' @examplesIf FALSE
8 | #' options <- rcmd_process_options(cmd = "config", cmdargs = "CC")
9 | #' rp <- rcmd_process$new(options)
10 | #' rp$wait()
11 | #' rp$read_output_lines()
12 | #' @export
13 |
14 | rcmd_process <- R6::R6Class(
15 | "rcmd_process",
16 | inherit = processx::process,
17 | public = list(
18 | #' @description
19 | #' Start an `R CMD` process.
20 | #' @param options A list of options created via
21 | #' [rcmd_process_options()].
22 | #' @return A new `rcmd_process` object.
23 | initialize = function(options) rcmdp_init(self, private, super, options)
24 | ),
25 | private = list(
26 | options = NULL,
27 | finalize = function() {
28 | unlink(private$options$tmp_files, recursive = TRUE)
29 | if ("finalize" %in% ls(super)) super$finalize()
30 | }
31 | )
32 | )
33 |
34 | rcmdp_init <- function(self, private, super, options) {
35 | ## This contains the context that we set up in steps
36 | options <- convert_and_check_my_args(options)
37 |
38 | options <- setup_context(options)
39 | options <- setup_rcmd_binary_and_args(options)
40 |
41 | private$options <- options
42 |
43 | oldwd <- getwd()
44 | setwd(options$wd)
45 | on.exit(setwd(oldwd), add = TRUE)
46 |
47 | with_envvar(
48 | options$env,
49 | do.call(
50 | super$initialize,
51 | c(
52 | list(
53 | options$bin,
54 | options$real_cmdargs,
55 | stdout = options$stdout,
56 | stderr = options$stderr,
57 | poll_connection = options$poll_connection
58 | ),
59 | options$extra
60 | )
61 | )
62 | )
63 |
64 | invisible(self)
65 | }
66 |
--------------------------------------------------------------------------------
/R/rcmd.R:
--------------------------------------------------------------------------------
1 | #' Run an `R CMD` command
2 | #'
3 | #' Run an `R CMD` command form within R. This will usually start
4 | #' another R process, from a shell script.
5 | #'
6 | #' Starting from `callr` 2.0.0, `rcmd()` has safer defaults, the same as
7 | #' the `rcmd_safe()` default values. Use [rcmd_copycat()] for the old
8 | #' defaults.
9 | #'
10 | #' @param cmd Command to run. See `R --help` from the command
11 | #' line for the various commands. In the current version of R (3.2.4)
12 | #' these are: `BATCH`, `COMPILE`, `SHLIB`, `INSTALL`, `REMOVE`, `build`,
13 | #' `check`, `LINK`, `Rprof`, `Rdconv`, `Rd2pdf`, `Rd2txt`, `Stangle`,
14 | #' `Sweave`, `Rdiff`, `config`, `javareconf`, `rtags`.
15 | #' @param cmdargs Command line arguments.
16 | #' @param stdout Optionally a file name to send the standard output to.
17 | #' @param stderr Optionally a file name to send the standard error to.
18 | #' It may be the same as `stdout`, in which case standard error is
19 | #' redirected to standard output. It can also be the special string
20 | #' `"2>&1"`, in which case standard error will be redirected to standard
21 | #' output.
22 | #' @param poll_connection Whether to have a control connection to
23 | #' the process. This is used to transmit messages from the subprocess
24 | #' to the parent.
25 | #' @param echo Whether to echo the complete command run by `rcmd`.
26 | #' @param wd Working directory to use for running the command. Defaults
27 | #' to the current working directory.
28 | #' @param fail_on_status Whether to throw an R error if the command returns
29 | #' with a non-zero status code. By default no error is thrown.
30 | #' @inheritParams r
31 | #' @inheritSection r Security considerations
32 | #' @return A list with the command line `$command`),
33 | #' standard output (`$stdout`), standard error (`stderr`),
34 | #' exit status (`$status`) of the external `R CMD` command, and
35 | #' whether a timeout was reached (`$timeout`).
36 | #'
37 | #' @family R CMD commands
38 | #' @export
39 | #'
40 | #' @examplesIf FALSE
41 | #' rcmd("config", "CC")
42 |
43 | rcmd <- function(
44 | cmd,
45 | cmdargs = character(),
46 | libpath = .libPaths(),
47 | repos = default_repos(),
48 | stdout = NULL,
49 | stderr = NULL,
50 | poll_connection = TRUE,
51 | echo = FALSE,
52 | show = FALSE,
53 | callback = NULL,
54 | block_callback = NULL,
55 | spinner = show && interactive(),
56 | system_profile = FALSE,
57 | user_profile = "project",
58 | env = rcmd_safe_env(),
59 | timeout = Inf,
60 | wd = ".",
61 | fail_on_status = FALSE,
62 | ...
63 | ) {
64 | ## This contains the context that we set up in steps
65 | options <- convert_and_check_my_args(as.list(environment()))
66 | options$extra <- list(...)
67 |
68 | options <- setup_context(options)
69 | options <- setup_callbacks(options)
70 | options <- setup_rcmd_binary_and_args(options)
71 |
72 | ## This cleans up everything...
73 | on.exit(unlink(options$tmp_files, recursive = TRUE), add = TRUE)
74 |
75 | run_r(options)
76 | }
77 |
78 | #' @rdname rcmd
79 | #' @export
80 |
81 | rcmd_safe <- rcmd
82 |
83 | #' `rcmd_safe_env` returns a set of environment variables that are
84 | #' more appropriate for [rcmd_safe()]. It is exported to allow manipulating
85 | #' these variables (e.g. add an extra one), before passing them to the
86 | #' [rcmd()] functions.
87 | #'
88 | #' It currently has the following variables:
89 | #' * `CYGWIN="nodosfilewarning"`: On Windows, do not warn about MS-DOS
90 | #' style file names.
91 | #' * `R_TESTS=""` This variable is set by `R CMD check`, and makes the
92 | #' child R process load a startup file at startup, from the current
93 | #' working directory, that is assumed to be the `/test` directory
94 | #' of the package being checked. If the current working directory is
95 | #' changed to something else (as it typically is by `testthat`, then R
96 | #' cannot start. Setting it to the empty string ensures that `callr` can
97 | #' be used from unit tests.
98 | #' * `R_BROWSER="false"`: typically we don't want to start up a browser
99 | #' from the child R process.
100 | #' * `R_PDFVIEWER="false"`: similarly for the PDF viewer.
101 | #'
102 | #' Note that `callr` also sets the `R_ENVIRON`, `R_ENVIRON_USER`,
103 | #' `R_PROFILE` and `R_PROFILE_USER` environment variables
104 | #' appropriately, unless these are set by the user in the `env` argument
105 | #' of the `r`, etc. calls.
106 | #'
107 | #' @return A named character vector of environment variables.
108 | #'
109 | #' @export
110 |
111 | rcmd_safe_env <- function() {
112 | vars <- c(
113 | CYGWIN = "nodosfilewarning",
114 | R_TESTS = "",
115 | R_BROWSER = "false",
116 | R_PDFVIEWER = "false"
117 | )
118 |
119 | vars
120 | }
121 |
122 | #' Call and `R CMD` command, while mimicking the current R session
123 | #'
124 | #' This function is similar to [rcmd()], but it has slightly different
125 | #' defaults:
126 | #' * The `repos` options is unchanged.
127 | #' * No extra environment variables are defined.
128 | #'
129 | #' @inheritSection r Security considerations
130 | #' @inheritParams rcmd
131 | #' @param ... Additional arguments are passed to [rcmd()].
132 | #'
133 | #' @family R CMD commands
134 | #' @export
135 |
136 | rcmd_copycat <- function(
137 | cmd,
138 | cmdargs = character(),
139 | libpath = .libPaths(),
140 | repos = getOption("repos"),
141 | env = character(),
142 | ...
143 | ) {
144 | rcmd(cmd, cmdargs = cmdargs, libpath = libpath, repos = repos, env = env, ...)
145 | }
146 |
--------------------------------------------------------------------------------
/R/result.R:
--------------------------------------------------------------------------------
1 | #' Read the result object from the output file, or the error
2 | #'
3 | #' Even if an error happens, the output file might still exist,
4 | #' because [saveRDS()] creates the file before evaluating its object
5 | #' argument. So we need to check for the error file to decide
6 | #' if an error happened.
7 | #'
8 | #' @param output List of the output object from [run()] and
9 | #' the name of the result file to read. For the error file,
10 | #' `.error` is appended to this.
11 | #' @param options The context, including all parameters.
12 | #' @return If no error happened, the result is returned. Otherwise
13 | #' we handle the error.
14 | #'
15 | #' @keywords internal
16 |
17 | get_result <- function(output, options) {
18 | res <- options$result_file
19 |
20 | ## Timeout?
21 | if (output$timeout) throw(new_callr_crash_error(output))
22 |
23 | ## No output file and no error file? Some other (system?) error then,
24 | ## unless exit status was zero, which is probably just quit().
25 | ## (Newer R versions do not write a corrupt RDS file in this case.)
26 | ret <- NULL
27 | errorres <- paste0(res, ".error")
28 | killmsg <- paste(
29 | "could not start R, exited with non-zero status,",
30 | "has crashed or was killed"
31 | )
32 | if (!file.exists(res) && !file.exists(errorres)) {
33 | if (is.na(output$status) || output$status != 0) {
34 | throw(new_callr_crash_error(output, killmsg))
35 | } else {
36 | return(ret)
37 | }
38 | }
39 |
40 | ## No error file? Then probably all is well, return the output
41 | ## If this is corrupt, then the R process has crashed
42 | ## This cannot happen from R 3.5.0, because that version only writes
43 | ## out the output file if no error or crash has happened.
44 | ## (Older R versions write a corrupt RDS file in this case.)
45 | if (!file.exists(errorres)) {
46 | tryCatch(
47 | ret <- readRDS(res),
48 | error = function(e) {
49 | if (is.na(output$status) || output$status != 0) {
50 | throw(new_callr_crash_error(output, killmsg))
51 | } else {
52 | throw(new_callr_crash_error(
53 | output,
54 | "could not read result from callr"
55 | ))
56 | }
57 | }
58 | )
59 |
60 | return(ret)
61 | }
62 |
63 | # To work around an errors.R bug, if the format method is registered
64 | # from another package. errors.R expects that the parent error has a
65 | # message, but if it is an interrupt (and perhaps some other rare cases),
66 | # it does not.
67 | fix_msg <- function(cnd) {
68 | if (is.null(cnd$message)) {
69 | if (inherits(cnd, "interrupt")) {
70 | cnd$message <- "interrupt"
71 | } else {
72 | cnd$message <- ""
73 | }
74 | }
75 | cnd
76 | }
77 |
78 | ## The error RDS might be corrupt, too, if we crashed/got killed after
79 | ## an error
80 | tryCatch(
81 | remerr <- readRDS(errorres),
82 | error = function(e) throw(new_callr_crash_error(output, killmsg))
83 | )
84 |
85 | if (remerr[[1]] == "error") {
86 | throw(callr_remote_error(remerr, output), parent = fix_msg(remerr[[3]]))
87 | } else if (remerr[[1]] == "stack") {
88 | throw(
89 | callr_remote_error_with_stack(remerr, output),
90 | parent = fix_msg(remerr[[2]])
91 | )
92 | } else if (remerr[[1]] == "debugger") {
93 | utils::debugger(clean_stack(remerr[[3]]))
94 | } else {
95 | throw(new_error("Unknown callr error strategy: ", remerr[[1]])) # nocov
96 | }
97 | }
98 |
99 | clean_stack <- function(stack) {
100 | att <- attributes(stack)
101 | att$names <- utils::head(utils::tail(att$names, -11), -2)
102 | res <- utils::head(utils::tail(stack, -11), -2)
103 | attributes(res) <- att
104 |
105 | res
106 | }
107 |
--------------------------------------------------------------------------------
/R/rscript.R:
--------------------------------------------------------------------------------
1 | #' Run an R script
2 | #'
3 | #' It uses the `Rscript` program corresponding to the current R version,
4 | #' to run the script. It streams `stdout` and `stderr` of the process.
5 | #'
6 | #' @inheritSection r Security considerations
7 | #' @inheritParams rcmd
8 | #' @param script Path of the script to run.
9 | #' @param color Whether to use terminal colors in the child process,
10 | #' assuming they are active in the parent process.
11 | #'
12 | #' @export
13 |
14 | rscript <- function(
15 | script,
16 | cmdargs = character(),
17 | libpath = .libPaths(),
18 | repos = default_repos(),
19 | stdout = NULL,
20 | stderr = NULL,
21 | poll_connection = TRUE,
22 | echo = FALSE,
23 | show = TRUE,
24 | callback = NULL,
25 | block_callback = NULL,
26 | spinner = FALSE,
27 | system_profile = FALSE,
28 | user_profile = "project",
29 | env = rcmd_safe_env(),
30 | timeout = Inf,
31 | wd = ".",
32 | fail_on_status = TRUE,
33 | color = TRUE,
34 | ...
35 | ) {
36 | load_hook <- rscript_load_hook_color(color)
37 |
38 | options <- convert_and_check_my_args(as.list(environment()))
39 | options$extra <- list(...)
40 |
41 | options <- setup_context(options)
42 | options <- setup_callbacks(options)
43 |
44 | options <- setup_rscript_binary_and_args(options)
45 |
46 | ## This cleans up everything...
47 | on.exit(unlink(options$tmp_files, recursive = TRUE), add = TRUE)
48 |
49 | invisible(run_r(options))
50 | }
51 |
52 | rscript_load_hook_color <- function(color) {
53 | if (!color) return("")
54 |
55 | nc <- tryCatch(
56 | cli::num_ansi_colors(),
57 | error = function(e) 1L
58 | )
59 | if (nc == 1) return("")
60 |
61 | expr <- substitute(
62 | options(crayon.enabled = TRUE, crayon.colors = `_nc_`),
63 | list("_nc_" = nc)
64 | )
65 | paste0(deparse(expr), "\n")
66 | }
67 |
68 | #' External `Rscript` process
69 | #'
70 | #' @description
71 | #' An `Rscript script.R` command that runs in the background. This is an
72 | #' R6 class that extends the [processx::process] class.
73 | #'
74 | #' @name rscript_process
75 | #' @examplesIf FALSE
76 | #' options <- rscript_process_options(script = "script.R")
77 | #' rp <- rscript_process$new(options)
78 | #' rp$wait()
79 | #' rp$read_output_lines()
80 | #' @export
81 |
82 | rscript_process <- R6::R6Class(
83 | "rscript_process",
84 | inherit = processx::process,
85 | public = list(
86 | #' @description Create a new `Rscript` process.
87 | #' @param options A list of options created via
88 | #' [rscript_process_options()].
89 | initialize = function(options) rscript_init(self, private, super, options)
90 | ),
91 | private = list(
92 | options = NULL,
93 | finalize = function() {
94 | unlink(private$options$tmp_files, recursive = TRUE)
95 | if ("finalize" %in% ls(super)) super$finalize()
96 | }
97 | )
98 | )
99 |
100 | rscript_init <- function(self, private, super, options) {
101 | options$load_hook <- rscript_load_hook_color(options$color)
102 | options <- convert_and_check_my_args(options)
103 | options <- setup_context(options)
104 | options <- setup_rscript_binary_and_args(options)
105 |
106 | private$options <- options
107 |
108 | oldwd <- getwd()
109 | setwd(options$wd)
110 | on.exit(setwd(oldwd), add = TRUE)
111 |
112 | with_envvar(
113 | options$env,
114 | do.call(
115 | super$initialize,
116 | c(
117 | list(
118 | options$bin,
119 | options$real_cmdargs,
120 | stdout = options$stdout,
121 | stderr = options$stderr,
122 | poll_connection = options$poll_connection
123 | ),
124 | options$extra
125 | )
126 | )
127 | )
128 |
129 | invisible(self)
130 | }
131 |
--------------------------------------------------------------------------------
/R/run.R:
--------------------------------------------------------------------------------
1 | run_r <- function(options) {
2 | oldwd <- getwd()
3 | setwd(options$wd)
4 | on.exit(setwd(oldwd), add = TRUE)
5 |
6 | ## We redirect stderr to stdout if either of these are true:
7 | ## - stderr is the string "2>&1"
8 | ## - both stdout and stderr are non-null, and they are the same
9 | stderr_to_stdout <- with(
10 | options,
11 | (!is.null(stderr) && stderr == "2>&1") ||
12 | (!is.null(stdout) && !is.null(stderr) && stdout == stderr)
13 | )
14 |
15 | res <- with(
16 | options,
17 | with_envvar(
18 | env,
19 | do.call(
20 | processx::run,
21 | c(
22 | list(
23 | bin,
24 | args = real_cmdargs,
25 | stdout_line_callback = real_callback(stdout),
26 | stderr_line_callback = real_callback(stderr),
27 | stdout_callback = real_block_callback,
28 | stderr_callback = real_block_callback,
29 | stderr_to_stdout = stderr_to_stdout,
30 | echo_cmd = echo,
31 | echo = show,
32 | spinner = spinner,
33 | error_on_status = fail_on_status,
34 | timeout = timeout
35 | ),
36 | extra
37 | )
38 | )
39 | )
40 | )
41 |
42 | res$command <- c(options$bin, options$real_cmdargs)
43 | res
44 | }
45 |
--------------------------------------------------------------------------------
/R/test-helpers.R:
--------------------------------------------------------------------------------
1 | is_true_check_env_var <- function(x, default = "") {
2 | # like utils:::str2logical
3 | val <- Sys.getenv(x, default)
4 | if (isTRUE(as.logical(val))) return(TRUE)
5 | tolower(val) %in% c("1", "yes")
6 | }
7 |
8 | isFALSE <- function(x) {
9 | is.logical(x) && length(x) == 1L && !is.na(x) && !x
10 | }
11 |
12 | is_false_check_env_var <- function(x, default = "") {
13 | # like utils:::str2logical
14 | val <- Sys.getenv(x, default)
15 | if (isFALSE(as.logical(val))) return(TRUE)
16 | tolower(val) %in% c("0", "no")
17 | }
18 |
19 | # Only skip if _R_CHECK_FORCE_SUGGESTS_ is false
20 |
21 | skip_if_not_installed <- function(pkg) {
22 | if (!is_false_check_env_var("_R_CHECK_FORCE_SUGGESTS_")) return()
23 | testthat::skip_if_not_installed(pkg)
24 | }
25 |
--------------------------------------------------------------------------------
/R/utils.R:
--------------------------------------------------------------------------------
1 | #' Default value for the `repos` option in callr subprocesses
2 | #'
3 | #' callr sets the `repos` option in subprocesses, to make sure that
4 | #' a CRAN mirror is set up. This is because the subprocess cannot bring
5 | #' up the menu of CRAN mirrors for the user to choose from.
6 | #'
7 | #' @return Named character vector, the default value of the `repos`
8 | #' option in callr subprocesses.
9 | #'
10 | #' @export
11 | #' @examples
12 | #' default_repos()
13 |
14 | default_repos <- function() {
15 | opt <- getOption("repos")
16 | was_list <- is.list(opt)
17 | if (!"CRAN" %in% names(opt) || opt[["CRAN"]] == "@CRAN@") {
18 | opt[["CRAN"]] <- "https://cloud.r-project.org"
19 | }
20 | if (!was_list) opt <- unlist(opt)
21 | opt
22 | }
23 |
24 | remove_source <- function(x) {
25 | if (is.function(x)) {
26 | body(x) <- remove_source(body(x))
27 | x
28 | } else if (is.call(x)) {
29 | attr(x, "srcref") <- NULL
30 | attr(x, "wholeSrcref") <- NULL
31 | attr(x, "srcfile") <- NULL
32 |
33 | x[] <- lapply(x, remove_source)
34 | x
35 | } else {
36 | x
37 | }
38 | }
39 |
40 | `%||%` <- function(l, r) if (is.null(l)) r else l
41 |
42 | is.named <- function(x) {
43 | length(names(x)) == length(x) && all(names(x) != "")
44 | }
45 |
46 | set_envvar <- function(envs) {
47 | if (length(envs) == 0) return()
48 |
49 | stopifnot(is.named(envs))
50 |
51 | old <- Sys.getenv(names(envs), names = TRUE, unset = NA)
52 | set <- !is.na(envs)
53 |
54 | both_set <- set & !is.na(old)
55 |
56 | if (any(set)) do.call("Sys.setenv", as.list(envs[set]))
57 | if (any(!set)) Sys.unsetenv(names(envs)[!set])
58 |
59 | invisible(old)
60 | }
61 |
62 | with_envvar <- function(new, code) {
63 | old <- set_envvar(new)
64 | on.exit(set_envvar(old))
65 | force(code)
66 | }
67 |
68 | os_platform <- function() .Platform$OS.type
69 |
70 | enumerate <- function(x) {
71 | if (length(x) == 0) {
72 | ""
73 | } else if (length(x) == 1) {
74 | x
75 | } else {
76 | l <- length(x)
77 | paste0(paste(x[-l], collapse = ", "), " and ", x[[l]])
78 | }
79 | }
80 |
81 | ## Thanks to Romain for the idea!
82 | ## https://github.com/romainfrancois/trump/blob/
83 | ## 7845b83343afa356e4259c054e7c9a910034f170/R/trump.R
84 |
85 | crash <- function() {
86 | get("attach")(
87 | structure(
88 | attr(file(), "conn_id"),
89 | class = "UserDefinedDatabase"
90 | )
91 | )
92 | }
93 |
94 | is_flag <- function(x) {
95 | is.logical(x) && length(x) == 1 && !is.na(x)
96 | }
97 |
98 | is_string <- function(x) {
99 | is.character(x) &&
100 | length(x) == 1 &&
101 | !is.na(x)
102 | }
103 |
104 | read_all <- function(filename) {
105 | con <- file(filename, open = "rb")
106 | on.exit(close(con), add = TRUE)
107 | res <- raw(0)
108 | while (length(more <- readBin(con, what = "raw", 10000)) && length(more)) {
109 | res <- c(res, more)
110 | }
111 | rawToChar(res)
112 | }
113 |
114 | is_complete_expression <- function(x) {
115 | err <- NULL
116 | tryCatch(parse(text = x), error = function(e) err <<- e)
117 | if (is.null(err)) return(TRUE)
118 | exp <- tryCatch(parse(text = "1+"), error = function(e) e$message)
119 | exp1 <- strsplit(exp, "\n")[[1]][[1]]
120 | msg <- sub("^.*:\\s*([^:]+)$", "\\1", exp1, perl = TRUE)
121 | !grepl(msg, conditionMessage(err), fixed = TRUE)
122 | }
123 |
124 | bold <- function(x) {
125 | tryCatch(
126 | cli::style_bold(x),
127 | error = function(e) x
128 | )
129 | }
130 |
131 | update_history <- function(cmd) {
132 | tmp <- tempfile("callr-hst-")
133 | on.exit(unlink(tmp, recursive = TRUE))
134 | utils::savehistory(tmp)
135 | cat(cmd, "\n", sep = "", file = tmp, append = TRUE)
136 | utils::loadhistory(tmp)
137 | }
138 |
139 | #' Find supported sub-architectures for the current R installation
140 | #'
141 | #' This function uses a heuristic, which might fail, so its result
142 | #' should be taken as a best guess.
143 | #'
144 | #' @return Character vector of supported architectures. If the
145 | #' current R build is not a multi-architecture build, then an empty
146 | #' string scalar is returned.
147 | #'
148 | #' @export
149 | #' @examples
150 | #' supported_archs()
151 |
152 | supported_archs <- function() {
153 | oldwd <- getwd()
154 | on.exit(setwd(oldwd), add = TRUE)
155 | setwd(file.path(R.home(), "bin"))
156 | archs <- list.dirs(recursive = FALSE)
157 | archs <- sub("^[.]?[/\\\\]", "", archs)
158 | archs <- setdiff(archs, "exec")
159 | if (length(archs) == 0) {
160 | if (nzchar(.Platform$r_arch)) {
161 | archs <- .Platform$r_arch
162 | } else {
163 | archs <- Sys.getenv("R_ARCH")
164 | }
165 | }
166 | archs
167 | }
168 |
--------------------------------------------------------------------------------
/R/x-client.R:
--------------------------------------------------------------------------------
1 | client_env <- local({
2 | env <- new.env(parent = emptyenv())
3 | env$`__callr_data__` <- new.env(parent = baseenv())
4 |
5 | rsfile <- file.path("R", "aaa-rstudio-detect.R")
6 | sys.source(rsfile, envir = env$`__callr_data__`, keep.source = FALSE)
7 | errfile <- file.path("R", "standalone-errors.R")
8 | sys.source(errfile, envir = env$`__callr_data__`, keep.source = FALSE)
9 | loadfile <- file.path("R", "load-client.R")
10 | sys.source(loadfile, envir = env$`__callr_data__`, keep.source = FALSE)
11 |
12 | env
13 | })
14 |
--------------------------------------------------------------------------------
/_pkgdown.yml:
--------------------------------------------------------------------------------
1 | url: https://callr.r-lib.org
2 |
3 | template:
4 | package: tidytemplate
5 | bootstrap: 5
6 | includes:
7 | in_header: |
8 |
9 |
10 | development:
11 | mode: auto
12 |
13 | authors:
14 | "Winston Chang":
15 | href: https://github.com/wch
16 |
17 | navbar:
18 | structure:
19 | left: [reference, articles, news]
20 | components:
21 | articles:
22 | text: Articles
23 | menu:
24 | - text: Tutorials
25 | - text: Persistent R sessions
26 | href: articles/r-session.html
27 | - text: -------
28 | - text: Blog posts
29 | - text: "A multi process task queue in 100 lines of R code"
30 | href: https://www.tidyverse.org/blog/2019/09/callr-task-q
31 |
32 | reference:
33 | - title: Introduction
34 | contents:
35 | - callr
36 |
37 | - title: Run R
38 | contents:
39 | - r
40 | - r_copycat
41 | - r_safe
42 | - r_vanilla
43 |
44 | - title: Run R in the background
45 | contents:
46 | - r_bg
47 | - r_process
48 | - r_process_options
49 |
50 | - title: Persistent background R session
51 | contents:
52 | - r_session
53 | - r_session_options
54 | - r_session_debug
55 |
56 | - title: Run R CMD
57 | contents:
58 | - rcmd
59 | - rcmd_copycat
60 | - rcmd_safe
61 | - rcmd_safe_env
62 |
63 | - title: Run R CMD in the background
64 | contents:
65 | - rcmd_bg
66 | - rcmd_process
67 | - rcmd_process_options
68 |
69 | - title: Run Rscript
70 | contents:
71 | - rscript
72 |
73 | - title: Run Rscript in the background
74 | contents:
75 | - rscript_process
76 | - rscript_process_options
77 |
78 | - title: Miscellaneous utilities
79 | contents:
80 | - default_repos
81 | - supported_archs
82 | - add_hook
83 |
84 | - title: Re-exported functions
85 | contents:
86 | - process
87 | - run
88 | - poll
89 |
--------------------------------------------------------------------------------
/air.toml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/r-lib/callr/565a5090975b5f3c5cca24304bd46f0f7af2ae0b/air.toml
--------------------------------------------------------------------------------
/callr.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: XeLaTeX
14 |
15 | AutoAppendNewline: Yes
16 | StripTrailingWhitespace: Yes
17 |
18 | BuildType: Package
19 | PackageUseDevtools: Yes
20 | PackageInstallArgs: --no-multiarch --with-keep.source
21 | PackageRoxygenize: rd,collate,namespace
22 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 |
3 | coverage:
4 | status:
5 | project:
6 | default:
7 | target: auto
8 | threshold: 1%
9 | informational: true
10 | patch:
11 | default:
12 | target: auto
13 | threshold: 1%
14 | informational: true
15 |
--------------------------------------------------------------------------------
/inst/COPYRIGHTS:
--------------------------------------------------------------------------------
1 | (c) 2015-2016 Ascent Digital Services (formerly Mango Solutions)
2 | (c) 2017 Gábor Csárdi
3 | (c) 2017-2024 Posit Software, PBC (formerly RStudio)
4 |
--------------------------------------------------------------------------------
/inst/WORDLIST:
--------------------------------------------------------------------------------
1 | CMD
2 | CTRL
3 | Codecov
4 | ESC
5 | Eval
6 | Finalizer
7 | ORCID
8 | PBC
9 | REPL
10 | callr's
11 | cli
12 | cliapp
13 | cloneable
14 | dll
15 | env
16 | eval
17 | finalizer
18 | funder
19 | github
20 | https
21 | igraph
22 | interruptible
23 | keyring
24 | lifecycle
25 | macOS
26 | packrat
27 | pkgdown
28 | processx
29 | renv
30 | stderr
31 | stdout
32 | subcommand
33 | subprocess
34 | subprocesses
35 |
--------------------------------------------------------------------------------
/inst/developer-notes.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Design of the internals
4 |
5 | There are a lot of possible ways to implement this package, to minimize
6 | duplication. This is the API we want:
7 |
8 | ```r
9 | r(fun, args, ...)
10 | r_safe(fun, args, ...)
11 |
12 | r_bg(fun, args, ...)
13 | r_bg_safe(fun, args, ...)
14 |
15 | rcmd(cmd, cmdargs, ...)
16 | rcmd_safe(cmd, cmdargs, ...)
17 |
18 | rcmd_bg(cmd, cmdargs, ...)
19 | rcmd_bg_safe(cmd, cmdargs ...)
20 | ```
21 |
22 | The `_safe` versions are easy to deal with, they just call the non-`_safe`
23 | versions with different arguments.
24 |
25 | For the other versions, this is what they need to do:
26 |
27 | ### `r`
28 |
29 | 1. convert / check arguments
30 | 2. save function to a file
31 | 3. create script file
32 | 4. set up profiles
33 | 5. set up library path
34 | 6. set up LIB and PROFILE env vars
35 | 7. set up callbacks (combine show and callbacks)
36 | 8. run the R process
37 | 9. write stdout & stderr to file, if needed
38 | 10. fail on status, if requested
39 | 11. get the result
40 |
41 | ### `r_bg`
42 |
43 | 1. convert / check arguments
44 | 2. save function to a file
45 | 3. create script file
46 | 4. set up profiles
47 | 5. set up library path
48 | 7. set up LIB and PROFILE env vars
49 | 8. start the R process in the bg
50 |
51 | ### `rcmd`
52 |
53 | 1. convert / check arguments
54 | 2. get the R binary and its arguments
55 | 3. set specified wd
56 | 4. set up profiles
57 | 5. set up library path
58 | 6. set up callbacks (combine show and callbacks)
59 | 7. set up LIB and PROFILE env vars
60 | 8. run the R process
61 | 9. write stdout & stderr to file, if needed
62 | 10. fail on status, if requested
63 |
64 | ### `rcmd_bg`
65 |
66 | 1. convert/check arguments
67 | 2. get the R binary and its arguments
68 | 3. set specified wd
69 | 4. set up profiles
70 | 5. set up library path
71 | 7. set up LIB and PROFILE env vars
72 | 8. run the R process in the bg
73 |
74 | ### Building blocks
75 |
76 | #### Always run, `check_my_args`:
77 |
78 | 1. convert / check arguments
79 |
80 | #### Run by `r` and `r_bg`, `setup_script_files`:
81 |
82 | 1. save function to a file
83 | 2. create script file
84 |
85 | #### Always run, `setup_context`:
86 |
87 | This is run with `.` as the working directory for `r` and `r_bg`.
88 |
89 | 1. set specified wd
90 | 2. set up profiles
91 | 3. set up library path
92 | 4. set up LIB and PROFILE env vars
93 |
--------------------------------------------------------------------------------
/inst/header.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | # callr
5 |
6 | > Call R from R
7 |
8 |
9 | [](https://lifecycle.r-lib.org/articles/stages.html)
10 | [](https://github.com/r-lib/callr/actions/workflows/R-CMD-check.yaml)
11 | [](https://www.r-pkg.org/pkg/callr)
12 | [](https://www.r-pkg.org/pkg/callr)
13 | [](https://app.codecov.io/gh/r-lib/callr?branch=main)
14 |
15 |
16 |
17 | It is sometimes useful to perform a computation in a separate R process,
18 | without affecting the current R process at all. This packages does exactly
19 | that.
20 |
21 | ---
22 |
--------------------------------------------------------------------------------
/man/add_hook.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/hook.R
3 | \name{add_hook}
4 | \alias{add_hook}
5 | \title{Add a user hook to be executed before launching an R subprocess}
6 | \usage{
7 | add_hook(...)
8 | }
9 | \arguments{
10 | \item{...}{Named argument specifying a hook function to add, or \code{NULL} to
11 | delete the named hook.}
12 | }
13 | \value{
14 | \code{add_hook} is called for its side-effects.
15 | }
16 | \description{
17 | This function allows users of \code{callr} to specify functions that get invoked
18 | whenever an R session is launched. The function can modify the environment
19 | variables and command line arguments.
20 | }
21 | \details{
22 | The prototype of the hook function is \verb{function (options)}, and it is
23 | expected to return the modified \code{options}.
24 | }
25 |
--------------------------------------------------------------------------------
/man/convert_and_check_my_args.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/check.R
3 | \name{convert_and_check_my_args}
4 | \alias{convert_and_check_my_args}
5 | \title{Convert and check function arguments}
6 | \usage{
7 | convert_and_check_my_args(options)
8 | }
9 | \arguments{
10 | \item{options}{List of options.}
11 | }
12 | \description{
13 | This function is used for all variants of \code{r} and \code{rcmd}. An argument
14 | name is only used to refer to one kind of object, to make this possible.
15 | }
16 | \details{
17 | The benefit of having a single \code{options} object is to avoid passing
18 | around a lot of arguments all the time.
19 |
20 | The benefit of making this object internal (i.e. that the \code{r}, etc.
21 | functions have multiple arguments instead of a single \code{options} list),
22 | is that documentation and usage is more user friendly (e.g. command-
23 | completion works in the editor.
24 | }
25 | \keyword{internal}
26 |
--------------------------------------------------------------------------------
/man/default_repos.Rd:
--------------------------------------------------------------------------------
1 | % Generated by roxygen2: do not edit by hand
2 | % Please edit documentation in R/utils.R
3 | \name{default_repos}
4 | \alias{default_repos}
5 | \title{Default value for the \code{repos} option in callr subprocesses}
6 | \usage{
7 | default_repos()
8 | }
9 | \value{
10 | Named character vector, the default value of the \code{repos}
11 | option in callr subprocesses.
12 | }
13 | \description{
14 | callr sets the \code{repos} option in subprocesses, to make sure that
15 | a CRAN mirror is set up. This is because the subprocess cannot bring
16 | up the menu of CRAN mirrors for the user to choose from.
17 | }
18 | \examples{
19 | default_repos()
20 | }
21 |
--------------------------------------------------------------------------------
/man/figures/bg-dark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/man/figures/bg.svg:
--------------------------------------------------------------------------------
1 |