Skip to content

Commit d905392

Browse files
committed
feat(session): implement file-based reconnection and suppress verbose logs
1 parent a0edd8e commit d905392

3 files changed

Lines changed: 185 additions & 80 deletions

File tree

‎sess/R/server.R‎

Lines changed: 119 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,33 @@ sess_app <- function(port = NULL, token = NULL, use_rstudioapi = TRUE, use_httpg
2020
# Temporary file for static plot serving
2121
.sess_env$latest_plot_path <- file.path(.sess_env$tempdir, "sess_plot.png")
2222

23+
is_manual <- (!is.null(port) && !is.na(port) && nzchar(port)) ||
24+
(!is.null(token) && !is.na(token) && nzchar(token))
25+
2326
if (is.null(port) || is.na(port)) {
2427
port <- Sys.getenv("SESS_PORT")
2528
}
2629
if (is.null(token) || is.na(token) || !nzchar(token)) {
2730
token <- Sys.getenv("SESS_TOKEN")
2831
}
2932

33+
# Derive file path for fallback/reconnection
34+
pid <- Sys.getpid()
35+
home <- path.expand("~")
36+
file_path <- file.path(home, ".vscode-R", "sessions", sprintf("%d.json", pid))
37+
38+
if (!nzchar(port) || !nzchar(token)) {
39+
if (file.exists(file_path)) {
40+
tryCatch({
41+
config <- jsonlite::fromJSON(readLines(file_path, warn = FALSE))
42+
port <- config$port
43+
token <- config$token
44+
}, error = function(e) NULL)
45+
}
46+
}
47+
3048
if (!nzchar(port) || !nzchar(token)) {
31-
warning("[sess] SESS_PORT or SESS_TOKEN not set. Cannot connect to VS Code.")
49+
warning("[sess] Connection info not available. Cannot connect to VS Code.")
3250
return(invisible(NULL))
3351
}
3452

@@ -37,93 +55,116 @@ sess_app <- function(port = NULL, token = NULL, use_rstudioapi = TRUE, use_httpg
3755
cat(sprintf("\r%s\n\n%s", msg, prompt))
3856
}
3957

40-
url <- sprintf("ws://127.0.0.1:%s/?token=%s", port, token)
41-
ws <- websocket::WebSocket$new(url, autoConnect = FALSE)
42-
43-
ws$onOpen(function(event) {
44-
.sess_env$ws <- ws
45-
print_async_msg("[sess] Connected to VS Code")
46-
47-
# Send the attach handshake immediately upon connection
48-
notify_client("attach", list(
49-
version = sprintf("%s.%s", R.version$major, R.version$minor),
50-
pid = Sys.getpid(),
51-
tempdir = .sess_env$tempdir,
52-
wd = getwd(),
53-
info = list(
54-
command = commandArgs()[[1L]],
55-
version = R.version.string,
56-
start_time = format(Sys.time())
57-
)
58-
))
59-
})
60-
61-
ws$onMessage(function(event) {
62-
# Handle JSON-RPC 2.0 messages COMING FROM the client
63-
payload <- tryCatch(jsonlite::fromJSON(event$data), error = function(e) NULL)
64-
65-
if (!is.null(payload) && !is.null(payload$id)) {
66-
if (!is.null(payload$method)) {
67-
# It's a Request from the Client (e.g., 'workspace', 'plot_latest')
68-
handlers <- list(
69-
"workspace" = function(p) get_workspace_data(),
70-
"hover" = function(p) handle_hover(p$expr),
71-
"completion" = function(p) handle_complete(p$expr, p$trigger),
72-
"plot_latest" = function(p) handle_plot_latest(p)
58+
connect <- function() {
59+
url <- sprintf("ws://127.0.0.1:%s/?token=%s", port, token)
60+
ws <- websocket::WebSocket$new(
61+
url,
62+
autoConnect = FALSE,
63+
accessLogChannels = "none",
64+
errorLogChannels = "none"
65+
)
66+
67+
ws$onOpen(function(event) {
68+
.sess_env$ws <- ws
69+
print_async_msg("[sess] Connected to VS Code")
70+
71+
# Send the attach handshake immediately upon connection
72+
notify_client("attach", list(
73+
version = sprintf("%s.%s", R.version$major, R.version$minor),
74+
pid = Sys.getpid(),
75+
tempdir = .sess_env$tempdir,
76+
wd = getwd(),
77+
info = list(
78+
command = commandArgs()[[1L]],
79+
version = R.version.string,
80+
start_time = format(Sys.time())
7381
)
74-
75-
if (payload$method %in% names(handlers)) {
76-
res <- tryCatch(
77-
{
78-
handlers[[payload$method]](payload$params)
79-
},
80-
error = function(e) {
81-
warning(sprintf(
82-
"[sess] Error in handler for '%s': %s",
83-
payload$method, e$message
84-
))
85-
NULL
86-
}
82+
))
83+
})
84+
85+
ws$onMessage(function(event) {
86+
# Handle JSON-RPC 2.0 messages COMING FROM the client
87+
payload <- tryCatch(jsonlite::fromJSON(event$data), error = function(e) NULL)
88+
89+
if (!is.null(payload) && !is.null(payload$id)) {
90+
if (!is.null(payload$method)) {
91+
# It's a Request from the Client (e.g., 'workspace', 'plot_latest')
92+
handlers <- list(
93+
"workspace" = function(p) get_workspace_data(),
94+
"hover" = function(p) handle_hover(p$expr),
95+
"completion" = function(p) handle_complete(p$expr, p$trigger),
96+
"plot_latest" = function(p) handle_plot_latest(p)
8797
)
8898

89-
succ_resp <- list(
90-
jsonrpc = "2.0",
91-
id = payload$id,
92-
result = res
93-
)
94-
ws$send(jsonlite::toJSON(succ_resp, auto_unbox = TRUE, null = "null", force = TRUE))
99+
if (payload$method %in% names(handlers)) {
100+
res <- tryCatch(
101+
{
102+
handlers[[payload$method]](payload$params)
103+
},
104+
error = function(e) {
105+
warning(sprintf(
106+
"[sess] Error in handler for '%s': %s",
107+
payload$method, e$message
108+
))
109+
NULL
110+
}
111+
)
112+
113+
succ_resp <- list(
114+
jsonrpc = "2.0",
115+
id = payload$id,
116+
result = res
117+
)
118+
ws$send(jsonlite::toJSON(succ_resp, auto_unbox = TRUE, null = "null", force = TRUE))
119+
} else {
120+
err_resp <- list(
121+
jsonrpc = "2.0",
122+
id = payload$id,
123+
error = list(code = -32601, message = "Method not found")
124+
)
125+
ws$send(jsonlite::toJSON(err_resp, auto_unbox = TRUE, null = "null", force = TRUE))
126+
}
95127
} else {
96-
err_resp <- list(
97-
jsonrpc = "2.0",
98-
id = payload$id,
99-
error = list(code = -32601, message = "Method not found")
100-
)
101-
ws$send(jsonlite::toJSON(err_resp, auto_unbox = TRUE, null = "null", force = TRUE))
102-
}
103-
} else {
104-
# It's a Response (to our RStudio API request)
105-
if (!is.null(payload$result)) {
106-
.sess_env$pending_responses[[as.character(payload$id)]] <-
107-
payload$result
108-
} else if (!is.null(payload$error)) {
109-
.sess_env$pending_responses[[as.character(payload$id)]] <-
110-
structure(payload$error, class = "json_rpc_error")
128+
# It's a Response (to our RStudio API request)
129+
if (!is.null(payload$result)) {
130+
.sess_env$pending_responses[[as.character(payload$id)]] <-
131+
payload$result
132+
} else if (!is.null(payload$error)) {
133+
.sess_env$pending_responses[[as.character(payload$id)]] <-
134+
structure(payload$error, class = "json_rpc_error")
135+
}
111136
}
112137
}
113-
}
114-
})
138+
})
115139

116-
ws$onClose(function(event) {
117-
.sess_env$ws <- NULL
118-
print_async_msg("[sess] Disconnected from VS Code")
119-
})
140+
ws$onClose(function(event) {
141+
.sess_env$ws <- NULL
142+
if (is_manual) {
143+
print_async_msg("[sess] Disconnected from VS Code.")
144+
return()
145+
}
146+
print_async_msg("[sess] Disconnected from VS Code. Retrying in 5 seconds...")
147+
later::later(function() {
148+
if (file.exists(file_path)) {
149+
tryCatch({
150+
config <- jsonlite::fromJSON(readLines(file_path, warn = FALSE))
151+
port <<- config$port
152+
token <<- config$token
153+
}, error = function(e) NULL)
154+
}
155+
connect()
156+
}, 5)
157+
})
158+
159+
ws$onError(function(event) {
160+
print_async_msg(sprintf("[sess] WebSocket error: %s", event$message))
161+
})
120162

121-
ws$onError(function(event) {
122-
print_async_msg(sprintf("[sess] WebSocket error: %s", event$message))
123-
})
163+
ws$connect()
164+
}
124165

125166
# Connect to VS Code
126-
ws$connect()
167+
connect()
127168

128169
# Register runtime hooks
129170
if (is.na(use_rstudioapi)) use_rstudioapi <- TRUE

‎src/rTerminal.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ export async function runFromLineToEnd(): Promise<void> {
179179
await runTextInTerm(text);
180180
}
181181

182-
import { getGlobalSessionServer } from './session';
182+
import { getGlobalSessionServer, writeSessionFile } from './session';
183183

184184
export async function makeTerminalOptions(): Promise<vscode.TerminalOptions> {
185185
const workspaceFolderPath = getCurrentWorkspaceFolder()?.uri.fsPath;
@@ -220,6 +220,13 @@ export async function createRTerm(preserveshow?: boolean): Promise<boolean> {
220220
rTerm = vscode.window.createTerminal(termOptions);
221221
rTerm.show(preserveshow);
222222

223+
void rTerm.processId.then(async (pid) => {
224+
if (pid) {
225+
const { port, token } = await getGlobalSessionServer();
226+
await writeSessionFile(pid.toString(), port, token);
227+
}
228+
});
229+
223230
return true;
224231
}
225232

‎src/session.ts‎

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import * as fs from 'fs-extra';
44
import * as path from 'path';
5+
import * as os from 'os';
56
import * as vscode from 'vscode';
67
import { commands, Uri, ViewColumn, Webview, window, workspace, env } from 'vscode';
78

@@ -96,7 +97,10 @@ export function deploySessionWatcher(extensionPath: string): void {
9697
resDir = path.join(extensionPath, 'dist', 'resources');
9798

9899
// Initialize the WebSocket server when the extension activates
99-
void getGlobalSessionServer().catch(err => {
100+
void getGlobalSessionServer().then(async (srv) => {
101+
await pruneSessionFiles();
102+
await updateActiveTerminalFiles(srv.port, srv.token);
103+
}).catch(err => {
100104
console.error('Failed to initialize global session server', err);
101105
});
102106

@@ -117,6 +121,59 @@ const pendingRequests = new Map<number, { resolve: (value: unknown) => void, rej
117121

118122
let globalSessionServer: { port: number, token: string } | undefined;
119123

124+
function isPidRunning(pid: number): boolean {
125+
try {
126+
process.kill(pid, 0);
127+
return true;
128+
} catch (e: any) {
129+
return e.code !== 'ESRCH';
130+
}
131+
}
132+
133+
async function pruneSessionFiles() {
134+
const homeDir = os.homedir();
135+
const sessionsDir = path.join(homeDir, '.vscode-R', 'sessions');
136+
if (!await fs.pathExists(sessionsDir)) {
137+
return;
138+
}
139+
const files = await fs.readdir(sessionsDir);
140+
for (const file of files) {
141+
if (file.endsWith('.json')) {
142+
const pidStr = path.basename(file, '.json');
143+
const pid = parseInt(pidStr, 10);
144+
if (!isNaN(pid)) {
145+
if (!isPidRunning(pid)) {
146+
try {
147+
await fs.remove(path.join(sessionsDir, file));
148+
} catch (e) {
149+
console.error(`Failed to remove stale session file ${file}`, e);
150+
}
151+
}
152+
}
153+
}
154+
}
155+
}
156+
157+
export async function writeSessionFile(pid: string, port: number, token: string) {
158+
const homeDir = os.homedir();
159+
const sessionsDir = path.join(homeDir, '.vscode-R', 'sessions');
160+
await fs.ensureDir(sessionsDir);
161+
const filePath = path.join(sessionsDir, `${pid}.json`);
162+
await fs.writeJson(filePath, { port, token });
163+
}
164+
165+
async function updateActiveTerminalFiles(port: number, token: string) {
166+
const terminals = vscode.window.terminals;
167+
for (const term of terminals) {
168+
if (term.name === 'R Interactive') {
169+
const pid = await term.processId;
170+
if (pid) {
171+
await writeSessionFile(pid.toString(), port, token);
172+
}
173+
}
174+
}
175+
}
176+
120177
export async function getGlobalSessionServer(): Promise<{ port: number, token: string }> {
121178
if (globalSessionServer) {
122179
return globalSessionServer;

0 commit comments

Comments
 (0)