1#![allow(dead_code)]
2use std::{collections::HashMap, fs::OpenOptions, sync::Arc, time::Duration};
3
4use anyhow::Result;
5use tokio::{
6 process::{Child, Command},
7 sync::Mutex,
8 time::timeout,
9};
10
11use crate::{
12 core::watcher::WatchContext,
13 exec::{OutpuStrategy, PipeRegistry, metrics::monitor_process},
14};
15
16pub struct CommandOutput {
17 pub status_code: Option<i32>,
18 pub cpu_usage: f32,
19 pub mem_usage_kb: u64,
20}
21
22pub async fn run_command_with_timeout(
23 program: &str,
24 args: &[String],
25 current_dir: &str,
26 timeout_secs: u64,
27 output: &OutpuStrategy,
28 env: Option<HashMap<String, String>>,
29 pipe_registry: Arc<Mutex<PipeRegistry>>,
30) -> Result<CommandOutput> {
31 let mut cmd = Command::new(program);
33
34 let full = std::iter::once(program.to_string())
35 .chain(args.iter().cloned())
36 .collect();
37
38 cmd.args(args).current_dir(current_dir);
39 output.configure(&mut cmd, full, pipe_registry).await?;
40
41 if let Some(vars) = env {
42 for (k, v) in vars {
43 cmd.env(k, v);
44 }
45 }
46
47 let mut child = cmd.spawn()?;
48
49 let metrics_rx = match child.id() {
53 Some(pid) => {
54 let (tx, rx) = tokio::sync::mpsc::channel(1);
55 tokio::spawn(async move {
56 let metrics = monitor_process(pid).await;
57 let _ = tx.send(metrics).await;
58 });
59 Some(rx)
60 }
61 None => None,
62 };
63
64 let duration = Duration::from_secs(timeout_secs);
65
66 let run_future = async {
67 let status = child.wait().await?;
68
69 let (cpu_usage, mem_usage_kb) = match metrics_rx {
70 Some(mut rx) => rx.recv().await.unwrap_or((0.0, 0)),
71 None => (0.0, 0),
72 };
73 anyhow::Ok((status, cpu_usage, mem_usage_kb))
74 };
75
76 match timeout(duration, run_future).await {
77 Ok(Ok((status, cpu_usage, mem_usage_kb))) => Ok(CommandOutput {
78 status_code: status.code(),
79 cpu_usage,
80 mem_usage_kb,
81 }),
82 Ok(Err(e)) => Err(anyhow::anyhow!("Error during execution : {}", e)),
83 Err(_) => {
84 child.kill().await.ok();
85 Err(anyhow::anyhow!(
86 "Command timeout after {} seconds, process killed",
87 timeout_secs
88 ))
89 }
90 }
91}
92
93pub async fn run_command_background(
94 program: &str,
95 args: &[String],
96 current_dir: &str,
97 stdout_file: std::fs::File,
98 stderr_file: std::fs::File,
99 env: Option<HashMap<String, String>>,
100) -> Result<Child> {
101 use std::process::Stdio;
102 let stdout_stdio = Stdio::from(stdout_file);
103 let stderr_stdio = Stdio::from(stderr_file);
104
105 let mut cmd = Command::new(program);
106 cmd.args(args)
107 .current_dir(current_dir)
108 .stdout(stdout_stdio)
109 .stderr(stderr_stdio);
110
111 if let Some(vars) = env {
112 for (k, v) in vars {
113 cmd.env(k, v);
114 }
115 }
116
117 let child = cmd.spawn()?;
118
119 Ok(child)
120}
121
122pub async fn exec_timeout(
123 parts: Vec<String>,
124 ctx: &WatchContext,
125 timeout: u64,
126 env: Option<HashMap<String, String>>,
127 output_strategy: &OutpuStrategy,
128 pipe_registry: Arc<Mutex<PipeRegistry>>,
129) -> Result<CommandOutput, anyhow::Error> {
130 let program = &parts[0];
131 let args = &parts[1..];
132
133 match run_command_with_timeout(
134 program,
135 args,
136 &ctx.project_dir,
137 timeout,
138 output_strategy,
139 env,
140 pipe_registry,
141 )
142 .await
143 {
144 Ok(output) => {
145 if output.status_code != Some(0) {
146 tracing::error!(
147 target: "fleet",
148 log_path = %ctx.log_path().display(),
149 message = %format!("Command failed with exit code {:?}", output.status_code),
150 );
151 return Err(anyhow::anyhow!("Failed command: {:?}", parts));
152 }
153 tracing::info!(
154 target: "fleet",
155 log_path = %ctx.log_path().display(),
156 message = %format!("Command {program} succeeded"),
157 );
158 Ok(output)
159 }
160 Err(e) => {
161 tracing::error!(
162 target: "fleet",
163 log_path = %ctx.log_path().display(),
164 message = %format!("Command error or timeout: {parts:?}"),
165 );
166 tracing::error!(
167 target: "fleet",
168 log_path = %ctx.log_path().display(),
169 message = %e.to_string(),
170 );
171 Err(anyhow::anyhow!("**Command error:**: `{parts:?}`\n{e}"))
172 }
173 }
174}
175
176pub async fn exec_background(
177 parts: Vec<String>,
178 ctx: &WatchContext,
179 env: Option<HashMap<String, String>>,
180) -> Result<(), anyhow::Error> {
181 let program = &parts[0];
182 let args = &parts[1..];
183 tracing::info!(
184 target: "fleet",
185 log_path = %ctx.log_path().display(),
186 message = "Command marked as blocking: running in background without waiting",
187 );
188 let log_path = ctx.log_path();
189
190 let stdout_file = OpenOptions::new()
191 .create(true)
192 .append(true)
193 .open(&log_path)?;
194 let stderr_file = OpenOptions::new()
195 .create(true)
196 .append(true)
197 .open(&log_path)?;
198
199 match run_command_background(
200 program,
201 args,
202 &ctx.project_dir,
203 stdout_file,
204 stderr_file,
205 env,
206 )
207 .await
208 {
209 Ok(_child) => {}
210 Err(e) => {
211 tracing::error!(
212 target: "fleet",
213 log_path = %ctx.log_path().display(),
214 message = %format!("Failed to launch background command: {e}"),
215 );
216 return Err(e);
217 }
218 }
219
220 tracing::info!(
221 target: "fleet",
222 log_path = %ctx.log_path().display(),
223 message = "Background command launched",
224 );
225 Ok(())
226}