1- # ' Start the client R IPC Server
1+ # ' Start the client R IPC connection
22# '
3- # ' @param port Integer. The port to use for the server. If NULL, it will use
4- # ' SESS_PORT env var or a random port.
5- # ' @param token String. The token to use for authentication. If NULL, it will
6- # ' use SESS_TOKEN env var or a random token.
7- # ' @param use_rstudioapi Logical. Should the rstudioapi emulation layer be
8- # ' enabled? Defaults to TRUE.
3+ # ' @param port Integer. The port of the VS Code WebSocket server. If NULL, it will use SESS_PORT env var.
4+ # ' @param token String. The authentication token. If NULL, it will use SESS_TOKEN env var.
5+ # ' @param use_rstudioapi Logical. Should the rstudioapi emulation layer be enabled? Defaults to TRUE.
96# ' @param use_httpgd Logical. Should httpgd be used for plotting if available? Defaults to TRUE
107# ' @export
118sess_app <- function (port = NULL , token = NULL , use_rstudioapi = TRUE , use_httpgd = TRUE ) {
129 # Initialize state
1310 .sess_env $ server <- NULL
1411 .sess_env $ ws <- NULL
15-
16- # Use token if provided, otherwise fallback to SESS_TOKEN env var, or random token
17- if (is.null(token ) || is.na(token ) || ! nzchar(token )) {
18- env_token <- Sys.getenv(" SESS_TOKEN" )
19- .sess_env $ token <- if (nzchar(env_token )) {
20- env_token
21- } else {
22- paste0(sample(c(letters , 0 : 9 ), 32 , replace = TRUE ), collapse = " " )
23- }
24- } else {
25- .sess_env $ token <- token
26- }
2712 .sess_env $ pending_responses <- list ()
2813
2914 # Specific tempdir for vscode-R
@@ -33,128 +18,110 @@ sess_app <- function(port = NULL, token = NULL, use_rstudioapi = TRUE, use_httpg
3318 # Temporary file for static plot serving
3419 .sess_env $ latest_plot_path <- file.path(.sess_env $ tempdir , " sess_plot.png" )
3520
36- app_handlers <- list (
37- # --- WEBSOCKET HANDLER ---
38- onWSOpen = function (ws ) {
39- # 1. Authentication Check
40- # Extract token from QUERY_STRING (e.g., "?token=xyz")
41- query_string <- ws $ request $ QUERY_STRING
42- parsed_query <- tryCatch(
43- {
44- # Simple parsing for ?token=value
45- parts <- strsplit(query_string , " &" )[[1 ]]
46- token_part <- parts [grep(" token=" , parts )]
47- if (length(token_part ) > 0 ) {
48- sub(" ^\\ ??token=" , " " , token_part [1 ])
49- } else {
50- " "
51- }
52- },
53- error = function (e ) " "
54- )
21+ if (is.null(port ) || is.na(port )) {
22+ port <- Sys.getenv(" SESS_PORT" )
23+ }
24+ if (is.null(token ) || is.na(token ) || ! nzchar(token )) {
25+ token <- Sys.getenv(" SESS_TOKEN" )
26+ }
5527
56- print_async_msg <- function ( msg ) {
57- prompt <- if (interactive()) getOption( " prompt " ) else " "
58- cat(sprintf( " \r %s \n\n %s " , msg , prompt ))
59- }
28+ if ( ! nzchar( port ) || ! nzchar( token ) ) {
29+ warning( " [sess] SESS_PORT or SESS_TOKEN not set. Cannot connect to VS Code. " )
30+ return ( invisible ( NULL ))
31+ }
6032
61- if (parsed_query != .sess_env $ token ) {
62- print_async_msg(" [sess] Unauthorized WebSocket connection attempt" )
63- ws $ close()
64- return ()
65- }
33+ print_async_msg <- function (msg ) {
34+ prompt <- if (interactive()) getOption(" prompt" ) else " "
35+ cat(sprintf(" \r %s\n\n %s" , msg , prompt ))
36+ }
6637
67- # Bind the active websocket to our environment
68- .sess_env $ ws <- ws
69- print_async_msg(" [sess] Client connected" )
70-
71- # Send the attach handshake immediately upon connection (JSON-RPC Notification)
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())
38+ url <- sprintf(" ws://127.0.0.1:%s/?token=%s" , port , token )
39+ ws <- websocket :: WebSocket $ new(url , autoConnect = FALSE )
40+
41+ ws $ onOpen(function (event ) {
42+ .sess_env $ ws <- ws
43+ print_async_msg(" [sess] Connected to VS Code" )
44+
45+ # Send the attach handshake immediately upon connection
46+ notify_client(" attach" , list (
47+ version = sprintf(" %s.%s" , R.version $ major , R.version $ minor ),
48+ pid = Sys.getpid(),
49+ tempdir = .sess_env $ tempdir ,
50+ wd = getwd(),
51+ info = list (
52+ command = commandArgs()[[1L ]],
53+ version = R.version.string ,
54+ start_time = format(Sys.time())
55+ )
56+ ))
57+ })
58+
59+ ws $ onMessage(function (event ) {
60+ # Handle JSON-RPC 2.0 messages COMING FROM the client
61+ payload <- tryCatch(jsonlite :: fromJSON(event $ data ), error = function (e ) NULL )
62+
63+ if (! is.null(payload ) && ! is.null(payload $ id )) {
64+ if (! is.null(payload $ method )) {
65+ # It's a Request from the Client (e.g., 'workspace', 'plot_latest')
66+ handlers <- list (
67+ " workspace" = function (p ) get_workspace_data(),
68+ " hover" = function (p ) handle_hover(p $ expr ),
69+ " completion" = function (p ) handle_complete(p $ expr , p $ trigger ),
70+ " plot_latest" = function (p ) handle_plot_latest(p )
8171 )
82- ))
83-
84- ws $ onMessage(function (binary , message ) {
85- # Handle JSON-RPC 2.0 messages COMING FROM the client
86- payload <- tryCatch(jsonlite :: fromJSON(message ), error = function (e ) NULL )
87-
88- if (! is.null(payload ) && ! is.null(payload $ id )) {
89- if (! is.null(payload $ method )) {
90- # It's a Request from the Client (e.g., 'workspace', 'plot_latest')
91- handlers <- list (
92- " workspace" = function (p ) get_workspace_data(),
93- " hover" = function (p ) handle_hover(p $ expr ),
94- " completion" = function (p ) handle_complete(p $ expr , p $ trigger ),
95- " plot_latest" = function (p ) handle_plot_latest(p )
96- )
97-
98- if (payload $ method %in% names(handlers )) {
99- res <- tryCatch(
100- {
101- handlers [[payload $ method ]](payload $ params )
102- },
103- error = function (e ) {
104- # Handle unexpected R errors in handlers
105- warning(sprintf(
106- " [sess] Error in handler for '%s': %s" ,
107- payload $ method , e $ message
108- ))
109- NULL
110- }
111- )
112-
113- # Send successful response
114- succ_resp <- list (
115- jsonrpc = " 2.0" ,
116- id = payload $ id ,
117- result = res
118- )
119- ws $ send(jsonlite :: toJSON(succ_resp , auto_unbox = TRUE , null = " null" , force = TRUE ))
120- } else {
121- # Method not found
122- err_resp <- list (
123- jsonrpc = " 2.0" ,
124- id = payload $ id ,
125- error = list (code = - 32601 , message = " Method not found" )
126- )
127- ws $ send(jsonlite :: toJSON(err_resp , auto_unbox = TRUE , null = " null" , force = TRUE ))
128- }
129- } else {
130- # It's a Response (to our RStudio API request)
131- if (! is.null(payload $ result )) {
132- .sess_env $ pending_responses [[as.character(payload $ id )]] <-
133- payload $ result
134- } else if (! is.null(payload $ error )) {
135- .sess_env $ pending_responses [[as.character(payload $ id )]] <-
136- structure(payload $ error , class = " json_rpc_error" )
72+
73+ if (payload $ method %in% names(handlers )) {
74+ res <- tryCatch(
75+ {
76+ handlers [[payload $ method ]](payload $ params )
77+ },
78+ error = function (e ) {
79+ warning(sprintf(
80+ " [sess] Error in handler for '%s': %s" ,
81+ payload $ method , e $ message
82+ ))
83+ NULL
13784 }
138- }
85+ )
86+
87+ succ_resp <- list (
88+ jsonrpc = " 2.0" ,
89+ id = payload $ id ,
90+ result = res
91+ )
92+ ws $ send(jsonlite :: toJSON(succ_resp , auto_unbox = TRUE , null = " null" , force = TRUE ))
93+ } else {
94+ err_resp <- list (
95+ jsonrpc = " 2.0" ,
96+ id = payload $ id ,
97+ error = list (code = - 32601 , message = " Method not found" )
98+ )
99+ ws $ send(jsonlite :: toJSON(err_resp , auto_unbox = TRUE , null = " null" , force = TRUE ))
139100 }
140- })
141-
142- ws $ onClose(function () {
143- .sess_env $ ws <- NULL
144- print_async_msg(" [sess] Client disconnected" )
145- })
101+ } else {
102+ # It's a Response (to our RStudio API request)
103+ if (! is.null(payload $ result )) {
104+ .sess_env $ pending_responses [[as.character(payload $ id )]] <-
105+ payload $ result
106+ } else if (! is.null(payload $ error )) {
107+ .sess_env $ pending_responses [[as.character(payload $ id )]] <-
108+ structure(payload $ error , class = " json_rpc_error" )
109+ }
110+ }
146111 }
147- )
112+ } )
148113
149- # Start the httpuv server on a specific or random port
150- if (is.null(port ) || is.na(port )) {
151- env_port <- Sys.getenv(" SESS_PORT" )
152- port <- if (nzchar(env_port )) as.integer(env_port ) else httpuv :: randomPort()
153- }
154- .sess_env $ server <- httpuv :: startServer(" 127.0.0.1" , port , app = app_handlers )
114+ ws $ onClose(function (event ) {
115+ .sess_env $ ws <- NULL
116+ print_async_msg(" [sess] Disconnected from VS Code" )
117+ })
118+
119+ ws $ onError(function (event ) {
120+ print_async_msg(sprintf(" [sess] WebSocket error: %s" , event $ message ))
121+ })
155122
156- # Print the connection string to the console.
157- cat(sprintf( " \n [sess] Server address: ws://127.0.0.1:%d?token=%s \n\n " , port , .sess_env $ token ) )
123+ # Connect to VS Code
124+ ws $ connect( )
158125
159126 # Register runtime hooks
160127 if (is.na(use_rstudioapi )) use_rstudioapi <- TRUE
0 commit comments