use std::path::PathBuf;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
use crate::process_identity::process_identity;
pub fn wait_until(mut pred: impl FnMut() -> bool, budget: Duration) -> bool {
let deadline = Instant::now() + budget;
while Instant::now() < deadline {
if pred() {
return true;
}
std::thread::sleep(Duration::from_millis(25));
}
false
}
pub fn temp_dir(tag: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!(
"omniterm_health_test_{tag}_{}_{}",
std::process::id(),
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&dir).expect("create temp dir");
dir
}
pub fn spawn_fake_tmux_control(sleep_secs: u32) -> Child {
Command::new("python3")
.args([
"-c",
&format!(
"import os; os.execv('/bin/bash', ['tmux', '-C', '-c', 'sleep {sleep_secs} & wait'])"
),
])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn fake tmux -C")
}
pub fn wait_argv0(pid: u32, argv0: &str) {
let ready = wait_until(
|| process_identity(pid).and_then(|i| i.argv.first().cloned()).as_deref() == Some(argv0),
Duration::from_secs(5),
);
assert!(ready, "execv 换影超时:pid={pid} argv0 未变为 {argv0:?}");
}
pub fn wait_child_exit(child: &mut Child) -> bool {
wait_until(|| matches!(child.try_wait(), Ok(Some(_))), Duration::from_secs(5))
}
pub struct FakeSocketHolder {
pub child: Child,
pub socket_path: PathBuf,
dir: PathBuf,
}
const SPAWN_BUSY_RETRIES: u32 = 40;
const SPAWN_BUSY_BACKOFF: Duration = Duration::from_millis(50);
impl FakeSocketHolder {
pub fn spawn(tag: &str, exe_basename: &str) -> Self {
let dir = temp_dir(tag);
let exe_path = dir.join(exe_basename);
let python = which::which("python3").expect("tests require python3");
std::fs::copy(&python, &exe_path).expect("copy python3 as fake exe");
let socket_path = dir.join("fake.sock");
let script = format!(
"import socket,time;s=socket.socket(socket.AF_UNIX);s.bind({:?});s.listen(4);time.sleep(120)",
socket_path.display().to_string()
);
let mut cmd = Command::new(&exe_path);
cmd.arg("-c").arg(&script).stdout(Stdio::null()).stderr(Stdio::null());
let mut child = None;
for attempt in 0..SPAWN_BUSY_RETRIES {
match cmd.spawn() {
Ok(c) => {
child = Some(c);
break;
}
Err(e) if e.kind() == std::io::ErrorKind::ExecutableFileBusy => {
assert!(attempt + 1 < SPAWN_BUSY_RETRIES, "ETXTBSY 重试耗尽:{e:?}");
std::thread::sleep(SPAWN_BUSY_BACKOFF);
}
Err(e) => panic!("spawn fake socket holder: {e:?}"),
}
}
let child = child.expect("fake socket holder 应已 spawn");
let holder = Self { child, socket_path, dir };
let bound = wait_until(|| holder.socket_path.exists(), Duration::from_secs(5));
assert!(bound, "假 socket 未按时 bind:{}", holder.socket_path.display());
holder
}
}
impl Drop for FakeSocketHolder {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
let _ = std::fs::remove_dir_all(&self.dir);
}
}