mod commands;
mod groups;
mod heartbeat;
mod inventory;
mod process;
mod self_update;
use std::path::PathBuf;
use anyhow::{Context, Result};
use clap::Parser;
use kanade_shared::config::load_agent_config;
use kanade_shared::{default_paths, subject};
use tracing::info;
const AGENT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[derive(Parser, Debug)]
#[command(
name = "kanade-agent",
about = "Windows endpoint management agent (kanade)",
version
)]
struct Cli {
#[arg(long)]
config: Option<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info,kanade_agent=debug".into()),
)
.init();
cleanup_stale_upgrade_artifacts();
let cli = Cli::parse();
let cfg_path =
default_paths::find_config(cli.config.as_deref(), "KANADE_AGENT_CONFIG", "agent.toml")?;
let cfg =
load_agent_config(&cfg_path).with_context(|| format!("load config from {cfg_path:?}"))?;
info!(
pc_id = %cfg.agent.id,
nats_url = %cfg.agent.nats_url,
version = AGENT_VERSION,
"starting kanade-agent",
);
let client = async_nats::connect(&cfg.agent.nats_url)
.await
.with_context(|| format!("connect to NATS at {}", cfg.agent.nats_url))?;
info!("connected to NATS");
let cmd_all = client.subscribe(subject::COMMANDS_ALL).await?;
let cmd_self = client
.subscribe(subject::commands_pc(&cfg.agent.id))
.await?;
info!(
commands_all = subject::COMMANDS_ALL,
commands_self = %subject::commands_pc(&cfg.agent.id),
"subscribed",
);
let pc_id = cfg.agent.id.clone();
tokio::spawn(heartbeat::heartbeat_loop(
client.clone(),
pc_id.clone(),
AGENT_VERSION.to_string(),
));
tokio::spawn(inventory::inventory_loop(
client.clone(),
pc_id.clone(),
cfg.inventory.clone(),
));
tokio::spawn(self_update::run(client.clone(), AGENT_VERSION.to_string()));
if !cfg.agent.groups.is_empty() {
tracing::warn!(
local_groups = ?cfg.agent.groups,
"agent.toml::[agent] groups is deprecated; use `kanade agent groups set` instead — local value is ignored",
);
}
tokio::spawn(groups::manage(client.clone(), pc_id.clone()));
let _ = tokio::join!(
commands::command_loop(client.clone(), pc_id.clone(), cmd_all),
commands::command_loop(client.clone(), pc_id.clone(), cmd_self),
);
Ok(())
}
fn cleanup_stale_upgrade_artifacts() {
let Ok(current) = std::env::current_exe() else {
return;
};
let Some(exe_dir) = current.parent() else {
return;
};
let Some(exe_name) = current.file_name().and_then(|n| n.to_str()) else {
return;
};
for suffix in ["old", "new"] {
let path = exe_dir.join(format!("{exe_name}.{suffix}"));
if !path.exists() {
continue;
}
match std::fs::remove_file(&path) {
Ok(_) => tracing::info!(?path, suffix, "removed stale upgrade artifact"),
Err(e) => {
tracing::warn!(?path, suffix, error = %e, "couldn't remove stale upgrade artifact")
}
}
}
}