-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 <lionel.hry@proton.me> * Improvements to style and simplifications * Add some comments + install packages rpc method --------- Co-authored-by: Lionel Henry <lionel.hry@proton.me>
- Loading branch information
Showing
4 changed files
with
221 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
#' @export | ||
.ps.reticulate_open <- function(input="") { | ||
.ps.Call("ps_reticulate_open", input) | ||
} | ||
|
||
#' Called by the front-end right before starting the reticulate session. | ||
#' | ||
#' At this point it should be fine to load Python if it's not loaded, and | ||
#' check if it can be started and if necessary packages are installed. | ||
#' @export | ||
.ps.rpc.reticulate_check_prerequisites <- function() { | ||
|
||
# This should return a list with the following fields: | ||
# python: NULL or string | ||
# venv: NULL or string | ||
# ipykernel: NULL or string | ||
# error: NULL or string | ||
|
||
config <- tryCatch({ | ||
reticulate::py_discover_config() | ||
}, error = function(err) { | ||
err | ||
}) | ||
|
||
if (inherits(config, "error")) { | ||
# py_discover_config() can fail if the user forced a Python session | ||
# via RETICULATE_PYTHON, but this version doesn't exist. | ||
return(list(error = conditionMessage(config))) | ||
} | ||
|
||
if (is.null(config) || is.null(config$python)) { | ||
# The front-end will offer to install Python. | ||
return(list(python = NULL, error = NULL)) | ||
} | ||
|
||
python <- config$python | ||
venv <- config$virtualenv | ||
|
||
# Check that python can be loaded, if it can't we will throw | ||
# an error, which is unrecoverable. | ||
config <- tryCatch({ | ||
reticulate::py_config() | ||
}, error = function(err) { | ||
err | ||
}) | ||
|
||
if (inherits(config, "error")) { | ||
return(list(python = python, venv = venv, error = conditionMessage(config))) | ||
} | ||
|
||
# Now check ipykernel | ||
ipykernel <- tryCatch({ | ||
reticulate::py_module_available("ipykernel") | ||
}, error = function(err) { | ||
err | ||
}) | ||
|
||
if (inherits(ipykernel, "error")) { | ||
return(list(python = python, venv = venv, error = conditionMessage(ipykernel))) | ||
} | ||
|
||
list( | ||
python = config$python, | ||
venv = venv, | ||
ipykernel = ipykernel, | ||
error = NULL | ||
) | ||
} | ||
|
||
#' @export | ||
.ps.rpc.reticulate_start_kernel <- function(kernelPath, connectionFile, logFile, logLevel) { | ||
# Starts an IPykernel in a separate thread with information provided by | ||
# the caller. | ||
# It it's essentially executing the kernel startup script: | ||
# https://github.com/posit-dev/positron/blob/main/extensions/positron-python/python_files/positron/positron_language_server.py | ||
# and passing the communication files that Positron Jupyter's Adapter sets up. | ||
tryCatch({ | ||
reticulate:::py_run_file_on_thread( | ||
file = kernelPath, | ||
args = c( | ||
"-f", connectionFile, | ||
"--logfile", logFile, | ||
"--loglevel", logLevel, | ||
"--session-mode", "console" | ||
) | ||
) | ||
# Empty string means that no error happened. | ||
"" | ||
}, error = function(err) { | ||
conditionMessage(err) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
use std::ops::Deref; | ||
use std::sync::LazyLock; | ||
use std::sync::Mutex; | ||
|
||
use amalthea::comm::comm_channel::CommMsg; | ||
use amalthea::comm::event::CommManagerEvent; | ||
use amalthea::socket::comm::CommInitiator; | ||
use amalthea::socket::comm::CommSocket; | ||
use crossbeam::channel::Sender; | ||
use harp::RObject; | ||
use libr::R_NilValue; | ||
use libr::SEXP; | ||
use serde_json::json; | ||
use stdext::result::ResultOrLog; | ||
use stdext::spawn; | ||
use stdext::unwrap; | ||
use uuid::Uuid; | ||
|
||
use crate::interface::RMain; | ||
|
||
static RETICULATE_COMM_ID: LazyLock<Mutex<Option<String>>> = LazyLock::new(|| Mutex::new(None)); | ||
|
||
pub struct ReticulateService { | ||
comm: CommSocket, | ||
comm_manager_tx: Sender<CommManagerEvent>, | ||
} | ||
|
||
impl ReticulateService { | ||
fn start(comm_id: String, comm_manager_tx: Sender<CommManagerEvent>) -> anyhow::Result<String> { | ||
let comm = CommSocket::new( | ||
CommInitiator::BackEnd, | ||
comm_id.clone(), | ||
String::from("positron.reticulate"), | ||
); | ||
|
||
let service = Self { | ||
comm, | ||
comm_manager_tx, | ||
}; | ||
|
||
let event = CommManagerEvent::Opened(service.comm.clone(), serde_json::Value::Null); | ||
service | ||
.comm_manager_tx | ||
.send(event) | ||
.or_log_error("Reticulate: Could not open comm."); | ||
|
||
spawn!(format!("ark-reticulate-{}", comm_id), move || { | ||
service | ||
.handle_messages() | ||
.or_log_error("Reticulate: Error handling messages"); | ||
}); | ||
|
||
Ok(comm_id) | ||
} | ||
|
||
fn handle_messages(&self) -> Result<(), anyhow::Error> { | ||
loop { | ||
let msg = unwrap!(self.comm.incoming_rx.recv(), Err(err) => { | ||
log::error!("Reticulate: Error while receiving message from frontend: {err:?}"); | ||
break; | ||
}); | ||
|
||
if let CommMsg::Close = msg { | ||
break; | ||
} | ||
} | ||
|
||
// before finalizing the thread we make sure to send a close message to the front end | ||
self.comm | ||
.outgoing_tx | ||
.send(CommMsg::Close) | ||
.or_log_error("Reticulate: Could not send close message to the front-end"); | ||
|
||
// Reset the global comm_id before closing | ||
let mut comm_id_guard = RETICULATE_COMM_ID.lock().unwrap(); | ||
log::info!("Reticulate Thread closing {:?}", (*comm_id_guard).clone()); | ||
*comm_id_guard = None; | ||
|
||
Ok(()) | ||
} | ||
} | ||
|
||
// Creates a client instance reticulate can use to communicate with the front-end. | ||
// We should aim at having at most **1** client per R session. | ||
// Further actions that reticulate can ask the front-end can be requested through | ||
// the comm_id that is returned by this function. | ||
#[harp::register] | ||
pub unsafe extern "C" fn ps_reticulate_open(input: SEXP) -> Result<SEXP, anyhow::Error> { | ||
let main = RMain::get(); | ||
|
||
let input: RObject = input.try_into()?; | ||
let input_code: Option<String> = input.try_into()?; | ||
|
||
let mut comm_id_guard = RETICULATE_COMM_ID.lock().unwrap(); | ||
|
||
// If there's an id already registered, we just need to send the focus event | ||
if let Some(id) = comm_id_guard.deref() { | ||
// There's a comm_id registered, we just send the focus event | ||
main.get_comm_manager_tx().send(CommManagerEvent::Message( | ||
id.clone(), | ||
CommMsg::Data(json!({ | ||
"method": "focus", | ||
"params": { | ||
"input": input_code | ||
} | ||
})), | ||
))?; | ||
return Ok(R_NilValue); | ||
} | ||
|
||
let id = Uuid::new_v4().to_string(); | ||
*comm_id_guard = Some(id.clone()); | ||
|
||
ReticulateService::start(id, main.get_comm_manager_tx().clone())?; | ||
|
||
Ok(R_NilValue) | ||
} |