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`, plus `--alias` and `--rename` (issue #231).
7//! `dispatch_query_command` is the single entry point the CLI calls before it
8//! starts interpreting the command line as a 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: result.exit_code,
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),
110/// or with a target the Docker containers and snapshot images of one execution.
111pub fn handle_cleanup(store: Option<&ExecutionStore>, dry_run: bool) -> i32 {
112    handle_cleanup_target(store, dry_run, None)
113}
114
115pub fn handle_cleanup_target(
116    store: Option<&ExecutionStore>,
117    dry_run: bool,
118    target: Option<&str>,
119) -> i32 {
120    if let Some(target) = target {
121        let result = crate::execution_cleanup::cleanup_execution(
122            store,
123            target,
124            dry_run,
125            &crate::execution_control::SystemCommandRunner,
126        );
127        if !result.success {
128            if let Some(output) = &result.output {
129                println!("{}", output);
130            }
131        }
132        return report_result(result);
133    }
134    let Some(store) = store else {
135        eprintln!("Error: Execution tracking is disabled.");
136        return 1;
137    };
138
139    let result = store.cleanup_stale(CleanupOptions {
140        dry_run,
141        ..Default::default()
142    });
143
144    for error in &result.errors {
145        eprintln!("Error: {}", error);
146    }
147
148    if result.records.is_empty() {
149        println!("No stale records found.");
150        return 0;
151    }
152
153    if dry_run {
154        println!(
155            "Found {} stale record(s) that would be cleaned up:\n",
156            result.records.len()
157        );
158    } else {
159        println!("Cleaned up {} stale record(s):\n", result.cleaned);
160    }
161
162    for record in &result.records {
163        let start_time_display = chrono::DateTime::parse_from_rfc3339(&record.start_time)
164            .map(|dt| dt.format("%Y-%m-%d %H:%M:%S").to_string())
165            .unwrap_or_else(|_| record.start_time.clone());
166
167        println!("  UUID: {}", record.uuid);
168        println!("  Command: {}", record.command);
169        println!("  Started: {}", start_time_display);
170        println!(
171            "  PID: {}",
172            record
173                .pid
174                .map(|p| p.to_string())
175                .unwrap_or("N/A".to_string())
176        );
177        println!();
178    }
179
180    if dry_run {
181        println!("Run with --cleanup to actually clean up these records.");
182    }
183    0
184}
185
186/// Run the query/control mode selected by the wrapper options, if any.
187///
188/// Returns the process exit code, or `None` when no query mode is active and
189/// the invocation should be treated as a command to run.
190pub fn dispatch_query_command(
191    options: &WrapperOptions,
192    store: Option<&ExecutionStore>,
193    command: &str,
194) -> Option<i32> {
195    let output_format = options.output_format.as_deref();
196
197    if let Some(ref identifier) = options.status {
198        return Some(report_result(
199            query_status(store, identifier, output_format).into(),
200        ));
201    }
202    if options.list {
203        return Some(report_result(
204            list_executions_filtered(store, output_format, options.running).into(),
205        ));
206    }
207    if let Some(ref identifier) = options.upload_log {
208        return Some(
209            match crate::log_uploader::upload_execution_log_with_options(
210                store,
211                identifier,
212                options.no_sanitize,
213                std::env::var("START_VERBOSE").is_ok_and(|value| value == "1" || value == "true"),
214            ) {
215                Ok(code) => code,
216                Err(error) => {
217                    eprintln!("Error: {}", error);
218                    1
219                }
220            },
221        );
222    }
223    if let Some(ref identifier) = options.stop {
224        return Some(report_result(
225            control_execution(store, identifier, ControlAction::Stop).into(),
226        ));
227    }
228    if let Some(ref identifier) = options.terminate {
229        return Some(report_result(
230            control_execution(store, identifier, ControlAction::Terminate).into(),
231        ));
232    }
233    if let Some(ref identifier) = options.attach {
234        return Some(report_result(
235            attach_execution(store, identifier, options.read_only).into(),
236        ));
237    }
238    if let Some(ref identifier) = options.resume {
239        let new_command = if command.trim().is_empty() {
240            None
241        } else {
242            Some(command)
243        };
244        return Some(report_result(
245            resume_execution_with_options(store, identifier, new_command, output_format, options)
246                .into(),
247        ));
248    }
249    if options.resume_all {
250        return Some(report_result(
251            resume_all_executions(store, output_format).into(),
252        ));
253    }
254    if options.cleanup {
255        return Some(handle_cleanup_target(
256            store,
257            options.cleanup_dry_run,
258            options.cleanup_target.as_deref(),
259        ));
260    }
261    let naming = options
262        .rename
263        .as_deref()
264        .map(|id| (id, true))
265        .or_else(|| options.alias.as_deref().map(|id| (id, false)));
266    if let Some((identifier, rename)) = naming {
267        let name = options.new_name.as_deref().unwrap_or("");
268        let result = crate::execution_identity::name_execution(store, identifier, name, rename);
269        let success = result.is_ok();
270        let (output, error) = match result {
271            Ok(output) => (Some(output), None),
272            Err(error) => (None, Some(error)),
273        };
274        return Some(report_result(QueryOutcome {
275            success,
276            output,
277            error,
278            exit_code: None,
279        }));
280    }
281
282    None
283}