#![allow(clippy::missing_errors_doc)]
pub mod mock;
pub mod orphan_scan;
pub mod real;
pub use mock::MockRunner;
pub use orphan_scan::{scan_orphans, OrphanProcess};
pub use real::RealRunner;
pub use crate::vortix_core::ports::process::{
CommandOutcome, CommandRunner as CommandRunnerTrait, CommandSpec, DetachedHandle,
ExitStatusInfo, Kind, PrivilegeReq, ProcessError,
};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum CommandRunner {
Real(RealRunner),
Mock(MockRunner),
}
impl CommandRunner {
pub async fn run(&self, spec: CommandSpec) -> Result<CommandOutcome, ProcessError> {
match self {
CommandRunner::Real(r) => r.run(spec).await,
CommandRunner::Mock(m) => m.run(spec).await,
}
}
pub async fn spawn_detached(&self, spec: CommandSpec) -> Result<DetachedHandle, ProcessError> {
match self {
CommandRunner::Real(r) => r.spawn_detached(spec).await,
CommandRunner::Mock(m) => m.spawn_detached(spec).await,
}
}
pub fn run_blocking(&self, spec: CommandSpec) -> Result<CommandOutcome, ProcessError> {
match self {
CommandRunner::Real(r) => r.run_blocking(spec),
CommandRunner::Mock(m) => m.run_sync(spec),
}
}
pub fn spawn_detached_blocking(
&self,
spec: CommandSpec,
) -> Result<DetachedHandle, ProcessError> {
match self {
CommandRunner::Real(r) => r.spawn_detached_blocking(spec),
CommandRunner::Mock(m) => m.spawn_detached_sync(spec),
}
}
#[must_use]
pub fn real() -> Self {
Self::Real(RealRunner::new())
}
#[must_use]
pub fn as_real(&self) -> Option<&RealRunner> {
match self {
Self::Real(r) => Some(r),
Self::Mock(_) => None,
}
}
#[must_use]
pub fn mock_default_success() -> Self {
Self::Mock(MockRunner::with_default_success())
}
}
impl Default for CommandRunner {
fn default() -> Self {
Self::mock_default_success()
}
}
use std::sync::OnceLock;
static GLOBAL_RUNNER: OnceLock<CommandRunner> = OnceLock::new();
pub fn set_global_runner(runner: CommandRunner) {
let _ = GLOBAL_RUNNER.set(runner);
}
pub fn global_runner() -> &'static CommandRunner {
GLOBAL_RUNNER.get_or_init(CommandRunner::mock_default_success)
}
pub fn run(spec: CommandSpec) -> Result<CommandOutcome, ProcessError> {
global_runner().run_blocking(spec)
}
pub fn spawn_detached(spec: CommandSpec) -> Result<DetachedHandle, ProcessError> {
global_runner().spawn_detached_blocking(spec)
}
pub fn run_to_output(spec: CommandSpec) -> std::io::Result<std::process::Output> {
match run(spec) {
Ok(outcome) => Ok(outcome_to_output(outcome)),
Err(ProcessError::NonZeroExit { code, stderr, .. }) => {
Ok(make_output(code.unwrap_or(1), Vec::new(), stderr))
}
Err(ProcessError::Timeout { program, duration }) => Err(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!("`{program}` timed out after {duration:?}"),
)),
Err(ProcessError::ProgramNotFound { program }) => Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("`{program}` not found on PATH"),
)),
Err(ProcessError::PrivilegeDenied { program }) => Err(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
format!("`{program}` requires root"),
)),
Err(ProcessError::Killed { program, signal }) => Err(std::io::Error::other(format!(
"`{program}` killed by signal {signal}"
))),
Err(ProcessError::IoError { source, .. }) => Err(source),
}
}
fn outcome_to_output(outcome: CommandOutcome) -> std::process::Output {
let fallback_code = i32::from(!outcome.exit_status.success);
make_output(
outcome.exit_status.code.unwrap_or(fallback_code),
outcome.stdout,
outcome.stderr,
)
}
#[cfg(unix)]
fn make_output(code: i32, stdout: Vec<u8>, stderr: Vec<u8>) -> std::process::Output {
use std::os::unix::process::ExitStatusExt;
std::process::Output {
status: std::process::ExitStatus::from_raw(code << 8),
stdout,
stderr,
}
}
#[cfg(not(unix))]
fn make_output(_code: i32, stdout: Vec<u8>, stderr: Vec<u8>) -> std::process::Output {
std::process::Output {
status: std::process::ExitStatus::default(),
stdout,
stderr,
}
}