use std::io;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
#[cfg(doc)]
use super::remove_beacon;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WalpinHeartbeat {
pub pid: u32,
pub process_role: String,
pub started_at: i64,
pub oldest_tx_age_secs: f64,
pub oldest_tx_label: Option<String>,
#[serde(default)]
pub oldest_tx_started_at: Option<i64>,
pub updated_at: i64,
#[serde(default, alias = "interval_ms")]
pub sweep_interval_ms: u64,
#[serde(default)]
pub attribution_basis: Option<String>,
}
impl WalpinHeartbeat {
pub fn attribution_is_evidence_backed(&self) -> bool {
self.attribution_basis.as_deref() == Some("origin")
}
pub fn current_oldest_tx_age_secs(&self, now_epoch_secs: i64) -> f64 {
match self.oldest_tx_started_at {
Some(started_at) => (now_epoch_secs - started_at).max(0) as f64,
None => self.oldest_tx_age_secs,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LiveWalpinEntry {
pub heartbeat: WalpinHeartbeat,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct WalpinBeacon {
pub pid: u32,
pub process_role: String,
pub started_at: i64,
#[serde(default, alias = "interval_ms")]
pub sweep_interval_ms: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub enum WalpinPidHealth {
Reporting(WalpinHeartbeat),
RegisteredSilent { pid: u32 },
Unknown { pid: u32, reason: &'static str },
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct WalpinReport {
pub entries: Vec<WalpinPidHealth>,
pub sidecar_listing_truncated: bool,
pub cleanup_would_reap: usize,
pub orphan_temps_reaped: usize,
}
impl WalpinReport {
pub fn reporting(&self) -> impl Iterator<Item = &WalpinHeartbeat> {
self.entries.iter().filter_map(|e| match e {
WalpinPidHealth::Reporting(hb) => Some(hb),
_ => None,
})
}
pub fn registered_silent_pids(&self) -> impl Iterator<Item = u32> + '_ {
self.entries.iter().filter_map(|e| match e {
WalpinPidHealth::RegisteredSilent { pid } => Some(*pid),
_ => None,
})
}
pub fn unknown_pids(&self) -> impl Iterator<Item = u32> + '_ {
self.entries.iter().filter_map(|e| match e {
WalpinPidHealth::Unknown { pid, .. } => Some(*pid),
_ => None,
})
}
pub fn fully_attributed(&self) -> bool {
self.unknown_pids().next().is_none()
}
}
pub(super) fn io_other(msg: impl Into<String>) -> io::Error {
io::Error::other(msg.into())
}
pub fn sidecar_dir_for(db_path: &Path) -> PathBuf {
let mut file = db_path.file_name().unwrap_or_default().to_os_string();
file.push(".walpin");
match db_path.parent() {
Some(parent) => parent.join(file),
None => PathBuf::from(file),
}
}
pub fn sidecar_enabled(is_file_backed: bool) -> bool {
crate::env::env_flag("KHIVE_WALPIN_SIDECAR", is_file_backed)
}
#[cfg(any(windows, test))]
pub(super) fn windows_attribute_tag_is_acceptable(
file_attributes: u32,
reparse_tag: u32,
require_directory: bool,
) -> bool {
const FILE_ATTRIBUTE_DIRECTORY: u32 = 0x10;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400;
let is_directory = file_attributes & FILE_ATTRIBUTE_DIRECTORY != 0;
let is_reparse = file_attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0;
is_directory == require_directory && !is_reparse && reparse_tag == 0
}
#[cfg(any(windows, test))]
pub(super) fn windows_final_path_matches(expected: &[u16], opened: &[u16]) -> bool {
expected == opened
}
#[cfg(any(windows, test))]
pub(super) fn windows_relative_child_name_is_safe(name: &str) -> bool {
!name.is_empty() && name != "." && name != ".." && !name.contains(['/', '\\', '\0'])
}
#[cfg(any(windows, test))]
pub(super) fn windows_owner_dacl_is_restricted(
ace_count: u32,
ace_type: u8,
ace_flags: u8,
access_mask: u32,
owner_matches: bool,
owner_is_token_user: bool,
dacl_protected: bool,
) -> bool {
const ACCESS_ALLOWED_ACE_TYPE: u8 = 0;
const OBJECT_AND_CONTAINER_INHERIT: u8 = 0x03;
const FILE_ALL_ACCESS: u32 = 0x001f_01ff;
ace_count == 1
&& ace_type == ACCESS_ALLOWED_ACE_TYPE
&& ace_flags == OBJECT_AND_CONTAINER_INHERIT
&& access_mask == FILE_ALL_ACCESS
&& owner_matches
&& owner_is_token_user
&& dacl_protected
}
#[cfg(unix)]
#[derive(Clone, Copy)]
pub(super) enum ProducerTempKind {
Heartbeat,
Beacon,
}
#[cfg(unix)]
pub(super) fn producer_temp_identity(name: &str) -> Option<(u32, ProducerTempKind)> {
let inner = name.strip_prefix('.')?.strip_suffix(".tmp")?;
let (pid, record_kind) = inner.split_once('.')?;
let pid = pid.parse::<u32>().ok().filter(|pid| *pid > 0)?;
let kind = match record_kind {
"json" => ProducerTempKind::Heartbeat,
"beacon" => ProducerTempKind::Beacon,
_ => return None,
};
Some((pid, kind))
}