use std::{net::SocketAddr, path::PathBuf, sync::Arc};
use clap::{Parser, Subcommand};
use miette::Result;
use node_semver::Version;
use tracing::{debug, warn};
use bestool_tamanu::{
config::load_config,
server_info::{fetch_device_key_with, query_device_key_row},
};
use bestool_alertd::doctor::{DoctorTask, SweepTamanu};
use super::{TamanuArgs, try_find_tamanu};
use crate::actions::Context;
#[derive(Debug, Clone, Parser)]
#[clap(verbatim_doc_comment)]
pub struct AlertdArgs {
#[command(subcommand)]
command: Command,
}
#[derive(Debug, Clone, Parser)]
struct DaemonArgs {
#[arg(long, value_name = "GLOB")]
glob: Vec<String>,
#[arg(long)]
no_server: bool,
#[arg(long)]
server_addr: Vec<SocketAddr>,
#[arg(long, default_value = "600")]
watchdog_timeout: u64,
#[arg(long)]
no_watchdog: bool,
}
#[derive(Debug, Clone, Subcommand)]
enum Command {
Run {
#[command(flatten)]
daemon: DaemonArgs,
},
Status {
#[arg(long)]
server_addr: Vec<SocketAddr>,
},
#[cfg(windows)]
Install,
#[cfg(windows)]
Uninstall,
#[cfg(windows)]
ConfigureRecovery,
#[cfg(windows)]
#[command(hide = true)]
Service {
#[command(flatten)]
daemon: DaemonArgs,
},
}
pub async fn run(args: AlertdArgs, ctx: Context) -> Result<()> {
match args.command {
Command::Status { server_addr } => {
let addrs = if server_addr.is_empty() {
bestool_alertd::commands::default_server_addrs()
} else {
server_addr
};
bestool_alertd::commands::get_status(&addrs).await
}
Command::Run { daemon } => {
let install = try_find_tamanu(ctx.require::<TamanuArgs>()).await?;
let daemon_config = build_config(install, daemon).await?;
bestool_alertd::run(daemon_config).await
}
#[cfg(windows)]
Command::Install => {
use std::ffi::OsString;
bestool_alertd::windows_service::install_service_with_args(&[
OsString::from("tamanu"),
OsString::from("alertd"),
OsString::from("service"),
])
}
#[cfg(windows)]
Command::Uninstall => bestool_alertd::windows_service::uninstall_service(),
#[cfg(windows)]
Command::ConfigureRecovery => bestool_alertd::windows_service::configure_recovery(),
#[cfg(windows)]
Command::Service { daemon } => {
let install = try_find_tamanu(ctx.require::<TamanuArgs>()).await?;
match bestool_alertd::windows_service::is_recovery_configured() {
Ok(false) => {
tracing::info!("failure recovery not configured, applying automatically");
if let Err(e) = bestool_alertd::windows_service::configure_recovery() {
tracing::warn!("failed to auto-configure recovery: {e}");
}
}
Err(e) => {
tracing::warn!("failed to check recovery configuration: {e}");
}
Ok(true) => {}
}
let daemon_config = build_config(install, daemon).await?;
bestool_alertd::windows_service::run_service(daemon_config)
}
}
}
async fn build_config(
install: Option<(Version, PathBuf)>,
DaemonArgs {
glob,
no_server,
server_addr,
watchdog_timeout,
no_watchdog,
}: DaemonArgs,
) -> Result<bestool_alertd::DaemonConfig> {
if !glob.is_empty() {
warn!("--glob is deprecated and does nothing; alert definitions are no longer loaded");
}
let tamanu = match install {
Some((version, root)) => {
let config = load_config(&root, None)?;
debug!(?config, "parsed Tamanu config");
let database_url = config.database_url();
Some(SweepTamanu {
version,
root,
config: Arc::new(config),
database_url,
})
}
None => {
warn!("no Tamanu on this host; doctor sweeps will skip Tamanu checks");
None
}
};
let pg_pool = match &tamanu {
Some(t) => {
Some(bestool_postgres::pool::create_pool(&t.database_url, "bestool-alertd").await?)
}
None => None,
};
let watchdog = if no_watchdog {
None
} else {
Some(std::time::Duration::from_secs(watchdog_timeout))
};
let device_key_pem = fetch_device_key_with(|| async {
match &pg_pool {
Some(pool) => match pool.get().await {
Ok(conn) => query_device_key_row(&conn).await,
Err(err) => {
warn!(%err, "could not get DB conn for deviceKey fetch");
None
}
},
None => None,
}
})
.await;
let tamanu_version = tamanu
.as_ref()
.map(|t| t.version.to_string())
.unwrap_or_else(|| "0.0.0".into());
let mut daemon_config = bestool_alertd::DaemonConfig::new(
pg_pool.clone(),
tamanu.as_ref().map(|t| t.database_url.clone()),
tamanu_version,
)
.with_no_server(no_server)
.with_server_addrs(server_addr)
.with_watchdog_timeout(watchdog)
.with_task(Arc::new(DoctorTask::new(
env!("CARGO_PKG_VERSION").to_string(),
tamanu,
)));
if let Some(pem) = device_key_pem {
daemon_config = daemon_config.with_device_key_pem(pem);
}
Ok(daemon_config)
}