hatchery_server/
memory.rs1use crate::config::{MEMORY_PORT_MAX, MEMORY_PORT_MIN};
8use crate::error::ServerError;
9use crate::subservice::{ManagedProcess, State, Status, Subservice};
10use std::net::{Ipv4Addr, SocketAddr, TcpListener};
11use std::path::{Path, PathBuf};
12use tokio::process::Command;
13
14const PASS_THROUGH_ENV: &[&str] = &["PATH", "HOME", "LANG", "RUST_LOG", "TMPDIR"];
16
17pub struct MemorySubservice {
18 bin: PathBuf,
19 data_dir: PathBuf,
20 process: ManagedProcess,
21 port: Option<u16>,
22 failed: Option<String>,
23}
24
25impl MemorySubservice {
26 pub fn new(bin: PathBuf, data_dir: &Path) -> Self {
27 Self {
28 bin,
29 data_dir: data_dir.join("memory"),
30 process: ManagedProcess::new(),
31 port: None,
32 failed: None,
33 }
34 }
35
36 pub fn pick_port() -> Result<u16, ServerError> {
38 (MEMORY_PORT_MIN..=MEMORY_PORT_MAX)
39 .find(|port| TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, *port))).is_ok())
40 .ok_or_else(|| ServerError::sub("memory", "no free loopback port in 18200-18299"))
41 }
42}
43
44impl Subservice for MemorySubservice {
45 fn name(&self) -> &'static str {
46 "memory"
47 }
48
49 fn on_demand(&self) -> bool {
50 true
51 }
52
53 fn start(&mut self) -> Result<(), ServerError> {
54 if self.process.alive() {
55 return Ok(());
56 }
57 let result = (|| {
58 std::fs::create_dir_all(&self.data_dir)?;
59 let port = Self::pick_port()?;
60 debug_assert!((MEMORY_PORT_MIN..=MEMORY_PORT_MAX).contains(&port));
61 let mut command = Command::new(&self.bin);
62 command.env_clear();
63 for name in PASS_THROUGH_ENV {
64 if let Some(value) = std::env::var_os(name) {
65 command.env(name, value);
66 }
67 }
68 command
69 .env("MEMORY4AGENT_PORT", port.to_string())
70 .env("MEMORY4AGENT_DATA_DIR", &self.data_dir);
71 self.process.spawn(command)?;
72 Ok::<u16, ServerError>(port)
73 })();
74 match result {
75 Ok(port) => {
76 self.port = Some(port);
77 self.failed = None;
78 Ok(())
79 }
80 Err(error) => {
81 self.port = None;
82 self.failed = Some(error.to_string());
83 Err(ServerError::sub("memory", error.to_string()))
84 }
85 }
86 }
87
88 fn stop(&mut self) {
89 self.process.stop();
90 self.port = None;
91 }
92
93 fn status(&mut self) -> Status {
94 let state = if self.process.alive() {
95 State::Running
96 } else if self.failed.is_some() {
97 State::Failed
98 } else {
99 State::Stopped
100 };
101 let port = if state == State::Running { self.port } else { None };
102 Status { name: "memory", state, port, detail: self.failed.clone() }
103 }
104}
105
106#[cfg(all(test, unix))]
107mod tests {
108 use super::*;
109 use std::os::unix::fs::PermissionsExt;
110
111 fn fake_server(dir: &Path) -> PathBuf {
112 let path = dir.join("fake-memory-server");
113 std::fs::write(&path, "#!/bin/sh\nenv > \"$MEMORY4AGENT_DATA_DIR/env.tmp\" && mv \"$MEMORY4AGENT_DATA_DIR/env.tmp\" \"$MEMORY4AGENT_DATA_DIR/env.txt\"\nexec sleep 30\n").unwrap();
114 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
115 path
116 }
117
118 #[tokio::test]
119 async fn is_on_demand_and_runs_on_loopback_range_with_scrubbed_env() {
120 let dir = tempfile::tempdir().unwrap();
121 std::env::set_var("HATCHERY_TEST_SECRET_NOT_FOR_CHILD", "value");
122 let mut service = MemorySubservice::new(fake_server(dir.path()), dir.path());
123 assert!(service.on_demand());
124 assert_eq!(service.status().state, State::Stopped);
125 service.start().unwrap();
126 let status = service.status();
127 assert_eq!(status.state, State::Running);
128 let port = status.port.unwrap();
129 assert!((MEMORY_PORT_MIN..=MEMORY_PORT_MAX).contains(&port));
130 for _ in 0..500 {
132 if dir.path().join("memory/env.txt").exists() {
133 break;
134 }
135 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
136 }
137 let env = std::fs::read_to_string(dir.path().join("memory/env.txt")).unwrap();
138 assert!(env.contains(&format!("MEMORY4AGENT_PORT={port}")));
139 assert!(!env.contains("HATCHERY_TEST_SECRET_NOT_FOR_CHILD"), "{env}");
140 service.stop();
141 assert_eq!(service.status().state, State::Stopped);
142 std::env::remove_var("HATCHERY_TEST_SECRET_NOT_FOR_CHILD");
143 }
144
145 #[test]
146 fn skips_busy_ports_and_stays_in_range() {
147 let first = MemorySubservice::pick_port().unwrap();
148 let _held = TcpListener::bind(SocketAddr::from((Ipv4Addr::LOCALHOST, first))).unwrap();
149 let second = MemorySubservice::pick_port().unwrap();
150 assert_ne!(first, second);
151 assert!((MEMORY_PORT_MIN..=MEMORY_PORT_MAX).contains(&second));
152 }
153
154 #[test]
155 fn missing_binary_fails_without_a_port() {
156 let dir = tempfile::tempdir().unwrap();
157 let mut service = MemorySubservice::new(dir.path().join("absent"), dir.path());
158 assert!(service.start().is_err());
159 let status = service.status();
160 assert_eq!(status.state, State::Failed);
161 assert!(status.port.is_none());
162 }
163}