hive-agent: bound each claude plugin call with a deadline
The claude plugin marketplace add/update and install calls fetch over the network and are awaited before the web UI is spawned. One hung fetch stopped the agent from booting at all, against the module's own promise that plugin failures never block boot. Each call now runs under a 2 minute deadline; on expiry the child is killed, a warning is logged and boot continues, as for any other plugin failure.
This commit is contained in:
parent
c6ca9ab845
commit
8be90ae8f2
1 changed files with 57 additions and 18 deletions
|
|
@ -11,12 +11,41 @@
|
|||
//! plugin specs resolve against current index data. Marketplace update
|
||||
//! failures are non-fatal — stale index is better than no install attempt.
|
||||
|
||||
use std::io;
|
||||
use std::process::Output;
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::process::Command;
|
||||
|
||||
const PLUGINS_PATH: &str = "/etc/hyperhive/claude-plugins.json";
|
||||
const MARKETPLACES_PATH: &str = "/etc/hyperhive/claude-marketplaces.json";
|
||||
const AUTO_UPDATE_PATH: &str = "/etc/hyperhive/claude-plugins-auto-update.json";
|
||||
|
||||
/// Per-call deadline for `claude plugin …`; a git clone over a slow link fits, a hung fetch does not.
|
||||
const PLUGIN_CALL_TIMEOUT: Duration = Duration::from_mins(2);
|
||||
|
||||
/// Run `claude plugin <args>` under [`PLUGIN_CALL_TIMEOUT`]. On timeout the
|
||||
/// child is killed (`kill_on_drop`) and a `TimedOut` error is returned.
|
||||
async fn run_claude_plugin(args: &[&str]) -> io::Result<Output> {
|
||||
let mut cmd = Command::new("claude");
|
||||
cmd.arg("plugin").args(args);
|
||||
run_bounded(cmd, PLUGIN_CALL_TIMEOUT).await
|
||||
}
|
||||
|
||||
/// Run `cmd` to completion within `deadline`; the child is killed when the
|
||||
/// deadline passes.
|
||||
async fn run_bounded(mut cmd: Command, deadline: Duration) -> io::Result<Output> {
|
||||
let call = cmd.kill_on_drop(true).output();
|
||||
tokio::time::timeout(deadline, call)
|
||||
.await
|
||||
.unwrap_or_else(|_| {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
format!("no exit within {}s", deadline.as_secs_f32()),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// Add every marketplace from `/etc/hyperhive/claude-marketplaces.json`
|
||||
/// via `claude plugin marketplace add <source>`. Idempotent: re-add of
|
||||
/// an existing marketplace is treated as success (claude prints an
|
||||
|
|
@ -34,11 +63,7 @@ async fn add_marketplaces() {
|
|||
}
|
||||
};
|
||||
for source in sources {
|
||||
match Command::new("claude")
|
||||
.args(["plugin", "marketplace", "add", &source])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
match run_claude_plugin(&["marketplace", "add", &source]).await {
|
||||
Ok(out) if out.status.success() => {
|
||||
tracing::info!(source = %source, "claude plugin marketplace add ok");
|
||||
}
|
||||
|
|
@ -56,7 +81,7 @@ async fn add_marketplaces() {
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(source = %source, error = ?e, "claude plugin marketplace add spawn failed");
|
||||
tracing::warn!(source = %source, error = ?e, "claude plugin marketplace add spawn failed or timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -74,11 +99,7 @@ async fn auto_update_enabled() -> bool {
|
|||
/// Update all configured plugin marketplaces. Non-fatal — logs a warning
|
||||
/// on failure but does not abort the install sequence.
|
||||
async fn update_marketplaces() {
|
||||
match Command::new("claude")
|
||||
.args(["plugin", "marketplace", "update"])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
match run_claude_plugin(&["marketplace", "update"]).await {
|
||||
Ok(out) if out.status.success() => {
|
||||
tracing::info!("claude plugin marketplace update ok");
|
||||
}
|
||||
|
|
@ -90,7 +111,7 @@ async fn update_marketplaces() {
|
|||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "claude plugin marketplace update spawn failed (non-fatal)");
|
||||
tracing::warn!(error = ?e, "claude plugin marketplace update spawn failed or timed out (non-fatal)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -118,11 +139,7 @@ pub async fn install_configured() {
|
|||
tracing::debug!("claudePluginsAutoUpdate=false, skipping marketplace update");
|
||||
}
|
||||
for spec in specs {
|
||||
match Command::new("claude")
|
||||
.args(["plugin", "install", &spec])
|
||||
.output()
|
||||
.await
|
||||
{
|
||||
match run_claude_plugin(&["install", &spec]).await {
|
||||
Ok(out) if out.status.success() => {
|
||||
tracing::info!(spec = %spec, "claude plugin install ok");
|
||||
}
|
||||
|
|
@ -136,8 +153,30 @@ pub async fn install_configured() {
|
|||
);
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(spec = %spec, error = ?e, "claude plugin install spawn failed");
|
||||
tracing::warn!(spec = %spec, error = ?e, "claude plugin install spawn failed or timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_bounded_times_out_a_hung_child() {
|
||||
let mut cmd = Command::new("sleep");
|
||||
cmd.arg("30");
|
||||
let err = run_bounded(cmd, Duration::from_millis(100))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.kind(), io::ErrorKind::TimedOut);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_bounded_returns_output_of_a_prompt_child() {
|
||||
let cmd = Command::new("true");
|
||||
let out = run_bounded(cmd, Duration::from_secs(5)).await;
|
||||
assert!(out.unwrap().status.success());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue