use serde::{Deserialize, Serialize};
use std::path::PathBuf;
const ARF_IPC_SESSIONS_DIR: &str = "ARF_IPC_SESSIONS_DIR";
#[derive(Debug, Serialize, Deserialize)]
pub struct SessionInfo {
pub pid: u32,
pub socket_path: String,
pub r_version: Option<String>,
pub cwd: String,
pub started_at: String,
#[serde(default)]
pub log_file: Option<String>,
#[serde(default)]
pub history_session_id: Option<i64>,
}
pub fn sessions_dir() -> Option<PathBuf> {
if let Some(override_dir) = std::env::var_os(ARF_IPC_SESSIONS_DIR) {
let path = PathBuf::from(override_dir);
if !path.as_os_str().is_empty() {
return Some(path);
}
}
dirs::cache_dir().map(|d| d.join("arf").join("sessions"))
}
pub fn write_session(info: &SessionInfo) -> std::io::Result<()> {
let dir = sessions_dir().ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "cache directory not found")
})?;
#[cfg(unix)]
{
use std::os::unix::fs::DirBuilderExt;
std::fs::DirBuilder::new()
.recursive(true)
.mode(0o700)
.create(&dir)?;
}
#[cfg(not(unix))]
{
std::fs::create_dir_all(&dir)?;
}
let path = dir.join(format!("{}.json", info.pid));
let json = serde_json::to_string_pretty(info).map_err(std::io::Error::other)?;
#[cfg(unix)]
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&path)?;
file.write_all(json.as_bytes())?;
}
#[cfg(not(unix))]
{
std::fs::write(&path, &json)?;
}
log::info!("Session file written: {}", path.display());
Ok(())
}
pub fn clear_session_history_id(pid: u32) {
let Some(dir) = sessions_dir() else { return };
let path = dir.join(format!("{pid}.json"));
let contents = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return,
Err(e) => {
log::debug!("Could not read session file {}: {}", path.display(), e);
return;
}
};
let mut info: SessionInfo = match serde_json::from_str(&contents) {
Ok(i) => i,
Err(e) => {
log::debug!("Could not parse session file {}: {}", path.display(), e);
return;
}
};
info.history_session_id = None;
info.pid = pid;
if let Err(e) = write_session(&info) {
log::debug!("Could not rewrite session file {}: {}", path.display(), e);
}
}
pub fn remove_session(pid: u32) {
if let Some(dir) = sessions_dir() {
let path = dir.join(format!("{pid}.json"));
if let Err(e) = std::fs::remove_file(&path) {
log::debug!("Could not remove session file {}: {}", path.display(), e);
}
}
}
pub fn list_sessions() -> Vec<SessionInfo> {
let dir = match sessions_dir() {
Some(d) if d.exists() => d,
_ => return Vec::new(),
};
let mut sessions = Vec::new();
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => return Vec::new(),
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "json")
&& let Ok(contents) = std::fs::read_to_string(&path)
&& let Ok(info) = serde_json::from_str::<SessionInfo>(&contents)
{
if is_process_alive(info.pid) {
sessions.push(info);
} else {
let _ = std::fs::remove_file(&path);
}
}
}
sessions
}
pub fn find_session(pid: Option<u32>) -> Option<SessionInfo> {
let sessions = list_sessions();
match pid {
Some(target_pid) => sessions.into_iter().find(|s| s.pid == target_pid),
None => {
if sessions.len() == 1 {
sessions.into_iter().next()
} else {
None
}
}
}
}
fn is_process_alive(pid: u32) -> bool {
#[cfg(unix)]
{
let ret = unsafe { libc::kill(pid as libc::pid_t, 0) };
if ret == 0 {
return true;
}
std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(windows)]
{
use std::process::Command;
Command::new("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/FO", "CSV", "/NH"])
.output()
.is_ok_and(|o| {
let out = String::from_utf8_lossy(&o.stdout);
out.lines().any(|line| {
line.split(',')
.nth(1)
.is_some_and(|f| f.trim_matches('"') == pid.to_string())
})
})
}
}