watch : supervision d'un agent avec redemarrage automatique et backoff (issue #35) - check_once testable, notification desktop, Ctrl-C n'affecte pas l'agent detache
This commit is contained in:
@@ -25,6 +25,7 @@ pub mod status_cmd;
|
||||
pub mod timeline_cmd;
|
||||
pub mod uninstall_cmd;
|
||||
pub mod update_cmd;
|
||||
pub mod watch_cmd;
|
||||
pub mod version_cmd;
|
||||
|
||||
use crate::app::App;
|
||||
@@ -57,6 +58,12 @@ pub fn execute_command(app: &App, cmd: &Command) -> Result<i32> {
|
||||
follow,
|
||||
} => logs_cmd::run(app, agent, *lines, *follow),
|
||||
Command::Open { agent } => open_cmd::run(app, agent),
|
||||
Command::Watch {
|
||||
agent,
|
||||
restart,
|
||||
notify,
|
||||
interval,
|
||||
} => watch_cmd::run(app, agent, *restart, *notify, *interval),
|
||||
Command::Alias(sub) => alias_cmd::run(app, sub),
|
||||
Command::Start(a) => run_cmd::start(app, a),
|
||||
Command::Stop {
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
//! watch: supervise one agent and optionally restart it on crash.
|
||||
|
||||
use super::*;
|
||||
use crate::cli::StartArgs;
|
||||
use crate::process;
|
||||
use std::time::Duration;
|
||||
|
||||
/// One supervision check. Returns whether the agent is running afterwards.
|
||||
/// Restarts are background starts (event + session recorded as usual).
|
||||
pub fn check_once(app: &App, agent_name: &str, restart: bool, notify: bool) -> bool {
|
||||
let entry = app.state.get(agent_name).ok().flatten();
|
||||
let running = entry
|
||||
.as_ref()
|
||||
.and_then(|e| e.pid)
|
||||
.map(process::is_running)
|
||||
.unwrap_or(false);
|
||||
if running {
|
||||
return true;
|
||||
}
|
||||
if !restart {
|
||||
app.log.warn(&format!("'{agent_name}' is not running"));
|
||||
return false;
|
||||
}
|
||||
let agent = match app.catalog.resolve(agent_name) {
|
||||
Some(a) => a,
|
||||
None => {
|
||||
app.log.error(&format!("unknown agent '{agent_name}'"));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
app.log.warn(&format!("'{agent_name}' is not running — restarting"));
|
||||
let opts = StartArgs {
|
||||
agent: Some(agent_name.to_string()),
|
||||
background: true,
|
||||
..Default::default()
|
||||
};
|
||||
match run_cmd::start(app, &opts) {
|
||||
Ok(_) => {
|
||||
if notify {
|
||||
process::desktop_notify("agent-manager", &format!("{} restarted", agent.title()));
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(e) => {
|
||||
app.log.error(&format!("restart failed: {e:#}"));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(app: &App, agent: &str, restart: bool, notify: bool, interval: u64) -> Result<i32> {
|
||||
require_agent(app, agent)?;
|
||||
let interval = interval.max(1);
|
||||
app.log.info(&format!("watching '{agent}' every {interval}s (Ctrl-C to stop)"));
|
||||
let mut backoff = 1u64;
|
||||
loop {
|
||||
let ok = check_once(app, agent, restart, notify);
|
||||
if ok {
|
||||
backoff = 1;
|
||||
std::thread::sleep(Duration::from_secs(interval));
|
||||
} else if restart {
|
||||
app.log.warn(&format!("retrying in {backoff}s"));
|
||||
std::thread::sleep(Duration::from_secs(backoff));
|
||||
backoff = (backoff * 2).min(60);
|
||||
} else {
|
||||
std::thread::sleep(Duration::from_secs(interval));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use clap::Parser;
|
||||
|
||||
#[test]
|
||||
fn check_once_reports_dead_agent_without_restart() {
|
||||
let guard = tempfile::tempdir().unwrap();
|
||||
let dir = guard.path().to_path_buf();
|
||||
std::mem::forget(guard);
|
||||
let cfg = dir.join("config.yaml");
|
||||
std::fs::write(
|
||||
&cfg,
|
||||
concat!(
|
||||
"version: \"1.0\"\n",
|
||||
"settings:\n",
|
||||
" auto_install_deps: false\n",
|
||||
"agents: []\n",
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
let cli = crate::cli::Cli::parse_from(["am", "--config", cfg.to_str().unwrap()]);
|
||||
let mut app = App::from_cli(cli).unwrap();
|
||||
let mut p = app.paths.clone();
|
||||
p.install_dir = dir.join("agents");
|
||||
p.bin_dir = dir.join("agents").join("bin");
|
||||
p.log_dir = dir.join("logs");
|
||||
p.state_file = dir.join("state.json");
|
||||
p.probe_cache_file = dir.join("probe-cache.json");
|
||||
p.config_dir = Some(dir.join("config"));
|
||||
app.paths = p;
|
||||
// Agent absent de l état : pas running, pas de restart -> false.
|
||||
assert!(!check_once(&app, "claude-code", false, false));
|
||||
// Agent inconnu avec restart : erreur loguée, false.
|
||||
assert!(!check_once(&app, "inconnu", true, false));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user