use std::collections::HashMap;
use std::io;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, ExitStatus, Stdio};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessRole {
Runtime,
Extension,
}
impl std::fmt::Display for ProcessRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProcessRole::Runtime => write!(f, "runtime"),
ProcessRole::Extension => write!(f, "extension"),
}
}
}
#[derive(Debug, Clone)]
pub struct ProcessConfig {
binary_path: PathBuf,
additional_env: HashMap<String, String>,
args: Vec<String>,
inherit_stdio: bool,
role: ProcessRole,
}
impl ProcessConfig {
pub fn new(binary_path: impl Into<PathBuf>, role: ProcessRole) -> Self {
Self {
binary_path: binary_path.into(),
additional_env: HashMap::new(),
args: Vec::new(),
inherit_stdio: true,
role,
}
}
#[must_use]
pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.additional_env.insert(key.into(), value.into());
self
}
#[must_use]
pub fn arg(mut self, arg: impl Into<String>) -> Self {
self.args.push(arg.into());
self
}
#[must_use]
pub fn inherit_stdio(mut self, inherit: bool) -> Self {
self.inherit_stdio = inherit;
self
}
pub fn role(&self) -> ProcessRole {
self.role
}
pub fn binary_path(&self) -> &Path {
&self.binary_path
}
}
pub struct ManagedProcess {
child: Child,
pid: u32,
role: ProcessRole,
binary_name: String,
}
impl ManagedProcess {
pub fn pid(&self) -> u32 {
self.pid
}
pub fn role(&self) -> ProcessRole {
self.role
}
pub fn binary_name(&self) -> &str {
&self.binary_name
}
pub fn child_mut(&mut self) -> &mut Child {
&mut self.child
}
pub fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait()
}
pub fn kill(&mut self) -> io::Result<()> {
self.child.kill()
}
pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
self.child.try_wait()
}
}
impl Drop for ManagedProcess {
fn drop(&mut self) {
if let Ok(None) = self.child.try_wait() {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
}
impl std::fmt::Debug for ManagedProcess {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ManagedProcess")
.field("pid", &self.pid)
.field("role", &self.role)
.field("binary_name", &self.binary_name)
.finish()
}
}
#[derive(Debug)]
pub enum ProcessError {
BinaryNotFound(PathBuf),
SpawnFailed(io::Error),
Terminated {
pid: u32,
status: Option<ExitStatus>,
},
}
impl std::fmt::Display for ProcessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProcessError::BinaryNotFound(path) => {
write!(f, "Binary not found: {}", path.display())
}
ProcessError::SpawnFailed(e) => {
write!(f, "Failed to spawn process: {}", e)
}
ProcessError::Terminated { pid, status } => {
write!(f, "Process {} terminated unexpectedly", pid)?;
if let Some(s) = status {
write!(f, " with status {:?}", s)?;
}
Ok(())
}
}
}
}
impl std::error::Error for ProcessError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ProcessError::SpawnFailed(e) => Some(e),
_ => None,
}
}
}
pub(crate) fn spawn_process(
config: ProcessConfig,
lambda_env: HashMap<String, String>,
) -> Result<ManagedProcess, ProcessError> {
if !config.binary_path.exists() {
return Err(ProcessError::BinaryNotFound(config.binary_path));
}
let binary_name = config
.binary_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let mut cmd = Command::new(&config.binary_path);
for (key, value) in lambda_env {
cmd.env(key, value);
}
for (key, value) in config.additional_env {
cmd.env(key, value);
}
for arg in &config.args {
cmd.arg(arg);
}
if config.inherit_stdio {
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());
} else {
cmd.stdout(Stdio::null());
cmd.stderr(Stdio::null());
}
cmd.stdin(Stdio::null());
let child = cmd.spawn().map_err(ProcessError::SpawnFailed)?;
let pid = child.id();
tracing::debug!(
"Spawned {} process: {} (PID: {})",
config.role,
binary_name,
pid
);
Ok(ManagedProcess {
child,
pid,
role: config.role,
binary_name,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_config_builder() {
let config = ProcessConfig::new("/usr/bin/echo", ProcessRole::Runtime)
.env("FOO", "bar")
.arg("--help")
.inherit_stdio(false);
assert_eq!(config.role(), ProcessRole::Runtime);
assert_eq!(config.binary_path(), Path::new("/usr/bin/echo"));
assert!(!config.inherit_stdio);
assert_eq!(config.additional_env.get("FOO"), Some(&"bar".to_string()));
assert_eq!(config.args, vec!["--help"]);
}
#[test]
fn test_process_role_display() {
assert_eq!(ProcessRole::Runtime.to_string(), "runtime");
assert_eq!(ProcessRole::Extension.to_string(), "extension");
}
}