Skip to content

Commit 0797ae6

Browse files
authored
feat: add session attachment preparation API (#1868)
External R frontends cannot discover vscode-R's managed sess package through ordinary package lookup. Exposing installation paths would also require consumers to duplicate revision checks, installation consent, and setup locking. This adds `session.prepareAttachment()` so a consumer can execute opaque R code prepared by vscode-R, including through its own local sess proxy endpoint. This provides the higher-level preparation API discussed in #1865, while keeping `getConnectionInfo()` independent of package preparation. - Return a per-call, disposable `scriptPath` and R `command`; keep the shared consent service and managed library details internal. Guard script, consent, and IPC initialization against shutdown/redeploy races. - Reuse the existing managed sess setup for external and manual attachment. Generated scripts handle errors and interrupts without preventing the next R prompt, and manual attachment passes the configured RStudio API emulation setting. - Document resource lifetime, local endpoint requirements, existing automatic selection behavior, terminal registration after connection, and the R-side consent/setup waits. Add regression coverage for endpoint rejection, independent disposal, concurrent preparation, teardown races, and generated R script execution. This PR covers preparation only. Connection lifecycle notifications, terminal rebinding, and external terminal attach hooks remain follow-up work.
1 parent b013dda commit 0797ae6

11 files changed

Lines changed: 609 additions & 57 deletions

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Added the extension API's `session.prepareAttachment()` for extensions that own an R process and need vscode-R's managed `sess` setup. R errors and interrupts during manual attachment are now reported as messages while leaving the R prompt usable.
56
- The httpgd plot backend now requires the `httpgd` R package version 2.0.1 or later. Older versions are treated as unavailable when choosing the plot backend. (#1855)
67

78
## 3.0.1 - 2026-09-28

‎R/attach_sess.R‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ vscode_r_prepare_sess <- function(pkg_path, managed_root, consent_dir,
1616
if (loaded) {
1717
loaded_revision <- sess_loaded_source_revision()
1818
if (!identical(loaded_revision, expected)) {
19-
stop("A different sess namespace is already loaded. Restart R before attaching the session watcher.")
19+
stop(paste(
20+
"A different sess namespace is already loaded.",
21+
"Close this R session and start a new one before attaching the session watcher.",
22+
"Reloading VS Code does not restart a persistent R session."
23+
))
2024
}
2125
ns <- asNamespace("sess")
2226
} else {
@@ -178,7 +182,8 @@ vscode_r_prepare_sess <- function(pkg_path, managed_root, consent_dir,
178182
vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
179183
source_helper, installer_helper, plot_backend,
180184
timeout_seconds = 180,
181-
setup_timeout_seconds = 300) {
185+
setup_timeout_seconds = 300,
186+
use_rstudioapi = TRUE) {
182187
registered <- getOption("vscodeR.terminalStartup")
183188
profile_process <- is.list(registered) && identical(registered$pid, Sys.getpid())
184189
startup_context <- NULL
@@ -213,7 +218,8 @@ vscode_r_attach_sess <- function(endpoint, pkg_path, managed_root, consent_dir,
213218
return(invisible(FALSE))
214219
}
215220
connect <- get("connect", envir = ns, inherits = FALSE)
216-
connect(endpoint = endpoint, plot_backend = plot_backend)
221+
connect(endpoint = endpoint, plot_backend = plot_backend,
222+
use_rstudioapi = use_rstudioapi)
217223
}
218224

219225
if (profile_process) {

‎R/tests/attach_sess.R‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ writeLines(c(
1616
paste0("Config/vscode-R/source-revision: ", revision)
1717
), file.path(pkg, "DESCRIPTION"))
1818
writeLines(c(
19-
"connect <- function(endpoint=NULL, plot_backend=NULL, ...) TRUE",
19+
"connect <- function(endpoint=NULL, plot_backend=NULL, use_rstudioapi=TRUE, ...) {",
20+
" options(vscodeR.test.use_rstudioapi=use_rstudioapi); TRUE",
21+
"}",
2022
"notify_client <- function(...) NULL", "request_client <- function(...) NULL",
2123
".onLoad <- function(libname, pkgname) {",
2224
" complete <- Sys.getenv('VSCODE_R_TEST_INSTALL_COMPLETE', '')",
@@ -113,12 +115,13 @@ writeLines(c(
113115
" stopifnot(file.exists(file.path(root, 'user-profile-ran'))) ",
114116
" stopifnot(any(normalizePath(.libPaths()) == normalizePath(project_library)))",
115117
"}",
116-
"attach_sess <- function(endpoint='endpoint', timeout=180, setup_timeout=8) {",
118+
"attach_sess <- function(endpoint='endpoint', timeout=180, setup_timeout=8, use_rstudioapi=TRUE) {",
117119
" vscode_r_attach_sess(endpoint, pkg, managed_root, consent,",
118120
" file.path(getwd(), 'R', 'sess_source.R'),",
119121
" if (nzchar(install_helper)) install_helper else",
120122
" file.path(getwd(), 'R', 'sess-package-install.R'),",
121-
" 'standard', timeout_seconds=timeout, setup_timeout_seconds=setup_timeout)",
123+
" 'standard', timeout_seconds=timeout, setup_timeout_seconds=setup_timeout,",
124+
" use_rstudioapi=use_rstudioapi)",
122125
"}",
123126
"publish_result <- function(value) {",
124127
" temporary <- tempfile(tmpdir=dirname(result_file))",
@@ -134,14 +137,20 @@ writeLines(c(
134137
" utils::install.packages(pkg, repos=NULL, type='source', lib=expected, quiet=TRUE)",
135138
" if (mode == 'prepared') writeLines(revision, ready_path)",
136139
" if (mode %in% c('exact', 'prepared')) {",
137-
" result <- attach_sess(timeout=0)",
140+
" result <- attach_sess(timeout=0, use_rstudioapi=FALSE)",
138141
" stopifnot(identical(result, TRUE), same_paths(.libPaths(), ordinary))",
142+
" stopifnot(identical(getOption('vscodeR.test.use_rstudioapi'), FALSE))",
139143
" stopifnot(identical(sess_loaded_source_revision(), revision))",
140144
" actual <- getNamespaceInfo(asNamespace('sess'), 'path')",
141145
" stopifnot(same_paths(actual, file.path(expected, 'sess')))",
142146
" if (mode == 'prepared') stopifnot(identical(readLines(ready_path), revision))",
143147
" stopifnot(length(list.files(consent, pattern='\\\\.request$')) == 0L)",
144148
" if (mode == 'exact') {",
149+
" legacy_result <- vscode_r_attach_sess('endpoint', pkg, managed_root, consent,",
150+
" file.path(getwd(), 'R', 'sess_source.R'),",
151+
" file.path(getwd(), 'R', 'sess-package-install.R'), 'standard', 0)",
152+
" stopifnot(identical(legacy_result, TRUE),",
153+
" identical(getOption('vscodeR.test.use_rstudioapi'), TRUE))",
145154
" blocked_file <- file.path(root, 'missing-directory', 'startup')",
146155
" context <- vscode_r_startup_register(blocked_file, strrep('c', 32), 'endpoint')",
147156
" called <- FALSE",

‎src/api.d.ts‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,48 @@ export interface RSessionActivationOptions {
3333
terminal?: vscode.Terminal;
3434
}
3535

36+
export interface RSessionPreparationOptions {
37+
/**
38+
* A local sess IPC endpoint on the current R host, such as a proxy socket.
39+
* When omitted, vscode-R's endpoint is used. Only sess IPC is supported.
40+
* On Windows, only a local `\\.\pipe\...` named pipe with a non-empty
41+
* name containing ASCII letters, digits, underscore, period, or hyphen is
42+
* accepted. On other platforms, an absolute local socket path is required.
43+
*/
44+
connection?: {
45+
transport: 'sess';
46+
endpoint: string;
47+
};
48+
}
49+
50+
/** Opaque R code and script resource for preparing sess in a consumer-owned R process. */
51+
export interface RSessionPreparation extends vscode.Disposable {
52+
/** Source this file in the target R process; its contents are private to vscode-R. */
53+
readonly scriptPath: string;
54+
/** R source(...) expression for scriptPath. This is R code, not a shell command. */
55+
readonly command: string;
56+
}
57+
3658
export interface RSessionApi {
3759
/** Returns connection details, or undefined when the session watcher is disabled. */
3860
getConnectionInfo(): Promise<RSessionConnectionInfo | undefined>;
61+
/**
62+
* Prepare managed sess integration for a consumer-owned R process on the current host.
63+
* Source `command` in that process after its user profile. The command handles R errors
64+
* and interrupts itself, reports a diagnostic and returns `invisible(FALSE)`; preparation
65+
* failures reject this promise. Returns undefined, without starting resources, when the
66+
* session watcher is disabled. The returned script is valid only for this extension-host
67+
* lifetime. Keep the resource until source execution finishes, then dispose it. If the
68+
* consumer cannot observe completion, retain it for the R process lifetime. Disposing
69+
* removes only this script; it does not stop the R process or disconnect sess.
70+
*
71+
* Attaching follows vscode-R's current automatic-selection rules and may change the UI's
72+
* selected session. After connection, call `activate(sessionId, { terminal })` to register
73+
* the consumer's execution terminal; connection replacement clears that registration.
74+
* During script execution, R may wait up to 180 seconds for install consent or 300 seconds
75+
* for another setup's lock. These are R-side waits, not a total startup timeout.
76+
*/
77+
prepareAttachment(options?: RSessionPreparationOptions): Promise<RSessionPreparation | undefined>;
3978
/**
4079
* Activates a connected session, optionally binding its execution terminal.
4180
* Omitting terminal preserves any existing binding. Returns false for unknown

‎src/extension.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
6262
const rExtension = new apiImplementation.RExtensionImplementation();
6363
rExtension.session = {
6464
getConnectionInfo: session.getConnectionInfo,
65+
prepareAttachment: session.prepareAttachment,
6566
activate: session.activateSessionById,
6667
};
6768

0 commit comments

Comments
 (0)