doum_cli/tools/
executor.rs1use 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#[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 pub fn stdout_string(&self) -> String {
19 String::from_utf8_lossy(&self.stdout).to_string()
20 }
21
22 pub fn stderr_string(&self) -> String {
24 String::from_utf8_lossy(&self.stderr).to_string()
25 }
26
27 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
53pub 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
77fn 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 c.arg(format!(
90 "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; {}",
91 command
92 ));
93 c
94 }
95 ShellType::Cmd => {
96 let mut c = Command::new("cmd.exe");
98 c.arg("/C");
99 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
111fn 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", };
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
131fn run_with_timeout(mut cmd: Command, timeout: Option<Duration>) -> Result<Output> {
133 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 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 Ok(Some(_status)) => {
153 return child
154 .wait_with_output()
155 .context("Failed to wait for command output");
156 }
157 Ok(None) => {
159 if start.elapsed() >= timeout {
160 let _ = child.kill();
162 let output = child
163 .wait_with_output()
164 .context("Failed to collect output after killing command")?;
165
166 anyhow::bail!(
168 "Command timed out after {:?}. Partial output:\n{}",
169 timeout,
170 String::from_utf8_lossy(&output.stdout)
171 );
172 }
173
174 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}