holodeck_simctl_core/
recorder.rs1use tokio::process::Child;
2use tokio::sync::Mutex;
3
4#[derive(Default)]
9pub struct Recorder {
10 child: Mutex<Option<Child>>,
11}
12
13impl Recorder {
14 pub fn new() -> Self {
15 Self { child: Mutex::new(None) }
16 }
17
18 pub async fn is_running(&self) -> bool {
19 let mut guard = self.child.lock().await;
20 match guard.as_mut() {
21 Some(child) => matches!(child.try_wait(), Ok(None)),
22 None => false,
23 }
24 }
25
26 pub async fn start(&self, launch_path: &str, arguments: &[String]) -> std::io::Result<()> {
29 let mut guard = self.child.lock().await;
30 if let Some(child) = guard.as_mut()
31 && matches!(child.try_wait(), Ok(None))
32 {
33 return Ok(());
34 }
35 let child = tokio::process::Command::new(launch_path)
36 .args(arguments)
37 .stdout(std::process::Stdio::null())
38 .stderr(std::process::Stdio::null())
39 .spawn()?;
40 *guard = Some(child);
41 Ok(())
42 }
43
44 pub async fn stop(&self) {
45 let mut child = {
46 let mut guard = self.child.lock().await;
47 let Some(child) = guard.take() else {
48 return;
49 };
50 child
51 };
52 if let Some(pid) = child.id() {
53 unsafe {
56 libc::kill(pid as libc::pid_t, libc::SIGINT);
57 }
58 }
59 let _ = child.wait().await;
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 #[tokio::test]
70 async fn not_running_before_start() {
71 let recorder = Recorder::new();
72 assert!(!recorder.is_running().await);
73 }
74
75 #[tokio::test]
76 async fn tracks_running_state_across_start_and_stop() {
77 let recorder = Recorder::new();
78 recorder.start("/bin/sleep", &["5".to_string()]).await.unwrap();
79 assert!(recorder.is_running().await);
80 recorder.stop().await;
81 assert!(!recorder.is_running().await);
82 }
83
84 #[tokio::test]
85 async fn start_is_idempotent_while_already_running() {
86 let recorder = Recorder::new();
87 recorder.start("/bin/sleep", &["5".to_string()]).await.unwrap();
88 recorder.start("/bin/sleep", &["5".to_string()]).await.unwrap();
89 recorder.stop().await;
90 }
91}