//! Start externer Prozesse ohne Konsolenfenster, mit Zeitlimit und UTF-8-Dekodierung. use std::path::Path; use std::process::Stdio; use std::time::Duration; use tokio::process::Command; use crate::error::{AppError, AppResult}; /// Ergebnis eines beendeten Prozesses. #[derive(Debug, Clone)] pub struct CmdOutput { /// Exit-Code (`None`, wenn der Prozess beendet wurde). pub code: Option, pub stdout: String, pub stderr: String, } /// Dekodiert Prozessausgabe als UTF-8 und entfernt ein führendes BOM. pub fn decode(bytes: &[u8]) -> String { let text = String::from_utf8_lossy(bytes); text.trim_start_matches('\u{feff}').to_owned() } /// Erzeugt ein Kommando, das unter Windows kein Konsolenfenster öffnet. pub fn command(program: &Path) -> Command { let mut cmd = Command::new(program); #[cfg(windows)] { const CREATE_NO_WINDOW: u32 = 0x0800_0000; cmd.creation_flags(CREATE_NO_WINDOW); } cmd.kill_on_drop(true) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); cmd } /// Führt ein Programm aus und wartet höchstens `timeout`. pub async fn run(program: &Path, args: &[&str], timeout: Duration) -> AppResult { run_env(program, args, &[], timeout).await } /// Wie [`run`], setzt zusätzlich Umgebungsvariablen (z. B. um Benutzereingaben sicher zu übergeben). pub async fn run_env( program: &Path, args: &[&str], envs: &[(&str, &str)], timeout: Duration, ) -> AppResult { let mut cmd = command(program); cmd.args(args).envs(envs.iter().copied()); tracing::debug!(program = %program.display(), ?args, "Prozess wird gestartet"); let child = cmd.spawn().map_err(|source| AppError::Spawn { program: program.display().to_string(), source, })?; let output = tokio::time::timeout(timeout, child.wait_with_output()) .await .map_err(|_| AppError::Timeout(timeout.as_secs()))??; Ok(CmdOutput { code: output.status.code(), stdout: decode(&output.stdout), stderr: decode(&output.stderr), }) }