use std::path::{Path, PathBuf};
use std::sync::Arc;
pub(crate) const LOG_CLAIM_ENV: &str = "MISE_TASK_OTEL_LOG_CLAIM";
#[derive(Clone, Debug)]
pub(crate) struct LogClaimWatcher {
dir: Arc<PathBuf>,
}
impl LogClaimWatcher {
pub(crate) fn new(dir: PathBuf) -> Self {
Self { dir: Arc::new(dir) }
}
pub(crate) fn path(&self) -> &Path {
&self.dir
}
pub(crate) fn claimed(&self) -> bool {
let Ok(entries) = std::fs::read_dir(self.path()) else {
return false;
};
let mut claimed = false;
for entry in entries.flatten() {
let Some(pid) = entry
.file_name()
.to_str()
.and_then(|n| n.parse::<u32>().ok())
else {
continue;
};
if process_is_alive(pid) {
claimed = true;
} else {
trace!("otel: reclaiming log stream from dead pid {pid}");
let _ = std::fs::remove_file(entry.path());
}
}
claimed
}
}
#[cfg(unix)]
fn process_is_alive(pid: u32) -> bool {
if pid == 0 {
return false;
}
match nix::sys::signal::kill(nix::unistd::Pid::from_raw(pid as i32), None) {
Ok(()) => true,
Err(nix::errno::Errno::EPERM) => true,
Err(_) => false,
}
}
#[cfg(windows)]
fn process_is_alive(pid: u32) -> bool {
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE, STILL_ACTIVE};
use windows_sys::Win32::System::Threading::{
GetExitCodeProcess, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION,
};
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle.is_null() || handle == INVALID_HANDLE_VALUE {
return windows_sys::Win32::Foundation::GetLastError()
== windows_sys::Win32::Foundation::ERROR_ACCESS_DENIED;
}
let mut code = 0u32;
let ok = GetExitCodeProcess(handle, &mut code);
CloseHandle(handle);
ok == 0 || code == STILL_ACTIVE as u32
}
}
#[derive(Debug)]
pub(crate) struct LogClaim {
path: PathBuf,
}
impl LogClaim {
pub(crate) fn acquire() -> Option<Self> {
let dir = PathBuf::from(std::env::var_os(LOG_CLAIM_ENV)?);
let path = dir.join(std::process::id().to_string());
if let Err(err) = std::fs::File::create(&path) {
debug!(
"otel: failed to claim log stream at {}: {err}",
path.display()
);
return None;
}
trace!("otel: claimed log stream at {}", path.display());
Some(Self { path })
}
}
impl Drop for LogClaim {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dead_pid() -> u32 {
#[cfg(unix)]
let mut child = std::process::Command::new("true").spawn().unwrap();
#[cfg(windows)]
let mut child = std::process::Command::new("cmd")
.args(["/c", "exit"])
.spawn()
.unwrap();
let pid = child.id();
child.wait().unwrap();
pid
}
fn live_pid() -> u32 {
std::process::id()
}
fn claim_as(dir: &Path, pid: u32) -> LogClaim {
let path = dir.join(pid.to_string());
std::fs::File::create(&path).unwrap();
LogClaim { path }
}
#[test]
fn watcher_reports_claim_lifecycle() {
let dir = tempfile::tempdir().unwrap();
let watcher = LogClaimWatcher::new(dir.path().to_path_buf());
assert!(!watcher.claimed(), "unclaimed before anyone registers");
let claim = claim_as(dir.path(), live_pid());
assert!(watcher.claimed());
drop(claim);
assert!(
!watcher.claimed(),
"released once the owner drops its claim"
);
}
#[cfg(unix)]
#[test]
fn concurrent_claims_hold_the_stream_until_the_last_is_released() {
let dir = tempfile::tempdir().unwrap();
let watcher = LogClaimWatcher::new(dir.path().to_path_buf());
let mut other = std::process::Command::new("sleep")
.arg("30")
.spawn()
.unwrap();
let a = claim_as(dir.path(), live_pid());
let b = claim_as(dir.path(), other.id());
drop(a);
assert!(watcher.claimed(), "b is still exporting its own lines");
drop(b);
assert!(!watcher.claimed());
other.kill().unwrap();
other.wait().unwrap();
}
#[test]
fn stale_claim_from_a_dead_process_is_released() {
let dir = tempfile::tempdir().unwrap();
let watcher = LogClaimWatcher::new(dir.path().to_path_buf());
let stale = dir.path().join(dead_pid().to_string());
std::fs::File::create(&stale).unwrap();
assert!(
!watcher.claimed(),
"a claim whose owner is gone must not suppress the outer task"
);
assert!(
!stale.exists(),
"the stale claim should be cleaned up so later checks stay cheap"
);
}
#[test]
fn unrecognised_entries_do_not_suppress_the_outer_task() {
let dir = tempfile::tempdir().unwrap();
let watcher = LogClaimWatcher::new(dir.path().to_path_buf());
std::fs::write(dir.path().join("not-a-pid"), "").unwrap();
assert!(!watcher.claimed());
}
#[test]
fn acquire_registers_this_process() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var(LOG_CLAIM_ENV, dir.path()) };
let claim = LogClaim::acquire();
unsafe { std::env::remove_var(LOG_CLAIM_ENV) };
let claim = claim.expect("claim should be acquired");
let watcher = LogClaimWatcher::new(dir.path().to_path_buf());
assert!(watcher.claimed());
assert!(dir.path().join(std::process::id().to_string()).exists());
drop(claim);
assert!(!watcher.claimed());
}
}