Skip to main content

actr_cli/commands/
stop.rs

1use crate::commands::process::{kill_process, terminate_process, wait_for_exit};
2use crate::commands::runtime_state::{RuntimeStateStore, RuntimeStatus, resolve_hyper_dir};
3use crate::core::{Command, CommandContext, CommandResult, ComponentType};
4use crate::error::ActrCliError;
5use anyhow::Result;
6use async_trait::async_trait;
7use chrono::Utc;
8use clap::Args;
9use std::path::PathBuf;
10use std::time::Duration;
11
12#[derive(Args, Debug)]
13pub struct StopCommand {
14    /// WID (or unique prefix, min 8 chars) of the runtime to stop
15    #[arg(value_name = "WID")]
16    pub wid: String,
17
18    /// Runtime configuration file
19    #[arg(short = 'c', long = "config", value_name = "FILE")]
20    pub config: Option<PathBuf>,
21
22    /// Hyper data directory
23    #[arg(long = "hyper-dir", value_name = "DIR")]
24    pub hyper_dir: Option<PathBuf>,
25
26    /// Graceful shutdown timeout in seconds
27    #[arg(long = "timeout", default_value_t = 5)]
28    pub timeout: u64,
29
30    /// Send SIGKILL after graceful shutdown timeout
31    #[arg(long = "force")]
32    pub force: bool,
33}
34
35#[async_trait]
36impl Command for StopCommand {
37    async fn execute(&self, _ctx: &CommandContext) -> Result<CommandResult> {
38        let hyper_dir = resolve_hyper_dir(self.config.as_deref(), self.hyper_dir.as_deref())?;
39        let store = RuntimeStateStore::new(hyper_dir);
40        let entry = store.resolve_wid_prefix(&self.wid).await?;
41        let wid = entry.record.wid.clone();
42        let wid_short = entry.wid_short();
43        let pid = entry.record.pid;
44
45        // Mark the runtime as stopped in the state store, then print `message`.
46        let finish = |msg: String| {
47            let store = &store;
48            let wid = wid.clone();
49            async move {
50                store.mark_stopped_by_wid(&wid, Utc::now()).await?;
51                println!("{msg}");
52                crate::error::Result::Ok(())
53            }
54        };
55
56        if entry.status != RuntimeStatus::Running {
57            finish(format!("Runtime already stopped: {wid_short}")).await?;
58            return Ok(CommandResult::Success(String::new()));
59        }
60
61        if !terminate_process(pid)? {
62            finish(format!("Runtime already stopped: {wid_short}")).await?;
63            return Ok(CommandResult::Success(String::new()));
64        }
65        if wait_for_exit(pid, Duration::from_secs(self.timeout)).await {
66            finish(format!("Stopped runtime: {wid_short}")).await?;
67            return Ok(CommandResult::Success(String::new()));
68        }
69
70        if !self.force {
71            return Err(ActrCliError::command_error(format!(
72                "Timed out after {}s while stopping {}. Retry with --force.",
73                self.timeout, wid_short
74            ))
75            .into());
76        }
77
78        if !kill_process(pid)? {
79            finish(format!("Runtime already stopped: {wid_short}")).await?;
80            return Ok(CommandResult::Success(String::new()));
81        }
82        if wait_for_exit(pid, Duration::from_secs(1)).await {
83            finish(format!("Force stopped runtime: {wid_short}")).await?;
84            return Ok(CommandResult::Success(String::new()));
85        }
86
87        Err(ActrCliError::command_error(format!("Process {pid} did not exit after SIGKILL")).into())
88    }
89
90    fn required_components(&self) -> Vec<ComponentType> {
91        vec![]
92    }
93
94    fn name(&self) -> &str {
95        "stop"
96    }
97
98    fn description(&self) -> &str {
99        "Stop a detached runtime instance"
100    }
101}