Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ jobs:
- run: pnpm run build
- name: Verify managed jgd preparation
run: pnpm run test:prepare-jgd
- name: Verify managed rstudioapi preparation
run: pnpm run test:prepare-rstudioapi
- name: Verify installed sess identity
run: |
source("R/sess_source.R")
Expand Down
74 changes: 60 additions & 14 deletions R/attach_sess.R
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
# Shared terminal and manual-attach preparation. Installation requires a
# single-use grant from the extension process; .libPaths() is never changed.

# Source the tool preparation helper into a fresh environment. Returns NULL
# after reporting the failure when it cannot be loaded.
vscode_r_load_prepare_helper <- function(helper, package) {
setup <- new.env(parent = environment(vscode_r_load_prepare_helper))
loaded <- nzchar(helper) && file.exists(helper) &&
isTRUE(tryCatch({
sys.source(helper, envir = setup)
TRUE
}, error = function(error) {
message("vscode-R could not load managed ", package, " setup resources: ",
conditionMessage(error))
FALSE
}))
if (loaded) setup else NULL
}

vscode_r_with_jgd <- function(
plot_backend,
connect,
jgd_helper = Sys.getenv("VSCODE_R_JGD_HELPER", unset = ""),
tool_prepare_helper = Sys.getenv("VSCODE_R_TOOL_PREPARE_HELPER", unset = ""),
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
tool_consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
tool_library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
Expand All @@ -15,17 +32,8 @@ vscode_r_with_jgd <- function(
if (!plot_backend %in% c("jgd", "auto")) {
return(connect(allow_jgd = TRUE))
}
jgd_setup <- new.env(parent = environment())
loaded <- nzchar(jgd_helper) && file.exists(jgd_helper) &&
isTRUE(tryCatch({
sys.source(jgd_helper, envir = jgd_setup)
TRUE
}, error = function(error) {
message("vscode-R could not load managed jgd setup resources: ",
conditionMessage(error))
FALSE
}))
if (loaded && is.function(jgd_setup$vscode_r_connect_with_jgd)) {
jgd_setup <- vscode_r_load_prepare_helper(tool_prepare_helper, "jgd")
if (!is.null(jgd_setup) && is.function(jgd_setup$vscode_r_connect_with_jgd)) {
return(jgd_setup$vscode_r_connect_with_jgd(
plot_backend, connect, tool_root, tool_consent_directory,
tool_library_helper, tool_consent_helper, can_prompt, minimum_version
Expand All @@ -35,6 +43,33 @@ vscode_r_with_jgd <- function(
connect(allow_jgd = FALSE)
}

# Prepare rstudioapi before sess starts its emulation. Declining or failing only
# prints a message; sess emulation stays enabled and patches rstudioapi
# whenever it is loaded. Does nothing when emulation is disabled.
vscode_r_with_rstudioapi <- function(
use_rstudioapi,
tool_prepare_helper = Sys.getenv("VSCODE_R_TOOL_PREPARE_HELPER", unset = ""),
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
tool_consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
tool_library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
tool_consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
can_prompt = identical(Sys.getenv("VSCODE_R_RSTUDIOAPI_CAN_PROMPT", unset = "TRUE"), "TRUE"),
minimum_version = Sys.getenv("VSCODE_R_RSTUDIOAPI_MINIMUM_VERSION", unset = "")
) {
if (identical(use_rstudioapi, FALSE)) {
return(invisible(FALSE))
}
setup <- vscode_r_load_prepare_helper(tool_prepare_helper, "rstudioapi")
if (!is.null(setup) && is.function(setup$vscode_r_prepare_rstudioapi)) {
return(invisible(isTRUE(setup$vscode_r_prepare_rstudioapi(
use_rstudioapi, tool_root, tool_consent_directory,
tool_library_helper, tool_consent_helper, can_prompt, minimum_version
))))
}
message("vscode-R could not load managed rstudioapi setup resources; continuing without it.")
invisible(FALSE)
}

vscode_r_prepare_sess <- function(pkg_path, managed_root, consent_dir,
source_helper, installer_helper,
timeout_seconds = 180,
Expand Down Expand Up @@ -231,10 +266,16 @@ vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
tool_consent_helper = Sys.getenv(
"VSCODE_R_TOOL_CONSENT_HELPER", unset = ""
),
jgd_helper = Sys.getenv("VSCODE_R_JGD_HELPER", unset = ""),
tool_prepare_helper = Sys.getenv(
"VSCODE_R_TOOL_PREPARE_HELPER", unset = ""
),
jgd_can_prompt = TRUE,
jgd_minimum_version = Sys.getenv(
"VSCODE_R_JGD_MINIMUM_VERSION", unset = ""
),
rstudioapi_can_prompt = TRUE,
rstudioapi_minimum_version = Sys.getenv(
"VSCODE_R_RSTUDIOAPI_MINIMUM_VERSION", unset = ""
)) {
if (!is.null(attachment_id) &&
!(is.character(attachment_id) && length(attachment_id) == 1L &&
Expand Down Expand Up @@ -291,13 +332,18 @@ vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
args$attachment_id <- attachment_id
args$auto_activate <- auto_activate
}
vscode_r_with_rstudioapi(
use_rstudioapi, tool_prepare_helper, tool_root, tool_consent_directory,
tool_library_helper, tool_consent_helper, rstudioapi_can_prompt,
rstudioapi_minimum_version
)
vscode_r_with_jgd(
plot_backend,
function(allow_jgd = TRUE) {
args$allow_jgd <- allow_jgd
do.call(connect, args)
},
jgd_helper, tool_root, tool_consent_directory,
tool_prepare_helper, tool_root, tool_consent_directory,
tool_library_helper, tool_consent_helper, jgd_can_prompt, jgd_minimum_version
)
}
Expand Down
107 changes: 83 additions & 24 deletions R/prepare-jgd.R → R/prepare-tools.R
Original file line number Diff line number Diff line change
Expand Up @@ -20,68 +20,127 @@ vscode_r_connect_with_jgd <- function(
connect(allow_jgd = ready)
}

# Prepare optional plot support without making sess startup depend on CRAN access
# or user consent. The managed library is loaded before sess checks the package.
vscode_r_prepare_jgd <- function(
plot_backend,
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
can_prompt = identical(Sys.getenv("VSCODE_R_JGD_CAN_PROMPT", unset = "TRUE"), "TRUE"),
minimum_version = Sys.getenv("VSCODE_R_JGD_MINIMUM_VERSION", unset = "")
# Select, install if approved, and eagerly load one tool package. Returns TRUE
# when a compatible copy is loaded, otherwise prints a short message and
# returns FALSE so the caller can continue with its fallback. `policy` is the
# consent policy sent to the extension ("always" or "once"); `fallback` and
# `support` only word the messages. An already loaded namespace is never replaced.
vscode_r_prepare_tool_package <- function(
package,
policy,
tool_root,
consent_directory,
library_helper,
consent_helper,
can_prompt,
minimum_version,
support,
fallback,
report_load_failure = TRUE
) {
if (!is.character(plot_backend) || length(plot_backend) != 1L ||
is.na(plot_backend) || !plot_backend %in% c("jgd", "auto")) {
return(FALSE)
}
if (!(is.logical(can_prompt) && length(can_prompt) == 1L && !is.na(can_prompt))) {
stop("can_prompt must be a single non-NA logical value.", call. = FALSE)
}
helpers <- c(library_helper, consent_helper)
if (any(!nzchar(helpers)) || any(!file.exists(helpers)) || !nzchar(tool_root)) {
message("vscode-R could not locate managed jgd setup resources; using the available plot fallback.")
message("vscode-R could not locate managed ", package, " setup resources; ", fallback, ".")
return(FALSE)
}

tryCatch({
tool_environment <- new.env(parent = environment(vscode_r_prepare_jgd))
tool_environment <- new.env(parent = environment(vscode_r_prepare_tool_package))
for (helper in helpers) {
sys.source(helper, envir = tool_environment)
}
if (!can_prompt) {
# A compatible ordinary or managed copy remains usable without resolving a new plan.
return(!is.null(tool_environment$tool_library_load(
"jgd", minimum_version, tool_root
package, minimum_version, tool_root
)))
}
prepared <- tool_environment$tool_library_prepare(
"jgd",
package,
minimum_version,
tool_root,
consent_directory,
plot_backend,
policy,
consent_timeout_seconds = 180,
install_timeout_seconds = 300,
resolution_timeout_seconds = 30
)
if (is.null(prepared)) {
message("vscode-R optional jgd installation was not approved; using the available plot fallback.")
message("vscode-R optional ", package, " installation was not approved; ", fallback, ".")
return(FALSE)
}
namespace <- tool_environment$tool_library_load(
"jgd", minimum_version, tool_root
package, minimum_version, tool_root
)
if (is.null(namespace)) {
message("vscode-R could not load a compatible jgd package; using the available plot fallback.")
if (report_load_failure) {
message("vscode-R could not load a compatible ", package, " package; ", fallback, ".")
}
return(FALSE)
}
TRUE
}, error = function(error) {
message("vscode-R could not prepare optional jgd plot support: ", conditionMessage(error))
message("vscode-R could not prepare optional ", support, ": ", conditionMessage(error))
FALSE
}, interrupt = function(error) {
message("vscode-R jgd preparation was interrupted; using the available plot fallback.")
message("vscode-R ", package, " preparation was interrupted; ", fallback, ".")
FALSE
})
}

# Prepare optional plot support without making sess startup depend on CRAN access
# or user consent. The managed library is loaded before sess checks the package.
vscode_r_prepare_jgd <- function(
plot_backend,
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
can_prompt = identical(Sys.getenv("VSCODE_R_JGD_CAN_PROMPT", unset = "TRUE"), "TRUE"),
minimum_version = Sys.getenv("VSCODE_R_JGD_MINIMUM_VERSION", unset = "")
) {
if (!is.character(plot_backend) || length(plot_backend) != 1L ||
is.na(plot_backend) || !plot_backend %in% c("jgd", "auto")) {
return(FALSE)
}
# An explicit jgd backend is offered installation on every request; auto only once.
vscode_r_prepare_tool_package(
"jgd",
if (identical(plot_backend, "jgd")) "always" else "once",
tool_root, consent_directory, library_helper, consent_helper,
can_prompt, minimum_version,
support = "jgd plot support",
fallback = "using the available plot fallback"
)
}

# Make rstudioapi available before sess starts its emulation, so that
# requireNamespace("rstudioapi") succeeds even in isolated project libraries.
# Emulation stays enabled whatever the outcome: sess patches rstudioapi when it
# is loaded now or later. Does nothing when emulation is disabled.
vscode_r_prepare_rstudioapi <- function(
use_rstudioapi,
tool_root = Sys.getenv("VSCODE_R_TOOL_ROOT", unset = ""),
consent_directory = Sys.getenv("VSCODE_R_TOOL_CONSENT_DIRECTORY", unset = ""),
library_helper = Sys.getenv("VSCODE_R_TOOL_LIBRARY_HELPER", unset = ""),
consent_helper = Sys.getenv("VSCODE_R_TOOL_CONSENT_HELPER", unset = ""),
can_prompt = identical(Sys.getenv("VSCODE_R_RSTUDIOAPI_CAN_PROMPT", unset = "TRUE"), "TRUE"),
minimum_version = Sys.getenv("VSCODE_R_RSTUDIOAPI_MINIMUM_VERSION", unset = "")
) {
# sess treats an unset (NA) value as enabled.
if (identical(use_rstudioapi, FALSE)) {
return(FALSE)
}
vscode_r_prepare_tool_package(
"rstudioapi", "once",
tool_root, consent_directory, library_helper, consent_helper,
can_prompt, minimum_version,
support = "rstudioapi support",
fallback = "continuing without it",
# An incompatible loaded namespace is already reported by the loader.
report_load_failure = FALSE
)
}
1 change: 1 addition & 0 deletions R/profile.R
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ invisible(local({
plot_backend <- Sys.getenv("SESS_PLOT_BACKEND", "auto")
connect <- get("connect", envir = ns, inherits = FALSE)
use_rstudioapi <- as.logical(Sys.getenv("SESS_RSTUDIOAPI", "TRUE"))
vscode_r_with_rstudioapi(use_rstudioapi)
result <- vscode_r_with_jgd(plot_backend, function(allow_jgd = TRUE) {
connect(use_rstudioapi = use_rstudioapi, plot_backend = plot_backend,
allow_jgd = allow_jgd)
Expand Down
3 changes: 3 additions & 0 deletions R/sess-package-install.R
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ sess_install <- function(pkg_path, library, repos, interactive = FALSE) {

deps <- if ("Imports" %in% colnames(description)) description[1L, "Imports"] else ""
deps <- trimws(gsub("\\s*\\(.*\\)", "", unlist(strsplit(deps, ","))))
# The private Interactive runtime loads rstudioapi itself. Ordinary terminals
# obtain it on demand from the extension's tool library instead.
if (interactive) deps <- c(deps, "rstudioapi")
installed <- utils::installed.packages(lib.loc = unique(c(library, .libPaths())))
missing <- setdiff(deps[nzchar(deps)], rownames(installed))
if (length(missing)) {
Expand Down
2 changes: 1 addition & 1 deletion R/sess_source.R
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ sess_load_namespace <- function(library, revision, normal_libraries = .libPaths(
if ("processx" %in% imports) {
loadNamespace("ps", lib.loc = support_paths)
}
for (package in intersect(c("jsonlite", "later", "processx", "rstudioapi"), imports)) {
for (package in intersect(c("jsonlite", "later", "processx"), imports)) {
loadNamespace(package, lib.loc = support_paths)
}
ns <- loadNamespace("sess", lib.loc = paths)
Expand Down
21 changes: 11 additions & 10 deletions R/tests/prepare-jgd.R
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ local({
}, add = TRUE)
installed <- utils::installed.packages(lib.loc = .libPaths())
runtime_dependencies <- unique(c(
"processx", "later", "jsonlite", "rstudioapi",
unlist(tools::package_dependencies(c("processx", "later", "jsonlite", "rstudioapi"),
"processx", "later", "jsonlite",
unlist(tools::package_dependencies(c("processx", "later", "jsonlite"),
db = installed, which = c("Depends", "Imports"),
recursive = TRUE), use.names = FALSE)
))
Expand Down Expand Up @@ -133,7 +133,7 @@ local({
"stopifnot(!ambient %in% normalizePath(.libPaths(), winslash='/'));",
"source(file.path(getwd(), 'R', 'tool-library.R'));",
"source(file.path(getwd(), 'R', 'tool-consent.R'));",
"source(file.path(getwd(), 'R', 'prepare-jgd.R'));",
"source(file.path(getwd(), 'R', 'prepare-tools.R'));",
"source(file.path(getwd(), 'R', 'attach_sess.R'));",
"options(repos=c(CRAN=repo), pkgType='source');",
"consent <- file.path(root, 'consent'); dir.create(consent, showWarnings=FALSE);",
Expand Down Expand Up @@ -170,11 +170,12 @@ local({
"}",
"timeout_before <- getOption('timeout'); libraries_before <- .libPaths();",
"answer <- vscode_r_attach_sess('endpoint', 'unused', file.path(root, 'tools'), consent,",
" 'unused', 'unused', 'jgd', timeout_seconds=0, tool_root=file.path(root, 'tools'),",
" 'unused', 'unused', 'jgd', timeout_seconds=0, use_rstudioapi=FALSE,",
" tool_root=file.path(root, 'tools'),",
" tool_consent_directory=consent, tool_library_helper=tool_library_helper,",
" tool_consent_helper=file.path(getwd(), 'R', 'tool-consent.R'),",
" jgd_helper=if (mode == 'helper-missing') file.path(root, 'missing-prepare-jgd.R') else",
" file.path(getwd(), 'R', 'prepare-jgd.R'),",
" tool_prepare_helper=if (mode == 'helper-missing') file.path(root, 'missing-prepare-tools.R') else",
" file.path(getwd(), 'R', 'prepare-tools.R'),",
" jgd_can_prompt=!(mode %in% c('no-prompt', 'no-prompt-compatible', 'socket-empty')),",
" jgd_minimum_version='0.1.0');",
"stopifnot(isTRUE(answer), identical(Sys.getenv('JGD_SOCKET'), 'fixture-socket'));",
Expand Down Expand Up @@ -230,7 +231,7 @@ local({
"stopifnot(!ambient %in% normalizePath(.libPaths(), winslash='/'));",
"source(file.path(getwd(), 'R', 'tool-library.R'));",
"source(file.path(getwd(), 'R', 'tool-consent.R'));",
"source(file.path(getwd(), 'R', 'prepare-jgd.R'));",
"source(file.path(getwd(), 'R', 'prepare-tools.R'));",
"source(file.path(getwd(), 'R', 'attach_sess.R'));",
"options(repos=c(CRAN=repo), pkgType='source', sess.quiet=TRUE);",
"sess_ns <- loadNamespace('sess', lib.loc=sess_library);",
Expand Down Expand Up @@ -263,7 +264,7 @@ local({
"answer <- vscode_r_with_jgd('jgd', function(allow_jgd=TRUE)",
" connect_env$connect(endpoint=endpoint, use_rstudioapi=FALSE,",
" plot_backend='jgd', allow_jgd=allow_jgd),",
" jgd_helper=file.path(getwd(), 'R', 'prepare-jgd.R'),",
" tool_prepare_helper=file.path(getwd(), 'R', 'prepare-tools.R'),",
" tool_root=file.path(root, paste0(mode, '-tools')),",
" tool_consent_directory=file.path(root, 'consent'),",
" tool_library_helper=file.path(getwd(), 'R', 'tool-library.R'),",
Expand Down Expand Up @@ -472,15 +473,15 @@ local({
" VSCODE_R_SESS_STARTUP_TOKEN='0123456789abcdef0123456789abcdef',",
" VSCODE_R_SESS_STARTUP_ENDPOINT='fixture-endpoint',",
paste0(" VSCODE_R_SESS_STARTUP_HELPER=", r_literal(file.path(getwd(), "R", "terminal-startup.R")), ","),
paste0(" VSCODE_R_JGD_HELPER=", r_literal(file.path(getwd(), "R", "prepare-jgd.R")), ","),
paste0(" VSCODE_R_TOOL_PREPARE_HELPER=", r_literal(file.path(getwd(), "R", "prepare-tools.R")), ","),
paste0(" VSCODE_R_TOOL_ROOT=", r_literal(file.path(profile_root, "tools")), ","),
paste0(" VSCODE_R_TOOL_CONSENT_DIRECTORY=", r_literal(profile_consent), ","),
paste0(" VSCODE_R_TOOL_LIBRARY_HELPER=", r_literal(file.path(getwd(), "R", "tool-library.R")), ","),
paste0(" VSCODE_R_TOOL_CONSENT_HELPER=", r_literal(file.path(getwd(), "R", "tool-consent.R")), ","),
" VSCODE_R_JGD_CAN_PROMPT='TRUE',",
" VSCODE_R_JGD_MINIMUM_VERSION='0.1.0',",
paste0(" R_PROFILE_USER_OLD=", r_literal(empty_user_profile), ","),
" SESS_PLOT_BACKEND='jgd', JGD_SOCKET='fixture-socket')",
" SESS_PLOT_BACKEND='jgd', SESS_RSTUDIOAPI='FALSE', JGD_SOCKET='fixture-socket')",
paste0("options(repos=c(CRAN=", r_literal(repo), "), pkgType='source')"),
"source(file.path(getwd(), 'R', 'profile.R'))"
), startup_profile)
Expand Down
Loading
Loading