Skip to content

Commit 4cc60f7

Browse files
dfalbellionel-
andauthored
Reticulate support (#506)
* Add rpc method to query reticulate interpreters * Add events for reticulate * Make sure thre's a single reticulate thread * Remove unncessary reticulate focus command * Cleanup * Remove unnecessary id * Send code to R main thread * Input code * Allow optional * Correctly acquire input * Create a method to start kernels * Add install reticulate method * Add more method to make checks on R environment * Apply suggestions from code review Co-authored-by: Lionel Henry <[email protected]> * Improvements to style and simplifications * Add some comments + install packages rpc method --------- Co-authored-by: Lionel Henry <[email protected]>
1 parent 7705cb0 commit 4cc60f7

File tree

4 files changed

+221
-0
lines changed

4 files changed

+221
-0
lines changed

crates/ark/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod modules_utils;
2626
pub mod plots;
2727
pub mod r_task;
2828
pub mod request;
29+
pub mod reticulate;
2930
pub mod shell;
3031
pub mod signals;
3132
pub mod srcref;

crates/ark/src/modules/positron/package.R

+11
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@
2121
#' @export
2222
.ps.rpc.is_installed <- .ps.is_installed
2323

24+
#' @export
25+
.ps.rpc.install_packages <- function(packages) {
26+
for (pkg in packages) {
27+
if (.ps.rpc.isPackageAttached(pkg)) {
28+
stop("Should not install a package if it's already attached.")
29+
}
30+
}
31+
utils::install.packages(packages)
32+
TRUE
33+
}
34+
2435
#' @export
2536
.ps.rpc.isPackageAttached <- function(pkg) {
2637
if (!is_string(pkg)) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#' @export
2+
.ps.reticulate_open <- function(input="") {
3+
.ps.Call("ps_reticulate_open", input)
4+
}
5+
6+
#' Called by the front-end right before starting the reticulate session.
7+
#'
8+
#' At this point it should be fine to load Python if it's not loaded, and
9+
#' check if it can be started and if necessary packages are installed.
10+
#' @export
11+
.ps.rpc.reticulate_check_prerequisites <- function() {
12+
13+
# This should return a list with the following fields:
14+
# python: NULL or string
15+
# venv: NULL or string
16+
# ipykernel: NULL or string
17+
# error: NULL or string
18+
19+
config <- tryCatch({
20+
reticulate::py_discover_config()
21+
}, error = function(err) {
22+
err
23+
})
24+
25+
if (inherits(config, "error")) {
26+
# py_discover_config() can fail if the user forced a Python session
27+
# via RETICULATE_PYTHON, but this version doesn't exist.
28+
return(list(error = conditionMessage(config)))
29+
}
30+
31+
if (is.null(config) || is.null(config$python)) {
32+
# The front-end will offer to install Python.
33+
return(list(python = NULL, error = NULL))
34+
}
35+
36+
python <- config$python
37+
venv <- config$virtualenv
38+
39+
# Check that python can be loaded, if it can't we will throw
40+
# an error, which is unrecoverable.
41+
config <- tryCatch({
42+
reticulate::py_config()
43+
}, error = function(err) {
44+
err
45+
})
46+
47+
if (inherits(config, "error")) {
48+
return(list(python = python, venv = venv, error = conditionMessage(config)))
49+
}
50+
51+
# Now check ipykernel
52+
ipykernel <- tryCatch({
53+
reticulate::py_module_available("ipykernel")
54+
}, error = function(err) {
55+
err
56+
})
57+
58+
if (inherits(ipykernel, "error")) {
59+
return(list(python = python, venv = venv, error = conditionMessage(ipykernel)))
60+
}
61+
62+
list(
63+
python = config$python,
64+
venv = venv,
65+
ipykernel = ipykernel,
66+
error = NULL
67+
)
68+
}
69+
70+
#' @export
71+
.ps.rpc.reticulate_start_kernel <- function(kernelPath, connectionFile, logFile, logLevel) {
72+
# Starts an IPykernel in a separate thread with information provided by
73+
# the caller.
74+
# It it's essentially executing the kernel startup script:
75+
# https://github.com/posit-dev/positron/blob/main/extensions/positron-python/python_files/positron/positron_language_server.py
76+
# and passing the communication files that Positron Jupyter's Adapter sets up.
77+
tryCatch({
78+
reticulate:::py_run_file_on_thread(
79+
file = kernelPath,
80+
args = c(
81+
"-f", connectionFile,
82+
"--logfile", logFile,
83+
"--loglevel", logLevel,
84+
"--session-mode", "console"
85+
)
86+
)
87+
# Empty string means that no error happened.
88+
""
89+
}, error = function(err) {
90+
conditionMessage(err)
91+
})
92+
}

crates/ark/src/reticulate.rs

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use std::ops::Deref;
2+
use std::sync::LazyLock;
3+
use std::sync::Mutex;
4+
5+
use amalthea::comm::comm_channel::CommMsg;
6+
use amalthea::comm::event::CommManagerEvent;
7+
use amalthea::socket::comm::CommInitiator;
8+
use amalthea::socket::comm::CommSocket;
9+
use crossbeam::channel::Sender;
10+
use harp::RObject;
11+
use libr::R_NilValue;
12+
use libr::SEXP;
13+
use serde_json::json;
14+
use stdext::result::ResultOrLog;
15+
use stdext::spawn;
16+
use stdext::unwrap;
17+
use uuid::Uuid;
18+
19+
use crate::interface::RMain;
20+
21+
static RETICULATE_COMM_ID: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None));
22+
23+
pub struct ReticulateService {
24+
comm: CommSocket,
25+
comm_manager_tx: Sender<CommManagerEvent>,
26+
}
27+
28+
impl ReticulateService {
29+
fn start(comm_id: String, comm_manager_tx: Sender<CommManagerEvent>) -> anyhow::Result<String> {
30+
let comm = CommSocket::new(
31+
CommInitiator::BackEnd,
32+
comm_id.clone(),
33+
String::from("positron.reticulate"),
34+
);
35+
36+
let service = Self {
37+
comm,
38+
comm_manager_tx,
39+
};
40+
41+
let event = CommManagerEvent::Opened(service.comm.clone(), serde_json::Value::Null);
42+
service
43+
.comm_manager_tx
44+
.send(event)
45+
.or_log_error("Reticulate: Could not open comm.");
46+
47+
spawn!(format!("ark-reticulate-{}", comm_id), move || {
48+
service
49+
.handle_messages()
50+
.or_log_error("Reticulate: Error handling messages");
51+
});
52+
53+
Ok(comm_id)
54+
}
55+
56+
fn handle_messages(&self) -> Result<(), anyhow::Error> {
57+
loop {
58+
let msg = unwrap!(self.comm.incoming_rx.recv(), Err(err) => {
59+
log::error!("Reticulate: Error while receiving message from frontend: {err:?}");
60+
break;
61+
});
62+
63+
if let CommMsg::Close = msg {
64+
break;
65+
}
66+
}
67+
68+
// before finalizing the thread we make sure to send a close message to the front end
69+
self.comm
70+
.outgoing_tx
71+
.send(CommMsg::Close)
72+
.or_log_error("Reticulate: Could not send close message to the front-end");
73+
74+
// Reset the global comm_id before closing
75+
let mut comm_id_guard = RETICULATE_COMM_ID.lock().unwrap();
76+
log::info!("Reticulate Thread closing {:?}", (*comm_id_guard).clone());
77+
*comm_id_guard = None;
78+
79+
Ok(())
80+
}
81+
}
82+
83+
// Creates a client instance reticulate can use to communicate with the front-end.
84+
// We should aim at having at most **1** client per R session.
85+
// Further actions that reticulate can ask the front-end can be requested through
86+
// the comm_id that is returned by this function.
87+
#[harp::register]
88+
pub unsafe extern "C" fn ps_reticulate_open(input: SEXP) -> Result<SEXP, anyhow::Error> {
89+
let main = RMain::get();
90+
91+
let input: RObject = input.try_into()?;
92+
let input_code: Option<String> = input.try_into()?;
93+
94+
let mut comm_id_guard = RETICULATE_COMM_ID.lock().unwrap();
95+
96+
// If there's an id already registered, we just need to send the focus event
97+
if let Some(id) = comm_id_guard.deref() {
98+
// There's a comm_id registered, we just send the focus event
99+
main.get_comm_manager_tx().send(CommManagerEvent::Message(
100+
id.clone(),
101+
CommMsg::Data(json!({
102+
"method": "focus",
103+
"params": {
104+
"input": input_code
105+
}
106+
})),
107+
))?;
108+
return Ok(R_NilValue);
109+
}
110+
111+
let id = Uuid::new_v4().to_string();
112+
*comm_id_guard = Some(id.clone());
113+
114+
ReticulateService::start(id, main.get_comm_manager_tx().clone())?;
115+
116+
Ok(R_NilValue)
117+
}

0 commit comments

Comments
 (0)