use std::path::PathBuf;
use std::time::Duration;
use anyhow::Result;
use clap::Parser;
use tokio::time::timeout;
use super::control;
use crate::daemon::client::DaemonClient;
use crate::daemon::protocol::StatusReport;
use crate::daemon::{server, DaemonServiceKind, ServiceSelection};
const STATUS_PROBE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Parser)]
pub struct RestartCommand {
#[arg(long, value_name = "PATH")]
pub socket: Option<PathBuf>,
#[arg(long, value_name = "SVC", value_delimiter = ',')]
pub services: Vec<DaemonServiceKind>,
}
impl RestartCommand {
pub async fn execute(self) -> Result<()> {
let socket_path = server::resolve_socket(self.socket)?;
let client = DaemonClient::new(&socket_path);
let probe = timeout(STATUS_PROBE_TIMEOUT, client.status()).await.ok();
if probe.is_none() {
tracing::warn!(
"daemon status probe timed out after {STATUS_PROBE_TIMEOUT:?}; \
restarting without preserving its current service selection"
);
}
let (is_running, running) = classify_probe(probe);
if is_running {
client.shutdown().await.ok();
#[cfg(not(target_os = "linux"))]
control::wait_until_down(&socket_path).await?;
}
let services = resolve_services(&self.services, running.as_ref());
control::launch(&socket_path, &services)?;
control::wait_until_ready(&socket_path).await?;
println!("daemon restarted (socket {})", socket_path.display());
Ok(())
}
}
fn classify_probe(probe: Option<Result<StatusReport>>) -> (bool, Option<StatusReport>) {
match probe {
Some(Ok(report)) => (true, Some(report)),
Some(Err(_)) => (false, None),
None => (true, None),
}
}
fn resolve_services(
flag: &[DaemonServiceKind],
running: Option<&StatusReport>,
) -> ServiceSelection {
if !flag.is_empty() {
return ServiceSelection::resolve(flag, None);
}
match running {
Some(report) => {
ServiceSelection::from_service_names(report.services.iter().map(|s| s.name.as_str()))
}
None => ServiceSelection::All,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::daemon::service::ServiceStatus;
fn report(names: &[&str]) -> StatusReport {
StatusReport {
services: names
.iter()
.map(|n| ServiceStatus {
name: (*n).to_string(),
healthy: true,
summary: String::new(),
detail: serde_json::Value::Null,
})
.collect(),
version: None,
..StatusReport::default()
}
}
#[test]
fn classify_probe_reads_a_live_daemons_selection() {
let (is_running, running) = classify_probe(Some(Ok(report(&["worktrees"]))));
assert!(is_running);
assert_eq!(
resolve_services(&[], running.as_ref()),
ServiceSelection::Only(vec![DaemonServiceKind::Worktrees])
);
}
#[test]
fn classify_probe_treats_a_socket_error_as_down() {
let (is_running, running) = classify_probe(Some(Err(anyhow::anyhow!("refused"))));
assert!(!is_running);
assert!(running.is_none());
assert_eq!(
resolve_services(&[], running.as_ref()),
ServiceSelection::All
);
}
#[test]
fn classify_probe_treats_a_timeout_as_running_but_opaque() {
let (is_running, running) = classify_probe(None);
assert!(is_running);
assert!(running.is_none());
assert_eq!(
resolve_services(&[], running.as_ref()),
ServiceSelection::All
);
}
#[test]
fn an_explicit_flag_wins_over_the_running_selection() {
let selection = resolve_services(
&[DaemonServiceKind::Worktrees],
Some(&report(&["browser-bridge", "snowflake"])),
);
assert_eq!(
selection,
ServiceSelection::Only(vec![DaemonServiceKind::Worktrees])
);
}
#[test]
fn an_omitted_flag_preserves_the_running_selection() {
let selection = resolve_services(&[], Some(&report(&["worktrees", "sessions"])));
assert_eq!(
selection,
ServiceSelection::Only(vec![
DaemonServiceKind::Worktrees,
DaemonServiceKind::Sessions
])
);
}
#[test]
fn a_down_daemon_with_no_flag_falls_back_to_all() {
assert_eq!(resolve_services(&[], None), ServiceSelection::All);
}
#[test]
fn a_full_daemon_restarts_as_all_not_a_frozen_explicit_list() {
let selection = resolve_services(
&[],
Some(&report(&[
"browser-bridge",
"snowflake",
"worktrees",
"sessions",
"github",
])),
);
assert_eq!(selection, ServiceSelection::All);
}
}