netsky 0.1.6

netsky CLI: the viable system launcher and subcommand dispatcher
Documentation
use std::cell::RefCell;
use std::ffi::OsString;
use std::time::Duration;

use chrono::Utc;
use netsky_db::{Db, SessionEvent};

thread_local! {
    static DB: RefCell<Option<Db>> = const { RefCell::new(None) };
}

pub fn record_cli_invocation(argv: &[OsString], exit_code: Option<i64>, duration: Duration) {
    let Some((bin, argv_json)) = cli_args(argv) else {
        return;
    };
    let duration_ms = duration.as_millis().try_into().ok();
    let result = Db::open().and_then(|db| {
        db.migrate()?;
        db.record_cli(
            Utc::now(),
            &bin,
            &argv_json,
            exit_code,
            duration_ms,
            &host(),
        )
    });
    if let Err(err) = result {
        eprintln!("[netsky cli] meta-db record failed: {err}");
    }
}

pub fn record_session(agent: &str, session_num: i64, event: SessionEvent) {
    let _ = with_db(|db| db.record_session(Utc::now(), agent, session_num, event));
}

pub fn record_tick(source: &str, detail: serde_json::Value) {
    let detail_json = detail.to_string();
    let _ = with_db(|db| db.record_tick(Utc::now(), source, &detail_json));
}

pub fn record_directive(
    source: &str,
    chat_id: Option<&str>,
    raw_text: &str,
    resolved_action: Option<&str>,
    agent: Option<&str>,
    status: Option<&str>,
    detail: serde_json::Value,
) {
    let detail_json = detail.to_string();
    let _ = with_db(|db| {
        db.record_owner_directive(netsky_db::OwnerDirectiveRecord {
            ts_utc: Utc::now(),
            source,
            chat_id,
            raw_text,
            resolved_action,
            agent,
            status,
            detail_json: Some(&detail_json),
        })
    });
}

pub fn record_watchdog(
    event: &str,
    agent: Option<&str>,
    severity: Option<&str>,
    status: Option<&str>,
    detail: serde_json::Value,
) {
    let detail_json = detail.to_string();
    let _ = with_db(|db| {
        db.record_watchdog_event(netsky_db::WatchdogEventRecord {
            ts_utc: Utc::now(),
            event,
            agent,
            severity,
            status,
            detail_json: Some(&detail_json),
        })
    });
}

fn with_db<F, T>(f: F) -> Option<T>
where
    F: FnOnce(&Db) -> netsky_db::Result<T>,
{
    DB.with(|cell| {
        if cell.borrow().is_none() {
            let db = Db::open().ok()?;
            db.migrate().ok()?;
            cell.replace(Some(db));
        }

        let borrow = cell.borrow();
        let db = borrow.as_ref()?;
        f(db).ok()
    })
}

fn cli_args(argv: &[OsString]) -> Option<(String, String)> {
    let bin = argv.first()?.to_string_lossy().into_owned();
    let args: Vec<String> = argv
        .iter()
        .skip(1)
        .map(|s| s.to_string_lossy().into_owned())
        .collect();
    let argv_json = serde_json::to_string(&args).ok()?;
    Some((bin, argv_json))
}

fn host() -> String {
    std::env::var("HOSTNAME")
        .or_else(|_| std::env::var("COMPUTERNAME"))
        .unwrap_or_else(|_| "unknown".to_string())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn cli_args_serializes_tail() {
        let argv = vec![
            OsString::from("netsky"),
            OsString::from("up"),
            OsString::from("2"),
        ];
        let (bin, json) = cli_args(&argv).expect("cli args");
        assert_eq!(bin, "netsky");
        assert_eq!(json, "[\"up\",\"2\"]");
    }
}