69 lines
2.1 KiB
Rust
69 lines
2.1 KiB
Rust
//! 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<i32>,
|
|
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<CmdOutput> {
|
|
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<CmdOutput> {
|
|
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),
|
|
})
|
|
}
|