actr_cli/commands/
stop.rs1use 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 #[arg(value_name = "WID")]
16 pub wid: String,
17
18 #[arg(short = 'c', long = "config", value_name = "FILE")]
20 pub config: Option<PathBuf>,
21
22 #[arg(long = "hyper-dir", value_name = "DIR")]
24 pub hyper_dir: Option<PathBuf>,
25
26 #[arg(long = "timeout", default_value_t = 5)]
28 pub timeout: u64,
29
30 #[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 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}