Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
05ef11b
fix: isolate sess installation in managed libraries
eitsupi Oct 10, 2026
771ff7c
fix: prepare sess in the console R process
eitsupi Oct 10, 2026
15e630f
style: align sess setup continuation arguments
eitsupi Oct 10, 2026
5b6a415
feat: allow suppressing sess prompts for the current revision
eitsupi Oct 10, 2026
38b5b4d
fix: serialize managed sess setup
eitsupi Oct 10, 2026
ee36b35
fix: require a ready marker for managed sess
eitsupi Oct 10, 2026
0bc28da
docs: note managed sess dependency limitations
eitsupi Oct 10, 2026
c662787
fix: keep terminal input pending during sess setup
eitsupi Oct 10, 2026
8e2e433
test: isolate terminal startup timers and cleanup
eitsupi Oct 10, 2026
827c1a5
test: separate sess attach integration checks
eitsupi Oct 10, 2026
8b0310d
test: wait for R evaluation before interrupting
eitsupi Oct 10, 2026
06489f3
fix: honor sess startup state before sending terminal input
eitsupi Oct 10, 2026
9986ef1
test: provide ready status for managed terminal fixtures
eitsupi Oct 10, 2026
4f9d24f
test: keep interrupt synchronization within one R expression
eitsupi Oct 10, 2026
67b085a
test: simplify managed sess regression coverage
eitsupi Oct 10, 2026
521c33d
style: use consistent braces in sess test fixture
eitsupi Oct 10, 2026
88d8e09
fix: separate terminal startup from sess setup
eitsupi Oct 10, 2026
3aa6dc4
style: fix terminal startup R lint warnings
eitsupi Oct 10, 2026
1f646af
test: synchronize terminal and hover fixtures
eitsupi Oct 10, 2026
b30991f
fix: publish sess startup state on automatic reconnect
eitsupi Oct 10, 2026
8a05ab1
test: read Interactive workspace from active extension
eitsupi Oct 10, 2026
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
13 changes: 9 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,15 @@ it to avoid repeating preparation when installed through remotes.
`Config/vscode-R/source-revision` controls installation independently of package
versions and the runtime `protocol_version` handshake. vscode-R installs only
the bundled snapshot; missing Imports come from the configured repository.
Missing or different installed metadata requires the bundle, and installation
verifies the exact source revision and visibility through `.libPaths()`. Run the
source/bootstrap checks with `pnpm run test:sess-source`, or just the
base-R identity checks with `pnpm run test:sess-identity`.
Ordinary terminals and manual attach ask before installing a missing or different
copy. They install into an extension-managed library, keep normal `.libPaths()`
unchanged, and explicitly load the matching namespace; exact matches in normal
libraries remain usable. Preparation and installation use the target R's
platform, version, and normal libraries; when needed, R asks the extension for
consent. R Interactive prepares its separate runtime as part of starting an
Interactive session. Run the source/bootstrap checks with
`pnpm run test:sess-source`, or just the base-R identity checks with
`pnpm run test:sess-identity`.

## Testing R Interactive

Expand Down
223 changes: 223 additions & 0 deletions R/attach_sess.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Shared terminal and manual-attach preparation. Installation requires a
# single-use grant from the extension process; .libPaths() is never changed.
vscode_r_prepare_sess <- function(pkg_path, managed_root, consent_dir,
source_helper, installer_helper,
timeout_seconds = 180,
setup_timeout_seconds = 300) {
source(source_helper, local = TRUE)
expected <- sess_source_revision(file.path(pkg_path, "DESCRIPTION"))
if (is.null(expected)) {
stop("Bundled sess has no valid source revision.")
}
runtime <- sess_runtime_identity()
managed_library <- sess_managed_library(managed_root, expected)

loaded <- "sess" %in% loadedNamespaces()
if (loaded) {
loaded_revision <- sess_loaded_source_revision()
if (!identical(loaded_revision, expected)) {
stop("A different sess namespace is already loaded. Restart R before attaching the session watcher.")
}
ns <- asNamespace("sess")
} else {
library <- sess_find_source_library(expected, .libPaths())
if (!is.null(library)) {
ns <- sess_load_namespace(library, expected)
} else {
lock_parent <- dirname(managed_library)
dir.create(lock_parent, recursive = TRUE, showWarnings = FALSE)
if (!dir.exists(lock_parent) || file.access(lock_parent, 2L) != 0L) {
stop("The vscode-R managed sess setup directory is not writable.")
}
lock_path <- file.path(lock_parent, ".setup-lock")
# Keep lock ownership rules in sync with src/interactive/backends/sessPreparation.ts:
# mkdir claims atomically; only the owner releases via on.exit; timeout never clears a stale lock.
# This revision lock covers consent, install, and load; a follower without a ready copy does not prompt.
# Publish .ready with the source revision only after the exact namespace has loaded successfully.
lock_deadline <- Sys.time() + setup_timeout_seconds
lock_timeout_message <- paste(
"Timed out waiting for another sess setup. Its owner may have crashed;",
"retrying alone will not clear the stale lock. Remove",
shQuote(lock_path), "only if its owner has exited, then retry."
)
followed_setup <- FALSE
repeat {
acquired <- dir.create(lock_path, showWarnings = FALSE, mode = "0700")
if (isTRUE(acquired)) {
on.exit(unlink(lock_path, recursive = TRUE, force = TRUE), add = TRUE)
break
}
followed_setup <- TRUE
if (!file.exists(lock_path)) {
if (file.access(lock_parent, 2L) != 0L) {
stop("The vscode-R managed sess setup directory is not writable.")
}
if (Sys.time() >= lock_deadline) {
stop(lock_timeout_message)
}
Sys.sleep(0.1)
next
}
if (!dir.exists(lock_path)) {
stop("A file is blocking the vscode-R managed sess setup lock.")
}
if (Sys.time() >= lock_deadline) {
stop(lock_timeout_message)
}
Sys.sleep(0.1)
}

ready_path <- file.path(lock_parent, ".ready")
ready_revision <- tryCatch(
readLines(ready_path, warn = FALSE, n = 2L),
warning = function(e) character(),
error = function(e) character()
)
installed <- sess_find_source_library(expected, managed_library)
if (length(ready_revision) == 1L && identical(ready_revision, expected) &&
!is.null(installed)) {
ns <- sess_load_namespace(installed, expected)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One potential issue of reusing sess is with renv. If multiple renv projects exist, their isolated environments may not have the dependencies required by sess installed, even if those dependencies are available in another project environment.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Fred-Wu How about 0bc28da?
While it may be possible to resolve this in the future, the implementation would be extremely complex at this point, so I felt it should be noted as a known limitation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially could be resolved if using one managed library for all vscode-R dependent packages. Leave it to later time then.

} else if (followed_setup) {
return(NULL)
} else {
existing <- any(vapply(.libPaths(), function(library) {
file.exists(file.path(library, "sess", "DESCRIPTION"))
}, FALSE))
reason <- if (existing) {
"mismatch"
} else {
"missing"
}
if (!dir.exists(consent_dir)) {
stop("The extension's sess consent service is unavailable. Restart VS Code and try again.")
}
new_id_part <- function() {
temporary <- basename(tempfile(pattern = "request-", tmpdir = consent_dir))
gsub("[^A-Za-z0-9_-]", "", sub("^request-", "", temporary))
}
id <- paste0(new_id_part(), new_id_part())
if (!grepl("^[A-Za-z0-9_-]{16,64}$", id)) {
stop("Could not create a unique sess installation request.")
}
request <- paste(
"vscode-r-sess-consent-v1", id, expected, runtime, reason, sep = "\n")
request_path <- file.path(consent_dir, paste0(id, ".request"))
response_path <- file.path(consent_dir, paste0(id, ".response"))
temporary_path <- tempfile(pattern = paste0(id, "-"), tmpdir = consent_dir)
on.exit(unlink(c(temporary_path, request_path, response_path)), add = TRUE)
writeLines(request, temporary_path, useBytes = TRUE)
if (.Platform$OS.type == "unix") {
Sys.chmod(temporary_path, "0600")
}
if (!file.rename(temporary_path, request_path)) {
stop("Could not request permission to install bundled sess.")
}

deadline <- Sys.time() + timeout_seconds
response <- ""
while (Sys.time() < deadline && dir.exists(consent_dir) && !nzchar(response)) {
if (file.exists(response_path)) {
lines <- tryCatch(
readLines(response_path, warn = FALSE, n = 2L),
error = function(e) character())
if (length(lines) == 1L && lines %in% c("approve", "decline")) {
response <- lines
} else {
stop("Invalid response to the sess installation request.")
}
} else {
Sys.sleep(0.2)
}
}
if (!identical(response, "approve")) {
message("Bundled sess was not installed. The session watcher was not attached.")
return(NULL)
}

unlink(ready_path, force = TRUE)
ready_link <- Sys.readlink(ready_path)
if (file.exists(ready_path) || dir.exists(ready_path) ||
(length(ready_link) && !is.na(ready_link) && nzchar(ready_link))) {
stop("Could not clear the previous vscode-R managed sess completion marker.")
}
configured <- getOption("repos")
repo <- if ("CRAN" %in% names(configured)) {
configured[["CRAN"]]
} else if (length(configured)) {
configured[[1L]]
} else {
"https://cloud.r-project.org"
}
if (!length(repo) || is.na(repo) || !nzchar(repo) || identical(repo, "@CRAN@")) {
repo <- "https://cloud.r-project.org"
}
dir.create(managed_library, recursive = TRUE, showWarnings = FALSE)
if (file.access(managed_library, 2L) != 0L) {
stop("The vscode-R managed sess library is not writable.")
}
installer <- new.env(parent = baseenv())
sys.source(installer_helper, envir = installer)
installer$sess_install(pkg_path, managed_library, repo)
ns <- sess_load_namespace(managed_library, expected)
ready_temporary <- tempfile(pattern = ".ready-", tmpdir = lock_parent)
on.exit(unlink(ready_temporary), add = TRUE)
writeLines(expected, ready_temporary, useBytes = TRUE)
if (.Platform$OS.type == "unix") {
Sys.chmod(ready_temporary, "0600")
}
if (!file.rename(ready_temporary, ready_path)) {
stop("Could not publish the vscode-R managed sess completion marker.")
}
}
}
}

ns
}

vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
source_helper, installer_helper, plot_backend,
timeout_seconds = 180,
setup_timeout_seconds = 300) {
registered <- getOption("vscodeR.terminalStartup")
profile_process <- is.list(registered) && identical(registered$pid, Sys.getpid())
startup_context <- NULL
if (profile_process) {
notifier_available <- exists("vscode_r_startup_existing", mode = "function") &&
exists("vscode_r_startup_run", mode = "function")
if (!notifier_available) {
startup_helper <- Sys.getenv("VSCODE_R_SESS_STARTUP_HELPER", unset = "")
if (nzchar(startup_helper) && file.exists(startup_helper)) {
tryCatch(source(startup_helper, local = TRUE), error = function(error) {
message("vscode-R could not load terminal startup notifier: ", conditionMessage(error))
})
}
notifier_available <- exists("vscode_r_startup_existing", mode = "function") &&
exists("vscode_r_startup_run", mode = "function")
}
if (!notifier_available) {
message("vscode-R terminal startup notifier is unavailable; the session watcher was not attached.")
return(invisible(FALSE))
}
startup_context <- vscode_r_startup_existing(endpoint)
if (is.null(startup_context)) {
message("vscode-R could not validate terminal startup status; the session watcher was not attached.")
return(invisible(FALSE))
}
}

attach <- function() {
ns <- vscode_r_prepare_sess(pkg_path, managed_root, consent_dir, source_helper, installer_helper,
timeout_seconds, setup_timeout_seconds)
if (is.null(ns)) {
return(invisible(FALSE))
}
connect <- get("connect", envir = ns, inherits = FALSE)
connect(endpoint = endpoint, plot_backend = plot_backend)
}

if (profile_process) {
return(vscode_r_startup_run(startup_context, attach))
}
attach()
}
32 changes: 9 additions & 23 deletions R/install_sess.R
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
local({
args <- commandArgs(trailingOnly = TRUE)
pkg_path <- Sys.getenv("VSCODE_R_SESS_PKG_PATH", unset = "")
if (!nzchar(pkg_path) && length(args) >= 1) {
pkg_path <- args[1]
}
pkg_path <- if (length(args) >= 1L && nzchar(args[1L])) args[1L] else
Sys.getenv("VSCODE_R_SESS_PKG_PATH", unset = "")

if (!nzchar(pkg_path)) {
stop("Missing pkg_path (set VSCODE_R_SESS_PKG_PATH or pass as first command arg)")
}

repo <- Sys.getenv("VSCODE_R_SESS_REPO", unset = "")
if (!nzchar(repo) && length(args) >= 2) {
repo <- args[2]
}
repo <- if (length(args) >= 2L && nzchar(args[2L])) args[2L] else
Sys.getenv("VSCODE_R_SESS_REPO", unset = "")
if (!nzchar(repo)) {
configured <- getOption("repos")
repo <- if ("CRAN" %in% names(configured)) {
Expand All @@ -23,6 +19,7 @@ local({
""
}
}
library_arg <- if (length(args) >= 3L) args[3L] else ""
if (!length(repo) || is.na(repo) || !nzchar(repo) || identical(repo, "@CRAN@")) {
repo <- "https://cloud.r-project.org"
}
Expand Down Expand Up @@ -51,28 +48,17 @@ local({
stop("Bundled sess has no valid source revision. Rebuild or reinstall the vscode-R extension.")
}

library <- Sys.getenv("VSCODE_R_SESS_LIBRARY", unset = "")
private_library <- nzchar(library)
if (!private_library) {
library <- .libPaths()[1L]
if (file.access(library, 2L) != 0L) {
user_library <- strsplit(Sys.getenv("R_LIBS_USER"), .Platform$path.sep, fixed = TRUE)[[1L]]
if (!length(user_library) || !nzchar(user_library[[1L]])) stop("No writable R library is available.")
library <- path.expand(user_library[[1L]])
}
library <- if (nzchar(library_arg)) library_arg else
Sys.getenv("VSCODE_R_SESS_LIBRARY", unset = "")
if (!nzchar(library)) {
stop("Missing VSCODE_R_SESS_LIBRARY. Refusing to install sess into a default R library.")
}
dir.create(library, recursive = TRUE, showWarnings = FALSE)
if (file.access(library, 2L) != 0L) stop(paste("R library is not writable:", library))
if (!private_library) .libPaths(c(library, .libPaths()))
installer <- new.env(parent = baseenv())
sys.source(file.path(script_directory, "sess-package-install.R"), envir = installer)
installer$sess_install(pkg_path, library, repo,
interactive = identical(Sys.getenv("VSCODE_R_SESS_INTERACTIVE"), "1"))
# Ordinary terminals must see the bundled source through their search path.
# Interactive uses its isolated library and the shared API/source verifier.
if (!private_library && !identical(sess_installed_source_revision(), expected_revision)) {
stop("sess installation did not make the bundled source available in .libPaths(). Check the installation log.")
}
if ("sess" %in% loadedNamespaces()) {
message("sess was already loaded. Restart R to use the newly installed source.")
}
Expand Down
79 changes: 71 additions & 8 deletions R/profile.R
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,75 @@ local({
invisible()
})

if (requireNamespace("sess", quietly = TRUE)) {
local({
local({
initialize_sess <- function() {
bundled_path <- Sys.getenv("VSCODE_R_SESS_PKG_PATH", unset = "")
if (!nzchar(bundled_path)) {
return(invisible(FALSE))
}

helper <- Sys.getenv("VSCODE_R_SESS_SOURCE_HELPER", unset = "")
attach_helper <- Sys.getenv("VSCODE_R_SESS_ATTACH_HELPER", unset = "")
installer_helper <- Sys.getenv("VSCODE_R_SESS_INSTALLER_HELPER", unset = "")
managed_root <- Sys.getenv("VSCODE_R_SESS_ROOT", unset = "")
consent_directory <- Sys.getenv("VSCODE_R_SESS_CONSENT_DIRECTORY", unset = "")
resources <- c(helper, attach_helper, installer_helper)
resources_ok <- all(nzchar(c(resources, managed_root))) &&
all(file.exists(resources))
if (!resources_ok) {
message("vscode-R could not locate its sess setup resources; the session watcher was not started.")
return(invisible(FALSE))
}
source(attach_helper, local = TRUE)
ns <- vscode_r_prepare_sess(bundled_path, managed_root, consent_directory, helper, installer_helper)
if (is.null(ns)) {
return(invisible(FALSE))
}

plot_backend <- Sys.getenv("SESS_PLOT_BACKEND", "auto")
sess::connect(
use_rstudioapi = as.logical(Sys.getenv("SESS_RSTUDIOAPI", "TRUE")),
plot_backend = plot_backend
)
})
}
connect <- get("connect", envir = ns, inherits = FALSE)
use_rstudioapi <- as.logical(Sys.getenv("SESS_RSTUDIOAPI", "TRUE"))
result <- connect(use_rstudioapi = use_rstudioapi, plot_backend = plot_backend)
isTRUE(result)
}

startup_file <- Sys.getenv("VSCODE_R_SESS_STARTUP_FILE", unset = "")
startup_context <- NULL
startup_allowed <- TRUE
if (nzchar(startup_file)) {
startup_helper <- Sys.getenv("VSCODE_R_SESS_STARTUP_HELPER", unset = "")
startup_token <- Sys.getenv("VSCODE_R_SESS_STARTUP_TOKEN", unset = "")
startup_endpoint <- Sys.getenv("VSCODE_R_SESS_STARTUP_ENDPOINT", unset = "")
if (!nzchar(startup_helper) || !file.exists(startup_helper)) {
message("vscode-R could not locate its terminal startup notifier; the session watcher was not started.")
startup_allowed <- FALSE
} else {
tryCatch({
sys.source(startup_helper, envir = environment())
startup_context <- vscode_r_startup_register(startup_file, startup_token, startup_endpoint)
}, error = function(error) {
message("vscode-R could not register terminal startup status: ", conditionMessage(error))
})
if (is.null(startup_context)) {
message("vscode-R could not register terminal startup status; the session watcher was not started.")
startup_allowed <- FALSE
}
}
}

if (startup_allowed) {
tryCatch({
if (nzchar(startup_file)) {
vscode_r_startup_run(startup_context, initialize_sess)
} else {
initialize_sess()
}
}, error = function(error) {
message("vscode-R could not start the session watcher: ", conditionMessage(error))
invisible(FALSE)
}, interrupt = function(error) {
message("vscode-R session watcher startup was interrupted: ", conditionMessage(error))
invisible(FALSE)
})
}
})
Loading
Loading