use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
static DEVICE_HOOKS: AtomicU64 = AtomicU64::new(0);
static FOREIGN_HOOKS: AtomicU64 = AtomicU64::new(0);
static OWN_HOOKS: AtomicU64 = AtomicU64::new(0);
static LAST_KIND: AtomicU64 = AtomicU64::new(0);
static LAST_TS: AtomicU64 = AtomicU64::new(0);
pub(super) fn hook(injected: bool, tag: usize) {
let counter = if !injected {
note_last(1);
&DEVICE_HOOKS
} else if tag == actl_core::handoff::INPUT_TAG {
note_last(3);
&OWN_HOOKS
} else {
note_last(2);
&FOREIGN_HOOKS
};
counter.fetch_add(1, Ordering::SeqCst);
}
fn note_last(kind: u64) {
LAST_KIND.store(kind, Ordering::SeqCst);
LAST_TS.store(actl_core::state::unix_ms(), Ordering::SeqCst);
}
pub(super) fn last_source() -> Option<serde_json::Value> {
let kind = LAST_KIND.load(Ordering::SeqCst);
let ts = LAST_TS.load(Ordering::SeqCst);
let label = match kind {
1 => "device",
2 => "foreign_injected",
3 => "own_injection",
_ => return None,
};
let age_ms = actl_core::state::unix_ms().saturating_sub(ts);
Some(serde_json::json!({"kind": label, "age_ms": age_ms}))
}
#[derive(Default)]
struct State {
own: u64,
external: u64,
last_external: Option<serde_json::Value>,
}
static STATE: Mutex<State> = Mutex::new(State {
own: 0,
external: 0,
last_external: None,
});
pub(super) fn record(reason: &str, copied: u32, capacity: u32, header: u32, kind: u32) {
if let Ok(mut state) = STATE.lock() {
if reason == "own_injection" {
note_last(3);
state.own = state.own.saturating_add(1);
} else {
match reason {
"device_input" | "tagged_device_input" => note_last(1),
"unrecognized_tag" => note_last(2),
_ => {}
}
state.external = state.external.saturating_add(1);
state.last_external = Some(serde_json::json!({
"reason":reason,"copied_bytes":copied,"capacity_bytes":capacity,
"header_bytes":header,"kind":kind,
"ts_ms":actl_core::state::unix_ms()
}));
}
}
}
pub(super) fn snapshot() -> serde_json::Value {
match STATE.lock() {
Ok(state) => serde_json::json!({"own_events":state.own,
"external_events":state.external,"last_external":state.last_external,
"hook_device_events":DEVICE_HOOKS.load(Ordering::SeqCst),
"hook_foreign_injected_events":FOREIGN_HOOKS.load(Ordering::SeqCst),
"hook_own_injected_events":OWN_HOOKS.load(Ordering::SeqCst)}),
Err(_) => serde_json::json!({"reason":"diagnostic_lock_unavailable"}),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn last_source_classifies_hook_and_raw_events() {
assert!(last_source().is_none() || !last_source().unwrap()["kind"].is_null());
hook(false, 0);
assert_eq!(last_source().unwrap()["kind"], "device");
hook(true, actl_core::handoff::INPUT_TAG);
assert_eq!(last_source().unwrap()["kind"], "own_injection");
hook(true, 0xdead);
assert_eq!(last_source().unwrap()["kind"], "foreign_injected");
record("device_input", 48, 48, 48, 0);
assert_eq!(last_source().unwrap()["kind"], "device");
record("unrecognized_tag", 48, 48, 48, 1);
assert_eq!(last_source().unwrap()["kind"], "foreign_injected");
let source = last_source().unwrap();
assert!(source["age_ms"].is_u64(), "age present: {source}");
}
}