use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
pub const STAGE_MARKER: &str = "STAGE";
pub const TICK_MARKER: &str = "TICK";
pub const TICK_EVERY: Duration = Duration::from_secs(60);
pub type Sink = Box<dyn Fn(&str) + Send + Sync>;
pub fn stage_line(token: &str, monotonic_ms: u128, detail: &str) -> String {
let detail = detail.replace(['\r', '\n'], " ");
let detail = detail.trim();
if detail.is_empty() {
format!("{STAGE_MARKER} {token} +{monotonic_ms}ms")
} else {
format!("{STAGE_MARKER} {token} +{monotonic_ms}ms {detail}")
}
}
pub fn tick_line(token: &str, uptime_s: u64) -> String {
format!("{TICK_MARKER} stage={token} up={uptime_s}s")
}
pub struct Narrator {
t0: Instant,
stage: Mutex<&'static str>,
sink: Sink,
}
impl Narrator {
pub fn new(sink: impl Fn(&str) + Send + Sync + 'static) -> Narrator {
Narrator { t0: Instant::now(), stage: Mutex::new("exec"), sink: Box::new(sink) }
}
pub fn monotonic_ms(&self) -> u128 {
self.t0.elapsed().as_millis()
}
pub fn stage(&self) -> &'static str {
*self.stage.lock().unwrap_or_else(|p| p.into_inner())
}
pub fn at(&self, stage: &'static str, detail: &str) -> String {
*self.stage.lock().unwrap_or_else(|p| p.into_inner()) = stage;
let line = stage_line(stage, self.monotonic_ms(), detail);
(self.sink)(&line);
line
}
pub fn tick(&self) -> String {
let line = tick_line(self.stage(), self.t0.elapsed().as_secs());
(self.sink)(&line);
line
}
pub fn say(&self, line: &str) {
(self.sink)(line);
}
}
pub const TICK_DUTY: &str = "narrate-tick";
pub fn start_ticks(n: Arc<Narrator>) -> bool {
start_ticks_every(n, TICK_EVERY)
}
pub fn start_ticks_every(n: Arc<Narrator>, every: Duration) -> bool {
let mut first = true;
let say: crate::duty::Say = Arc::new(|l: &str| eprintln!("{l}"));
crate::duty::start(TICK_DUTY, every, say, move || {
if !std::mem::take(&mut first) {
n.tick();
}
crate::duty::Round::Again
})
.is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_ticks_are_a_listed_duty() {
let _one = crate::duty::tests::ONE.lock().unwrap_or_else(|p| p.into_inner());
let (_, n) = collecting();
assert!(start_ticks_every(Arc::new(n), Duration::from_millis(5)));
let d = crate::duty::find(TICK_DUTY).expect("the ticks are a running duty");
assert!(d.stop(Duration::from_secs(5)));
}
fn collecting() -> (Arc<Mutex<Vec<String>>>, Narrator) {
let lines = Arc::new(Mutex::new(Vec::new()));
let l = Arc::clone(&lines);
(lines, Narrator::new(move |s: &str| l.lock().unwrap().push(s.to_string())))
}
#[test]
fn stages_and_ticks_reach_the_sink_in_the_shared_spelling() {
let (lines, n) = collecting();
let l = n.at("pairing", "the installer's listener serves\nsecond");
assert!(l.starts_with("STAGE pairing +") && l.contains("ms the installer's listener serves second"), "{l}");
assert_eq!(n.stage(), "pairing");
assert_eq!(n.tick(), "TICK stage=pairing up=0s");
assert_eq!(stage_line("exec", 7, " "), "STAGE exec +7ms");
assert_eq!(lines.lock().unwrap().len(), 2);
let (lines, n) = collecting();
n.at("serving", "");
assert!(start_ticks_every(Arc::new(n), Duration::from_millis(20)));
std::thread::sleep(Duration::from_millis(200));
assert!(lines.lock().unwrap().iter().any(|l| l == "TICK stage=serving up=0s"), "{:?}", lines.lock().unwrap());
}
}