Skip to main content

start_command/
query_commands.rs

1//! Query and control command handlers.
2//!
3//! Every option in this module addresses an execution that already exists in
4//! the tracking store instead of launching a new one: `--status`, `--list`,
5//! `--upload-log`, `--stop`, `--terminate`, `--attach`, `--resume`,
6//! `--resume-all` and `--cleanup`. `dispatch_query_command` is the single entry
7//! point the CLI calls before it starts interpreting the command line as a
8//! command to run (issue #162).
9
10use crate::args_parser::WrapperOptions;
11use crate::execution_attach::{attach_execution, ExecutionAttachResult};
12use crate::execution_control::{control_execution, ControlAction, ExecutionControlResult};
13use crate::execution_resume::{resume_execution_with_options, ExecutionResumeResult};
14use crate::execution_resume_all::{resume_all_executions, ExecutionResumeAllResult};
15use crate::execution_store::{CleanupOptions, ExecutionStore};
16use crate::log_uploader::upload_execution_log;
17use crate::status_formatter::{list_executions_filtered, query_status, StatusQueryResult};
18
19/// Uniform shape every query handler reports back.
20pub struct QueryOutcome {
21    pub success: bool,
22    pub output: Option<String>,
23    pub error: Option<String>,
24    pub exit_code: Option<i32>,
25}
26
27impl From<StatusQueryResult> for QueryOutcome {
28    fn from(result: StatusQueryResult) -> Self {
29        QueryOutcome {
30            success: result.success,
31            output: result.output,
32            error: result.error,
33            exit_code: None,
34        }
35    }
36}
37
38impl From<ExecutionControlResult> for QueryOutcome {
39    fn from(result: ExecutionControlResult) -> Self {
40        QueryOutcome {
41            success: result.success,
42            output: result.output,
43            error: result.error,
44            exit_code: None,
45        }
46    }
47}
48
49impl From<ExecutionAttachResult> for QueryOutcome {
50    fn from(result: ExecutionAttachResult) -> Self {
51        QueryOutcome {
52            success: result.success,
53            output: result.output,
54            error: result.error,
55            exit_code: result.exit_code,
56        }
57    }
58}
59
60impl From<ExecutionResumeResult> for QueryOutcome {
61    fn from(result: ExecutionResumeResult) -> Self {
62        QueryOutcome {
63            success: result.success,
64            output: result.output,
65            error: result.error,
66            exit_code: None,
67        }
68    }
69}
70
71impl From<ExecutionResumeAllResult> for QueryOutcome {
72    fn from(result: ExecutionResumeAllResult) -> Self {
73        QueryOutcome {
74            success: result.success,
75            output: result.output,
76            error: result.error,
77            exit_code: None,
78        }
79    }
80}
81
82/// Print a handler result, returning the process exit code.
83pub fn report_result(result: QueryOutcome) -> i32 {
84    if result.success {
85        if let Some(output) = result.output {
86            println!("{}", output);
87        }
88        return result.exit_code.unwrap_or(0);
89    }
90    if let Some(error) = result.error {
91        eprintln!("Error: {}", error);
92    }
93    result.exit_code.unwrap_or(1)
94}
95
96/// Handle `--upload-log`, which reports its own exit code.
97pub fn handle_upload_log_query(store: Option<&ExecutionStore>, identifier: &str) -> i32 {
98    match upload_execution_log(store, identifier) {
99        Ok(code) => code,
100        Err(error) => {
101            eprintln!("Error: {}", error);
102            1
103        }
104    }
105}
106
107/// Handle `--cleanup` / `--cleanup-dry-run`.
108///
109/// Cleans up stale "executing" records (processes that crashed or were killed).
110pub fn handle_cleanup(store: Option<&ExecutionStore>, dry_run: bool) -> i32 {
111    let Some(store) = store else {
112        eprintln!("Error: Execution tracking is disabled.");
113        return 1;
114    };
115
116    let result = store.cleanup_stale(CleanupOptions {
117        dry_run,
118        ..Default::default()
119    });
120
121    for error in &result.errors {
122        eprintln!("Error: {}", error);
123    }
124
125    if result.records.is_empty() {
126        println!("No stale records found.");
127        return 0;
128    }
129
130    if dry_run {
131        println!(
132            "Found {} stale record(s) that would be cleaned up:\n",
133            result.records.len()
134        );
135    } else {
136        println!("Cleaned up {} stale record(s):\n", result.cleaned);
137    }
138
139    for record in &result.records {
140        let start_time_display = chrono::DateTime::parse_from_rfc3339(&record.start_time)
141            .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
142            .unwrap_or_else(|_| record.start_time.clone());
143
144        println!("  UUID: {}", record.uuid);
145        println!("  Command: {}", record.command);
146        println!("  Started: {}", start_time_display);
147        println!(
148            "  PID: {}",
149            record
150                .pid
151                .map(|p| p.to_string())
152                .unwrap_or("N/A".to_string())
153        );
154        println!();
155    }
156
157    if dry_run {
158        println!("Run with --cleanup to actually clean up these records.");
159    }
160    0
161}
162
163/// Run the query/control mode selected by the wrapper options, if any.
164///
165/// Returns the process exit code, or `None` when no query mode is active and
166/// the invocation should be treated as a command to run.
167pub fn dispatch_query_command(
168    options: &WrapperOptions,
169    store: Option<&ExecutionStore>,
170    command: &str,
171) -> Option<i32> {
172    let output_format = options.output_format.as_deref();
173
174    if let Some(ref identifier) = options.status {
175        return Some(report_result(
176            query_status(store, identifier, output_format).into(),
177        ));
178    }
179    if options.list {
180        return Some(report_result(
181            list_executions_filtered(store, output_format, options.running).into(),
182        ));
183    }
184    if let Some(ref identifier) = options.upload_log {
185        return Some(
186            match crate::log_uploader::upload_execution_log_with_options(
187                store,
188                identifier,
189                options.no_sanitize,
190                std::env::var("START_VERBOSE").is_ok_and(|value| value == "1" || value == "true"),
191            ) {
192                Ok(code) => code,
193                Err(error) => {
194                    eprintln!("Error: {}", error);
195                    1
196                }
197            },
198        );
199    }
200    if let Some(ref identifier) = options.stop {
201        return Some(report_result(
202            control_execution(store, identifier, ControlAction::Stop).into(),
203        ));
204    }
205    if let Some(ref identifier) = options.terminate {
206        return Some(report_result(
207            control_execution(store, identifier, ControlAction::Terminate).into(),
208        ));
209    }
210    if let Some(ref identifier) = options.attach {
211        return Some(report_result(
212            attach_execution(store, identifier, options.read_only).into(),
213        ));
214    }
215    if let Some(ref identifier) = options.resume {
216        let new_command = if command.trim().is_empty() {
217            None
218        } else {
219            Some(command)
220        };
221        return Some(report_result(
222            resume_execution_with_options(store, identifier, new_command, output_format, options)
223                .into(),
224        ));
225    }
226    if options.resume_all {
227        return Some(report_result(
228            resume_all_executions(store, output_format).into(),
229        ));
230    }
231    if options.cleanup {
232        return Some(handle_cleanup(store, options.cleanup_dry_run));
233    }
234
235    None
236}