use rusqlite::{Connection, OptionalExtension, TransactionBehavior};
use super::{validate_record_id, ConversationStore};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoredOwner {
pub host: String,
pub boot_id: String,
pub pid: i64,
pub writer_fingerprint: String,
pub heartbeat_tick: i64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ClaimOutcome {
Claimed,
HeldBy { host: String, pid: i64 },
}
impl ConversationStore {
pub fn claim(&self, id: &str) -> anyhow::Result<ClaimOutcome> {
validate_record_id(id)?;
let now = (self.claim_clock)();
let conn = self.lock_conn();
let tx = rusqlite::Transaction::new_unchecked(&conn, TransactionBehavior::Immediate)?;
let existing = live_owner_row(&tx, id)?;
if let Some(owner) = &existing {
let is_ours =
owner.host == self.host && owner.boot_id == self.boot_id && owner.pid == self.pid;
if !is_ours && (self.liveness)(owner, now) {
return Ok(ClaimOutcome::HeldBy {
host: owner.host.clone(),
pid: owner.pid,
});
}
}
tx.execute(
"INSERT OR REPLACE INTO live_owners
(conversation_id, host, boot_id, pid, writer_fingerprint, heartbeat_tick)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![
id,
self.host,
self.boot_id,
self.pid,
self.writer_fingerprint,
now
],
)?;
tx.commit()?;
Ok(ClaimOutcome::Claimed)
}
pub fn release(&self, id: &str) -> anyhow::Result<()> {
let conn = self.lock_conn();
conn.execute(
"DELETE FROM live_owners
WHERE conversation_id = ?1 AND host = ?2 AND boot_id = ?3 AND pid = ?4",
rusqlite::params![id, self.host, self.boot_id, self.pid],
)?;
Ok(())
}
pub fn heartbeat(&self, id: &str) -> anyhow::Result<()> {
let now = (self.claim_clock)();
let conn = self.lock_conn();
conn.execute(
"UPDATE live_owners SET heartbeat_tick = ?2
WHERE conversation_id = ?1 AND host = ?3 AND boot_id = ?4 AND pid = ?5",
rusqlite::params![id, now, self.host, self.boot_id, self.pid],
)?;
Ok(())
}
pub fn live_owner(&self, id: &str) -> anyhow::Result<Option<StoredOwner>> {
let conn = self.lock_conn();
live_owner_row(&conn, id)
}
#[must_use]
pub fn is_owner_live(&self, owner: &StoredOwner) -> bool {
(self.liveness)(owner, (self.claim_clock)())
}
}
pub type LivenessFn = fn(owner: &StoredOwner, now: i64) -> bool;
const LIVENESS_STALE_AFTER_NANOS: i64 = 3_600 * 1_000_000_000;
pub(super) fn system_liveness(owner: &StoredOwner, now: i64) -> bool {
let (host, boot_id) = current_host_boot();
if owner.host == host && owner.boot_id == boot_id {
return pid_is_alive(owner.pid)
&& pid_identity_matches(pid_start_unix_nanos(owner.pid), owner.heartbeat_tick);
}
now.saturating_sub(owner.heartbeat_tick) < LIVENESS_STALE_AFTER_NANOS
}
pub(super) fn pid_identity_matches(started_at: Option<i64>, heartbeat_tick: i64) -> bool {
started_at.is_none_or(|started| started <= heartbeat_tick)
}
#[cfg(target_os = "linux")]
pub(super) fn pid_start_unix_nanos(pid: i64) -> Option<i64> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let after_comm = stat.rsplit_once(')')?.1;
let start_ticks: i64 = after_comm.split_whitespace().nth(19)?.parse().ok()?;
let ticks_per_sec = unsafe { libc::sysconf(libc::_SC_CLK_TCK) };
if ticks_per_sec <= 0 {
return None;
}
let btime_secs: i64 = std::fs::read_to_string("/proc/stat")
.ok()?
.lines()
.find_map(|line| line.strip_prefix("btime "))?
.trim()
.parse()
.ok()?;
btime_secs
.checked_add(start_ticks / ticks_per_sec)?
.checked_mul(1_000_000_000)
}
#[cfg(not(target_os = "linux"))]
fn pid_start_unix_nanos(_pid: i64) -> Option<i64> {
None
}
#[cfg(unix)]
pub(crate) fn pid_is_alive(pid: i64) -> bool {
let Ok(pid) = libc::pid_t::try_from(pid) else {
return false;
};
if pid <= 0 {
return false;
}
let rc = unsafe { libc::kill(pid, 0) };
rc == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
}
#[cfg(any(windows, test))]
pub(super) fn wait_probe_reports_live_or_unknown(result: u32, wait_object_0: u32) -> bool {
result != wait_object_0
}
#[cfg(any(windows, test))]
pub(super) fn open_process_failure_reports_live_or_unknown(
raw_error: Option<i32>,
error_invalid_parameter: i32,
) -> bool {
raw_error != Some(error_invalid_parameter)
}
#[cfg(windows)]
pub(crate) fn pid_is_alive(pid: i64) -> bool {
use windows_sys::Win32::Foundation::{CloseHandle, ERROR_INVALID_PARAMETER, WAIT_OBJECT_0};
use windows_sys::Win32::Storage::FileSystem::SYNCHRONIZE;
use windows_sys::Win32::System::Threading::{
OpenProcess, WaitForSingleObject, PROCESS_QUERY_LIMITED_INFORMATION,
};
let Ok(pid) = u32::try_from(pid) else {
return false;
};
if pid == 0 {
return false;
}
let handle = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, 0, pid) };
if handle.is_null() {
return open_process_failure_reports_live_or_unknown(
std::io::Error::last_os_error().raw_os_error(),
ERROR_INVALID_PARAMETER as i32,
);
}
let wait_result = unsafe { WaitForSingleObject(handle, 0) };
let running = wait_probe_reports_live_or_unknown(wait_result, WAIT_OBJECT_0);
unsafe { CloseHandle(handle) };
running
}
pub(super) fn current_host_boot() -> (String, String) {
let host = std::fs::read_to_string("/proc/sys/kernel/hostname")
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "localhost".to_string());
let boot_id = std::fs::read_to_string("/proc/sys/kernel/random/boot_id")
.map(|s| s.trim().to_string())
.unwrap_or_default();
(host, boot_id)
}
pub(super) fn live_owner_row(
conn: &Connection,
conversation_id: &str,
) -> anyhow::Result<Option<StoredOwner>> {
conn.query_row(
"SELECT host, boot_id, pid, writer_fingerprint, heartbeat_tick
FROM live_owners WHERE conversation_id = ?1",
rusqlite::params![conversation_id],
|row| {
Ok(StoredOwner {
host: row.get(0)?,
boot_id: row.get(1)?,
pid: row.get(2)?,
writer_fingerprint: row.get(3)?,
heartbeat_tick: row.get(4)?,
})
},
)
.optional()
.map_err(Into::into)
}