doum_cli/tools/
executor.rs

1use crate::system::env::{OsType, ShellType, SystemInfo};
2use anyhow::{Context, Result};
3use std::process::{Command, Output, Stdio};
4use std::thread;
5use std::time::{Duration, Instant};
6
7/// Result of command execution
8#[derive(Debug)]
9pub struct CommandOutput {
10    pub success: bool,
11    pub exit_code: i32,
12    pub stdout: Vec<u8>,
13    pub stderr: Vec<u8>,
14}
15
16impl CommandOutput {
17    /// convert stdout to UTF-8 string
18    pub fn stdout_string(&self) -> String {
19        String::from_utf8_lossy(&self.stdout).to_string()
20    }
21
22    /// convert stderr to UTF-8 string
23    pub fn stderr_string(&self) -> String {
24        String::from_utf8_lossy(&self.stderr).to_string()
25    }
26
27    /// formatted display of command output
28    pub fn display(&self) -> String {
29        let mut result = String::new();
30
31        if !self.stdout.is_empty() {
32            result.push_str("=== stdout ===\n");
33            result.push_str(&self.stdout_string());
34            result.push('\n');
35        }
36
37        if !self.stderr.is_empty() {
38            result.push_str("=== stderr ===\n");
39            result.push_str(&self.stderr_string());
40            result.push('\n');
41        }
42
43        if !self.success {
44            result.push_str("=== status ===\n");
45            result.push_str(&format!("Exit code: {}", self.exit_code));
46            result.push('\n');
47        }
48
49        result
50    }
51}
52
53/// Execute a command based on the system information
54pub fn execute_command(
55    command: &str,
56    system_info: &SystemInfo,
57    timeout: Option<Duration>,
58) -> Result<CommandOutput> {
59    let output = match system_info.os {
60        OsType::Windows => execute_command_windows(command, &system_info.shell, timeout)?,
61        OsType::Linux | OsType::MacOS => {
62            execute_command_unix(command, &system_info.shell, timeout)?
63        }
64    };
65
66    let success = output.status.success();
67    let exit_code = output.status.code().unwrap_or(-1);
68
69    Ok(CommandOutput {
70        success,
71        exit_code,
72        stdout: output.stdout,
73        stderr: output.stderr,
74    })
75}
76
77/// Execute command on Windows
78fn execute_command_windows(
79    command: &str,
80    shell: &ShellType,
81    timeout: Option<Duration>,
82) -> Result<Output> {
83    let cmd = match shell {
84        ShellType::PowerShell => {
85            let mut c = Command::new("powershell.exe");
86            c.arg("-NoProfile");
87            c.arg("-Command");
88            // Set output encoding to UTF-8
89            c.arg(format!(
90                "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; {}",
91                command
92            ));
93            c
94        }
95        ShellType::Cmd => {
96            // Default to cmd.exe
97            let mut c = Command::new("cmd.exe");
98            c.arg("/C");
99            // Set code page to UTF-8 before executing command
100            c.arg(format!("chcp 65001 >nul && {}", command));
101            c
102        }
103        _ => {
104            anyhow::bail!("Unsupported shell on Windows");
105        }
106    };
107
108    run_with_timeout(cmd, timeout)
109}
110
111/// Execute command on Unix-like systems
112fn execute_command_unix(
113    command: &str,
114    shell: &ShellType,
115    timeout: Option<Duration>,
116) -> Result<Output> {
117    let shell_path = match shell {
118        ShellType::Bash => "/bin/bash",
119        ShellType::Zsh => "/bin/zsh",
120        ShellType::Fish => "/usr/bin/fish",
121        _ => "/bin/sh", // default to sh
122    };
123
124    let mut cmd = Command::new(shell_path);
125    cmd.arg("-c");
126    cmd.arg(command);
127
128    run_with_timeout(cmd, timeout)
129}
130
131/// Run command with optional timeout
132fn run_with_timeout(mut cmd: Command, timeout: Option<Duration>) -> Result<Output> {
133    // setup to capture output
134    cmd.stdout(Stdio::piped());
135    cmd.stderr(Stdio::piped());
136
137    let mut child = cmd.spawn().context("Failed to spawn command")?;
138
139    match timeout {
140        None => {
141            // No timeout, wait normally
142            child
143                .wait_with_output()
144                .context("Failed to wait for command")
145        }
146        Some(timeout) => {
147            let start = Instant::now();
148
149            loop {
150                match child.try_wait() {
151                    // Process finished
152                    Ok(Some(_status)) => {
153                        return child
154                            .wait_with_output()
155                            .context("Failed to wait for command output");
156                    }
157                    // Still running
158                    Ok(None) => {
159                        if start.elapsed() >= timeout {
160                            // When timeout occurs, kill the process
161                            let _ = child.kill();
162                            let output = child
163                                .wait_with_output()
164                                .context("Failed to collect output after killing command")?;
165
166                            // Return timeout error with partial output
167                            anyhow::bail!(
168                                "Command timed out after {:?}. Partial output:\n{}",
169                                timeout,
170                                String::from_utf8_lossy(&output.stdout)
171                            );
172                        }
173
174                        // Delay before next poll
175                        thread::sleep(Duration::from_millis(10));
176                    }
177                    Err(e) => {
178                        anyhow::bail!("Failed to poll command status: {}", e);
179                    }
180                }
181            }
182        }
183    }
184}