Skip to content

Commit 66341ad

Browse files
authored
Merge pull request #1 from REditorSupport/improve-attach
Improve attach
2 parents fdc193b + d1ddf51 commit 66341ad

3 files changed

Lines changed: 147 additions & 9 deletions

File tree

R/install_sess.R

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
local({
22
args <- commandArgs(trailingOnly = TRUE)
3-
if (length(args) < 2) {
4-
stop("Missing arguments: pkg_path and repo")
3+
pkg_path <- Sys.getenv("VSCODE_R_SESS_PKG_PATH", unset = "")
4+
if (!nzchar(pkg_path) && length(args) >= 1) {
5+
pkg_path <- args[1]
6+
}
7+
8+
if (!nzchar(pkg_path)) {
9+
stop("Missing pkg_path (set VSCODE_R_SESS_PKG_PATH or pass as first command arg)")
10+
}
11+
12+
repo <- Sys.getenv("VSCODE_R_SESS_REPO", unset = "")
13+
if (!nzchar(repo) && length(args) >= 2) {
14+
repo <- args[2]
15+
}
16+
if (!nzchar(repo)) {
17+
repo <- getOption("repos")[["CRAN"]]
18+
}
19+
if (is.na(repo) || identical(repo, "@CRAN@")) {
20+
repo <- ""
521
}
6-
pkg_path <- args[1]
7-
repo <- args[2]
822

923
if (!file.exists(file.path(pkg_path, "DESCRIPTION"))) {
1024
stop(paste("DESCRIPTION file not found in", pkg_path))
@@ -23,7 +37,11 @@ local({
2337

2438
if (length(deps) > 0) {
2539
message("Installing dependencies: ", paste(deps, collapse = ", "))
26-
install.packages(deps, repos = repo)
40+
if (nzchar(repo)) {
41+
install.packages(deps, repos = repo)
42+
} else {
43+
install.packages(deps)
44+
}
2745
}
2846

2947
message("Installing sess package from: ", pkg_path)

src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,7 @@ export async function activate(context: vscode.ExtensionContext): Promise<apiImp
250250

251251
return rExtension;
252252
}
253+
254+
export async function deactivate(): Promise<void> {
255+
await session.shutdownSessionWatcher();
256+
}

src/session.ts

Lines changed: 120 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { config, readContent, setContext, UriIcon } from './util';
1313
import * as rTerminal from './rTerminal';
1414
import { purgeAddinPickerItems, RSEditOperation, RSRange } from './rstudioapi';
1515

16-
import { homeExtDir, rWorkspace, globalRHelp, globalPlotManager, sessionStatusBarItem } from './extension';
16+
import { extensionContext, homeExtDir, rWorkspace, globalRHelp, globalPlotManager, sessionStatusBarItem, tmpDir } from './extension';
1717

1818
import { showWebView } from './webViewer';
1919

@@ -114,6 +114,7 @@ const pendingRequests = new Map<number, { resolve: (value: unknown) => void, rej
114114
const readBuffers = new Map<IpcSocket, string>();
115115

116116
let globalSessionServer: net.Server | undefined;
117+
let attachSessionScriptPath: string | undefined;
117118

118119
function isPidRunning(pid: number): boolean {
119120
try {
@@ -200,7 +201,9 @@ export async function getGlobalPipePath(): Promise<string> {
200201

201202
for (let i = 0; i < lines.length - 1; i++) {
202203
const line = lines[i].trim();
203-
if (!line) continue;
204+
if (!line) {
205+
continue;
206+
}
204207
void (async () => {
205208
try {
206209
const message = JSON.parse(line) as Record<string, unknown>;
@@ -256,6 +259,96 @@ export async function getGlobalPipePath(): Promise<string> {
256259
});
257260
}
258261

262+
function asRStringLiteral(value: string): string {
263+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
264+
}
265+
266+
function getAttachSessionScriptPath(pipePath: string): string {
267+
if (pipePath.endsWith('.sock')) {
268+
return pipePath.replace(/\.sock$/, '.R');
269+
}
270+
const scriptBase = path.basename(pipePath).replace(/[^a-zA-Z0-9_.-]/g, '_') || 'attach_session';
271+
return path.join(tmpDir(), `${scriptBase}.R`);
272+
}
273+
274+
function buildAttachSessionScript(pipePath: string, sessPath: string, installSessScriptPath: string): string {
275+
return [
276+
'local({',
277+
` pipe_path <- ${asRStringLiteral(pipePath)}`,
278+
` sess_src <- ${asRStringLiteral(sessPath)}`,
279+
` install_sess_script <- ${asRStringLiteral(installSessScriptPath)}`,
280+
' bundled_version <- tryCatch(read.dcf(file.path(sess_src, "DESCRIPTION"))[1, "Version"], error = function(e) NA_character_)',
281+
' installed_version <- suppressWarnings(tryCatch(as.character(utils::packageVersion("sess")), error = function(e) NA_character_))',
282+
' needs_install <- is.na(installed_version) || (!is.na(bundled_version) && utils::compareVersion(installed_version, bundled_version) < 0)',
283+
' if (needs_install) {',
284+
' if (!file.exists(install_sess_script)) {',
285+
' stop(sprintf("install_sess.R not found: %s", install_sess_script))',
286+
' }',
287+
' Sys.setenv(VSCODE_R_SESS_PKG_PATH = sess_src)',
288+
' on.exit(Sys.unsetenv(c("VSCODE_R_SESS_PKG_PATH", "VSCODE_R_SESS_REPO")), add = TRUE)',
289+
' source(install_sess_script, local = TRUE)',
290+
' }',
291+
' sess::connect(pipe_path = pipe_path)',
292+
'})',
293+
'',
294+
].join('\n');
295+
}
296+
297+
export async function getAttachSessionCommand(): Promise<string> {
298+
const pipePath = await getGlobalPipePath();
299+
const sessPath = extensionContext.asAbsolutePath('sess').replace(/\\/g, '/');
300+
const installSessScriptPath = extensionContext.asAbsolutePath(path.join('R', 'install_sess.R')).replace(/\\/g, '/');
301+
const scriptPath = getAttachSessionScriptPath(pipePath);
302+
await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath, installSessScriptPath), { encoding: 'utf-8' });
303+
attachSessionScriptPath = scriptPath;
304+
305+
return `source(${asRStringLiteral(scriptPath)})`;
306+
}
307+
308+
async function removePathIfExists(pathLike: string): Promise<void> {
309+
try {
310+
if (await fs.pathExists(pathLike)) {
311+
await fs.remove(pathLike);
312+
}
313+
} catch (e) {
314+
console.warn(`[session cleanup] Failed to remove ${pathLike}`, e);
315+
}
316+
}
317+
318+
export async function shutdownSessionWatcher(): Promise<void> {
319+
const pipePath = globalPipePath;
320+
321+
for (const socket of activeConnections) {
322+
socket.destroy();
323+
}
324+
activeConnections.clear();
325+
pipeClient = undefined;
326+
readBuffers.clear();
327+
328+
if (globalSessionServer) {
329+
await new Promise<void>((resolve) => {
330+
try {
331+
globalSessionServer?.close(() => resolve());
332+
} catch {
333+
resolve();
334+
}
335+
});
336+
globalSessionServer = undefined;
337+
}
338+
339+
if (attachSessionScriptPath) {
340+
await removePathIfExists(attachSessionScriptPath);
341+
attachSessionScriptPath = undefined;
342+
}
343+
344+
if (pipePath && pipePath.endsWith('.sock')) {
345+
await removePathIfExists(pipePath);
346+
await removePathIfExists(pipePath.replace(/\.sock$/, '.R'));
347+
}
348+
349+
globalPipePath = undefined;
350+
}
351+
259352
export async function activateRSession(): Promise<void> {
260353
if (config().get<boolean>('sessionWatcher')) {
261354
console.info('[activateRSession]');
@@ -284,6 +377,30 @@ export async function activateRSession(): Promise<void> {
284377
}
285378
}
286379

380+
if (config().get<boolean>('alwaysUseActiveTerminal')) {
381+
if (terminal) {
382+
const command = await getAttachSessionCommand();
383+
terminal.sendText(command, true);
384+
terminal.show();
385+
return;
386+
}
387+
388+
const action = await window.showInformationMessage(
389+
'No active terminal is available. You can copy the attach command or create a managed R terminal.',
390+
'Copy Attach Command',
391+
'Create R Terminal'
392+
);
393+
394+
if (action === 'Copy Attach Command') {
395+
await connectToSession();
396+
return;
397+
}
398+
if (action === 'Create R Terminal') {
399+
await rTerminal.createRTerm();
400+
}
401+
return;
402+
}
403+
287404
console.info('[activateRSession] Creating new R terminal');
288405
await rTerminal.createRTerm();
289406
} else {
@@ -1008,8 +1125,7 @@ export async function sessionRequest(data: Record<string, unknown>): Promise<unk
10081125
}
10091126

10101127
export async function connectToSession(): Promise<void> {
1011-
const pipePath = await getGlobalPipePath();
1012-
const command = `sess::connect(pipe_path="${pipePath.replace(/\\/g, '\\\\')}")`;
1128+
const command = await getAttachSessionCommand();
10131129
void vscode.env.clipboard.writeText(command);
10141130
void vscode.window.showInformationMessage(`R command copied to clipboard: ${command}`);
10151131
}

0 commit comments

Comments
 (0)