use std::sync::OnceLock;
pub const WATERMARK_SYSLOG_ID: &str = "sysknife-audit-tip";
static WARNED_JOURNALD_UNAVAILABLE: OnceLock<()> = OnceLock::new();
pub fn emit_chain_tip_watermark(seq: u64, chain_hash_hex: &str) {
emit_watermark_impl(seq, chain_hash_hex);
}
#[cfg(not(test))]
fn emit_watermark_impl(seq: u64, hash_hex: &str) {
emit_via_systemd_cat(seq, hash_hex);
}
#[cfg(test)]
fn emit_watermark_impl(seq: u64, hash_hex: &str) {
if let Some(sink) = TEST_SINK.get() {
let mut guard = sink.lock().expect("test sink mutex poisoned");
guard.push(WatermarkCall {
seq,
chain_hash_hex: hash_hex.to_string(),
});
} else {
emit_via_systemd_cat(seq, hash_hex);
}
}
fn emit_via_systemd_cat(seq: u64, hash_hex: &str) {
use std::io::Write as _;
let message = format!("seq={seq} chain_hash={hash_hex}");
let result = std::process::Command::new("systemd-cat")
.args(["-t", WATERMARK_SYSLOG_ID, "-p", "info"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.and_then(|mut child| {
if let Some(mut stdin) = child.stdin.take() {
stdin.write_all(message.as_bytes())?;
}
child.wait()
});
match result {
Ok(status) if status.success() => {}
Ok(status) => {
WARNED_JOURNALD_UNAVAILABLE.get_or_init(|| {
eprintln!(
"sysknife-audit-tip: systemd-cat exited with {status}; \
audit watermarks will not be written to journald"
);
});
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
WARNED_JOURNALD_UNAVAILABLE.get_or_init(|| {
eprintln!(
"sysknife-audit-tip: systemd-cat not found on PATH; \
audit watermarks will not be written to journald"
);
});
}
Err(e) => {
WARNED_JOURNALD_UNAVAILABLE.get_or_init(|| {
eprintln!(
"sysknife-audit-tip: failed to spawn systemd-cat: {e}; \
audit watermarks will not be written to journald"
);
});
}
}
}
#[cfg(test)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WatermarkCall {
pub seq: u64,
pub chain_hash_hex: String,
}
#[cfg(test)]
pub type WatermarkSink = std::sync::Arc<std::sync::Mutex<Vec<WatermarkCall>>>;
#[cfg(test)]
static TEST_SINK: OnceLock<WatermarkSink> = OnceLock::new();
#[cfg(test)]
pub fn install_test_sink(sink: WatermarkSink) {
TEST_SINK
.set(sink)
.expect("watermark test sink already installed in this process");
}
#[cfg(test)]
pub fn take_watermarks(sink: &WatermarkSink) -> Vec<WatermarkCall> {
sink.lock()
.expect("test sink mutex poisoned")
.drain(..)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{Arc, Mutex};
fn new_sink() -> WatermarkSink {
Arc::new(Mutex::new(Vec::new()))
}
#[test]
fn syslog_identifier_constant_is_correct() {
assert_eq!(WATERMARK_SYSLOG_ID, "sysknife-audit-tip");
}
#[test]
fn sink_records_seq_and_hex() {
let sink = new_sink();
{
let mut guard = sink.lock().unwrap();
guard.push(WatermarkCall {
seq: 7,
chain_hash_hex: "abcd".to_string(),
});
}
let calls = take_watermarks(&sink);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].seq, 7);
assert_eq!(calls[0].chain_hash_hex, "abcd");
}
#[test]
fn take_watermarks_drains_sink() {
let sink = new_sink();
sink.lock().unwrap().push(WatermarkCall {
seq: 1,
chain_hash_hex: "ff".to_string(),
});
let first = take_watermarks(&sink);
let second = take_watermarks(&sink);
assert_eq!(first.len(), 1);
assert!(second.is_empty());
}
}