use std::{
ffi::OsString,
fs,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
process::Command,
sync::{
Once,
atomic::{AtomicU32, Ordering},
},
time::{Duration, Instant, SystemTime},
};
use crate::{emulator::Emulator, format::civil_from_days};
const SCRATCH_PREFIX: &str = "fleetcom_test2_";
pub(crate) fn temp(tag: &str) -> PathBuf {
static SEQ: AtomicU32 = AtomicU32::new(0);
sweep_dead_scratch();
let seq = SEQ.fetch_add(1, Ordering::Relaxed);
let d = std::env::temp_dir().join(format!(
"{SCRATCH_PREFIX}{tag}_{}_{seq}",
std::process::id()
));
let _ = fs::remove_dir_all(&d);
fs::create_dir_all(&d).unwrap();
d
}
fn sweep_dead_scratch() {
static ONCE: Once = Once::new();
ONCE.call_once(|| {
let Ok(entries) = fs::read_dir(std::env::temp_dir()) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if name
.to_str()
.and_then(scratch_pid_of)
.is_some_and(pid_is_dead)
{
let _ = fs::remove_dir_all(entry.path());
}
}
});
}
fn scratch_pid_of(name: &str) -> Option<i32> {
scratch_pid(name.strip_prefix(SCRATCH_PREFIX)?)
}
fn scratch_pid(suffix: &str) -> Option<i32> {
let (rest, seq) = suffix.rsplit_once('_')?;
let (_, pid) = rest.rsplit_once('_')?;
let digits = |s: &str| !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit());
if !digits(seq) || !digits(pid) {
return None;
}
pid.parse::<i32>().ok().filter(|p| *p > 0)
}
fn pid_is_dead(pid: i32) -> bool {
use nix::{errno::Errno, sys::signal::kill, unistd::Pid};
matches!(kill(Pid::from_raw(pid), None), Err(Errno::ESRCH))
}
#[test]
fn scratch_pid_reads_the_pid_field() {
assert_eq!(scratch_pid("tag_with_underscores_123_4"), Some(123));
assert_eq!(scratch_pid("session_mode_7_0"), Some(7));
assert_eq!(scratch_pid("tag_with_underscores_x_4"), None);
assert_eq!(scratch_pid("tag_123_x"), None);
assert_eq!(scratch_pid("tag_123"), None);
assert_eq!(scratch_pid("tag_0_4"), None);
}
#[test]
fn legacy_scratch_names_are_rejected_by_the_prefix() {
assert_eq!(scratch_pid_of("fleetcom_test_app_1007_456"), None);
assert_eq!(scratch_pid_of("fleetcom_test_tag_123"), None);
assert_eq!(scratch_pid_of("fleetcom_test2_app_1007_456_0"), Some(456));
assert_eq!(scratch_pid_of("unrelated_dir_123_4"), None);
}
pub(crate) fn wait_until(budget: Duration, mut pred: impl FnMut() -> bool) -> bool {
let deadline = Instant::now() + budget;
loop {
if pred() {
return true;
}
if Instant::now() >= deadline {
return false;
}
std::thread::sleep(Duration::from_millis(10));
}
}
pub(crate) fn now_ms() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}
pub(crate) fn read_pid(path: &Path) -> nix::unistd::Pid {
let mut pid = None;
wait_until(Duration::from_secs(5), || {
pid = fs::read_to_string(path)
.ok()
.and_then(|s| s.trim().parse::<i32>().ok());
pid.is_some()
});
nix::unistd::Pid::from_raw(pid.expect("pid file never appeared"))
}
pub(crate) fn dead_pid() -> u32 {
let mut child = Command::new("sh").arg("-c").arg("exit 0").spawn().unwrap();
let pid = child.id();
child.wait().unwrap();
pid
}
pub(crate) fn here() -> PathBuf {
std::env::current_dir().unwrap()
}
pub(crate) fn env_here() -> Vec<(OsString, OsString)> {
std::env::vars_os().collect()
}
pub(crate) fn sh_env() -> Vec<(OsString, OsString)> {
let mut env = env_here();
env.retain(|(k, _)| k != "SHELL");
env.push(("SHELL".into(), "/bin/sh".into()));
env
}
pub(crate) fn write_executable(path: &Path, body: &str) {
fs::write(path, format!("#!/bin/sh\n{body}\n")).unwrap();
fs::set_permissions(path, fs::Permissions::from_mode(0o700)).unwrap();
}
pub(crate) fn install_fake_notifier(path: &Path, record: &Path) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
write_executable(
path,
&format!("printf '%s\\n' \"$@\" > '{}'", record.display()),
);
}
pub(crate) const CORPUS_LINES: usize = 40;
pub(crate) const CORPUS_COLS: usize = 120;
pub(crate) fn corpus_emulator() -> Emulator {
Emulator::new(CORPUS_LINES as u16, CORPUS_COLS as u16, 2000)
}
pub(crate) fn v7_at(ms: u64, tail: u32) -> String {
format!(
"{:08x}-{:04x}-7000-8000-0000000{:05x}",
ms >> 16,
ms & 0xffff,
tail
)
}
pub(crate) fn write_rollout(home: &Path, ms: u64, tail: u32, cwd: &Path) -> String {
let id = v7_at(ms, tail);
let (y, m, d) = civil_from_days((ms / 86_400_000) as i64);
let dir = home
.join("sessions")
.join(format!("{y:04}"))
.join(format!("{m:02}"))
.join(format!("{d:02}"));
fs::create_dir_all(&dir).unwrap();
let meta = format!(
r#"{{"timestamp":"x","type":"session_meta","payload":{{"id":"{id}","cwd":"{}"}}}}"#,
cwd.display()
);
fs::write(
dir.join(format!("rollout-2026-07-13T09-00-00-{id}.jsonl")),
format!("{meta}\n{{}}\n"),
)
.unwrap();
id
}