use super::policy;
use super::protocol::{RSessionInfo, SessionResult};
use super::session;
use super::state::r_is_at_prompt;
use crate::sync::LockRecover;
use std::hash::{BuildHasher, Hasher, RandomState};
use std::sync::{Mutex, OnceLock, atomic::Ordering};
#[derive(Clone)]
struct SessionMeta {
socket_path: String,
started_at: String,
r_home: Option<String>,
log_file: Option<String>,
history_session_id: Option<i64>,
session_type: session::SessionType,
}
static SESSION_META: OnceLock<Mutex<SessionMeta>> = OnceLock::new();
pub(super) fn session_id() -> &'static str {
static SESSION_ID: OnceLock<String> = OnceLock::new();
SESSION_ID.get_or_init(|| {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos())
.unwrap_or_default();
let part = || {
let mut hasher = RandomState::new().build_hasher();
hasher.write_u32(std::process::id());
hasher.write_u128(nanos);
hasher.finish()
};
format!("{:016x}{:016x}", part(), part())
})
}
fn arf_session_base(meta: &SessionMeta) -> SessionResult {
SessionResult {
arf_version: env!("CARGO_PKG_VERSION").to_string(),
pid: std::process::id(),
session_id: session_id().to_string(),
os: std::env::consts::OS.to_string(),
arch: std::env::consts::ARCH.to_string(),
r_home: meta.r_home.clone(),
socket_path: meta.socket_path.clone(),
started_at: meta.started_at.clone(),
log_file: meta.log_file.clone(),
history_session_id: meta.history_session_id,
ipc_policy: policy::policy(meta.session_type),
handler_api: super::handlers::handler_status(),
r: None,
r_unavailable_reason: None,
hint: None,
}
}
pub fn clear_history_session_id() {
if let Some(m) = SESSION_META.get() {
let mut meta = m.lock_recover();
if meta.history_session_id.take().is_some() {
session::clear_session_history_id(std::process::id());
}
}
}
pub(super) fn set_session_meta(
socket_path: String,
started_at: String,
r_home: Option<String>,
log_file: Option<String>,
history_session_id: Option<i64>,
session_type: session::SessionType,
) {
let meta = SessionMeta {
socket_path,
started_at,
r_home,
log_file,
history_session_id,
session_type,
};
match SESSION_META.get() {
Some(m) => *m.lock_recover() = meta,
None => {
let _ = SESSION_META.set(Mutex::new(meta));
}
}
}
fn current_session_meta() -> SessionMeta {
match SESSION_META.get() {
Some(m) => m.lock_recover().clone(),
None => SessionMeta {
socket_path: "<uninitialized_socket_path>".to_string(),
started_at: "<uninitialized_started_at>".to_string(),
r_home: None,
log_file: None,
history_session_id: None,
session_type: session::SessionType::Interactive,
},
}
}
pub(super) fn collect_session_result(try_r: bool, reason: &str) -> SessionResult {
let meta = current_session_meta();
let mut result = arf_session_base(&meta);
if !try_r || !r_is_at_prompt().load(Ordering::Acquire) {
let reason = if reason.is_empty() {
"R is busy evaluating another expression"
} else {
reason
};
result.r_unavailable_reason = Some(reason.to_string());
result.hint = Some(if reason.contains("alternate mode") {
"Exit the current mode (shell, browser) to make R session info available.".to_string()
} else if reason.contains("pending") {
"Wait for the current IPC operation to complete, then retry.".to_string()
} else if reason.contains("Main thread") || reason.contains("handler dropped") {
"The arf process may be shutting down or unresponsive.".to_string()
} else if reason.contains("Timed out") {
"R may be busy with a long-running operation. Retry later.".to_string()
} else {
"R session information will be available when R returns to the prompt. \
Retry 'arf ipc session' later, or use 'arf ipc eval' with a timeout to wait."
.to_string()
});
return result;
}
match collect_r_session_info() {
Some(r_info) => {
result.r = Some(r_info);
}
None => {
result.r_unavailable_reason =
Some("Failed to collect R session information".to_string());
result.hint = Some(
"R may not be fully initialized. Try again later or use \
'arf ipc eval \"sessionInfo()\"' for raw output."
.to_string(),
);
}
}
result
}
pub fn read_r_version() -> Option<String> {
let tmpfile = tempfile::Builder::new()
.prefix(".arf_ipc_rver_")
.suffix(".txt")
.tempfile()
.ok()?;
let tmppath = tmpfile.path().display().to_string().replace('\\', "/");
let code = format!(r#"writeLines(paste0(R.version$major, ".", R.version$minor), "{tmppath}")"#);
let _ = arf_harp::eval_string(&code);
std::fs::read_to_string(tmpfile.path())
.ok()
.map(|s| s.trim().to_string())
}
fn collect_r_session_info() -> Option<RSessionInfo> {
let version = eval_r_scalar(r#"invisible(paste0(R.version$major, ".", R.version$minor))"#)
.unwrap_or_default();
if version.is_empty() {
return None;
}
Some(RSessionInfo {
version,
platform: eval_r_scalar("invisible(R.version$platform)").unwrap_or_default(),
locale: eval_r_scalar("invisible(Sys.getlocale())").unwrap_or_default(),
cwd: eval_r_scalar("invisible(getwd())").unwrap_or_default(),
loaded_namespaces: eval_r_character_vector("invisible(loadedNamespaces())")
.unwrap_or_default(),
attached_packages: eval_r_character_vector("invisible(.packages())").unwrap_or_default(),
lib_paths: eval_r_character_vector("invisible(.libPaths())").unwrap_or_default(),
})
}
fn eval_r_scalar(code: &str) -> Option<String> {
match arf_harp::eval_string(code) {
Ok(robj) => extract_r_string(robj.sexp()),
Err(e) => {
log::debug!("eval_r_scalar failed for `{code}`: {e}");
None
}
}
}
fn eval_r_character_vector(code: &str) -> Option<Vec<String>> {
match arf_harp::eval_string(code) {
Ok(robj) => extract_r_strings(robj.sexp()),
Err(e) => {
log::debug!("eval_r_character_vector failed for `{code}`: {e}");
None
}
}
}
fn extract_r_string(sexp: arf_libr::SEXP) -> Option<String> {
let lib = arf_libr::r_library().ok()?;
unsafe {
if (lib.rf_isstring)(sexp) == 0 || (lib.rf_length)(sexp) == 0 {
return None;
}
let elt = (lib.string_elt)(sexp, 0);
let cstr = (lib.r_charsxp)(elt);
if cstr.is_null() {
return None;
}
std::ffi::CStr::from_ptr(cstr)
.to_str()
.ok()
.map(|s| s.to_string())
}
}
fn extract_r_strings(sexp: arf_libr::SEXP) -> Option<Vec<String>> {
let lib = arf_libr::r_library().ok()?;
unsafe {
if (lib.rf_isstring)(sexp) == 0 {
return None;
}
let len = (lib.rf_length)(sexp) as isize;
let mut result = Vec::with_capacity(len as usize);
for i in 0..len {
let elt = (lib.string_elt)(sexp, i);
let cstr = (lib.r_charsxp)(elt);
if cstr.is_null() {
result.push(String::new());
} else if let Ok(s) = std::ffi::CStr::from_ptr(cstr).to_str() {
result.push(s.to_string());
} else {
result.push(String::new());
}
}
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
#[test]
#[serial]
fn test_session_result_reflects_session_meta() {
let session_id: i64 = 1_700_000_000_000_000_000;
set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
Some("/opt/R/4.4.1/lib/R".to_string()),
Some("/tmp/arf.log".to_string()),
Some(session_id),
session::SessionType::Interactive,
);
let result = collect_session_result(false, "test");
assert_eq!(result.log_file.as_deref(), Some("/tmp/arf.log"));
assert_eq!(result.r_home.as_deref(), Some("/opt/R/4.4.1/lib/R"));
assert_eq!(result.history_session_id, Some(session_id));
assert_eq!(
result.ipc_policy.silent,
crate::ipc::policy::SilentPolicy::Restricted {
allowed_functions: Vec::new()
}
);
let json = serde_json::to_value(&result).unwrap();
assert_eq!(json["r_home"], "/opt/R/4.4.1/lib/R");
assert_eq!(json["log_file"], "/tmp/arf.log");
assert_eq!(json["history_session_id"], session_id);
assert_eq!(json["ipc_policy"]["silent"]["mode"], "restricted");
set_session_meta(
"/tmp/test.sock".to_string(),
"2026-01-01T00:00:00+00:00".to_string(),
None,
None,
None,
session::SessionType::Interactive,
);
let result = collect_session_result(false, "test");
assert_eq!(result.log_file, None);
assert_eq!(result.r_home, None);
assert_eq!(result.history_session_id, None);
let json = serde_json::to_value(&result).unwrap();
for field in ["log_file", "r_home", "history_session_id"] {
assert!(
json.get(field).is_some(),
"{field} field should always be present in JSON"
);
assert!(json[field].is_null(), "{field} should be null when not set");
}
}
#[test]
fn test_session_id_is_a_stable_random_token() {
let id = session_id();
assert_eq!(id.len(), 32);
assert!(id.bytes().all(|b| b.is_ascii_hexdigit()), "{id}");
assert_eq!(id, session_id());
let json = serde_json::to_value(collect_session_result(false, "test")).unwrap();
assert_eq!(json["session_id"], id);
}
}