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\"]");
}
}