Skip to main content

core_lib/exec/
command.rs

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    // Lance le process avec pipes pour stdout et stderr
32    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    // Only monitor the child when we actually have its pid. Sampling PID 1
50    // ("init") instead would monitor an unrelated process that never exits,
51    // turning every command into a spurious timeout.
52    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}