lambda_simulator/
process.rs

1//! Process spawning and lifecycle management for Lambda simulation.
2//!
3//! This module provides utilities for spawning and managing runtime and extension
4//! processes within the Lambda simulator. It handles:
5//!
6//! - Automatic injection of Lambda environment variables
7//! - PID registration for freeze/thaw simulation
8//! - RAII-based cleanup on drop
9//! - Stdio inheritance for demos and debugging
10//!
11//! # Example
12//!
13//! ```no_run
14//! use lambda_simulator::{Simulator, FreezeMode};
15//! use lambda_simulator::process::ProcessRole;
16//!
17//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
18//! let simulator = Simulator::builder()
19//!     .freeze_mode(FreezeMode::Process)
20//!     .build()
21//!     .await?;
22//!
23//! // Spawn a runtime - PID automatically registered for freeze/thaw
24//! // In tests, use env!("CARGO_BIN_EXE_<name>") for binary path resolution
25//! let runtime = simulator.spawn_process(
26//!     "/path/to/my_runtime",
27//!     ProcessRole::Runtime,
28//! )?;
29//!
30//! // Spawn an extension
31//! let extension = simulator.spawn_process(
32//!     "/path/to/my_extension",
33//!     ProcessRole::Extension,
34//! )?;
35//!
36//! // Processes are automatically cleaned up when dropped
37//! # Ok(())
38//! # }
39//! ```
40
41use std::collections::HashMap;
42use std::io;
43use std::path::{Path, PathBuf};
44use std::process::{Child, Command, ExitStatus, Stdio};
45
46/// The role of a spawned process in the Lambda environment.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ProcessRole {
49    /// The Lambda runtime process that handles invocations.
50    Runtime,
51    /// A Lambda extension process that receives lifecycle events.
52    Extension,
53}
54
55impl std::fmt::Display for ProcessRole {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            ProcessRole::Runtime => write!(f, "runtime"),
59            ProcessRole::Extension => write!(f, "extension"),
60        }
61    }
62}
63
64/// Configuration for spawning a managed process.
65#[derive(Debug, Clone)]
66pub struct ProcessConfig {
67    binary_path: PathBuf,
68    additional_env: HashMap<String, String>,
69    args: Vec<String>,
70    inherit_stdio: bool,
71    role: ProcessRole,
72}
73
74impl ProcessConfig {
75    /// Creates a new process configuration.
76    ///
77    /// # Arguments
78    ///
79    /// * `binary_path` - Path to the executable binary
80    /// * `role` - Whether this is a runtime or extension process
81    pub fn new(binary_path: impl Into<PathBuf>, role: ProcessRole) -> Self {
82        Self {
83            binary_path: binary_path.into(),
84            additional_env: HashMap::new(),
85            args: Vec::new(),
86            inherit_stdio: true,
87            role,
88        }
89    }
90
91    /// Adds an environment variable to the process.
92    ///
93    /// These are merged with the Lambda environment variables provided by the
94    /// simulator, with these taking precedence.
95    #[must_use]
96    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
97        self.additional_env.insert(key.into(), value.into());
98        self
99    }
100
101    /// Adds a command-line argument to the process.
102    #[must_use]
103    pub fn arg(mut self, arg: impl Into<String>) -> Self {
104        self.args.push(arg.into());
105        self
106    }
107
108    /// Sets whether to inherit stdio from the parent process.
109    ///
110    /// When true (the default), stdout and stderr from the spawned process
111    /// will be visible. This is useful for demos and debugging.
112    #[must_use]
113    pub fn inherit_stdio(mut self, inherit: bool) -> Self {
114        self.inherit_stdio = inherit;
115        self
116    }
117
118    /// Returns the role of this process.
119    pub fn role(&self) -> ProcessRole {
120        self.role
121    }
122
123    /// Returns the binary path.
124    pub fn binary_path(&self) -> &Path {
125        &self.binary_path
126    }
127}
128
129/// A managed child process with automatic cleanup on drop.
130///
131/// When a `ManagedProcess` is dropped, it will:
132/// 1. Send SIGTERM (Unix) or terminate (Windows) to the process
133/// 2. Wait for the process to exit
134///
135/// This ensures no orphaned processes are left behind.
136pub struct ManagedProcess {
137    child: Child,
138    pid: u32,
139    role: ProcessRole,
140    binary_name: String,
141}
142
143impl ManagedProcess {
144    /// Returns the PID of the managed process.
145    pub fn pid(&self) -> u32 {
146        self.pid
147    }
148
149    /// Returns the role of this process.
150    pub fn role(&self) -> ProcessRole {
151        self.role
152    }
153
154    /// Returns the binary name (for logging).
155    pub fn binary_name(&self) -> &str {
156        &self.binary_name
157    }
158
159    /// Returns a mutable reference to the underlying child process.
160    pub fn child_mut(&mut self) -> &mut Child {
161        &mut self.child
162    }
163
164    /// Waits for the process to exit and returns the exit status.
165    pub fn wait(&mut self) -> io::Result<ExitStatus> {
166        self.child.wait()
167    }
168
169    /// Attempts to kill the process.
170    pub fn kill(&mut self) -> io::Result<()> {
171        self.child.kill()
172    }
173
174    /// Checks if the process has exited.
175    pub fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
176        self.child.try_wait()
177    }
178}
179
180impl Drop for ManagedProcess {
181    fn drop(&mut self) {
182        if let Ok(None) = self.child.try_wait() {
183            let _ = self.child.kill();
184            let _ = self.child.wait();
185        }
186    }
187}
188
189impl std::fmt::Debug for ManagedProcess {
190    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
191        f.debug_struct("ManagedProcess")
192            .field("pid", &self.pid)
193            .field("role", &self.role)
194            .field("binary_name", &self.binary_name)
195            .finish()
196    }
197}
198
199/// Error type for process spawning operations.
200#[derive(Debug)]
201pub enum ProcessError {
202    /// The specified binary was not found.
203    BinaryNotFound(PathBuf),
204    /// Failed to spawn the process.
205    SpawnFailed(io::Error),
206    /// The process terminated unexpectedly.
207    Terminated {
208        /// The process ID of the terminated process.
209        pid: u32,
210        /// The exit status, if available.
211        status: Option<ExitStatus>,
212    },
213}
214
215impl std::fmt::Display for ProcessError {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            ProcessError::BinaryNotFound(path) => {
219                write!(f, "Binary not found: {}", path.display())
220            }
221            ProcessError::SpawnFailed(e) => {
222                write!(f, "Failed to spawn process: {}", e)
223            }
224            ProcessError::Terminated { pid, status } => {
225                write!(f, "Process {} terminated unexpectedly", pid)?;
226                if let Some(s) = status {
227                    write!(f, " with status {:?}", s)?;
228                }
229                Ok(())
230            }
231        }
232    }
233}
234
235impl std::error::Error for ProcessError {
236    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
237        match self {
238            ProcessError::SpawnFailed(e) => Some(e),
239            _ => None,
240        }
241    }
242}
243
244/// Spawns a process with the given configuration and Lambda environment variables.
245///
246/// This is an internal function used by `Simulator::spawn_process`.
247pub(crate) fn spawn_process(
248    config: ProcessConfig,
249    lambda_env: HashMap<String, String>,
250) -> Result<ManagedProcess, ProcessError> {
251    if !config.binary_path.exists() {
252        return Err(ProcessError::BinaryNotFound(config.binary_path));
253    }
254
255    let binary_name = config
256        .binary_path
257        .file_name()
258        .and_then(|n| n.to_str())
259        .unwrap_or("unknown")
260        .to_string();
261
262    let mut cmd = Command::new(&config.binary_path);
263
264    for (key, value) in lambda_env {
265        cmd.env(key, value);
266    }
267
268    for (key, value) in config.additional_env {
269        cmd.env(key, value);
270    }
271
272    for arg in &config.args {
273        cmd.arg(arg);
274    }
275
276    if config.inherit_stdio {
277        cmd.stdout(Stdio::inherit());
278        cmd.stderr(Stdio::inherit());
279    } else {
280        cmd.stdout(Stdio::null());
281        cmd.stderr(Stdio::null());
282    }
283
284    cmd.stdin(Stdio::null());
285
286    let child = cmd.spawn().map_err(ProcessError::SpawnFailed)?;
287    let pid = child.id();
288
289    tracing::debug!(
290        "Spawned {} process: {} (PID: {})",
291        config.role,
292        binary_name,
293        pid
294    );
295
296    Ok(ManagedProcess {
297        child,
298        pid,
299        role: config.role,
300        binary_name,
301    })
302}
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_process_config_builder() {
310        let config = ProcessConfig::new("/usr/bin/echo", ProcessRole::Runtime)
311            .env("FOO", "bar")
312            .arg("--help")
313            .inherit_stdio(false);
314
315        assert_eq!(config.role(), ProcessRole::Runtime);
316        assert_eq!(config.binary_path(), Path::new("/usr/bin/echo"));
317        assert!(!config.inherit_stdio);
318        assert_eq!(config.additional_env.get("FOO"), Some(&"bar".to_string()));
319        assert_eq!(config.args, vec!["--help"]);
320    }
321
322    #[test]
323    fn test_process_role_display() {
324        assert_eq!(ProcessRole::Runtime.to_string(), "runtime");
325        assert_eq!(ProcessRole::Extension.to_string(), "extension");
326    }
327}