use chrono::Utc;
use marshal_entities::{Session, SessionId};
use serde_json::json;
use std::path::PathBuf;
const MAX_ANCESTOR_WALK: usize = 5;
pub fn read_nickname() -> Option<String> {
let dir = state_dir()?;
let mut pid = current_ppid()?;
for _ in 0..MAX_ANCESTOR_WALK {
let path = dir.join(format!("shim-by-ppid-{pid}.json"));
if let Ok(bytes) = std::fs::read(&path) {
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&bytes) {
if let Some(nick) = v
.get("nickname")
.and_then(|s| s.as_str())
.filter(|s| !s.is_empty())
{
return Some(nick.to_string());
}
}
}
if pid <= 1 {
return None;
}
pid = parent_of(pid)?;
}
None
}
pub fn write(session: &Session, session_id: &SessionId) {
let Some(ppid) = current_ppid() else { return };
let Some(dir) = state_dir() else { return };
let path = dir.join(format!("shim-by-ppid-{ppid}.json"));
if let Err(e) = std::fs::create_dir_all(&dir) {
log::debug!("[marshal-shim] state file mkdir {dir:?} failed: {e}");
return;
}
let body = json!({
"session_id": session_id.0.as_ref(),
"nickname": session.nickname,
"pid": session.pid,
"ppid": ppid,
"cwd": session.cwd,
"git_branch": session.git_branch,
"updated_at": Utc::now().timestamp_millis(),
});
let Ok(bytes) = serde_json::to_vec_pretty(&body) else {
return;
};
let tmp = path.with_extension("json.tmp");
if let Err(e) = std::fs::write(&tmp, &bytes) {
log::debug!("[marshal-shim] state file write {tmp:?} failed: {e}");
return;
}
if let Err(e) = std::fs::rename(&tmp, &path) {
log::debug!("[marshal-shim] state file rename {path:?} failed: {e}");
}
}
#[cfg(unix)]
fn current_ppid() -> Option<u32> {
Some(unsafe { libc::getppid() as u32 })
}
#[cfg(windows)]
fn current_ppid() -> Option<u32> {
use windows_sys::Win32::System::Threading::GetCurrentProcessId;
let pid = unsafe { GetCurrentProcessId() };
parent_of(pid)
}
#[cfg(not(any(unix, windows)))]
fn current_ppid() -> Option<u32> {
None
}
#[cfg(target_os = "linux")]
fn parent_of(pid: u32) -> Option<u32> {
let body = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
for line in body.lines() {
if let Some(rest) = line.strip_prefix("PPid:") {
return rest.trim().parse().ok();
}
}
None
}
#[cfg(target_os = "macos")]
fn parent_of(pid: u32) -> Option<u32> {
let out = std::process::Command::new("ps")
.args(["-o", "ppid=", "-p", &pid.to_string()])
.output()
.ok()?;
if !out.status.success() {
return None;
}
String::from_utf8(out.stdout).ok()?.trim().parse().ok()
}
#[cfg(windows)]
fn parent_of(pid: u32) -> Option<u32> {
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
use windows_sys::Win32::System::Diagnostics::ToolHelp::{
CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, TH32CS_SNAPPROCESS,
};
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE_VALUE {
return None;
}
let mut entry: PROCESSENTRY32 = std::mem::zeroed();
entry.dwSize = std::mem::size_of::<PROCESSENTRY32>() as u32;
let mut found = None;
if Process32First(snap, &mut entry) != 0 {
loop {
if entry.th32ProcessID == pid {
found = Some(entry.th32ParentProcessID);
break;
}
if Process32Next(snap, &mut entry) == 0 {
break;
}
}
}
CloseHandle(snap);
found
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
fn parent_of(_pid: u32) -> Option<u32> {
None
}
#[cfg(unix)]
fn state_dir() -> Option<PathBuf> {
if let Some(s) = std::env::var_os("XDG_STATE_HOME") {
let p = PathBuf::from(s);
if !p.as_os_str().is_empty() {
return Some(p.join("marshal"));
}
}
let home = std::env::var_os("HOME")?;
Some(PathBuf::from(home).join(".local/state/marshal"))
}
#[cfg(windows)]
fn state_dir() -> Option<PathBuf> {
let base = std::env::var_os("LOCALAPPDATA").or_else(|| std::env::var_os("APPDATA"))?;
Some(PathBuf::from(base).join("marshal").join("state"))
}
#[cfg(not(any(unix, windows)))]
fn state_dir() -> Option<PathBuf> {
None
}