Skip to content

Commit 929a9e9

Browse files
authored
feat: install jgd into a vscode-R-managed tool library on demand (#1907)
As suggested in #1856, packages that vscode-R needs as tools, rather than as project dependencies, should not have to live in user or project libraries. This applies especially to jgd, which has no R package dependencies. This PR lets ordinary R terminals and manual session attachment offer to install jgd into a vscode-R-managed **tool library** when the JGD plot viewer is available. Libraries owned by renv, pixi, conda, or the user are not modified. ## Tool library Unlike sess, jgd is a CRAN package that cannot be pinned to the extension's source revision, and it has no dependency tree to isolate. The tool library is therefore deliberately simpler than the managed sess library. sess installation is unchanged. - **Layout.** One library per R platform and major.minor version (`<globalStorage>/packages/<platform>/<major.minor>/library`), following the convention of `R_LIBS_USER`. Only the most recently installed version of each package is kept. - **Allowed packages.** Only packages on a fixed allowlist (currently jgd) can be installed, from the repositories configured in the R session. A version that declares non-base `Depends`, `Imports`, or `LinkingTo` is refused before installation. Packages are installed with `dependencies = FALSE`. - **Selection.** A compatible namespace that is already loaded is used as is. Otherwise, a compatible copy in the normal libraries is preferred, then the tool library copy, which is loaded with `loadNamespace(lib.loc = )`. `.libPaths()` is never changed. - **Installation.** Installation is serialized by a per-library directory lock, with the same rules as the managed sess setup, and verified in a child R process. A package is reinstalled only when it no longer satisfies the required minimum version (jgd 0.1.0); there is no automatic update. - **Known limitation.** Updating a copy that another R process has loaded can fail on Windows, or affect that process on Unix, as with an ordinary `install.packages()`. The error message asks the user to close other R sessions and retry. ## Consent The file-based consent transport used for sess is shared with a new package consent request (`vscode-r-package-consent-v1`). The request carries the resolved version, repository, package type, and whether a source build may be required. Approval applies only to that exact plan. The sess request format and behavior are unchanged. - With `r.plot.backend` set to `"jgd"`, installation is offered whenever setup requires it. - With `"auto"`, the offer appears at most once per VS Code window session. **Not now** or dismissal postpones it to a later window session. **Don't ask again** is remembered in the extension's global state for the current minimum jgd version, like the sess prompt for each bundled revision, so raising the minimum version offers installation again. Selecting `"jgd"` still offers installation. - When no offer can be shown, R uses an existing compatible copy or falls back without querying repositories, so terminal startup is not slowed down. ## Terminal and attach integration Before `sess::connect()`, the startup profile and manual attach prepare and load jgd when the plot backend is `jgd` or `auto`, and pass the result to the new `allow_jgd` argument of `sess::connect()`. When it is `FALSE`, sess does not select jgd, does not take the JGD socket from the discovery file, and keeps that decision for automatic reconnects. This prevents an incompatible jgd that is already loaded or installed in a normal library from being used. The bundled sess development version is bumped to 3.2.9000.9000. R waits for the decision with the same limits as the sess setup (consent 180 s, lock 300 s), and code sent during setup stays queued. Each repository metadata request uses a download timeout of at most 30 s. Declining, failures, offline repositories, or an incompatible loaded jgd fall back to httpgd or the standard viewer, without blocking the terminal or the session watcher. Experimental R Interactive is not changed.
1 parent 6505dad commit 929a9e9

32 files changed

Lines changed: 2685 additions & 191 deletions

‎.github/workflows/main.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,16 @@ jobs:
3838
- run: pnpm install --frozen-lockfile
3939
- name: Verify sess source identity and bootstrap
4040
run: pnpm run test:sess-source
41+
- name: Verify managed tool library
42+
run: pnpm run test:tool-library
43+
- name: Verify managed tool consent
44+
run: pnpm run test:tool-consent
4145
- name: Install R test dependencies
4246
run: install.packages(c("remotes", "pkgbuild", "languageserver", "lintr", "renv"))
4347
shell: Rscript {0}
4448
- run: pnpm run build
49+
- name: Verify managed jgd preparation
50+
run: pnpm run test:prepare-jgd
4551
- name: Verify installed sess identity
4652
run: |
4753
source("R/sess_source.R")

‎R/attach_sess.R‎

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,40 @@
11
# Shared terminal and manual-attach preparation. Installation requires a
22
# single-use grant from the extension process; .libPaths() is never changed.
3+
vscode_r_with_jgd <- function(
4+
plot_backend,
5+
connect,
6+
jgd_helper = Sys.getenv("VSCODE_R_JGD_HELPER", unset = ""),
7+
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
8+
tool_consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
9+
tool_library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
10+
tool_consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
11+
can_prompt = identical(Sys.getenv("VSCODE_R_JGD_CAN_PROMPT", unset = "TRUE"), "TRUE"),
12+
minimum_version = Sys.getenv("VSCODE_R_JGD_MINIMUM_VERSION", unset = "")
13+
) {
14+
if (!is.function(connect)) stop("connect must be a function.", call. = FALSE)
15+
if (!plot_backend %in% c("jgd", "auto")) {
16+
return(connect(allow_jgd = TRUE))
17+
}
18+
jgd_setup <- new.env(parent = environment())
19+
loaded <- nzchar(jgd_helper) && file.exists(jgd_helper) &&
20+
isTRUE(tryCatch({
21+
sys.source(jgd_helper, envir = jgd_setup)
22+
TRUE
23+
}, error = function(error) {
24+
message("vscode-R could not load managed jgd setup resources: ",
25+
conditionMessage(error))
26+
FALSE
27+
}))
28+
if (loaded && is.function(jgd_setup$vscode_r_connect_with_jgd)) {
29+
return(jgd_setup$vscode_r_connect_with_jgd(
30+
plot_backend, connect, tool_root, tool_consent_directory,
31+
tool_library_helper, tool_consent_helper, can_prompt, minimum_version
32+
))
33+
}
34+
message("vscode-R could not load managed jgd setup resources; using the available plot fallback.")
35+
connect(allow_jgd = FALSE)
36+
}
37+
338
vscode_r_prepare_sess <- function(pkg_path, managed_root, consent_dir,
439
source_helper, installer_helper,
540
timeout_seconds = 180,
@@ -185,7 +220,22 @@ vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
185220
setup_timeout_seconds = 300,
186221
use_rstudioapi = TRUE,
187222
attachment_id = NULL,
188-
auto_activate = TRUE) {
223+
auto_activate = TRUE,
224+
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
225+
tool_consent_directory = Sys.getenv(
226+
"VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""
227+
),
228+
tool_library_helper = Sys.getenv(
229+
"VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""
230+
),
231+
tool_consent_helper = Sys.getenv(
232+
"VSCODE_R_TOOL_CONSENT_HELPER", unset = ""
233+
),
234+
jgd_helper = Sys.getenv("VSCODE_R_JGD_HELPER", unset = ""),
235+
jgd_can_prompt = TRUE,
236+
jgd_minimum_version = Sys.getenv(
237+
"VSCODE_R_JGD_MINIMUM_VERSION", unset = ""
238+
)) {
189239
if (!is.null(attachment_id) &&
190240
!(is.character(attachment_id) && length(attachment_id) == 1L &&
191241
!is.na(attachment_id) &&
@@ -241,7 +291,15 @@ vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
241291
args$attachment_id <- attachment_id
242292
args$auto_activate <- auto_activate
243293
}
244-
do.call(connect, args)
294+
vscode_r_with_jgd(
295+
plot_backend,
296+
function(allow_jgd = TRUE) {
297+
args$allow_jgd <- allow_jgd
298+
do.call(connect, args)
299+
},
300+
jgd_helper, tool_root, tool_consent_directory,
301+
tool_library_helper, tool_consent_helper, jgd_can_prompt, jgd_minimum_version
302+
)
245303
}
246304

247305
if (profile_process) {

‎R/prepare-jgd.R‎

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Keep optional jgd preparation and its result shared by terminal connections.
2+
vscode_r_connect_with_jgd <- function(
3+
plot_backend,
4+
connect,
5+
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
6+
consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
7+
library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
8+
consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
9+
can_prompt = identical(Sys.getenv("VSCODE_R_JGD_CAN_PROMPT", unset = "TRUE"), "TRUE"),
10+
minimum_version = Sys.getenv("VSCODE_R_JGD_MINIMUM_VERSION", unset = "")
11+
) {
12+
if (!is.function(connect)) stop("connect must be a function.", call. = FALSE)
13+
if (!plot_backend %in% c("jgd", "auto")) {
14+
return(connect(allow_jgd = TRUE))
15+
}
16+
ready <- isTRUE(vscode_r_prepare_jgd(
17+
plot_backend, tool_root, consent_directory,
18+
library_helper, consent_helper, can_prompt, minimum_version
19+
))
20+
connect(allow_jgd = ready)
21+
}
22+
23+
# Prepare optional plot support without making sess startup depend on CRAN access
24+
# or user consent. The managed library is loaded before sess checks the package.
25+
vscode_r_prepare_jgd <- function(
26+
plot_backend,
27+
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
28+
consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
29+
library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
30+
consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
31+
can_prompt = identical(Sys.getenv("VSCODE_R_JGD_CAN_PROMPT", unset = "TRUE"), "TRUE"),
32+
minimum_version = Sys.getenv("VSCODE_R_JGD_MINIMUM_VERSION", unset = "")
33+
) {
34+
if (!is.character(plot_backend) || length(plot_backend) != 1L ||
35+
is.na(plot_backend) || !plot_backend %in% c("jgd", "auto")) {
36+
return(FALSE)
37+
}
38+
if (!(is.logical(can_prompt) && length(can_prompt) == 1L && !is.na(can_prompt))) {
39+
stop("can_prompt must be a single non-NA logical value.", call. = FALSE)
40+
}
41+
helpers <- c(library_helper, consent_helper)
42+
if (any(!nzchar(helpers)) || any(!file.exists(helpers)) || !nzchar(tool_root)) {
43+
message("vscode-R could not locate managed jgd setup resources; using the available plot fallback.")
44+
return(FALSE)
45+
}
46+
47+
tryCatch({
48+
tool_environment <- new.env(parent = environment(vscode_r_prepare_jgd))
49+
for (helper in helpers) {
50+
sys.source(helper, envir = tool_environment)
51+
}
52+
if (!can_prompt) {
53+
# A compatible ordinary or managed copy remains usable without resolving a new plan.
54+
return(!is.null(tool_environment$tool_library_load(
55+
"jgd", minimum_version, tool_root
56+
)))
57+
}
58+
prepared <- tool_environment$tool_library_prepare(
59+
"jgd",
60+
minimum_version,
61+
tool_root,
62+
consent_directory,
63+
plot_backend,
64+
consent_timeout_seconds = 180,
65+
install_timeout_seconds = 300,
66+
resolution_timeout_seconds = 30
67+
)
68+
if (is.null(prepared)) {
69+
message("vscode-R optional jgd installation was not approved; using the available plot fallback.")
70+
return(FALSE)
71+
}
72+
namespace <- tool_environment$tool_library_load(
73+
"jgd", minimum_version, tool_root
74+
)
75+
if (is.null(namespace)) {
76+
message("vscode-R could not load a compatible jgd package; using the available plot fallback.")
77+
return(FALSE)
78+
}
79+
TRUE
80+
}, error = function(error) {
81+
message("vscode-R could not prepare optional jgd plot support: ", conditionMessage(error))
82+
FALSE
83+
}, interrupt = function(error) {
84+
message("vscode-R jgd preparation was interrupted; using the available plot fallback.")
85+
FALSE
86+
})
87+
}

‎R/profile.R‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ invisible(local({
5252
plot_backend <- Sys.getenv("SESS_PLOT_BACKEND", "auto")
5353
connect <- get("connect", envir = ns, inherits = FALSE)
5454
use_rstudioapi <- as.logical(Sys.getenv("SESS_RSTUDIOAPI", "TRUE"))
55-
result <- connect(use_rstudioapi = use_rstudioapi, plot_backend = plot_backend)
55+
result <- vscode_r_with_jgd(plot_backend, function(allow_jgd = TRUE) {
56+
connect(use_rstudioapi = use_rstudioapi, plot_backend = plot_backend,
57+
allow_jgd = allow_jgd)
58+
})
5659
isTRUE(result)
5760
}
5861

‎R/tests/attach_sess.R‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ writeLines(c(
198198
" stopifnot(identical(result, TRUE), same_paths(.libPaths(), ordinary))",
199199
" stopifnot(identical(getOption('vscodeR.test.use_rstudioapi'), FALSE))",
200200
" stopifnot(identical(getOption('vscodeR.test.connect.args'),",
201-
" list(attachment_id='console_1', auto_activate=FALSE)))",
201+
" list(attachment_id='console_1', auto_activate=FALSE, allow_jgd=TRUE)))",
202202
" stopifnot(identical(sess_loaded_source_revision(), revision))",
203203
" actual <- getNamespaceInfo(asNamespace('sess'), 'path')",
204204
" stopifnot(same_paths(actual, file.path(expected, 'sess')))",
@@ -211,7 +211,7 @@ writeLines(c(
211211
" file.path(getwd(), 'R', 'sess-package-install.R'), 'standard', 0)",
212212
" stopifnot(identical(legacy_result, TRUE),",
213213
" identical(getOption('vscodeR.test.use_rstudioapi'), TRUE))",
214-
" stopifnot(identical(getOption('vscodeR.test.connect.args'), list()))",
214+
" stopifnot(identical(getOption('vscodeR.test.connect.args'), list(allow_jgd=TRUE)))",
215215
" blocked_file <- file.path(root, 'missing-directory', 'startup')",
216216
" context <- vscode_r_startup_register(blocked_file, strrep('c', 32), 'endpoint')",
217217
" called <- FALSE",

0 commit comments

Comments
 (0)