doum_cli/tools/
executor.rs

1use crate::system::error::{DoumError, Result};
2use crate::system::env::{SystemInfo, OsType, ShellType};
3use std::process::{Command, Output};
4use std::time::Duration;
5
6/// 명령 실행 결과
7#[derive(Debug)]
8pub struct CommandOutput {
9    pub success: bool,
10    pub exit_code: i32,
11    pub stdout: Vec<u8>,
12    pub stderr: Vec<u8>,
13}
14
15/// OS와 쉘에 맞게 명령 실행
16pub fn execute(command: &str, system_info: &SystemInfo) -> Result<CommandOutput> {
17    execute_with_timeout(command, system_info, None)
18}
19
20/// 타임아웃을 지정하여 명령 실행
21pub fn execute_with_timeout(
22    command: &str,
23    system_info: &SystemInfo,
24    timeout: Option<Duration>,
25) -> Result<CommandOutput> {
26    let output = match system_info.os {
27        OsType::Windows => execute_windows(command, &system_info.shell, timeout)?,
28        OsType::Linux | OsType::MacOS => execute_unix(command, &system_info.shell, timeout)?,
29    };
30
31    let exit_code = output.status.code().unwrap_or(-1);
32    let success = output.status.success();
33
34    Ok(CommandOutput {
35        stdout: output.stdout,
36        stderr: output.stderr,
37        exit_code,
38        success,
39    })
40}
41
42/// Windows에서 명령 실행
43fn execute_windows(command: &str, shell: &ShellType, _timeout: Option<Duration>) -> Result<Output> {
44    let mut cmd = match shell {
45        ShellType::PowerShell => {
46            let mut c = Command::new("powershell.exe");
47            c.arg("-NoProfile");
48            c.arg("-Command");
49            // PowerShell은 기본적으로 UTF-8 처리
50            c.arg(format!("[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; {}", command));
51            c
52        }
53        ShellType::Cmd | ShellType::Bash | ShellType::Zsh | ShellType::Fish | ShellType::Unknown => {
54            // Windows에서는 기본값으로 cmd.exe 사용
55            let mut c = Command::new("cmd.exe");
56            c.arg("/C");
57            // cmd는 chcp 65001로 UTF-8 설정
58            c.arg(format!("chcp 65001 >nul && {}", command));
59            c
60        }
61    };
62
63    // 타임아웃 구현은 향후 개선 가능 (현재는 기본 동작)
64    let output = cmd
65        .output()
66        .map_err(|e| DoumError::CommandExecution(format!("명령 실행 실패: {}", e)))?;
67
68    Ok(output)
69}
70
71/// Unix 계열에서 명령 실행
72fn execute_unix(command: &str, shell: &ShellType, _timeout: Option<Duration>) -> Result<Output> {
73    let shell_path = match shell {
74        ShellType::Bash => "/bin/bash",
75        ShellType::Zsh => "/bin/zsh",
76        ShellType::Fish => "/usr/bin/fish",
77        _ => "/bin/sh", // 기본값
78    };
79
80    let mut cmd = Command::new(shell_path);
81    cmd.arg("-c");
82    cmd.arg(command);
83
84    let output = cmd
85        .output()
86        .map_err(|e| DoumError::CommandExecution(format!("명령 실행 실패: {}", e)))?;
87
88    Ok(output)
89}
90
91impl CommandOutput {
92    /// stdout를 UTF-8 문자열로 변환
93    pub fn stdout_string(&self) -> String {
94        String::from_utf8_lossy(&self.stdout).to_string()
95    }
96
97    /// stderr를 UTF-8 문자열로 변환
98    pub fn stderr_string(&self) -> String {
99        String::from_utf8_lossy(&self.stderr).to_string()
100    }
101
102    /// 명령 실행 결과를 사람이 읽기 쉬운 형태로 출력
103    pub fn display(&self) -> String {
104        let mut result = String::new();
105
106        if !self.stdout.is_empty() {
107            result.push_str(&self.stdout_string());
108        }
109
110        if !self.stderr.is_empty() {
111            if !result.is_empty() {
112                result.push('\n');
113            }
114            result.push_str("=== stderr ===\n");
115            result.push_str(&self.stderr_string());
116        }
117
118        if !self.success {
119            if !result.is_empty() {
120                result.push('\n');
121            }
122            result.push_str(&format!("Exit code: {}", self.exit_code));
123        }
124
125        result
126    }
127}