From 9c1d75b119a153b2162ce641c3dedb7fc5f9fb02 Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 9 May 2026 11:02:56 +0800 Subject: [PATCH 1/2] Improve attach --- src/extension.ts | 4 ++ src/session.ts | 127 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 128 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 1c3de201..d1eb4bb2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -250,3 +250,7 @@ export async function activate(context: vscode.ExtensionContext): Promise { + await session.shutdownSessionWatcher(); +} diff --git a/src/session.ts b/src/session.ts index 5744b0f7..0fbd5972 100644 --- a/src/session.ts +++ b/src/session.ts @@ -13,7 +13,7 @@ import { config, readContent, setContext, UriIcon } from './util'; import * as rTerminal from './rTerminal'; import { purgeAddinPickerItems, RSEditOperation, RSRange } from './rstudioapi'; -import { homeExtDir, rWorkspace, globalRHelp, globalPlotManager, sessionStatusBarItem } from './extension'; +import { extensionContext, homeExtDir, rWorkspace, globalRHelp, globalPlotManager, sessionStatusBarItem, tmpDir } from './extension'; import { showWebView } from './webViewer'; @@ -114,6 +114,7 @@ const pendingRequests = new Map void, rej const readBuffers = new Map(); let globalSessionServer: net.Server | undefined; +let attachSessionScriptPath: string | undefined; function isPidRunning(pid: number): boolean { try { @@ -256,6 +257,103 @@ export async function getGlobalPipePath(): Promise { }); } +function asRStringLiteral(value: string): string { + return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; +} + +function getAttachSessionScriptPath(pipePath: string): string { + if (pipePath.endsWith('.sock')) { + return pipePath.replace(/\.sock$/, '.R'); + } + const scriptBase = path.basename(pipePath).replace(/[^a-zA-Z0-9_.-]/g, '_') || 'attach_session'; + return path.join(tmpDir(), `${scriptBase}.R`); +} + +function buildAttachSessionScript(pipePath: string, sessPath: string): string { + return [ + 'local({', + ` pipe_path <- ${asRStringLiteral(pipePath)}`, + ` sess_src <- ${asRStringLiteral(sessPath)}`, + ' bundled_version <- tryCatch(read.dcf(file.path(sess_src, "DESCRIPTION"))[1, "Version"], error = function(e) NA_character_)', + ' installed_version <- suppressWarnings(tryCatch(as.character(utils::packageVersion("sess")), error = function(e) NA_character_))', + ' needs_install <- is.na(installed_version) || (!is.na(bundled_version) && utils::compareVersion(installed_version, bundled_version) < 0)', + ' if (needs_install) {', + ' lib_candidates <- unique(c(.libPaths(), Sys.getenv("R_LIBS_USER", unset = "")))', + ' lib_candidates <- lib_candidates[nzchar(lib_candidates)]', + ' writable <- lib_candidates[vapply(lib_candidates, function(lib) {', + ' if (!dir.exists(lib)) {', + ' dir.create(lib, recursive = TRUE, showWarnings = FALSE)', + ' }', + ' file.access(lib, 2) == 0', + ' }, logical(1))]', + ' if (length(writable) < 1) {', + ' message("[vscode-R] Could not find a writable library path for this terminal.")', + ' message("[vscode-R] Install the bundled sess package manually:")', + ' message(sprintf("install.packages(%s, repos = NULL, type = \\"source\\")", shQuote(sess_src)))', + ' stop("No writable R library path available for installing sess")', + ' }', + ' install.packages(sess_src, repos = NULL, type = "source", lib = writable[[1]])', + ' }', + ' sess::connect(pipe_path = pipe_path)', + '})', + '', + ].join('\n'); +} + +export async function getAttachSessionCommand(): Promise { + const pipePath = await getGlobalPipePath(); + const sessPath = extensionContext.asAbsolutePath('sess').replace(/\\/g, '/'); + const scriptPath = getAttachSessionScriptPath(pipePath); + await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath), { encoding: 'utf-8' }); + attachSessionScriptPath = scriptPath; + + return `source(${asRStringLiteral(scriptPath)})`; +} + +async function removePathIfExists(pathLike: string): Promise { + try { + if (await fs.pathExists(pathLike)) { + await fs.remove(pathLike); + } + } catch (e) { + console.warn(`[session cleanup] Failed to remove ${pathLike}`, e); + } +} + +export async function shutdownSessionWatcher(): Promise { + const pipePath = globalPipePath; + + for (const socket of activeConnections) { + socket.destroy(); + } + activeConnections.clear(); + pipeClient = undefined; + readBuffers.clear(); + + if (globalSessionServer) { + await new Promise((resolve) => { + try { + globalSessionServer?.close(() => resolve()); + } catch { + resolve(); + } + }); + globalSessionServer = undefined; + } + + if (attachSessionScriptPath) { + await removePathIfExists(attachSessionScriptPath); + attachSessionScriptPath = undefined; + } + + if (pipePath && pipePath.endsWith('.sock')) { + await removePathIfExists(pipePath); + await removePathIfExists(pipePath.replace(/\.sock$/, '.R')); + } + + globalPipePath = undefined; +} + export async function activateRSession(): Promise { if (config().get('sessionWatcher')) { console.info('[activateRSession]'); @@ -284,6 +382,30 @@ export async function activateRSession(): Promise { } } + if (config().get('alwaysUseActiveTerminal')) { + if (terminal) { + const command = await getAttachSessionCommand(); + terminal.sendText(command, true); + terminal.show(); + return; + } + + const action = await window.showInformationMessage( + 'No active terminal is available. You can copy the attach command or create a managed R terminal.', + 'Copy Attach Command', + 'Create R Terminal' + ); + + if (action === 'Copy Attach Command') { + await connectToSession(); + return; + } + if (action === 'Create R Terminal') { + await rTerminal.createRTerm(); + } + return; + } + console.info('[activateRSession] Creating new R terminal'); await rTerminal.createRTerm(); } else { @@ -1008,8 +1130,7 @@ export async function sessionRequest(data: Record): Promise { - const pipePath = await getGlobalPipePath(); - const command = `sess::connect(pipe_path="${pipePath.replace(/\\/g, '\\\\')}")`; + const command = await getAttachSessionCommand(); void vscode.env.clipboard.writeText(command); void vscode.window.showInformationMessage(`R command copied to clipboard: ${command}`); } From d1ddf5177bcbc7a23326d6db55ec6a0def7162fc Mon Sep 17 00:00:00 2001 From: Kun Ren Date: Sat, 9 May 2026 13:06:29 +0800 Subject: [PATCH 2/2] Update install_sess --- R/install_sess.R | 28 +++++++++++++++++++++++----- src/session.ts | 29 ++++++++++++----------------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/R/install_sess.R b/R/install_sess.R index 4901e656..70af47d0 100644 --- a/R/install_sess.R +++ b/R/install_sess.R @@ -1,10 +1,24 @@ local({ args <- commandArgs(trailingOnly = TRUE) - if (length(args) < 2) { - stop("Missing arguments: pkg_path and repo") + pkg_path <- Sys.getenv("VSCODE_R_SESS_PKG_PATH", unset = "") + if (!nzchar(pkg_path) && length(args) >= 1) { + pkg_path <- args[1] + } + + 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] + } + if (!nzchar(repo)) { + repo <- getOption("repos")[["CRAN"]] + } + if (is.na(repo) || identical(repo, "@CRAN@")) { + repo <- "" } - pkg_path <- args[1] - repo <- args[2] if (!file.exists(file.path(pkg_path, "DESCRIPTION"))) { stop(paste("DESCRIPTION file not found in", pkg_path)) @@ -23,7 +37,11 @@ local({ if (length(deps) > 0) { message("Installing dependencies: ", paste(deps, collapse = ", ")) - install.packages(deps, repos = repo) + if (nzchar(repo)) { + install.packages(deps, repos = repo) + } else { + install.packages(deps) + } } message("Installing sess package from: ", pkg_path) diff --git a/src/session.ts b/src/session.ts index 0fbd5972..7e02f9bb 100644 --- a/src/session.ts +++ b/src/session.ts @@ -201,7 +201,9 @@ export async function getGlobalPipePath(): Promise { for (let i = 0; i < lines.length - 1; i++) { const line = lines[i].trim(); - if (!line) continue; + if (!line) { + continue; + } void (async () => { try { const message = JSON.parse(line) as Record; @@ -269,30 +271,22 @@ function getAttachSessionScriptPath(pipePath: string): string { return path.join(tmpDir(), `${scriptBase}.R`); } -function buildAttachSessionScript(pipePath: string, sessPath: string): string { +function buildAttachSessionScript(pipePath: string, sessPath: string, installSessScriptPath: string): string { return [ 'local({', ` pipe_path <- ${asRStringLiteral(pipePath)}`, ` sess_src <- ${asRStringLiteral(sessPath)}`, + ` install_sess_script <- ${asRStringLiteral(installSessScriptPath)}`, ' bundled_version <- tryCatch(read.dcf(file.path(sess_src, "DESCRIPTION"))[1, "Version"], error = function(e) NA_character_)', ' installed_version <- suppressWarnings(tryCatch(as.character(utils::packageVersion("sess")), error = function(e) NA_character_))', ' needs_install <- is.na(installed_version) || (!is.na(bundled_version) && utils::compareVersion(installed_version, bundled_version) < 0)', ' if (needs_install) {', - ' lib_candidates <- unique(c(.libPaths(), Sys.getenv("R_LIBS_USER", unset = "")))', - ' lib_candidates <- lib_candidates[nzchar(lib_candidates)]', - ' writable <- lib_candidates[vapply(lib_candidates, function(lib) {', - ' if (!dir.exists(lib)) {', - ' dir.create(lib, recursive = TRUE, showWarnings = FALSE)', - ' }', - ' file.access(lib, 2) == 0', - ' }, logical(1))]', - ' if (length(writable) < 1) {', - ' message("[vscode-R] Could not find a writable library path for this terminal.")', - ' message("[vscode-R] Install the bundled sess package manually:")', - ' message(sprintf("install.packages(%s, repos = NULL, type = \\"source\\")", shQuote(sess_src)))', - ' stop("No writable R library path available for installing sess")', + ' if (!file.exists(install_sess_script)) {', + ' stop(sprintf("install_sess.R not found: %s", install_sess_script))', ' }', - ' install.packages(sess_src, repos = NULL, type = "source", lib = writable[[1]])', + ' Sys.setenv(VSCODE_R_SESS_PKG_PATH = sess_src)', + ' on.exit(Sys.unsetenv(c("VSCODE_R_SESS_PKG_PATH", "VSCODE_R_SESS_REPO")), add = TRUE)', + ' source(install_sess_script, local = TRUE)', ' }', ' sess::connect(pipe_path = pipe_path)', '})', @@ -303,8 +297,9 @@ function buildAttachSessionScript(pipePath: string, sessPath: string): string { export async function getAttachSessionCommand(): Promise { const pipePath = await getGlobalPipePath(); const sessPath = extensionContext.asAbsolutePath('sess').replace(/\\/g, '/'); + const installSessScriptPath = extensionContext.asAbsolutePath(path.join('R', 'install_sess.R')).replace(/\\/g, '/'); const scriptPath = getAttachSessionScriptPath(pipePath); - await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath), { encoding: 'utf-8' }); + await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath, installSessScriptPath), { encoding: 'utf-8' }); attachSessionScriptPath = scriptPath; return `source(${asRStringLiteral(scriptPath)})`;