lambda_simulator/
process.rs1use std::collections::HashMap;
42use std::io;
43use std::path::{Path, PathBuf};
44use std::process::{Child, Command, ExitStatus, Stdio};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum ProcessRole {
49 Runtime,
51 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#[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 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 #[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 #[must_use]
103 pub fn arg(mut self, arg: impl Into<String>) -> Self {
104 self.args.push(arg.into());
105 self
106 }
107
108 #[must_use]
113 pub fn inherit_stdio(mut self, inherit: bool) -> Self {
114 self.inherit_stdio = inherit;
115 self
116 }
117
118 pub fn role(&self) -> ProcessRole {
120 self.role
121 }
122
123 pub fn binary_path(&self) -> &Path {
125 &self.binary_path
126 }
127}
128
129pub struct ManagedProcess {
137 child: Child,
138 pid: u32,
139 role: ProcessRole,
140 binary_name: String,
141}
142
143impl ManagedProcess {
144 pub fn pid(&self) -> u32 {
146 self.pid
147 }
148
149 pub fn role(&self) -> ProcessRole {
151 self.role
152 }
153
154 pub fn binary_name(&self) -> &str {
156 &self.binary_name
157 }
158
159 pub fn child_mut(&mut self) -> &mut Child {
161 &mut self.child
162 }
163
164 pub fn wait(&mut self) -> io::Result<ExitStatus> {
166 self.child.wait()
167 }
168
169 pub fn kill(&mut self) -> io::Result<()> {
171 self.child.kill()
172 }
173
174 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#[derive(Debug)]
201pub enum ProcessError {
202 BinaryNotFound(PathBuf),
204 SpawnFailed(io::Error),
206 Terminated {
208 pid: u32,
210 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
244pub(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}