Skip to content

Commit 02c9187

Browse files
renkun-kenrandy3k
authored andcommitted
Improve attach
1 parent 8099747 commit 02c9187

2 files changed

Lines changed: 128 additions & 3 deletions

File tree

‎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: 124 additions & 3 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 {
@@ -256,6 +257,103 @@ export async function getGlobalPipePath(): Promise<string> {
256257
});
257258
}
258259

260+
function asRStringLiteral(value: string): string {
261+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
262+
}
263+
264+
function getAttachSessionScriptPath(pipePath: string): string {
265+
if (pipePath.endsWith('.sock')) {
266+
return pipePath.replace(/\.sock$/, '.R');
267+
}
268+
const scriptBase = path.basename(pipePath).replace(/[^a-zA-Z0-9_.-]/g, '_') || 'attach_session';
269+
return path.join(tmpDir(), `${scriptBase}.R`);
270+
}
271+
272+
function buildAttachSessionScript(pipePath: string, sessPath: string): string {
273+
return [
274+
'local({',
275+
` pipe_path <- ${asRStringLiteral(pipePath)}`,
276+
` sess_src <- ${asRStringLiteral(sessPath)}`,
277+
' bundled_version <- tryCatch(read.dcf(file.path(sess_src, "DESCRIPTION"))[1, "Version"], error = function(e) NA_character_)',
278+
' installed_version <- suppressWarnings(tryCatch(as.character(utils::packageVersion("sess")), error = function(e) NA_character_))',
279+
' needs_install <- is.na(installed_version) || (!is.na(bundled_version) && utils::compareVersion(installed_version, bundled_version) < 0)',
280+
' if (needs_install) {',
281+
' lib_candidates <- unique(c(.libPaths(), Sys.getenv("R_LIBS_USER", unset = "")))',
282+
' lib_candidates <- lib_candidates[nzchar(lib_candidates)]',
283+
' writable <- lib_candidates[vapply(lib_candidates, function(lib) {',
284+
' if (!dir.exists(lib)) {',
285+
' dir.create(lib, recursive = TRUE, showWarnings = FALSE)',
286+
' }',
287+
' file.access(lib, 2) == 0',
288+
' }, logical(1))]',
289+
' if (length(writable) < 1) {',
290+
' message("[vscode-R] Could not find a writable library path for this terminal.")',
291+
' message("[vscode-R] Install the bundled sess package manually:")',
292+
' message(sprintf("install.packages(%s, repos = NULL, type = \\"source\\")", shQuote(sess_src)))',
293+
' stop("No writable R library path available for installing sess")',
294+
' }',
295+
' install.packages(sess_src, repos = NULL, type = "source", lib = writable[[1]])',
296+
' }',
297+
' sess::connect(pipe_path = pipe_path)',
298+
'})',
299+
'',
300+
].join('\n');
301+
}
302+
303+
export async function getAttachSessionCommand(): Promise<string> {
304+
const pipePath = await getGlobalPipePath();
305+
const sessPath = extensionContext.asAbsolutePath('sess').replace(/\\/g, '/');
306+
const scriptPath = getAttachSessionScriptPath(pipePath);
307+
await fs.writeFile(scriptPath, buildAttachSessionScript(pipePath, sessPath), { encoding: 'utf-8' });
308+
attachSessionScriptPath = scriptPath;
309+
310+
return `source(${asRStringLiteral(scriptPath)})`;
311+
}
312+
313+
async function removePathIfExists(pathLike: string): Promise<void> {
314+
try {
315+
if (await fs.pathExists(pathLike)) {
316+
await fs.remove(pathLike);
317+
}
318+
} catch (e) {
319+
console.warn(`[session cleanup] Failed to remove ${pathLike}`, e);
320+
}
321+
}
322+
323+
export async function shutdownSessionWatcher(): Promise<void> {
324+
const pipePath = globalPipePath;
325+
326+
for (const socket of activeConnections) {
327+
socket.destroy();
328+
}
329+
activeConnections.clear();
330+
pipeClient = undefined;
331+
readBuffers.clear();
332+
333+
if (globalSessionServer) {
334+
await new Promise<void>((resolve) => {
335+
try {
336+
globalSessionServer?.close(() => resolve());
337+
} catch {
338+
resolve();
339+
}
340+
});
341+
globalSessionServer = undefined;
342+
}
343+
344+
if (attachSessionScriptPath) {
345+
await removePathIfExists(attachSessionScriptPath);
346+
attachSessionScriptPath = undefined;
347+
}
348+
349+
if (pipePath && pipePath.endsWith('.sock')) {
350+
await removePathIfExists(pipePath);
351+
await removePathIfExists(pipePath.replace(/\.sock$/, '.R'));
352+
}
353+
354+
globalPipePath = undefined;
355+
}
356+
259357
export async function activateRSession(): Promise<void> {
260358
if (config().get<boolean>('sessionWatcher')) {
261359
console.info('[activateRSession]');
@@ -284,6 +382,30 @@ export async function activateRSession(): Promise<void> {
284382
}
285383
}
286384

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

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

0 commit comments

Comments
 (0)