diff --git a/crates/ark/src/browser.rs b/crates/ark/src/browser.rs index 4de512063..44f2c8f57 100644 --- a/crates/ark/src/browser.rs +++ b/crates/ark/src/browser.rs @@ -10,10 +10,10 @@ use harp::utils::r_normalize_path; use libr::Rf_ScalarLogical; use libr::SEXP; +use crate::console::Console; use crate::help::message::HelpEvent; use crate::help::message::ShowHelpUrlKind; use crate::help::message::ShowHelpUrlParams; -use crate::interface::RMain; use crate::ui::events::send_open_with_system_event; use crate::ui::events::send_show_url_event; @@ -26,17 +26,15 @@ pub unsafe extern "C-unwind" fn ps_browse_url(url: SEXP) -> anyhow::Result } fn is_help_url(url: &str) -> bool { - RMain::with(|main| main.is_help_url(url)) + Console::get().is_help_url(url) } fn handle_help_url(url: String) -> anyhow::Result<()> { - RMain::with(|main| { - let event = HelpEvent::ShowHelpUrl(ShowHelpUrlParams { - url, - kind: ShowHelpUrlKind::HelpProxy, - }); - main.send_help_event(event) - }) + let event = HelpEvent::ShowHelpUrl(ShowHelpUrlParams { + url, + kind: ShowHelpUrlKind::HelpProxy, + }); + Console::get().send_help_event(event) } unsafe fn ps_browse_url_impl(url: SEXP) -> anyhow::Result { diff --git a/crates/ark/src/connections/r_connection.rs b/crates/ark/src/connections/r_connection.rs index a88d5ff58..918234c37 100644 --- a/crates/ark/src/connections/r_connection.rs +++ b/crates/ark/src/connections/r_connection.rs @@ -36,7 +36,7 @@ use stdext::spawn; use stdext::unwrap; use uuid::Uuid; -use crate::interface::RMain; +use crate::console::Console; use crate::modules::ARK_ENVS; use crate::r_task; @@ -307,15 +307,15 @@ pub unsafe extern "C-unwind" fn ps_connection_opened( let id = Uuid::new_v4().to_string(); let id_r: RObject = id.clone().into(); - if !RMain::is_initialized() { - // If RMain is not initialized, we are probably in unit tests, so we + if !Console::is_initialized() { + // If Console is not initialized, we are probably in unit tests, so we // just don't start the connection and let the testing code manually do - // it. Note that RMain could be initialized in integration tests. - log::warn!("Connection Pane: RMain is not initialized. Connection will not be started."); + // it. Note that Console could be initialized in integration tests. + log::warn!("Connection Pane: Console is not initialized. Connection will not be started."); return Ok(id_r.sexp); } - let main = RMain::get(); + let console = Console::get(); let metadata = Metadata { name: RObject::view(name).to::()?, @@ -325,7 +325,7 @@ pub unsafe extern "C-unwind" fn ps_connection_opened( code: RObject::view(code).to::>().unwrap_or(None), }; - if let Err(err) = RConnection::start(metadata, main.get_comm_manager_tx().clone(), id) { + if let Err(err) = RConnection::start(metadata, console.get_comm_manager_tx().clone(), id) { log::error!("Connection Pane: Failed to start connection: {err:?}"); return Err(err); } @@ -335,45 +335,45 @@ pub unsafe extern "C-unwind" fn ps_connection_opened( #[harp::register] pub unsafe extern "C-unwind" fn ps_connection_closed(id: SEXP) -> Result { - let main = RMain::get(); - let id_ = RObject::view(id).to::()?; + let id = RObject::view(id).to::()?; - main.get_comm_manager_tx() - .send(CommManagerEvent::Message(id_, CommMsg::Close))?; + Console::get() + .get_comm_manager_tx() + .send(CommManagerEvent::Message(id, CommMsg::Close))?; Ok(R_NilValue) } #[harp::register] pub unsafe extern "C-unwind" fn ps_connection_updated(id: SEXP) -> Result { - let main = RMain::get(); let comm_id: String = RObject::view(id).to::()?; - let event = ConnectionsFrontendEvent::Update; - main.get_comm_manager_tx().send(CommManagerEvent::Message( - comm_id, - CommMsg::Data(serde_json::to_value(event)?), - ))?; + Console::get() + .get_comm_manager_tx() + .send(CommManagerEvent::Message( + comm_id, + CommMsg::Data(serde_json::to_value(event)?), + ))?; Ok(R_NilValue) } #[harp::register] pub unsafe extern "C-unwind" fn ps_connection_focus(id: SEXP) -> Result { - if !RMain::is_initialized() { + if !Console::is_initialized() { return Ok(R_NilValue); } - let main = RMain::get(); let comm_id: String = RObject::view(id).to::()?; - let event = ConnectionsFrontendEvent::Focus; - main.get_comm_manager_tx().send(CommManagerEvent::Message( - comm_id, - CommMsg::Data(serde_json::to_value(event)?), - ))?; + Console::get() + .get_comm_manager_tx() + .send(CommManagerEvent::Message( + comm_id, + CommMsg::Data(serde_json::to_value(event)?), + ))?; Ok(R_NilValue) } diff --git a/crates/ark/src/interface.rs b/crates/ark/src/console.rs similarity index 96% rename from crates/ark/src/interface.rs rename to crates/ark/src/console.rs index 3f5b4bb85..729c41e54 100644 --- a/crates/ark/src/interface.rs +++ b/crates/ark/src/console.rs @@ -1,14 +1,14 @@ // -// interface.rs +// console.rs // // Copyright (C) 2023-2026 Posit Software, PBC. All rights reserved. // // // All code in this file runs synchronously with R. We store the global -// state inside of a global `R_MAIN` singleton that implements `RMain`. +// state inside of a global `CONSOLE` singleton that implements `Console`. // The frontend methods called by R are forwarded to the corresponding -// `RMain` methods via `R_MAIN`. +// `Console` methods via `CONSOLE`. use std::cell::Cell; use std::cell::RefCell; @@ -80,7 +80,7 @@ use harp::srcref::srcref_list_get; use harp::srcref::SrcFile; use harp::utils::r_is_data_frame; use harp::utils::r_typeof; -use harp::R_MAIN_THREAD_ID; +use harp::CONSOLE_THREAD_ID; use libr::R_BaseNamespace; use libr::R_GlobalEnv; use libr::R_ProcessEvents; @@ -184,25 +184,25 @@ pub enum ConsoleNotification { // These values must be global in order for them to be accessible from R // callbacks, which do not have a facility for passing or returning context. -/// Used to wait for complete R startup in `RMain::wait_initialized()` or -/// check for it in `RMain::is_initialized()`. +/// Used to wait for complete R startup in `Console::wait_initialized()` or +/// check for it in `Console::is_initialized()`. /// /// We use the `once_cell` crate for init synchronisation because the stdlib /// equivalent `std::sync::Once` does not have a `wait()` method. static R_INIT: once_cell::sync::OnceCell<()> = once_cell::sync::OnceCell::new(); thread_local! { - /// The `RMain` singleton. + /// The `Console` singleton. /// /// It is wrapped in an `UnsafeCell` because we currently need to bypass the /// borrow checker rules (see https://github.com/posit-dev/ark/issues/663). /// The `UnsafeCell` itself is wrapped in a `RefCell` because that's the /// only way to get a `set()` method on the thread-local storage key and /// bypass the lazy initializer (which panics for other threads). - pub static R_MAIN: RefCell> = panic!("Must access `R_MAIN` from the R thread"); + pub static CONSOLE: RefCell> = panic!("Must access `CONSOLE` from the R thread"); } -pub struct RMain { +pub struct Console { kernel_request_rx: Receiver, /// Whether we are running in Console, Notebook, or Background mode. @@ -271,7 +271,7 @@ pub struct RMain { banner: Option, /// Raw error buffer provided to `Rf_error()` when throwing `r_read_console()` errors. - /// Stored in `RMain` to avoid memory leakage when `Rf_error()` jumps. + /// Stored in `Console` to avoid memory leakage when `Rf_error()` jumps. r_error_buffer: Option, /// `WriteConsole` output diverted from IOPub is stored here. This is only used @@ -527,8 +527,8 @@ pub(crate) enum ConsoleResult { Error(String), } -impl RMain { - /// Sets up the main R thread, initializes the `R_MAIN` singleton, +impl Console { + /// Sets up the main R thread, initializes the `CONSOLE` singleton, /// and starts R. Does not return! /// SAFETY: Must be called only once. Enforced with a panic. pub(crate) fn start( @@ -548,10 +548,10 @@ impl RMain { console_notification_rx: AsyncUnboundedReceiver, ) { // Set the main thread ID. - // Must happen before doing anything that checks `RMain::on_main_thread()`, + // Must happen before doing anything that checks `Console::on_main_thread()`, // like running an `r_task()` (posit-dev/positron#4973). unsafe { - R_MAIN_THREAD_ID = match R_MAIN_THREAD_ID { + CONSOLE_THREAD_ID = match CONSOLE_THREAD_ID { None => Some(std::thread::current().id()), Some(id) => panic!("`start()` must be called exactly 1 time. It has already been called from thread {id:?}."), }; @@ -559,7 +559,7 @@ impl RMain { let (tasks_interrupt_rx, tasks_idle_rx) = r_task::take_receivers(); - R_MAIN.set(UnsafeCell::new(RMain::new( + CONSOLE.set(UnsafeCell::new(Console::new( tasks_interrupt_rx, tasks_idle_rx, comm_manager_tx, @@ -572,7 +572,7 @@ impl RMain { session_mode, ))); - let main = RMain::get_mut(); + let console = Console::get_mut(); let mut r_args = r_args.clone(); @@ -630,7 +630,7 @@ impl RMain { let libraries = RLibraries::from_r_home_path(&r_home); libraries.initialize_pre_setup_r(); - crate::sys::interface::setup_r(&r_args); + crate::sys::console::setup_r(&r_args); libraries.initialize_post_setup_r(); @@ -654,7 +654,7 @@ impl RMain { log::error!("Can't load R modules: {err:?}"); }, Ok(namespace) => { - main.positron_ns = Some(namespace); + console.positron_ns = Some(namespace); }, } @@ -682,7 +682,7 @@ impl RMain { log::info!( "R has started and ark handlers have been registered, completing initialization." ); - Self::complete_initialization(main.banner.take(), kernel_init_tx); + Self::complete_initialization(console.banner.take(), kernel_init_tx); // Spawn handler loop for async messages from other components (e.g., LSP). // Note that we do it after init is complete to avoid deadlocking @@ -690,9 +690,9 @@ impl RMain { // by the `block_on()` behaviour in // https://github.com/posit-dev/ark/blob/bd827e73/crates/ark/src/r_task.rs#L261. r_task::spawn_interrupt({ - let dap_clone = main.debug_dap.clone(); + let dap_clone = console.debug_dap.clone(); || async move { - RMain::process_console_notifications(console_notification_rx, dap_clone).await + Console::process_console_notifications(console_notification_rx, dap_clone).await } }); @@ -701,10 +701,10 @@ impl RMain { // integration tests by spawning an async task. The deadlock is caused // by https://github.com/posit-dev/ark/blob/bd827e735970ca17102aeddfbe2c3ccf26950a36/crates/ark/src/r_task.rs#L261. // We should be able to remove this escape hatch in `r_task()` by - // instantiating an `RMain` in unit tests as well. + // instantiating an `Console` in unit tests as well. graphics_device::init_graphics_device( - main.get_comm_manager_tx().clone(), - main.get_iopub_tx().clone(), + console.get_comm_manager_tx().clone(), + console.get_iopub_tx().clone(), graphics_device_rx, ); @@ -718,7 +718,7 @@ impl RMain { } // Start the REPL. Does not return! - crate::sys::interface::run_r(); + crate::sys::console::run_r(); } /// Build the argument list from the command line arguments. The default @@ -856,7 +856,7 @@ impl RMain { R_INIT.wait(); } - /// Has the `RMain` singleton completed initialization. + /// Has the `Console` singleton completed initialization. /// /// This can return true when R might still not have finished starting up. /// See `wait_initialized()`. @@ -869,47 +869,31 @@ impl RMain { /// Access a reference to the singleton instance of this struct /// - /// SAFETY: Accesses must occur after `RMain::start()` initializes it. + /// SAFETY: Accesses must occur after `Console::start()` initializes it. pub fn get() -> &'static Self { - RMain::get_mut() + Console::get_mut() } /// Access a mutable reference to the singleton instance of this struct /// - /// SAFETY: Accesses must occur after `RMain::start()` initializes it. + /// SAFETY: Accesses must occur after `Console::start()` initializes it. /// Be aware that we're bypassing the borrow checker. The only guarantee we - /// have is that `R_MAIN` is only accessed from the R thread. If you're + /// have is that `CONSOLE` is only accessed from the R thread. If you're /// inspecting mutable state, or mutating state, you must reason the /// soundness by yourself. pub fn get_mut() -> &'static mut Self { - R_MAIN.with_borrow_mut(|cell| { + CONSOLE.with_borrow_mut(|cell| { let main_ref = cell.get_mut(); - // We extend the lifetime to `'static` as `R_MAIN` is effectively static once initialized. + // We extend the lifetime to `'static` as `CONSOLE` is effectively static once initialized. // This allows us to return a `&mut` from the unsafe cell to the caller. - unsafe { std::mem::transmute::<&mut RMain, &'static mut RMain>(main_ref) } + unsafe { std::mem::transmute::<&mut Console, &'static mut Console>(main_ref) } }) } - pub fn with(f: F) -> T - where - F: FnOnce(&RMain) -> T, - { - let main = Self::get(); - f(main) - } - - pub fn with_mut(f: F) -> T - where - F: FnOnce(&mut RMain) -> T, - { - let main = Self::get_mut(); - f(main) - } - pub fn on_main_thread() -> bool { let thread = std::thread::current(); - thread.id() == unsafe { R_MAIN_THREAD_ID.unwrap() } + thread.id() == unsafe { CONSOLE_THREAD_ID.unwrap() } } /// Provides read-only access to `iopub_tx` @@ -2161,7 +2145,7 @@ impl RMain { /// Invoked by R to write output to the console. fn write_console(buf: *const c_char, _buflen: i32, otype: i32) { if CAPTURE_CONSOLE_OUTPUT.load(Ordering::SeqCst) { - RMain::get_mut() + Console::get_mut() .captured_output .push_str(&console_to_utf8(buf).unwrap()); return; @@ -2172,9 +2156,9 @@ impl RMain { Err(err) => panic!("Failed to read from R buffer: {err:?}"), }; - let r_main = RMain::get_mut(); + let r_main = Console::get_mut(); - if !RMain::is_initialized() { + if !Console::is_initialized() { // During init, consider all output to be part of the startup banner match r_main.banner.as_mut() { Some(banner) => banner.push_str(&content), @@ -2324,7 +2308,7 @@ impl RMain { // this. unsafe { R_ProcessEvents() }; - crate::sys::interface::run_activity_handlers(); + crate::sys::console::run_activity_handlers(); // Run pending finalizers. We need to do this eagerly as otherwise finalizers // might end up being executed on the LSP thread. @@ -2559,7 +2543,7 @@ pub(crate) fn console_inputs() -> anyhow::Result { // --- Frontend methods --- // These functions are hooked up as R frontend methods. They call into our -// global `RMain` singleton. +// global `Console` singleton. #[no_mangle] pub extern "C-unwind" fn r_read_console( @@ -2576,19 +2560,19 @@ pub extern "C-unwind" fn r_read_console( // evaluation. Ideally R would extend their frontend API so that this would // only be necessary for backward compatibility with old versions of R. - let main = RMain::get_mut(); + let console = Console::get_mut(); // Propagate an EOF event (e.g. from a Shutdown request). We need to exit // from all consoles on the stack to let R shut down with an `exit()`. - if main.read_console_shutdown.get() { + if console.read_console_shutdown.get() { return 0; } // We've finished evaluating a dummy value to reset state in R's REPL, // and are now ready to evaluate the actual input, which is typically // just `.ark_last_value`. - if let Some(next_input) = main.read_console_nested_return_next_input.take() { - RMain::on_console_input(buf, buflen, next_input).unwrap(); + if let Some(next_input) = console.read_console_nested_return_next_input.take() { + Console::on_console_input(buf, buflen, next_input).unwrap(); return 1; } @@ -2601,11 +2585,11 @@ pub extern "C-unwind" fn r_read_console( // Technically this also resets time limits (see `base::setTimeLimit()`) but // these aren't supported in Ark because they cause errors when we poll R // events. - if main.last_error.is_some() && main.read_console_threw_error.get() { - main.read_console_threw_error.set(false); + if console.last_error.is_some() && console.read_console_threw_error.get() { + console.read_console_threw_error.set(false); // Evaluate last value so that `base::.Last.value` remains the same - RMain::on_console_input( + Console::on_console_input( buf, buflen, String::from("base::invisible(base::.Last.value)"), @@ -2617,46 +2601,48 @@ pub extern "C-unwind" fn r_read_console( // Keep track of state that we care about // - Track nesting depth of ReadConsole REPLs - main.read_console_depth - .set(main.read_console_depth.get() + 1); + console + .read_console_depth + .set(console.read_console_depth.get() + 1); // - Set current frame environment - let old_current_frame = main.read_console_frame.replace(harp::r_current_frame()); + let old_current_frame = console.read_console_frame.replace(harp::r_current_frame()); // Keep track of state that we use for workarounds while interacting // with the R REPL and force it to reset state // - Reset flag that helps us figure out when a nested REPL returns - main.read_console_nested_return.set(false); + console.read_console_nested_return.set(false); // - Reset flag that helps us figure out when an error occurred and needs a // reset of `R_EvalDepth` and friends - main.read_console_threw_error.set(true); + console.read_console_threw_error.set(true); exec_with_cleanup( || { - let main = RMain::get_mut(); - let result = r_read_console_impl(main, prompt, buf, buflen, hist); + let console = Console::get_mut(); + let result = r_read_console_impl(console, prompt, buf, buflen, hist); // If we get here, there was no error - main.read_console_threw_error.set(false); + console.read_console_threw_error.set(false); result }, || { - let main = RMain::get_mut(); + let console = Console::get_mut(); // We're exiting, decrease depth of nested consoles - main.read_console_depth - .set(main.read_console_depth.get() - 1); + console + .read_console_depth + .set(console.read_console_depth.get() - 1); // Set flag so that parent read console, if any, can detect that a // nested console returned (if it indeed returns instead of looping // for another iteration) - main.read_console_nested_return.set(true); + console.read_console_nested_return.set(true); // Restore current frame - main.read_console_frame.replace(old_current_frame); + console.read_console_frame.replace(old_current_frame); // Always stop debug session when yielding back to R. This prevents // the debug toolbar from lingering in situations like: @@ -2667,14 +2653,14 @@ pub extern "C-unwind" fn r_read_console( // // For a more practical example see Shiny app example in // https://github.com/rstudio/rstudio/pull/14848 - main.debug_is_debugging = false; - main.debug_stop(); + console.debug_is_debugging = false; + console.debug_stop(); }, ) } fn r_read_console_impl( - main: &mut RMain, + main: &mut Console, prompt: *const c_char, buf: *mut c_uchar, buflen: c_int, @@ -2694,19 +2680,19 @@ fn r_read_console_impl( let PendingInput { expr, srcref } = input; unsafe { - // The pointer protection stack is restored by `run_Rmainloop()` + // The pointer protection stack is restored by `run_Consoleloop()` // after a longjump to top-level, so it's safe to protect here // even if the evaluation throws let expr = libr::Rf_protect(expr.into()); let srcref = libr::Rf_protect(srcref.into()); - RMain::eval(expr, srcref, buf, buflen); + Console::eval(expr, srcref, buf, buflen); // Check if a nested read_console() just returned. If that's the // case, we need to reset the `R_ConsoleIob` by first returning // a dummy value causing a `PARSE_NULL` event. if main.read_console_nested_return.get() { - let next_input = RMain::console_input(buf, buflen); + let next_input = Console::console_input(buf, buflen); main.read_console_nested_return_next_input .set(Some(next_input)); @@ -2714,7 +2700,7 @@ fn r_read_console_impl( // evaluate a newline, that would cause a parent debug REPL // to interpret it as `n`, causing it to exit instead of // being a no-op. - RMain::on_console_input(buf, buflen, String::from(" ")).unwrap(); + Console::on_console_input(buf, buflen, String::from(" ")).unwrap(); main.read_console_nested_return.set(false); } @@ -2749,7 +2735,7 @@ fn r_read_console_impl( }, ConsoleResult::Error(message) => { - // Save error message in `RMain` to avoid leaking memory when + // Save error message in `Console` to avoid leaking memory when // `Rf_error()` jumps. Some gymnastics are required to deal with the // possibility of `CString` conversion failure since the error // message comes from the frontend and might be corrupted. @@ -2765,21 +2751,19 @@ fn new_cstring(x: String) -> CString { #[no_mangle] pub extern "C-unwind" fn r_write_console(buf: *const c_char, buflen: i32, otype: i32) { - if let Err(err) = r_sandbox(|| RMain::write_console(buf, buflen, otype)) { + if let Err(err) = r_sandbox(|| Console::write_console(buf, buflen, otype)) { panic!("Unexpected longjump while writing to console: {err:?}"); }; } #[no_mangle] pub extern "C-unwind" fn r_show_message(buf: *const c_char) { - let main = RMain::get(); - main.show_message(buf); + Console::get().show_message(buf); } #[no_mangle] pub extern "C-unwind" fn r_busy(which: i32) { - let main = RMain::get_mut(); - main.busy(which); + Console::get_mut().busy(which); } #[no_mangle] @@ -2790,8 +2774,7 @@ pub extern "C-unwind" fn r_suicide(buf: *const c_char) { #[no_mangle] pub unsafe extern "C-unwind" fn r_polled_events() { - let main = RMain::get_mut(); - if let Err(err) = r_sandbox(|| main.polled_events()) { + if let Err(err) = r_sandbox(|| Console::get_mut().polled_events()) { panic!("Unexpected longjump while polling events: {err:?}"); }; } @@ -2904,8 +2887,7 @@ unsafe extern "C-unwind" fn ps_insert_virtual_document( let uri: String = RObject::view(uri).try_into()?; let contents: String = RObject::view(contents).try_into()?; - let main = RMain::get_mut(); - main.insert_virtual_document(uri, contents); + Console::get_mut().insert_virtual_document(uri, contents); Ok(RObject::null().sexp) } diff --git a/crates/ark/src/console_annotate.rs b/crates/ark/src/console_annotate.rs index 7605baac7..c9f9352b5 100644 --- a/crates/ark/src/console_annotate.rs +++ b/crates/ark/src/console_annotate.rs @@ -22,10 +22,10 @@ use harp::object::RObject; use libr::SEXP; use url::Url; +use crate::console::Console; use crate::dap::dap::Breakpoint; use crate::dap::dap::BreakpointState; use crate::dap::dap::InvalidReason; -use crate::interface::RMain; /// Function name used for auto-stepping over injected calls such as breakpoints const AUTO_STEP_FUNCTION: &str = ".ark_auto_step"; @@ -894,8 +894,7 @@ pub unsafe extern "C-unwind" fn ps_annotate_source(code: SEXP, uri: SEXP) -> any let uri = Url::parse(&uri)?; - let main = RMain::get(); - let mut dap_guard = main.debug_dap.lock().unwrap(); + let mut dap_guard = Console::get().debug_dap.lock().unwrap(); // If there are no breakpoints for this file, return NULL to signal no // annotation needed. Scope the mutable borrow so we can re-borrow after. diff --git a/crates/ark/src/console_debug.rs b/crates/ark/src/console_debug.rs index f44575f14..1c5fdee15 100644 --- a/crates/ark/src/console_debug.rs +++ b/crates/ark/src/console_debug.rs @@ -21,10 +21,10 @@ use regex::Regex; use stdext::result::ResultExt; use url::Url; +use crate::console::Console; +use crate::console::DebugCallText; +use crate::console::DebugCallTextKind; use crate::dap::dap::DapBackendEvent; -use crate::interface::DebugCallText; -use crate::interface::DebugCallTextKind; -use crate::interface::RMain; use crate::modules::ARK_ENVS; use crate::srcref::ark_uri; use crate::thread::RThreadSafe; @@ -73,7 +73,7 @@ impl From<&FrameInfo> for FrameInfoId { } } -impl RMain { +impl Console { pub(crate) fn debug_start(&mut self, debug_preserve_focus: bool) { match self.debug_stack_info() { Ok(stack) => { @@ -413,7 +413,7 @@ pub unsafe extern "C-unwind" fn ps_is_breakpoint_enabled( let id: String = RObject::view(id).try_into()?; - let console = RMain::get_mut(); + let console = Console::get_mut(); let dap = console.debug_dap.lock().unwrap(); let enabled = dap.is_breakpoint_enabled(&uri, id); @@ -431,8 +431,7 @@ pub unsafe extern "C-unwind" fn ps_verify_breakpoint(uri: SEXP, id: SEXP) -> any return Ok(libr::R_NilValue); }; - let main = RMain::get(); - let mut dap = main.debug_dap.lock().unwrap(); + let mut dap = Console::get().debug_dap.lock().unwrap(); dap.verify_breakpoint(&uri, &id); Ok(libr::R_NilValue) @@ -442,7 +441,7 @@ pub unsafe extern "C-unwind" fn ps_verify_breakpoint(uri: SEXP, id: SEXP) -> any /// Called after each expression is successfully evaluated in source(). #[harp::register] pub unsafe extern "C-unwind" fn ps_verify_breakpoints(srcref: SEXP) -> anyhow::Result { - RMain::get().verify_breakpoints(RObject::view(srcref)); + Console::get().verify_breakpoints(RObject::view(srcref)); Ok(libr::R_NilValue) } @@ -462,8 +461,7 @@ pub unsafe extern "C-unwind" fn ps_verify_breakpoints_range( return Ok(libr::R_NilValue); }; - let main = RMain::get(); - let mut dap = main.debug_dap.lock().unwrap(); + let mut dap = Console::get().debug_dap.lock().unwrap(); dap.verify_breakpoints(&uri, start_line as u32, end_line as u32); Ok(libr::R_NilValue) diff --git a/crates/ark/src/data_explorer/r_data_explorer.rs b/crates/ark/src/data_explorer/r_data_explorer.rs index d0f1db1b5..2a10485ca 100644 --- a/crates/ark/src/data_explorer/r_data_explorer.rs +++ b/crates/ark/src/data_explorer/r_data_explorer.rs @@ -91,6 +91,7 @@ use stdext::unwrap; use tracing::Instrument; use uuid::Uuid; +use crate::console::Console; use crate::data_explorer::column_profile::handle_columns_profiles_requests; use crate::data_explorer::column_profile::ProcessColumnsProfilesParams; use crate::data_explorer::convert_to_code; @@ -100,7 +101,6 @@ use crate::data_explorer::format::format_string; use crate::data_explorer::table::Table; use crate::data_explorer::utils::display_type; use crate::data_explorer::utils::tbl_subset_with_view_indices; -use crate::interface::RMain; use crate::lsp::events::EVENTS; use crate::modules::ARK_ENVS; use crate::r_task; @@ -1323,8 +1323,7 @@ pub unsafe extern "C-unwind" fn ps_view_data_frame( let title = RObject::new(title); let title = unwrap!(String::try_from(title), Err(_) => "".to_string()); - let main = RMain::get(); - let comm_manager_tx = main.get_comm_manager_tx().clone(); + let comm_manager_tx = Console::get().get_comm_manager_tx().clone(); // If an environment is provided, watch the variable in the environment let env_info = if env != R_NilValue { diff --git a/crates/ark/src/debug.rs b/crates/ark/src/debug.rs index 254623848..b80c83e28 100644 --- a/crates/ark/src/debug.rs +++ b/crates/ark/src/debug.rs @@ -6,8 +6,8 @@ use harp::exec::RFunctionExt; use harp::utils::r_str_to_owned_utf8_unchecked; use harp::utils::r_typeof; -use crate::interface::RMain; -use crate::interface::CAPTURE_CONSOLE_OUTPUT; +use crate::console::Console; +use crate::console::CAPTURE_CONSOLE_OUTPUT; // To ensure the compiler includes the C entry points in `debug.c` in the binary, // we store function pointers in global variables that are declared "used" (even @@ -169,7 +169,7 @@ pub fn capture_console_output(cb: impl FnOnce()) -> *const ffi::c_char { let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| harp::try_catch(cb))); CAPTURE_CONSOLE_OUTPUT.store(old, Ordering::SeqCst); - let mut out = std::mem::take(&mut RMain::get_mut().captured_output); + let mut out = std::mem::take(&mut Console::get_mut().captured_output); // Unwrap catch-unwind's result and resume panic if needed let result = match result { diff --git a/crates/ark/src/errors.rs b/crates/ark/src/errors.rs index c58a8c240..b3a3f09c2 100644 --- a/crates/ark/src/errors.rs +++ b/crates/ark/src/errors.rs @@ -15,11 +15,11 @@ use libr::SEXP; use log::warn; use stdext::unwrap; -use crate::interface::RMain; +use crate::console::Console; #[harp::register] unsafe extern "C-unwind" fn ps_record_error(evalue: SEXP, traceback: SEXP) -> anyhow::Result { - let main = RMain::get_mut(); + let console = Console::get_mut(); // Convert to `RObject` for access to `try_from()` / `try_into()` methods. let evalue = RObject::new(evalue); @@ -35,7 +35,7 @@ unsafe extern "C-unwind" fn ps_record_error(evalue: SEXP, traceback: SEXP) -> an Vec::::new() }); - main.last_error = Some( + console.last_error = Some( // We don't fill out `ename` with anything meaningful because typically // R errors don't have names. We could consider using the condition class // here, which r-lib/tidyverse packages have been using more heavily. diff --git a/crates/ark/src/fixtures/dummy_frontend.rs b/crates/ark/src/fixtures/dummy_frontend.rs index b5a98fae2..4ceba3411 100644 --- a/crates/ark/src/fixtures/dummy_frontend.rs +++ b/crates/ark/src/fixtures/dummy_frontend.rs @@ -8,7 +8,7 @@ use std::sync::OnceLock; use amalthea::fixtures::dummy_frontend::DummyConnection; use amalthea::fixtures::dummy_frontend::DummyFrontend; -use crate::interface::SessionMode; +use crate::console::SessionMode; use crate::repos::DefaultRepos; // There can be only one frontend per process. Needs to be in a mutex because @@ -69,7 +69,7 @@ impl DummyArkFrontend { pub fn wait_for_cleanup() { use std::time::Duration; - use crate::sys::interface::CLEANUP_SIGNAL; + use crate::sys::console::CLEANUP_SIGNAL; let (lock, cvar) = &CLEANUP_SIGNAL; let result = cvar diff --git a/crates/ark/src/help/r_help.rs b/crates/ark/src/help/r_help.rs index 0e3176208..1c06b273a 100644 --- a/crates/ark/src/help/r_help.rs +++ b/crates/ark/src/help/r_help.rs @@ -27,10 +27,10 @@ use log::trace; use log::warn; use stdext::spawn; +use crate::console::Console; use crate::help::message::HelpEvent; use crate::help::message::ShowHelpUrlKind; use crate::help::message::ShowHelpUrlParams; -use crate::interface::RMain; use crate::methods::ArkGenerics; use crate::r_task; @@ -270,8 +270,8 @@ impl RHelp { unsafe { let env = (|| { #[cfg(not(test))] - if RMain::is_initialized() { - if let Ok(debug_env) = &RMain::get().read_console_frame.try_borrow() { + if Console::is_initialized() { + if let Ok(debug_env) = &Console::get().read_console_frame.try_borrow() { return (*debug_env).clone(); } } @@ -283,9 +283,13 @@ impl RHelp { Ok(obj) => obj, Err(err) => { // Could not parse/eval the topic; no custom handler. - log::warn!("Could not parse/eval help topic expression '{}': {:?}", topic, err); + log::warn!( + "Could not parse/eval help topic expression '{}': {:?}", + topic, + err + ); return Ok(None); - } + }, }; let handler: Option = @@ -339,7 +343,7 @@ impl HelpTopic { pub unsafe extern "C-unwind" fn ps_help_browse_external_url( url: SEXP, ) -> Result { - RMain::get().send_help_event(HelpEvent::ShowHelpUrl(ShowHelpUrlParams { + Console::get().send_help_event(HelpEvent::ShowHelpUrl(ShowHelpUrlParams { url: RObject::view(url).to::()?, kind: ShowHelpUrlKind::External, }))?; diff --git a/crates/ark/src/lib.rs b/crates/ark/src/lib.rs index 57b3f3b82..666bd6aa8 100644 --- a/crates/ark/src/lib.rs +++ b/crates/ark/src/lib.rs @@ -1,7 +1,7 @@ // // lib.rs // -// Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved. +// Copyright (C) 2023-2026 Posit Software, PBC. All rights reserved. // // @@ -9,6 +9,7 @@ pub mod analysis; pub mod ark_comm; pub mod browser; pub mod connections; +pub mod console; pub mod console_annotate; pub mod console_debug; pub mod control; @@ -20,7 +21,6 @@ pub mod errors; pub mod fixtures; pub mod help; pub mod help_proxy; -pub mod interface; pub mod json; pub mod logger; pub mod logger_hprof; diff --git a/crates/ark/src/lsp/backend.rs b/crates/ark/src/lsp/backend.rs index 73525920d..85900bd01 100644 --- a/crates/ark/src/lsp/backend.rs +++ b/crates/ark/src/lsp/backend.rs @@ -35,8 +35,8 @@ use tower_lsp::LspService; use tower_lsp::Server; use super::main_loop::LSP_HAS_CRASHED; -use crate::interface::ConsoleNotification; -use crate::interface::RMain; +use crate::console::Console; +use crate::console::ConsoleNotification; use crate::lsp::handlers::VirtualDocumentParams; use crate::lsp::handlers::VirtualDocumentResponse; use crate::lsp::handlers::ARK_VDOC_REQUEST; @@ -105,8 +105,7 @@ fn report_crash() { message: String::from(user_message), }); - let main = RMain::get(); - if let Some(ui_comm_tx) = main.get_ui_comm_tx() { + if let Some(ui_comm_tx) = Console::get().get_ui_comm_tx() { ui_comm_tx.send_event(event); } }); @@ -522,16 +521,16 @@ pub fn start_lsp( // Start main loop and hold onto the handle that keeps it alive let main_loop = state.start(); - // Forward event channel along to `RMain`. + // Forward event channel along to `Console`. // This also updates an outdated channel after a reconnect. - // `RMain` should be initialized by now, since the caller of this + // `Console` should be initialized by now, since the caller of this // function waits to receive the init notification sent on // `kernel_init_rx`. Even if it isn't, this should be okay because // `r_task()` defensively blocks until its sender is initialized. r_task({ let events_tx = events_tx.clone(); move || { - RMain::with_mut(|main| main.set_lsp_channel(events_tx)); + Console::get_mut().set_lsp_channel(events_tx); } }); @@ -575,10 +574,10 @@ pub fn start_lsp( } // Remove the LSP channel on the way out, we can no longer handle any LSP updates - // from `RMain`, at least until someone starts the LSP up again. + // from `Console`, at least until someone starts the LSP up again. r_task({ move || { - RMain::with_mut(|main| main.remove_lsp_channel()); + Console::get_mut().remove_lsp_channel(); } }); }) diff --git a/crates/ark/src/lsp/completions/sources/composite/search_path.rs b/crates/ark/src/lsp/completions/sources/composite/search_path.rs index 319e819ff..b6429476d 100644 --- a/crates/ark/src/lsp/completions/sources/composite/search_path.rs +++ b/crates/ark/src/lsp/completions/sources/composite/search_path.rs @@ -51,9 +51,9 @@ fn completions_from_search_path( unsafe { // Iterate through environments starting from the current frame environment. - #[cfg(not(test))] // Unit tests do not have an `RMain` - // Mem-Safety: Object protected by `RMain` for the duration of the `r_task()` - let mut env = crate::interface::RMain::get().read_console_frame().sexp; + #[cfg(not(test))] // Unit tests do not have an `Console` + // Mem-Safety: Object protected by `Console` for the duration of the `r_task()` + let mut env = crate::console::Console::get().read_console_frame().sexp; #[cfg(test)] let mut env = libr::R_GlobalEnv; diff --git a/crates/ark/src/lsp/diagnostics.rs b/crates/ark/src/lsp/diagnostics.rs index b7e8586fd..3dbe6459e 100644 --- a/crates/ark/src/lsp/diagnostics.rs +++ b/crates/ark/src/lsp/diagnostics.rs @@ -1136,7 +1136,7 @@ mod tests { use tower_lsp::lsp_types; use tower_lsp::lsp_types::Position; - use crate::interface::console_inputs; + use crate::console::console_inputs; use crate::lsp::document::Document; use crate::lsp::inputs::library::Library; use crate::lsp::inputs::package::Package; diff --git a/crates/ark/src/lsp/handler.rs b/crates/ark/src/lsp/handler.rs index 1c59616cf..06966b574 100644 --- a/crates/ark/src/lsp/handler.rs +++ b/crates/ark/src/lsp/handler.rs @@ -19,8 +19,8 @@ use tokio::runtime::Runtime; use tokio::sync::mpsc::UnboundedSender as AsyncUnboundedSender; use super::backend; -use crate::interface::ConsoleNotification; -use crate::interface::KernelInfo; +use crate::console::ConsoleNotification; +use crate::console::KernelInfo; pub struct Lsp { runtime: Arc, diff --git a/crates/ark/src/lsp/main_loop.rs b/crates/ark/src/lsp/main_loop.rs index fc24ab15d..0e92befda 100644 --- a/crates/ark/src/lsp/main_loop.rs +++ b/crates/ark/src/lsp/main_loop.rs @@ -29,7 +29,7 @@ use tower_lsp::Client; use url::Url; use super::backend::RequestResponse; -use crate::interface::ConsoleNotification; +use crate::console::ConsoleNotification; use crate::lsp; use crate::lsp::backend::LspMessage; use crate::lsp::backend::LspNotification; diff --git a/crates/ark/src/lsp/state_handlers.rs b/crates/ark/src/lsp/state_handlers.rs index 6a5bbfc39..a3cb89e21 100644 --- a/crates/ark/src/lsp/state_handlers.rs +++ b/crates/ark/src/lsp/state_handlers.rs @@ -43,7 +43,7 @@ use tracing::Instrument; use tree_sitter::Parser; use url::Url; -use crate::interface::ConsoleNotification; +use crate::console::ConsoleNotification; use crate::lsp; use crate::lsp::capabilities::Capabilities; use crate::lsp::config::indent_style_from_lsp; diff --git a/crates/ark/src/main.rs b/crates/ark/src/main.rs index fdfe91809..373d4860a 100644 --- a/crates/ark/src/main.rs +++ b/crates/ark/src/main.rs @@ -13,7 +13,7 @@ use std::env; use amalthea::kernel; use amalthea::kernel_spec::KernelSpec; use anyhow::Context; -use ark::interface::SessionMode; +use ark::console::SessionMode; use ark::logger; use ark::repos::DefaultRepos; use ark::signals::initialize_signal_block; diff --git a/crates/ark/src/modules.rs b/crates/ark/src/modules.rs index c07e39985..8d2aaa008 100644 --- a/crates/ark/src/modules.rs +++ b/crates/ark/src/modules.rs @@ -169,7 +169,7 @@ mod debug { use libr::SEXP; use stdext::spawn; - use crate::interface::RMain; + use crate::console::Console; use crate::r_task; pub fn spawn_watcher_thread(root: PathBuf) { @@ -252,7 +252,7 @@ mod debug { } r_task(|| { - let r_main = RMain::get(); + let r_main = Console::get(); if let Err(err) = import_file(&path, *src, r_main.positron_ns.as_ref().unwrap().sexp) { diff --git a/crates/ark/src/modules/positron/help.R b/crates/ark/src/modules/positron/help.R index c0fd46ff5..1cb03ddc2 100644 --- a/crates/ark/src/modules/positron/help.R +++ b/crates/ark/src/modules/positron/help.R @@ -53,7 +53,7 @@ help <- function(topic, package = NULL) { # If we found results of any kind, show them. # If we are running ark tests, don't show the results as this requires - # `ps_browse_url()` which needs a full `RMain` instance. + # `ps_browse_url()` which needs a full `Console` instance. if (length(results) > 0 && !in_ark_tests()) { print(results) } diff --git a/crates/ark/src/plots/graphics_device.rs b/crates/ark/src/plots/graphics_device.rs index 1f7fff3bf..421e67df5 100644 --- a/crates/ark/src/plots/graphics_device.rs +++ b/crates/ark/src/plots/graphics_device.rs @@ -50,8 +50,8 @@ use stdext::unwrap; use tokio::sync::mpsc::UnboundedReceiver as AsyncUnboundedReceiver; use uuid::Uuid; -use crate::interface::RMain; -use crate::interface::SessionMode; +use crate::console::Console; +use crate::console::SessionMode; use crate::modules::ARK_ENVS; use crate::r_task; @@ -61,7 +61,7 @@ pub(crate) enum GraphicsDeviceNotification { } thread_local! { - // Safety: Set once by `RMain` on initialization + // Safety: Set once by `Console` on initialization static DEVICE_CONTEXT: RefCell = panic!("Must access `DEVICE_CONTEXT` from the R thread"); } @@ -244,9 +244,8 @@ impl DeviceContext { /// comm is connected (i.e. we are connected to Positron) and if we are in /// [SessionMode::Console] mode. fn should_use_dynamic_plots(&self) -> bool { - RMain::with(|main| { - main.is_ui_comm_connected() && main.session_mode() == SessionMode::Console - }) + let console = Console::get(); + console.is_ui_comm_connected() && console.session_mode() == SessionMode::Console } /// Deactivation hook @@ -316,7 +315,7 @@ impl DeviceContext { /// Capture the current execution context for a new plot. /// /// First checks for context pushed via `on_execute_request()`, then falls back - /// to getting context from RMain's active request (for backwards compatibility + /// to getting context from Console's active request (for backwards compatibility /// and edge cases). fn capture_execution_context(&self) -> (String, String) { // First, check if we have a stored execution context from on_execute_request() @@ -324,12 +323,10 @@ impl DeviceContext { return ctx; } - // Fall back to getting context from RMain (for edge cases) - RMain::with(|main| { - main.get_execution_context().unwrap_or_else(|| { - // No active request - might be during startup or from R code - (String::new(), String::new()) - }) + // Fall back to getting context from Console (for edge cases) + Console::get().get_execution_context().unwrap_or_else(|| { + // No active request - might be during startup or from R code + (String::new(), String::new()) }) } @@ -574,7 +571,7 @@ impl DeviceContext { /// Process outstanding plot changes /// /// Uses execution context stored via `on_execute_request()` or falls back to - /// getting context from RMain's active request. + /// getting context from Console's active request. #[tracing::instrument(level = "trace", skip_all)] fn process_changes(&self) { let id = self.id(); diff --git a/crates/ark/src/r_task.rs b/crates/ark/src/r_task.rs index 12b44cb18..18b4eddda 100644 --- a/crates/ark/src/r_task.rs +++ b/crates/ark/src/r_task.rs @@ -18,8 +18,8 @@ use crossbeam::channel::Receiver; use crossbeam::channel::Sender; use uuid::Uuid; +use crate::console::Console; use crate::fixtures::r_test_init; -use crate::interface::RMain; /// Task channels for interrupt-time tasks static INTERRUPT_TASKS: LazyLock = LazyLock::new(|| TaskChannels::new()); @@ -33,7 +33,7 @@ pub(crate) type BoxFuture<'a, T> = Pin + 'a>>; type SharedOption = Arc>>; -/// Manages task channels for sending tasks to `R_MAIN`. +/// Manages task channels for sending tasks to `CONSOLE`. struct TaskChannels { tx: Sender, rx: Mutex>>, @@ -60,7 +60,7 @@ impl TaskChannels { /// Returns receivers for both interrupt and idle tasks. /// Initializes the task channels if they haven't been initialized yet. -/// Can only be called once (intended for `RMain` during init). +/// Can only be called once (intended for `Console` during init). pub(crate) fn take_receivers() -> (Receiver, Receiver) { (INTERRUPT_TASKS.take_rx(), IDLE_TASKS.take_rx()) } @@ -186,9 +186,9 @@ where T: 'env + Send, { // Escape hatch for unit tests - // In integration tests with dummy frontends, we have a "real" RMain and want to + // In integration tests with dummy frontends, we have a "real" Console and want to // go through the standard r-task path - if stdext::IS_TESTING && !RMain::is_initialized() { + if stdext::IS_TESTING && !Console::is_initialized() { let _lock = harp::fixtures::R_TEST_LOCK.lock(); r_test_init(); return f(); @@ -197,7 +197,7 @@ where // Recursive case: If we're on ark-r-main already, just run the // task and return. This allows `r_task(|| { r_task(|| {}) })` // to run without deadlocking. - if RMain::on_main_thread() { + if Console::on_main_thread() { return f(); } @@ -296,7 +296,7 @@ where Fut: Future + 'static, { // Escape hatch for unit tests - if stdext::IS_TESTING && !RMain::is_initialized() { + if stdext::IS_TESTING && !Console::is_initialized() { let _lock = harp::fixtures::R_TEST_LOCK.lock(); futures::executor::block_on(fun()); return; @@ -319,4 +319,4 @@ where } // Tests are tricky because `harp::fixtures::r_test_init()` is very bare bones and -// doesn't have an `R_MAIN` or `R_MAIN_TASKS_TX`. +// doesn't have an `CONSOLE` or `CONSOLE_TASKS_TX`. diff --git a/crates/ark/src/reticulate.rs b/crates/ark/src/reticulate.rs index 118e40661..1b46ee9ce 100644 --- a/crates/ark/src/reticulate.rs +++ b/crates/ark/src/reticulate.rs @@ -19,7 +19,7 @@ use stdext::spawn; use stdext::unwrap; use uuid::Uuid; -use crate::interface::RMain; +use crate::console::Console; static RETICULATE_OUTGOING_TX: LazyLock>>> = LazyLock::new(|| Mutex::new(None)); @@ -110,7 +110,7 @@ impl ReticulateService { // the comm_id that is returned by this function. #[harp::register] pub unsafe extern "C-unwind" fn ps_reticulate_open(input: SEXP) -> Result { - let main = RMain::get(); + let console = Console::get(); let input: RObject = input.try_into()?; // Reticulate sends `NULL` or a string with the code to be executed in the Python console. @@ -131,7 +131,7 @@ pub unsafe extern "C-unwind" fn ps_reticulate_open(input: SEXP) -> Result amalthea::Result { // Send the help event channel to the main R thread so it can // emit help events, to be delivered over the help comm. - RMain::with_mut(|main| main.set_help_fields(help_event_tx, r_port)); + Console::get_mut().set_help_fields(help_event_tx, r_port); Ok(true) }) diff --git a/crates/ark/src/srcref.rs b/crates/ark/src/srcref.rs index 5a1a3d7a5..a44a9928e 100644 --- a/crates/ark/src/srcref.rs +++ b/crates/ark/src/srcref.rs @@ -9,7 +9,7 @@ use harp::r_symbol; use harp::utils::r_typeof; use libr::*; -use crate::interface::RMain; +use crate::console::Console; use crate::modules::ARK_ENVS; use crate::r_task; use crate::variables::variable::is_binding_fancy; @@ -38,7 +38,7 @@ pub(crate) fn resource_loaded_namespaces() -> anyhow::Result<()> { pub(crate) async fn ns_populate_srcref(ns_name: String) -> anyhow::Result<()> { if let Some((uri, contents)) = ns_populate_srcref_without_vdoc_insertion(ns_name).await? { // Register the virtual document for the namespace - RMain::with_mut(|main| main.insert_virtual_document(uri, contents)); + Console::get_mut().insert_virtual_document(uri, contents); }; Ok(()) @@ -53,7 +53,7 @@ async fn ns_populate_srcref_without_vdoc_insertion( // Don't redo the work if we already did it. We don't expect a namespace to change. #[cfg(not(test))] - if RMain::with(|main| main.has_virtual_document(&ark_ns_uri(&ns_name))) { + if Console::get().has_virtual_document(&ark_ns_uri(&ns_name)) { return Ok(None); } diff --git a/crates/ark/src/start.rs b/crates/ark/src/start.rs index fcee6e625..31962ef49 100644 --- a/crates/ark/src/start.rs +++ b/crates/ark/src/start.rs @@ -20,10 +20,10 @@ use bus::Bus; use crossbeam::channel::bounded; use crossbeam::channel::unbounded; +use crate::console::ConsoleNotification; +use crate::console::SessionMode; use crate::control::Control; use crate::dap; -use crate::interface::ConsoleNotification; -use crate::interface::SessionMode; use crate::lsp; use crate::plots::graphics_device::GraphicsDeviceNotification; use crate::repos::DefaultRepos; @@ -141,7 +141,7 @@ pub fn start_kernel( } // Start R - crate::interface::RMain::start( + crate::console::Console::start( r_args, startup_file, comm_manager_tx, diff --git a/crates/ark/src/startup.rs b/crates/ark/src/startup.rs index 3020342d6..1d611d93a 100644 --- a/crates/ark/src/startup.rs +++ b/crates/ark/src/startup.rs @@ -17,7 +17,7 @@ use harp::exec::RFunctionExt; use libr::Rf_eval; use stdext::result::ResultExt; -use crate::interface::RMain; +use crate::console::Console; use crate::sys; pub(crate) fn should_ignore_site_r_profile(args: &Vec) -> bool { @@ -105,7 +105,7 @@ fn source_r_profile(path: &PathBuf) { text: message, }); - RMain::with(|main| main.get_iopub_tx().send(message).unwrap()) + Console::get().get_iopub_tx().send(message).unwrap() } fn find_site_r_profile(r_home: &PathBuf) -> Option { diff --git a/crates/ark/src/sys/unix.rs b/crates/ark/src/sys/unix.rs index cc72aed49..2289bdaec 100644 --- a/crates/ark/src/sys/unix.rs +++ b/crates/ark/src/sys/unix.rs @@ -7,7 +7,6 @@ pub mod console; pub mod control; -pub mod interface; pub mod path; pub mod signals; pub mod traps; diff --git a/crates/ark/src/sys/unix/console.rs b/crates/ark/src/sys/unix/console.rs index 2eec323fb..9d2e974b0 100644 --- a/crates/ark/src/sys/unix/console.rs +++ b/crates/ark/src/sys/unix/console.rs @@ -1,13 +1,158 @@ /* * console.rs * - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. + * Copyright (C) 2023-2026 Posit Software, PBC. All rights reserved. * */ use std::ffi::c_char; use std::ffi::CStr; +use std::sync::Condvar; +use std::sync::Mutex; +use libr::ptr_R_Busy; +use libr::ptr_R_ReadConsole; +use libr::ptr_R_ShowMessage; +use libr::ptr_R_Suicide; +use libr::ptr_R_WriteConsole; +use libr::ptr_R_WriteConsoleEx; +use libr::run_Rmainloop; +use libr::setup_Rmainloop; +use libr::R_Consolefile; +use libr::R_HomeDir; +use libr::R_InputHandlers; +use libr::R_Interactive; +use libr::R_Outputfile; +use libr::R_PolledEvents; +use libr::R_SignalHandlers; +use libr::R_checkActivity; +use libr::R_runHandlers; +use libr::R_running_as_main_program; +use libr::R_wait_usec; +use libr::Rf_initialize_R; + +use crate::console::r_busy; +use crate::console::r_polled_events; +use crate::console::r_read_console; +use crate::console::r_show_message; +use crate::console::r_suicide; +use crate::console::r_write_console; +use crate::console::Console; +use crate::signals::initialize_signal_handlers; + +// For shutdown signal in integration tests +pub static CLEANUP_SIGNAL: (Mutex, Condvar) = (Mutex::new(false), Condvar::new()); + +pub fn setup_r(args: &Vec) { + unsafe { + // Before `Rf_initialize_R()` + libr::set(R_running_as_main_program, 1); + + libr::set(R_SignalHandlers, 0); + + let mut c_args = Console::build_ark_c_args(args); + Rf_initialize_R(c_args.len() as i32, c_args.as_mut_ptr() as *mut *mut c_char); + + // Initialize the signal blocks and handlers (like interrupts). + // Don't do that in tests because that makes them uninterruptible. + if !stdext::IS_TESTING { + initialize_signal_handlers(); + } + + // Mark R session as interactive + // (Should have also been set by call to `Rf_initialize_R()`) + libr::set(R_Interactive, 1); + + // Log the value of R_HOME, so we can know if something hairy is afoot + let home = CStr::from_ptr(R_HomeDir()); + log::trace!("R_HOME: {:?}", home); + + // Redirect console + libr::set(R_Consolefile, std::ptr::null_mut()); + libr::set(R_Outputfile, std::ptr::null_mut()); + + libr::set(ptr_R_WriteConsole, None); + libr::set(ptr_R_WriteConsoleEx, Some(r_write_console)); + libr::set(ptr_R_ReadConsole, Some(r_read_console)); + libr::set(ptr_R_ShowMessage, Some(r_show_message)); + libr::set(ptr_R_Busy, Some(r_busy)); + libr::set(ptr_R_Suicide, Some(r_suicide)); + + // Install a CleanUp hook for integration tests that test the shutdown process. + // We confirm that shutdown occurs by waiting in the test until `CLEANUP_SIGNAL`'s + // condition variable sends a notification, which occurs in this cleanup method + // that is called during R's shutdown process. + if stdext::IS_TESTING { + libr::set(libr::ptr_R_CleanUp, Some(r_cleanup_for_tests)); + } + + // In tests R may be run from various threads. This confuses R's stack + // overflow checks so we disable those. This should not make it in + // production builds as it causes stack overflows to crash R instead of + // throwing an R error. + // + // This must be called _after_ `Rf_initialize_R()`, since that's where R + // detects the stack size and sets the default limit. + if stdext::IS_TESTING { + libr::set(libr::R_CStackLimit, usize::MAX); + } + + // Set up main loop + setup_Rmainloop(); + } +} + +pub fn run_r() { + unsafe { + // Listen for polled events + libr::set(R_wait_usec, 10000); + libr::set(R_PolledEvents, Some(r_polled_events)); + + run_Rmainloop(); + } +} + +pub fn run_activity_handlers() { + unsafe { + // Run handlers if we have data available. This is necessary + // for things like the HTML help server, which will listen + // for requests on an open socket() which would then normally + // be handled in a select() call when reading input from stdin. + // + // https://github.com/wch/r-source/blob/4ca6439c1ffc76958592455c44d83f95d5854b2a/src/unix/sys-std.c#L1084-L1086 + // + // We run this in a loop just to make sure the R help server can + // be as responsive as possible when rendering help pages. + // + // Note that the later package also adds an input handler to `R_InputHandlers` + // which runs the later event loop, so it's also important that we are fairly + // responsive for that as well (posit-dev/positron#7235). + let mut fdset = R_checkActivity(0, 1); + + while fdset != std::ptr::null_mut() { + R_runHandlers(libr::get(R_InputHandlers), fdset); + fdset = R_checkActivity(0, 1); + } + } +} + +#[no_mangle] +pub extern "C-unwind" fn r_cleanup_for_tests(_save_act: i32, _status: i32, _run_last: i32) { + // Signal that cleanup has started + let (lock, cvar) = &CLEANUP_SIGNAL; + + let mut started = lock.lock().unwrap(); + *started = true; + + cvar.notify_all(); + drop(started); + + // Sleep to give tests time to complete before we panic + std::thread::sleep(std::time::Duration::from_secs(5)); + + // Fallthrough to R which will call `exit()`. Note that panicking from here + // would be UB, we can't panic over a C stack. +} /// On Unix, we assume that the buffer to write to the console is /// already in UTF-8 pub fn console_to_utf8(x: *const c_char) -> anyhow::Result { diff --git a/crates/ark/src/sys/unix/interface.rs b/crates/ark/src/sys/unix/interface.rs deleted file mode 100644 index 30f146320..000000000 --- a/crates/ark/src/sys/unix/interface.rs +++ /dev/null @@ -1,155 +0,0 @@ -/* - * interface.rs - * - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. - * - */ - -use std::ffi::CStr; -use std::os::raw::c_char; -use std::sync::Condvar; -use std::sync::Mutex; - -use libr::ptr_R_Busy; -use libr::ptr_R_ReadConsole; -use libr::ptr_R_ShowMessage; -use libr::ptr_R_Suicide; -use libr::ptr_R_WriteConsole; -use libr::ptr_R_WriteConsoleEx; -use libr::run_Rmainloop; -use libr::setup_Rmainloop; -use libr::R_Consolefile; -use libr::R_HomeDir; -use libr::R_InputHandlers; -use libr::R_Interactive; -use libr::R_Outputfile; -use libr::R_PolledEvents; -use libr::R_SignalHandlers; -use libr::R_checkActivity; -use libr::R_runHandlers; -use libr::R_running_as_main_program; -use libr::R_wait_usec; -use libr::Rf_initialize_R; - -use crate::interface::r_busy; -use crate::interface::r_polled_events; -use crate::interface::r_read_console; -use crate::interface::r_show_message; -use crate::interface::r_suicide; -use crate::interface::r_write_console; -use crate::interface::RMain; -use crate::signals::initialize_signal_handlers; - -// For shutdown signal in integration tests -pub static CLEANUP_SIGNAL: (Mutex, Condvar) = (Mutex::new(false), Condvar::new()); - -pub fn setup_r(args: &Vec) { - unsafe { - // Before `Rf_initialize_R()` - libr::set(R_running_as_main_program, 1); - - libr::set(R_SignalHandlers, 0); - - let mut c_args = RMain::build_ark_c_args(args); - Rf_initialize_R(c_args.len() as i32, c_args.as_mut_ptr() as *mut *mut c_char); - - // Initialize the signal blocks and handlers (like interrupts). - // Don't do that in tests because that makes them uninterruptible. - if !stdext::IS_TESTING { - initialize_signal_handlers(); - } - - // Mark R session as interactive - // (Should have also been set by call to `Rf_initialize_R()`) - libr::set(R_Interactive, 1); - - // Log the value of R_HOME, so we can know if something hairy is afoot - let home = CStr::from_ptr(R_HomeDir()); - log::trace!("R_HOME: {:?}", home); - - // Redirect console - libr::set(R_Consolefile, std::ptr::null_mut()); - libr::set(R_Outputfile, std::ptr::null_mut()); - - libr::set(ptr_R_WriteConsole, None); - libr::set(ptr_R_WriteConsoleEx, Some(r_write_console)); - libr::set(ptr_R_ReadConsole, Some(r_read_console)); - libr::set(ptr_R_ShowMessage, Some(r_show_message)); - libr::set(ptr_R_Busy, Some(r_busy)); - libr::set(ptr_R_Suicide, Some(r_suicide)); - - // Install a CleanUp hook for integration tests that test the shutdown process. - // We confirm that shutdown occurs by waiting in the test until `CLEANUP_SIGNAL`'s - // condition variable sends a notification, which occurs in this cleanup method - // that is called during R's shutdown process. - if stdext::IS_TESTING { - libr::set(libr::ptr_R_CleanUp, Some(r_cleanup_for_tests)); - } - - // In tests R may be run from various threads. This confuses R's stack - // overflow checks so we disable those. This should not make it in - // production builds as it causes stack overflows to crash R instead of - // throwing an R error. - // - // This must be called _after_ `Rf_initialize_R()`, since that's where R - // detects the stack size and sets the default limit. - if stdext::IS_TESTING { - libr::set(libr::R_CStackLimit, usize::MAX); - } - - // Set up main loop - setup_Rmainloop(); - } -} - -pub fn run_r() { - unsafe { - // Listen for polled events - libr::set(R_wait_usec, 10000); - libr::set(R_PolledEvents, Some(r_polled_events)); - - run_Rmainloop(); - } -} - -pub fn run_activity_handlers() { - unsafe { - // Run handlers if we have data available. This is necessary - // for things like the HTML help server, which will listen - // for requests on an open socket() which would then normally - // be handled in a select() call when reading input from stdin. - // - // https://github.com/wch/r-source/blob/4ca6439c1ffc76958592455c44d83f95d5854b2a/src/unix/sys-std.c#L1084-L1086 - // - // We run this in a loop just to make sure the R help server can - // be as responsive as possible when rendering help pages. - // - // Note that the later package also adds an input handler to `R_InputHandlers` - // which runs the later event loop, so it's also important that we are fairly - // responsive for that as well (posit-dev/positron#7235). - let mut fdset = R_checkActivity(0, 1); - - while fdset != std::ptr::null_mut() { - R_runHandlers(libr::get(R_InputHandlers), fdset); - fdset = R_checkActivity(0, 1); - } - } -} - -#[no_mangle] -pub extern "C-unwind" fn r_cleanup_for_tests(_save_act: i32, _status: i32, _run_last: i32) { - // Signal that cleanup has started - let (lock, cvar) = &CLEANUP_SIGNAL; - - let mut started = lock.lock().unwrap(); - *started = true; - - cvar.notify_all(); - drop(started); - - // Sleep to give tests time to complete before we panic - std::thread::sleep(std::time::Duration::from_secs(5)); - - // Fallthrough to R which will call `exit()`. Note that panicking from here - // would be UB, we can't panic over a C stack. -} diff --git a/crates/ark/src/sys/windows.rs b/crates/ark/src/sys/windows.rs index bba637444..cd03ecaa8 100644 --- a/crates/ark/src/sys/windows.rs +++ b/crates/ark/src/sys/windows.rs @@ -7,7 +7,6 @@ pub mod console; pub mod control; -pub mod interface; mod locale; pub mod parent_monitor; pub mod path; diff --git a/crates/ark/src/sys/windows/console.rs b/crates/ark/src/sys/windows/console.rs index ceb417f97..9e7a60592 100644 --- a/crates/ark/src/sys/windows/console.rs +++ b/crates/ark/src/sys/windows/console.rs @@ -6,13 +6,227 @@ */ use std::ffi::c_char; +use std::ffi::c_int; use std::ffi::CStr; +use std::ffi::CString; +use std::mem::MaybeUninit; +use libr::cmdlineoptions; +use libr::get_R_HOME; +use libr::readconsolecfg; +use libr::run_Rmainloop; +use libr::setup_Rmainloop; +use libr::R_DefParamsEx; +use libr::R_HomeDir; +use libr::R_SetParams; +use libr::R_SignalHandlers; +use libr::R_common_command_line; +use libr::Rboolean_FALSE; use once_cell::sync::Lazy; use regex::bytes::Regex; use super::strings::code_page_to_utf8; use super::strings::get_system_code_page; +use crate::console::r_busy; +use crate::console::r_read_console; +use crate::console::r_show_message; +use crate::console::r_suicide; +use crate::console::r_write_console; +use crate::console::Console; +use crate::sys::windows::strings::system_to_utf8; + +pub fn setup_r(args: &Vec) { + unsafe { + libr::set(R_SignalHandlers, 0); + + // - We have to collect these before `cmdlineoptions()` is called, because + // it alters the env vars, which we then reset to our own paths in `R_SetParams()`. + // - `rhome` and `home` need to be set before `R_SetParams()` because it accesses them. + // - We convert to a `mut` pointer because the R API requires it, but it doesn't modify them. + // - `CString::new()` handles adding a nul terminator for us. + let r_home = get_r_home(); + let r_home = CString::new(r_home).unwrap(); + let r_home = r_home.as_ptr() as *mut c_char; + + let user_home = get_user_home(); + let user_home = CString::new(user_home).unwrap(); + let user_home = user_home.as_ptr() as *mut c_char; + + // Note that R does a lot of initialization here that's not accessible + // in any other way; e.g. the default translation domain is set within + // via `BindDomain()`. We don't supply the `args` here because we do a + // wholesale replacement of the options they set via `R_SetParams()` + // later on, so setting them here would have no effect anyways. + // https://github.com/rstudio/rstudio/issues/10308 + let mut c_args = Console::build_ark_c_args(&vec![]); + cmdlineoptions(c_args.len() as i32, c_args.as_mut_ptr() as *mut *mut c_char); + + let mut params_struct = MaybeUninit::uninit(); + let params: libr::Rstart = params_struct.as_mut_ptr(); + + // Set up initial defaults for `params` + // + // TODO: Windows + // We eventually need to use `RSTART_VERSION` (i.e., 1). It might just + // work as is but will require a little testing. It sets and initializes + // some additional useful callbacks, but is only available in newer R + // versions. + // R_DefParamsEx(params, bindings::RSTART_VERSION as i32); + R_DefParamsEx(params, 0); + + // Set up "common" command line arguments, inheriting R's "last flag + // wins" behavior for these. On the Unix side this is automatically + // called by `Rf_initialize_R()`. On the Windows side this is called by + // `cmdlineoptions()`, but because we call `R_SetParams()` later on to + // tweak some options, we have to fully rebuild the correct `params` + // list anyways so we don't supply any user `args` to + // `cmdlineoptions()` and instead pass them here. + // + // Notably sets: + // - `(*params).R_Quiet` via `--silent`, `--quiet`, `-q`, `--no-echo` + // - `(*params).R_Verbose` via `--verbose` + // - `(*params).NoRenviron` via `--no-environ`, `--vanilla` + // - `(*params).SaveAction` via `--save`, `--no-save`, `--vanilla`, `--no-echo` + // - `(*params).RestoreAction` via `--restore`, `--no-restore`, `--no-restore-data`, `--vanilla` + // - `R_RestoreHistory` (a global) via `--restore`, `--no-restore`, `--no-restore-history`, `--vanilla` + let mut c_args = Console::build_ark_c_args(args); + let mut c_args_len = c_args.len() as std::ffi::c_int; + R_common_command_line( + &mut c_args_len, + c_args.as_mut_ptr() as *mut *mut c_char, + params, + ); + + (*params).R_Interactive = 1; + (*params).CharacterMode = libr::UImode_RGui; + + // Never load the user or site `.Rprofile`s during `setup_Rmainloop()`. + // We do it for the user once ark is ready. We faithfully reimplement + // R's behavior for finding these files in `startup.rs`. + (*params).LoadInitFile = Rboolean_FALSE; + (*params).LoadSiteFile = Rboolean_FALSE; + + (*params).WriteConsole = None; + (*params).WriteConsoleEx = Some(r_write_console); + (*params).ReadConsole = Some(r_read_console); + (*params).ShowMessage = Some(r_show_message); + (*params).YesNoCancel = Some(r_yes_no_cancel); + (*params).Busy = Some(r_busy); + (*params).Suicide = Some(r_suicide); + + // This is assigned to `ptr_ProcessEvents` (which we don't set on Unix), + // in `R_SetParams()` by `R_SetWin32()` and gets called by `R_ProcessEvents()`. + // It gets called unconditionally, so we have to set it to something, even if a no-op. + (*params).CallBack = Some(r_callback); + + (*params).rhome = r_home; + (*params).home = user_home; + + // Sets the parameters to internal R globals, like all of the `ptr_*` function pointers + R_SetParams(params); + + // In tests R may be run from various threads. This confuses R's stack + // overflow checks so we disable those. This should not make it in + // production builds as it causes stack overflows to crash R instead of + // throwing an R error. + if stdext::IS_TESTING { + libr::set(libr::R_CStackLimit, usize::MAX); + } + + // R global ui initialization + libr::graphapp::GA_initapp(0, std::ptr::null_mut()); + readconsolecfg(); + + // Log the value of R_HOME, so we can know if something hairy is afoot + let home = CStr::from_ptr(R_HomeDir()); + log::trace!("R_HOME: {:?}", home); + + // Set up main loop + setup_Rmainloop(); + } +} + +pub fn run_r() { + unsafe { + run_Rmainloop(); + } +} + +pub fn run_activity_handlers() { + // Nothing to do on Windows +} + +// TODO: Windows +// It is possible we will want to use something other than `get_R_HOME()` and `getRUser()` for these. +// RStudio does use `get_R_HOME()`, but they have a custom helper instead of `getRUser()`. +// https://github.com/rstudio/rstudio/blob/d9c0b090d49752fe60e7a2ea4be3123cc3feeb6c/src/cpp/r/session/RDiscovery.cpp#L42 +// https://github.com/rstudio/rstudio/blob/d9c0b090d49752fe60e7a2ea4be3123cc3feeb6c/src/cpp/shared_core/system/Win32User.cpp#L164 +fn get_r_home() -> String { + let r_path = unsafe { get_R_HOME() }; + + if r_path.is_null() { + panic!("`get_R_HOME()` failed to report an R home."); + } + + let r_path_ctr = unsafe { CStr::from_ptr(r_path) }; + + // Removes nul terminator + let path = r_path_ctr.to_bytes(); + + // Try conversion to UTF-8 + let path = match system_to_utf8(path) { + Ok(path) => path, + Err(err) => { + let path = r_path_ctr.to_string_lossy().to_string(); + panic!("Failed to convert R home to UTF-8. Path '{path}'. Error: {err:?}."); + }, + }; + + path.to_string() +} + +fn get_user_home() -> String { + let r_path = unsafe { libr::getRUser() }; + + if r_path.is_null() { + panic!("`getRUser()` failed to report a user home directory."); + } + + let r_path_ctr = unsafe { CStr::from_ptr(r_path) }; + + // Removes nul terminator + let path = r_path_ctr.to_bytes(); + + // Try conversion to UTF-8 + let path = match system_to_utf8(path) { + Ok(path) => path, + Err(err) => { + let path = r_path_ctr.to_string_lossy().to_string(); + panic!("Failed to convert user home to UTF-8. Path '{path}'. Error: {err:?}."); + }, + }; + + path.to_string() +} + +#[no_mangle] +extern "C-unwind" fn r_callback() { + // Do nothing! +} + +#[no_mangle] +extern "C-unwind" fn r_yes_no_cancel(question: *const c_char) -> c_int { + // This seems to only be used on Windows during R's default `CleanUp` when + // `SA_SAVEASK` is used. We should replace `Cleanup` with our own version + // that resolves `SA_SAVEASK`, changes `saveact` to the resolved value, + // then calls R's default `CleanUp` with the new value. That way this never + // gets called (at which point we can make this an error). In the meantime + // we simply return `-1` to request "no save" on exit. + // https://github.com/wch/r-source/blob/bd5e9741c9b55c481a2e5d4f929cf1597ec08fcc/src/gnuwin32/system.c#L565 + let question = unsafe { CStr::from_ptr(question).to_str().unwrap() }; + log::warn!("Ignoring `YesNoCancel` question: '{question}'. Returning `NO`."); + return -1; +} // - (?-u) to disable unicode so it matches the bytes exactly // - (?s:.) so `.` matches anything INCLUDING new lines diff --git a/crates/ark/src/sys/windows/interface.rs b/crates/ark/src/sys/windows/interface.rs deleted file mode 100644 index 621e6a8c0..000000000 --- a/crates/ark/src/sys/windows/interface.rs +++ /dev/null @@ -1,225 +0,0 @@ -/* - * interface.rs - * - * Copyright (C) 2023 Posit Software, PBC. All rights reserved. - * - */ - -use std::ffi::c_char; -use std::ffi::c_int; -use std::ffi::CStr; -use std::ffi::CString; -use std::mem::MaybeUninit; - -use libr::cmdlineoptions; -use libr::get_R_HOME; -use libr::readconsolecfg; -use libr::run_Rmainloop; -use libr::setup_Rmainloop; -use libr::R_DefParamsEx; -use libr::R_HomeDir; -use libr::R_SetParams; -use libr::R_SignalHandlers; -use libr::R_common_command_line; -use libr::Rboolean_FALSE; - -use crate::interface::r_busy; -use crate::interface::r_read_console; -use crate::interface::r_show_message; -use crate::interface::r_suicide; -use crate::interface::r_write_console; -use crate::interface::RMain; -use crate::sys::windows::strings::system_to_utf8; - -pub fn setup_r(args: &Vec) { - unsafe { - libr::set(R_SignalHandlers, 0); - - // - We have to collect these before `cmdlineoptions()` is called, because - // it alters the env vars, which we then reset to our own paths in `R_SetParams()`. - // - `rhome` and `home` need to be set before `R_SetParams()` because it accesses them. - // - We convert to a `mut` pointer because the R API requires it, but it doesn't modify them. - // - `CString::new()` handles adding a nul terminator for us. - let r_home = get_r_home(); - let r_home = CString::new(r_home).unwrap(); - let r_home = r_home.as_ptr() as *mut c_char; - - let user_home = get_user_home(); - let user_home = CString::new(user_home).unwrap(); - let user_home = user_home.as_ptr() as *mut c_char; - - // Note that R does a lot of initialization here that's not accessible - // in any other way; e.g. the default translation domain is set within - // via `BindDomain()`. We don't supply the `args` here because we do a - // wholesale replacement of the options they set via `R_SetParams()` - // later on, so setting them here would have no effect anyways. - // https://github.com/rstudio/rstudio/issues/10308 - let mut c_args = RMain::build_ark_c_args(&vec![]); - cmdlineoptions(c_args.len() as i32, c_args.as_mut_ptr() as *mut *mut c_char); - - let mut params_struct = MaybeUninit::uninit(); - let params: libr::Rstart = params_struct.as_mut_ptr(); - - // Set up initial defaults for `params` - // - // TODO: Windows - // We eventually need to use `RSTART_VERSION` (i.e., 1). It might just - // work as is but will require a little testing. It sets and initializes - // some additional useful callbacks, but is only available in newer R - // versions. - // R_DefParamsEx(params, bindings::RSTART_VERSION as i32); - R_DefParamsEx(params, 0); - - // Set up "common" command line arguments, inheriting R's "last flag - // wins" behavior for these. On the Unix side this is automatically - // called by `Rf_initialize_R()`. On the Windows side this is called by - // `cmdlineoptions()`, but because we call `R_SetParams()` later on to - // tweak some options, we have to fully rebuild the correct `params` - // list anyways so we don't supply any user `args` to - // `cmdlineoptions()` and instead pass them here. - // - // Notably sets: - // - `(*params).R_Quiet` via `--silent`, `--quiet`, `-q`, `--no-echo` - // - `(*params).R_Verbose` via `--verbose` - // - `(*params).NoRenviron` via `--no-environ`, `--vanilla` - // - `(*params).SaveAction` via `--save`, `--no-save`, `--vanilla`, `--no-echo` - // - `(*params).RestoreAction` via `--restore`, `--no-restore`, `--no-restore-data`, `--vanilla` - // - `R_RestoreHistory` (a global) via `--restore`, `--no-restore`, `--no-restore-history`, `--vanilla` - let mut c_args = RMain::build_ark_c_args(args); - let mut c_args_len = c_args.len() as std::ffi::c_int; - R_common_command_line( - &mut c_args_len, - c_args.as_mut_ptr() as *mut *mut c_char, - params, - ); - - (*params).R_Interactive = 1; - (*params).CharacterMode = libr::UImode_RGui; - - // Never load the user or site `.Rprofile`s during `setup_Rmainloop()`. - // We do it for the user once ark is ready. We faithfully reimplement - // R's behavior for finding these files in `startup.rs`. - (*params).LoadInitFile = Rboolean_FALSE; - (*params).LoadSiteFile = Rboolean_FALSE; - - (*params).WriteConsole = None; - (*params).WriteConsoleEx = Some(r_write_console); - (*params).ReadConsole = Some(r_read_console); - (*params).ShowMessage = Some(r_show_message); - (*params).YesNoCancel = Some(r_yes_no_cancel); - (*params).Busy = Some(r_busy); - (*params).Suicide = Some(r_suicide); - - // This is assigned to `ptr_ProcessEvents` (which we don't set on Unix), - // in `R_SetParams()` by `R_SetWin32()` and gets called by `R_ProcessEvents()`. - // It gets called unconditionally, so we have to set it to something, even if a no-op. - (*params).CallBack = Some(r_callback); - - (*params).rhome = r_home; - (*params).home = user_home; - - // Sets the parameters to internal R globals, like all of the `ptr_*` function pointers - R_SetParams(params); - - // In tests R may be run from various threads. This confuses R's stack - // overflow checks so we disable those. This should not make it in - // production builds as it causes stack overflows to crash R instead of - // throwing an R error. - if stdext::IS_TESTING { - libr::set(libr::R_CStackLimit, usize::MAX); - } - - // R global ui initialization - libr::graphapp::GA_initapp(0, std::ptr::null_mut()); - readconsolecfg(); - - // Log the value of R_HOME, so we can know if something hairy is afoot - let home = CStr::from_ptr(R_HomeDir()); - log::trace!("R_HOME: {:?}", home); - - // Set up main loop - setup_Rmainloop(); - } -} - -pub fn run_r() { - unsafe { - run_Rmainloop(); - } -} - -pub fn run_activity_handlers() { - // Nothing to do on Windows -} - -// TODO: Windows -// It is possible we will want to use something other than `get_R_HOME()` and `getRUser()` for these. -// RStudio does use `get_R_HOME()`, but they have a custom helper instead of `getRUser()`. -// https://github.com/rstudio/rstudio/blob/d9c0b090d49752fe60e7a2ea4be3123cc3feeb6c/src/cpp/r/session/RDiscovery.cpp#L42 -// https://github.com/rstudio/rstudio/blob/d9c0b090d49752fe60e7a2ea4be3123cc3feeb6c/src/cpp/shared_core/system/Win32User.cpp#L164 -fn get_r_home() -> String { - let r_path = unsafe { get_R_HOME() }; - - if r_path.is_null() { - panic!("`get_R_HOME()` failed to report an R home."); - } - - let r_path_ctr = unsafe { CStr::from_ptr(r_path) }; - - // Removes nul terminator - let path = r_path_ctr.to_bytes(); - - // Try conversion to UTF-8 - let path = match system_to_utf8(path) { - Ok(path) => path, - Err(err) => { - let path = r_path_ctr.to_string_lossy().to_string(); - panic!("Failed to convert R home to UTF-8. Path '{path}'. Error: {err:?}."); - }, - }; - - path.to_string() -} - -fn get_user_home() -> String { - let r_path = unsafe { libr::getRUser() }; - - if r_path.is_null() { - panic!("`getRUser()` failed to report a user home directory."); - } - - let r_path_ctr = unsafe { CStr::from_ptr(r_path) }; - - // Removes nul terminator - let path = r_path_ctr.to_bytes(); - - // Try conversion to UTF-8 - let path = match system_to_utf8(path) { - Ok(path) => path, - Err(err) => { - let path = r_path_ctr.to_string_lossy().to_string(); - panic!("Failed to convert user home to UTF-8. Path '{path}'. Error: {err:?}."); - }, - }; - - path.to_string() -} - -#[no_mangle] -extern "C-unwind" fn r_callback() { - // Do nothing! -} - -#[no_mangle] -extern "C-unwind" fn r_yes_no_cancel(question: *const c_char) -> c_int { - // This seems to only be used on Windows during R's default `CleanUp` when - // `SA_SAVEASK` is used. We should replace `Cleanup` with our own version - // that resolves `SA_SAVEASK`, changes `saveact` to the resolved value, - // then calls R's default `CleanUp` with the new value. That way this never - // gets called (at which point we can make this an error). In the meantime - // we simply return `-1` to request "no save" on exit. - // https://github.com/wch/r-source/blob/bd5e9741c9b55c481a2e5d4f929cf1597ec08fcc/src/gnuwin32/system.c#L565 - let question = unsafe { CStr::from_ptr(question).to_str().unwrap() }; - log::warn!("Ignoring `YesNoCancel` question: '{question}'. Returning `NO`."); - return -1; -} diff --git a/crates/ark/src/thread.rs b/crates/ark/src/thread.rs index 00f213682..094b1f697 100644 --- a/crates/ark/src/thread.rs +++ b/crates/ark/src/thread.rs @@ -5,7 +5,7 @@ // // -use crate::interface::RMain; +use crate::console::Console; use crate::r_task; /// Private "shelter" around a Rust object (typically wrapping a `SEXP`, like @@ -95,7 +95,7 @@ impl Drop for RThreadSafe { } fn check_on_main_r_thread(f: &str) { - if !RMain::on_main_thread() && !stdext::IS_TESTING { + if !Console::on_main_thread() && !stdext::IS_TESTING { let thread = std::thread::current(); let name = thread.name().unwrap_or(""); let message = diff --git a/crates/ark/src/ui/events.rs b/crates/ark/src/ui/events.rs index 1f96a340f..241ac48f5 100644 --- a/crates/ark/src/ui/events.rs +++ b/crates/ark/src/ui/events.rs @@ -21,7 +21,7 @@ use harp::object::RObject; use libr::R_NilValue; use libr::SEXP; -use crate::interface::RMain; +use crate::console::Console; #[harp::register] pub unsafe extern "C-unwind" fn ps_ui_show_message(message: SEXP) -> anyhow::Result { @@ -31,8 +31,7 @@ pub unsafe extern "C-unwind" fn ps_ui_show_message(message: SEXP) -> anyhow::Res let event = UiFrontendEvent::ShowMessage(params); - let main = RMain::get(); - let ui_comm_tx = main + let ui_comm_tx = Console::get() .get_ui_comm_tx() .ok_or_else(|| ui_comm_not_connected("ui_show_message"))?; ui_comm_tx.send_event(event); @@ -52,8 +51,7 @@ pub unsafe extern "C-unwind" fn ps_ui_open_workspace( let event = UiFrontendEvent::OpenWorkspace(params); - let main = RMain::get(); - let ui_comm_tx = main + let ui_comm_tx = Console::get() .get_ui_comm_tx() .ok_or_else(|| ui_comm_not_connected("ui_open_workspace"))?; ui_comm_tx.send_event(event); @@ -81,8 +79,7 @@ pub unsafe extern "C-unwind" fn ps_ui_navigate_to_file( let event = UiFrontendEvent::OpenEditor(params); - let main = RMain::get(); - let ui_comm_tx = main + let ui_comm_tx = Console::get() .get_ui_comm_tx() .ok_or_else(|| ui_comm_not_connected("ui_navigate_to_file"))?; ui_comm_tx.send_event(event); @@ -97,8 +94,7 @@ pub unsafe extern "C-unwind" fn ps_ui_set_selection_ranges(ranges: SEXP) -> anyh let event = UiFrontendEvent::SetEditorSelections(params); - let main = RMain::get(); - let ui_comm_tx = main + let ui_comm_tx = Console::get() .get_ui_comm_tx() .ok_or_else(|| ui_comm_not_connected("ui_set_selection_ranges"))?; ui_comm_tx.send_event(event); @@ -113,8 +109,7 @@ pub fn send_show_url_event(url: &str) -> anyhow::Result<()> { }; let event = UiFrontendEvent::ShowUrl(params); - let main = RMain::get(); - let ui_comm_tx = main + let ui_comm_tx = Console::get() .get_ui_comm_tx() .ok_or_else(|| ui_comm_not_connected("show_url"))?; ui_comm_tx.send_event(event); @@ -135,8 +130,7 @@ pub fn send_open_with_system_event(path: &str) -> anyhow::Result<()> { }; let event = UiFrontendEvent::OpenWithSystem(params); - let main = RMain::get(); - let ui_comm_tx = main + let ui_comm_tx = Console::get() .get_ui_comm_tx() .ok_or_else(|| ui_comm_not_connected("open_with_system"))?; ui_comm_tx.send_event(event); diff --git a/crates/ark/src/ui/methods.rs b/crates/ark/src/ui/methods.rs index 2b88efa29..5e9778c24 100644 --- a/crates/ark/src/ui/methods.rs +++ b/crates/ark/src/ui/methods.rs @@ -20,13 +20,12 @@ use harp::object::RObject; use harp::utils::r_is_null; use libr::SEXP; -use crate::interface::RMain; +use crate::console::Console; use crate::ui::events::ps_ui_robj_as_ranges; #[harp::register] pub unsafe extern "C-unwind" fn ps_ui_last_active_editor_context() -> anyhow::Result { - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::LastActiveEditorContext)?; + let out = Console::get().call_frontend_method(UiFrontendRequest::LastActiveEditorContext)?; Ok(out.sexp) } @@ -44,15 +43,14 @@ pub unsafe extern "C-unwind" fn ps_ui_modify_editor_selections( } let params = ModifyEditorSelectionsParams { selections, values }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::ModifyEditorSelections(params))?; + let out = + Console::get().call_frontend_method(UiFrontendRequest::ModifyEditorSelections(params))?; Ok(out.sexp) } #[harp::register] pub unsafe extern "C-unwind" fn ps_ui_workspace_folder() -> anyhow::Result { - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::WorkspaceFolder)?; + let out = Console::get().call_frontend_method(UiFrontendRequest::WorkspaceFolder)?; Ok(out.sexp) } @@ -66,8 +64,7 @@ pub unsafe extern "C-unwind" fn ps_ui_show_dialog( message: RObject::view(message).try_into()?, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::ShowDialog(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::ShowDialog(params))?; Ok(out.sexp) } @@ -93,8 +90,7 @@ pub unsafe extern "C-unwind" fn ps_ui_show_question( }, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::ShowQuestion(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::ShowQuestion(params))?; Ok(out.sexp) } @@ -124,8 +120,7 @@ pub extern "C-unwind" fn ps_ui_show_prompt( timeout: timeout_secs, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::ShowPrompt(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::ShowPrompt(params))?; Ok(out.sexp) } @@ -135,8 +130,7 @@ pub unsafe extern "C-unwind" fn ps_ui_ask_for_password(prompt: SEXP) -> anyhow:: prompt: RObject::view(prompt).try_into()?, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::AskForPassword(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::AskForPassword(params))?; Ok(out.sexp) } @@ -150,8 +144,7 @@ pub unsafe extern "C-unwind" fn ps_ui_new_document( language_id: RObject::view(language_id).try_into()?, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::NewDocument(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::NewDocument(params))?; Ok(out.sexp) } @@ -161,8 +154,7 @@ pub unsafe extern "C-unwind" fn ps_ui_execute_command(command: SEXP) -> anyhow:: command: RObject::view(command).try_into()?, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::ExecuteCommand(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::ExecuteCommand(params))?; Ok(out.sexp) } @@ -178,8 +170,7 @@ pub unsafe extern "C-unwind" fn ps_ui_execute_code( allow_incomplete: false, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::ExecuteCode(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::ExecuteCode(params))?; Ok(out.sexp) } @@ -191,8 +182,7 @@ pub unsafe extern "C-unwind" fn ps_ui_evaluate_when_clause( when_clause: RObject::view(when_clause).try_into()?, }; - let main = RMain::get(); - let out = main.call_frontend_method(UiFrontendRequest::EvaluateWhenClause(params))?; + let out = Console::get().call_frontend_method(UiFrontendRequest::EvaluateWhenClause(params))?; Ok(out.sexp) } @@ -202,7 +192,6 @@ pub unsafe extern "C-unwind" fn ps_ui_debug_sleep(ms: SEXP) -> anyhow::Result { // Emit HTML output - let main = RMain::get(); - let iopub_tx = main.get_iopub_tx().clone(); - match main.session_mode() { + let console = Console::get(); + let iopub_tx = console.get_iopub_tx().clone(); + match console.session_mode() { SessionMode::Notebook | SessionMode::Background => { // In notebook mode, send the output as a Jupyter display_data message if let Err(err) = emit_html_output_jupyter(iopub_tx, path, label) { @@ -112,7 +112,7 @@ pub unsafe extern "C-unwind" fn ps_html_viewer( // TODO: What's the right thing to do in `Console` mode when // we aren't connected to Positron? Right now we error. - let ui_comm_tx = main + let ui_comm_tx = console .get_ui_comm_tx() .ok_or_else(|| anyhow::anyhow!("UI comm not connected."))?; diff --git a/crates/ark/tests/connections.rs b/crates/ark/tests/connections.rs index 0331c4135..e459863e6 100644 --- a/crates/ark/tests/connections.rs +++ b/crates/ark/tests/connections.rs @@ -33,7 +33,7 @@ fn open_dummy_connection() -> socket::comm::CommSocket { .unwrap(); // R returns the comm socket id that's used as key to communicate with the comm. - // but it didn't actually open the comm because RMain is not initialized in tests + // but it didn't actually open the comm because Console is not initialized in tests // thus we need to manually open the comm here, using our own CommManager. // we run this in a spare thread because it will block until we read the messsage stdext::spawn!("start-connection-thread", { diff --git a/crates/harp/src/fixtures/mod.rs b/crates/harp/src/fixtures/mod.rs index 960de18ca..ab27f5173 100644 --- a/crates/harp/src/fixtures/mod.rs +++ b/crates/harp/src/fixtures/mod.rs @@ -19,7 +19,7 @@ use libr::Rf_initialize_R; use crate::command::r_command_from_path; use crate::library::RLibraries; -use crate::R_MAIN_THREAD_ID; +use crate::CONSOLE_THREAD_ID; // FIXME: Needs to be a reentrant lock for idle tasks. We can probably do better // though. @@ -46,7 +46,7 @@ pub(crate) fn r_task(f: F) { pub fn r_test_init() { INIT.call_once(|| { unsafe { - R_MAIN_THREAD_ID = Some(std::thread::current().id()); + CONSOLE_THREAD_ID = Some(std::thread::current().id()); } // Set up R_HOME if necessary. diff --git a/crates/harp/src/lib.rs b/crates/harp/src/lib.rs index c505e48ec..2426099fd 100644 --- a/crates/harp/src/lib.rs +++ b/crates/harp/src/lib.rs @@ -91,8 +91,8 @@ pub fn initialize() { pub use error::Error; pub type Result = std::result::Result; -// ID of main thread. This is used to detect whether the current thread is -// the thread running R, see `RMain::on_main_thread()`. R should normally +// ID of Console thread. This is used to detect whether the current thread is +// the thread running R, see `Console::on_main_thread()`. R should normally // live on the main thread but detecting the main thread in a // cross-platform way is tricky, see https://docs.rs/is_main_thread. // @@ -100,7 +100,7 @@ pub type Result = std::result::Result; // creating a circular dependency on Ark. It's a constant initialised when // R starts up. Don't change its value. Since it's effectively read-only it // doesn't need synchronisation. -pub static mut R_MAIN_THREAD_ID: Option = None; +pub static mut CONSOLE_THREAD_ID: Option = None; pub fn r_null() -> libr::SEXP { unsafe { libr::R_NilValue }