This commit is contained in:
lda
2026-04-11 16:12:20 +07:00 Unverified
parent 524ade16ec
commit ba2c58313d
3 changed files with 177 additions and 50 deletions
+60
View File
@@ -0,0 +1,60 @@
use std::path::PathBuf;
use clap::{ArgAction, Args, Parser, Subcommand};
use crate::config;
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-agent.pid";
#[derive(Parser)]
#[command(name = "wakey-agent")]
#[command(version, about = "Outbound control-plane agent for Wakey")]
pub struct Cli {
/// Increase log verbosity. Use `-v` for debug and `-vv` for trace.
#[arg(short = 'v', long = "verbose", action = ArgAction::Count, global = true)]
pub verbose: u8,
#[command(subcommand)]
pub command: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Start the agent daemon and maintain the outbound control-plane session.
Serve(ServeArgs),
/// Enroll this router with a control plane and write agent config.
Enroll(EnrollArgs),
/// Reload a running agent daemon by sending SIGHUP.
Reload(ReloadArgs),
}
#[derive(Args)]
pub struct ServeArgs {
/// Path to the agent config file.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
pub config: PathBuf,
/// Path to pid file for reload signaling.
#[arg(long, default_value = DEFAULT_PID_FILE)]
pub pid_file: PathBuf,
}
#[derive(Args)]
pub struct EnrollArgs {
/// Base HTTPS URL of the control plane.
#[arg(long)]
pub server_url: String,
/// One-time or short-lived enroll token provided by the control plane.
#[arg(long)]
pub enroll_token: String,
/// Path to the agent config file to write.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
pub config: PathBuf,
}
#[derive(Args)]
pub struct ReloadArgs {
/// Path to pid file for reload signaling.
#[arg(long, default_value = DEFAULT_PID_FILE)]
pub pid_file: PathBuf,
}
+8 -50
View File
@@ -1,71 +1,29 @@
mod cli;
mod config; mod config;
mod dispatch; mod dispatch;
mod enroll; mod enroll;
mod protocol; mod protocol;
mod session; mod session;
mod serve;
mod tracing; mod tracing;
use std::path::PathBuf; use anyhow::Result;
use clap::Parser;
use clap::{ArgAction, Args, Parser, Subcommand}; use cli::{Cli, Command};
use ::tracing::info;
#[derive(Parser)]
#[command(name = "wakey-agent")]
#[command(version, about = "Outbound control-plane agent for Wakey")]
struct Cli {
/// Increase log verbosity. Use `-v` for debug and `-vv` for trace.
#[arg(short = 'v', long = "verbose", action = ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Start the agent daemon and maintain the outbound control-plane session.
Serve(ServeArgs),
/// Enroll this router with a control plane and write agent config.
Enroll(EnrollArgs),
}
#[derive(Args)]
struct ServeArgs {
/// Path to the agent config file.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
config: PathBuf,
}
#[derive(Args)]
struct EnrollArgs {
/// Base HTTPS URL of the control plane.
#[arg(long)]
server_url: String,
/// One-time or short-lived enroll token provided by the control plane.
#[arg(long)]
enroll_token: String,
/// Path to the agent config file to write.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
config: PathBuf,
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
tracing::init(cli.verbose); tracing::init(cli.verbose);
match cli.command { match cli.command {
Command::Serve(args) => { Command::Serve(args) => serve::serve(args).await?,
let config = config::load_config(&args.config)?;
info!(config_path = %args.config.display(), agent_id = %config.agent_id, "starting wakey-agent");
session::run(config).await?;
}
Command::Enroll(args) => { Command::Enroll(args) => {
let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?; let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
println!("agent_id={}", config.agent_id); println!("agent_id={}", config.agent_id);
println!("config={}", args.config.display()); println!("config={}", args.config.display());
} }
Command::Reload(args) => serve::reload_daemon(&args.pid_file)?,
} }
Ok(()) Ok(())
+109
View File
@@ -0,0 +1,109 @@
use std::path::Path;
use anyhow::{Context, Result};
use tracing::{info, warn};
use crate::cli::ServeArgs;
use crate::{config, session};
pub async fn serve(args: ServeArgs) -> Result<()> {
write_pid_file(&args.pid_file)?;
let mut cfg = config::load_config(&args.config)?;
info!(config_path = %args.config.display(), agent_id = %cfg.agent_id, "starting wakey-agent");
let mut worker = tokio::spawn(session::run(cfg.clone()));
#[cfg(unix)]
{
use tokio::signal::unix::{SignalKind, signal};
let mut hup = signal(SignalKind::hangup()).context("failed to install SIGHUP handler")?;
loop {
tokio::select! {
_ = tokio::signal::ctrl_c() => {
info!("ctrl-c received; shutting down wakey-agent");
worker.abort();
break;
}
_ = hup.recv() => {
match config::load_config(&args.config) {
Ok(new_cfg) => {
cfg = new_cfg;
info!(config_path = %args.config.display(), agent_id = %cfg.agent_id, "reload requested; restarting session with updated config");
worker.abort();
worker = tokio::spawn(session::run(cfg.clone()));
}
Err(err) => {
warn!(error = %err, config_path = %args.config.display(), "reload requested but config reload failed; keeping current session");
}
}
}
join = &mut worker => {
let _ = remove_pid_file(&args.pid_file);
return join.context("agent session join failed")?;
}
}
}
}
#[cfg(not(unix))]
{
tokio::signal::ctrl_c()
.await
.context("failed waiting for ctrl-c")?;
worker.abort();
}
let _ = remove_pid_file(&args.pid_file);
Ok(())
}
pub fn reload_daemon(pid_file: &Path) -> Result<()> {
let pid = read_pid(pid_file)?;
send_hup(pid)
}
fn write_pid_file(path: &Path) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create pid dir {}", parent.display()))?;
}
std::fs::write(path, format!("{}\n", std::process::id()))
.with_context(|| format!("failed to write pid file {}", path.display()))
}
fn remove_pid_file(path: &Path) -> Result<()> {
match std::fs::remove_file(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => {
Err(err).with_context(|| format!("failed to remove pid file {}", path.display()))
}
}
}
fn read_pid(path: &Path) -> Result<i32> {
let raw = std::fs::read_to_string(path)
.with_context(|| format!("failed to read pid file {}", path.display()))?;
let pid = raw
.trim()
.parse::<i32>()
.with_context(|| format!("invalid pid in {}", path.display()))?;
if pid <= 0 {
anyhow::bail!("invalid non-positive pid {pid}");
}
Ok(pid)
}
fn send_hup(pid: i32) -> Result<()> {
let status = std::process::Command::new("kill")
.arg("-HUP")
.arg(pid.to_string())
.status()
.context("failed to invoke kill -HUP")?;
if !status.success() {
anyhow::bail!("kill -HUP failed for pid {pid}");
}
Ok(())
}