use std::{collections::HashMap, time::Duration}; use chrono::{Datelike, Timelike}; use tokio::task::JoinHandle; use tracing::{Instrument, Level}; use tracing_subscriber::FmtSubscriber; use tzfile::{ArcTz, Tz}; use serde::Deserialize; use toml; mod guac; mod shotter; fn duration_until_next_minute() -> Duration { let now = chrono::Utc::now(); let interval = (60 - now.second()) * 1000; // not sure if this ever triggers if interval == 0 { return Duration::from_secs(59); } return Duration::from_millis(interval as u64); } #[derive(Deserialize, Debug, Clone)] struct Node { url: String, origin: String, } #[derive(Deserialize)] struct Config { root_path: std::path::PathBuf, webp_quality: f32, vms: HashMap, } #[tokio::main] async fn main() -> anyhow::Result<()> { // Init tracing #[cfg(debug_assertions)] let subscriber = FmtSubscriber::builder() .with_max_level(Level::TRACE) .compact() .finish(); #[cfg(not(debug_assertions))] let subscriber = FmtSubscriber::builder() .with_max_level(Level::INFO) .compact() .finish(); tracing::subscriber::set_global_default(subscriber)?; let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let config: Config = toml::from_str(std::fs::read_to_string("./config.toml")?.as_str())?; // Essentially this is meant to be a sentinel for "SCREENSHOT NOW!". // None will stop the task immediately, so basically this is a really bad way of implementing cancellation // (that isn't used currently) let (tx, _) = tokio::sync::broadcast::channel::>(10); let gmt_timezone = ArcTz::new(Tz::named("GMT")?); for (id, node) in config.vms.iter() { tracing::info!("Adding node {id} : {:?}", node); let mut _rx = tx.subscribe(); // These clones are scary, but they're only done once. // Additionally, in the case of the timezone, no actual clone is performed. let id_clone = id.clone(); let node_clone = node.clone(); let root = config.root_path.clone(); let tz = gmt_timezone.clone(); // Spawn the per-node task. let _: JoinHandle> = tokio::spawn(async move { while let Some(_) = _rx.recv().await? { let span = tracing::span!(Level::INFO, "node screenshot", node = id_clone.as_str()); let now = chrono::Utc::now().with_timezone(&tz); // start with /yyyy-mm-dd let date_path = root.join(format!( "{:04}-{:02}-{:02}", now.year(), now.month(), now.day() )); // then /yyyy-mm-dd/[node] let node_path = date_path.join(&id_clone); if !node_path.exists() { std::fs::create_dir_all(&node_path)?; } // add target webp path finally let file_path = node_path.join(format!( "{:02}-{:02}-{:02}.webp", now.hour(), now.minute(), now.second() )); // do it! match shotter::take_one_screenshot( &node_clone.url, &node_clone.origin, &id_clone, file_path.clone(), config.webp_quality, ) .instrument(span) .await { Ok(_) => {} Err(error) => { // FIXME: On WebSocket errors, should we just try again? tracing::error!("Error taking screenshot: {error}"); } }; } Ok(()) }); } loop { // Request a screenshot tx.send(Some(()))?; // Wait for the next :xx:00 minute let dur = duration_until_next_minute(); tracing::info!("Waiting {:?} to take next screenshot", dur); tokio::time::sleep(dur).await; } }