use std::{
hash::{Hash, Hasher},
path::{Path, PathBuf},
sync::Weak,
time::{Duration, SystemTime},
};
use super::live::LiveA2a;
pub const DEFAULT_A2A_WATCH_INTERVAL: Duration = Duration::from_secs(2);
const MIN_INTERVAL: Duration = Duration::from_millis(10);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[error(
"watching A2A files needs a Tokio runtime, and this thread is not running in one; call \
`watch_a2a` from within the runtime the host serves on"
)]
#[non_exhaustive]
pub struct A2aWatchError;
#[must_use = "the watch stops as soon as this handle is dropped"]
pub struct A2aWatch {
poll: tokio::task::JoinHandle<()>,
}
impl Drop for A2aWatch {
fn drop(&mut self) {
self.poll.abort();
}
}
impl std::fmt::Debug for A2aWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("A2aWatch")
.field("running", &!self.poll.is_finished())
.finish()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Stamp {
Missing,
Present {
len: u64,
modified: Option<SystemTime>,
content: Option<u64>,
},
}
pub(crate) fn stamps(paths: &[PathBuf]) -> Vec<Stamp> {
paths.iter().map(|path| stamp(path)).collect()
}
fn stamp(path: &Path) -> Stamp {
match std::fs::metadata(path) {
Ok(metadata) => Stamp::Present {
len: metadata.len(),
modified: metadata.modified().ok(),
content: std::fs::read(path).ok().map(|bytes| {
let mut hasher = std::hash::DefaultHasher::new();
bytes.hash(&mut hasher);
hasher.finish()
}),
},
Err(_) => Stamp::Missing,
}
}
#[derive(Debug, Default)]
struct Settling {
pending: Option<Vec<Stamp>>,
}
impl Settling {
fn observe(&mut self, now: Vec<Stamp>, seen: &[Stamp]) -> bool {
if now == seen {
self.pending = None;
return false;
}
if self.pending.as_ref() == Some(&now) {
self.pending = None;
return true;
}
self.pending = Some(now);
false
}
}
pub(crate) fn start(
live: Weak<LiveA2a>,
paths: Vec<PathBuf>,
interval: Duration,
) -> Result<A2aWatch, A2aWatchError> {
let runtime = tokio::runtime::Handle::try_current().map_err(|_| A2aWatchError)?;
let poll = runtime.spawn(async move {
let mut ticks = tokio::time::interval(interval.max(MIN_INTERVAL));
ticks.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
let mut settling = Settling::default();
loop {
ticks.tick().await;
let Some(live) = live.upgrade() else { break };
if !settling.observe(stamps(&paths), &live.seen()) {
continue;
}
if let Err(error) = live.reload().await {
eprintln!(
"Warning: A2A configuration changed but was not applied; the agents \
configured before stay: {error}"
);
}
}
});
Ok(A2aWatch { poll })
}
#[cfg(test)]
mod tests;