use std::fs;
use std::path::PathBuf;
use std::time::SystemTime;
#[cfg(any(target_os = "linux", target_os = "macos"))]
use std::time::{Duration, UNIX_EPOCH};
#[cfg(any(target_os = "linux", target_os = "macos"))]
use crate::config::profile::boot_epoch;
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
use crate::config::profile::{active_profile, foreign_lock_owners, process_creation_stamp};
use crate::config::profile::{owner_stamp, split_pid_ticks, stamp_owner_is_live};
fn fs_lock() -> std::sync::MutexGuard<'static, ()> {
crate::tests::HOME_ENV_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
}
fn stamped_lock(
dir: &tempfile::TempDir,
name: &str,
contents: &str,
written: SystemTime,
) -> PathBuf {
let path = dir.path().join(name);
fs::write(&path, contents).expect("write stamp");
fs::OpenOptions::new()
.write(true)
.open(&path)
.expect("open stamp")
.set_modified(written)
.expect("set mtime");
path
}
#[test]
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
fn owner_stamp_carries_a_creation_time_on_this_platform() {
let (pid, ticks) = split_pid_ticks(&owner_stamp()).expect("own stamp must parse");
assert_eq!(pid, std::process::id());
let ticks = ticks.expect("this platform can read a creation time");
assert_ne!(ticks, 0, "a zero creation time proves nothing");
assert_eq!(process_creation_stamp(pid), Some(ticks));
}
#[test]
#[cfg(not(any(windows, target_os = "linux", target_os = "macos")))]
fn owner_stamp_is_a_bare_pid_where_no_reading_exists() {
let (pid, ticks) = split_pid_ticks(&owner_stamp()).expect("own stamp must parse");
assert_eq!(pid, std::process::id());
assert_eq!(ticks, None);
}
#[test]
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
fn stamp_owner_is_live_rejects_a_recycled_creation_time() {
let _guard = fs_lock();
let dir = tempfile::tempdir().expect("tempdir");
let pid = std::process::id();
let ticks = process_creation_stamp(pid).expect("this platform reads a creation time");
let live = stamped_lock(
&dir,
"a.lock",
&format!("p:{pid}:{ticks}"),
SystemTime::now(),
);
assert!(
stamp_owner_is_live(&format!("{pid}:{ticks}"), &live),
"the live process's own reading must be accepted"
);
let recycled = stamped_lock(
&dir,
"b.lock",
&format!("p:{pid}:{}", ticks.saturating_add(1)),
SystemTime::now(),
);
assert!(
!stamp_owner_is_live(&format!("{pid}:{}", ticks.saturating_add(1)), &recycled),
"a recorded creation time that disagrees with the live process is a recycled PID"
);
}
#[test]
#[cfg(any(target_os = "linux", target_os = "macos"))]
fn stamp_owner_is_live_rejects_a_lock_written_before_the_boot() {
let _guard = fs_lock();
let boot = boot_epoch().expect("this platform reports a boot time");
let dir = tempfile::tempdir().expect("tempdir");
let pid = std::process::id();
let before = UNIX_EPOCH + Duration::from_secs(boot.saturating_sub(1) as u64);
for (name, field) in [("a.lock", owner_stamp()), ("b.lock", pid.to_string())] {
let path = stamped_lock(&dir, name, &format!("p:{field}"), before);
assert!(
!stamp_owner_is_live(&field, &path),
"{field} written before the boot must not read as live"
);
}
}
#[test]
fn stamp_owner_is_live_accepts_a_legacy_stamp_written_after_the_boot() {
let _guard = fs_lock();
let dir = tempfile::tempdir().expect("tempdir");
let pid = std::process::id();
let path = stamped_lock(&dir, "a.lock", &format!("p:{pid}"), SystemTime::now());
assert!(stamp_owner_is_live(&pid.to_string(), &path));
}
#[test]
#[cfg(any(windows, target_os = "linux", target_os = "macos"))]
fn foreign_lock_owners_drops_a_recycled_stamp() {
use std::process::{Command, Stdio};
let _guard = fs_lock();
let profile = active_profile().unwrap_or("default");
let mut cmd = if cfg!(windows) {
Command::new("ping")
} else {
Command::new("/bin/sleep")
};
if cfg!(windows) {
cmd.args(["-n", "60", "127.0.0.1"]);
} else {
cmd.args(["60"]);
}
let mut child = cmd
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn a helper process to own a live pid");
let live = child.id();
let ticks = process_creation_stamp(live).expect("this platform reads a creation time");
let dir = tempfile::tempdir().expect("tempdir");
let recycled = stamped_lock(
&dir,
"telegram_aaa.lock",
&format!("{profile}:{live}:{}", ticks.saturating_add(1)),
SystemTime::now(),
);
assert!(
!foreign_lock_owners(dir.path()).contains_key(&live),
"a recycled stamp must not be offered to the preempt path"
);
fs::write(&recycled, format!("{profile}:{live}:{ticks}")).expect("rewrite stamp");
let owners = foreign_lock_owners(dir.path());
let owner = owners
.get(&live)
.expect("the live owner must still be reported");
assert_eq!(owner.creation_ticks, Some(ticks));
assert_eq!(owner.channels, vec!["telegram".to_string()]);
let _ = child.kill();
let _ = child.wait();
}
#[test]
fn stamp_owner_is_live_rejects_a_dead_pid() {
let _guard = fs_lock();
let dir = tempfile::tempdir().expect("tempdir");
let path = stamped_lock(&dir, "a.lock", "p:4294967295:9000", SystemTime::now());
assert!(!stamp_owner_is_live("4294967295", &path));
assert!(!stamp_owner_is_live("4294967295:9000", &path));
assert!(!stamp_owner_is_live("0", &path));
}
#[test]
fn stamp_owner_is_live_rejects_a_garbage_pid_field() {
let _guard = fs_lock();
let dir = tempfile::tempdir().expect("tempdir");
let path = stamped_lock(&dir, "a.lock", "junk", SystemTime::now());
for field in ["", "not-a-pid", "101528ops:103104family:101507", "1.5"] {
assert!(
!stamp_owner_is_live(field, &path),
"{field:?} names no owner and must not read as live"
);
}
}
#[test]
fn stamp_owner_is_live_fails_open_when_the_boot_check_cannot_be_taken() {
let _guard = fs_lock();
let dir = tempfile::tempdir().expect("tempdir");
let missing = dir.path().join("gone.lock");
assert!(
stamp_owner_is_live(&std::process::id().to_string(), &missing),
"an unreadable lock file must not read as a stale owner"
);
}