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