nap_core/server/
process.rs1use anyhow::{Context, Result};
8use std::path::Path;
9use std::process::{Child, Command, Stdio};
10use std::time::Duration;
11
12pub struct LoreProcessManager {
14 #[allow(dead_code)]
15 nap_home: std::path::PathBuf,
16 log_path: std::path::PathBuf,
17 config_path: std::path::PathBuf,
18}
19
20impl LoreProcessManager {
21 pub fn new(nap_home: &Path) -> Self {
23 let config_path = nap_home.join("lore").join("config").join("local.toml");
24 let log_path = nap_home.join("lore").join("logs").join("loreserver.log");
25
26 Self {
27 nap_home: nap_home.to_path_buf(),
28 config_path,
29 log_path,
30 }
31 }
32
33 pub fn start(&self) -> Result<Child> {
35 if let Some(parent) = self.log_path.parent() {
37 std::fs::create_dir_all(parent).context("Failed to create log directory")?;
38 }
39
40 if !self.config_path.exists() {
42 anyhow::bail!(
43 "Lore configuration not found at '{}'. \
44 Run 'nap init' or ensure configuration generation has been completed \
45 before starting the Lore server.",
46 self.config_path.display()
47 );
48 }
49
50 let log_file =
52 std::fs::File::create(&self.log_path).context("Failed to create log file")?;
53
54 tracing::info!(
55 config = %self.config_path.display(),
56 log = %self.log_path.display(),
57 "Starting Lore server"
58 );
59
60 let child = Command::new("loreserver")
62 .arg("--config")
63 .arg(&self.config_path)
64 .stdout(Stdio::from(log_file.try_clone()?))
65 .stderr(Stdio::from(log_file))
66 .spawn()
67 .context("Failed to start Lore server. Is loreserver installed and on PATH?")?;
68
69 tracing::info!(pid = child.id(), "Lore server started successfully");
70
71 Ok(child)
72 }
73
74 pub fn stop(pid: u32) -> Result<()> {
76 tracing::info!(pid, "Stopping Lore server");
77
78 #[cfg(unix)]
79 {
80 use nix::sys::signal::kill;
81 use nix::unistd::Pid;
82
83 kill(Pid::from_raw(pid as i32), nix::sys::signal::Signal::SIGTERM)
84 .context("Failed to send SIGTERM to Lore server")?;
85 }
86
87 #[cfg(windows)]
88 {
89 use windows_sys::Win32::Foundation::CloseHandle;
90 use windows_sys::Win32::System::Threading::{
91 OpenProcess, PROCESS_TERMINATE, TerminateProcess,
92 };
93
94 unsafe {
95 let handle = OpenProcess(PROCESS_TERMINATE, 0, pid);
96 if handle == std::ptr::null_mut() {
97 anyhow::bail!("Failed to open process handle for PID {}", pid);
98 }
99 let result = TerminateProcess(handle, 1);
100 CloseHandle(handle);
101 if result == 0 {
102 anyhow::bail!("Failed to terminate process with PID {}", pid);
103 }
104 }
105 }
106
107 tracing::info!(pid, "Lore server stopped");
108 Ok(())
109 }
110
111 pub fn is_running(pid: u32) -> bool {
113 #[cfg(unix)]
114 {
115 use nix::sys::signal::kill;
116 use nix::unistd::Pid;
117
118 kill(Pid::from_raw(pid as i32), None).is_ok()
119 }
120
121 #[cfg(windows)]
122 {
123 use windows_sys::Win32::Foundation::CloseHandle;
124 use windows_sys::Win32::System::Threading::{OpenProcess, PROCESS_QUERY_INFORMATION};
125
126 unsafe {
127 let handle = OpenProcess(PROCESS_QUERY_INFORMATION, 0, pid);
128 if handle == std::ptr::null_mut() {
129 return false;
130 }
131 CloseHandle(handle);
132 true
133 }
134 }
135 }
136
137 pub async fn health_check(port: u16, timeout: Duration) -> Result<bool> {
139 let url = format!("http://127.0.0.1:{}/health_check", port);
140
141 let client = reqwest::Client::builder()
142 .timeout(timeout)
143 .build()
144 .context("Failed to build HTTP client for Lore health check")?;
145
146 let response = client.get(&url).send().await;
147
148 match response {
149 Ok(resp) => {
150 let is_healthy = resp.status().is_success();
151 if is_healthy {
152 tracing::debug!(
153 port,
154 url = %url,
155 "Lore server health check passed"
156 );
157 } else {
158 tracing::warn!(
159 port,
160 url = %url,
161 status = %resp.status(),
162 "Lore server health check returned non-success status"
163 );
164 }
165 Ok(is_healthy)
166 }
167 Err(e) => {
168 if e.is_timeout() {
169 tracing::debug!(
170 port,
171 url = %url,
172 timeout_ms = timeout.as_millis(),
173 "Lore server health check timed out after {:?} — server may still be starting",
174 timeout
175 );
176 } else if e.is_connect() {
177 tracing::debug!(
178 port,
179 url = %url,
180 "Lore server health check connection refused — server is not listening on port {}",
181 port
182 );
183 } else {
184 tracing::debug!(
185 port,
186 url = %url,
187 error = %e,
188 "Lore server health check failed"
189 );
190 }
191 Ok(false)
192 }
193 }
194 }
195
196 pub async fn wait_for_healthy(
198 port: u16,
199 timeout: Duration,
200 retry_interval: Duration,
201 ) -> Result<()> {
202 let start = std::time::Instant::now();
203 let mut attempt = 0;
204
205 while start.elapsed() < timeout {
206 attempt += 1;
207 tracing::debug!(
208 attempt,
209 elapsed_ms = start.elapsed().as_millis(),
210 timeout_ms = timeout.as_millis(),
211 "Health check attempt for Lore server on port {}",
212 port
213 );
214
215 if Self::health_check(port, retry_interval).await? {
216 tracing::info!(
217 attempt,
218 elapsed_ms = start.elapsed().as_millis(),
219 port,
220 "Lore server became healthy after {} attempts ({:?})",
221 attempt,
222 start.elapsed()
223 );
224 return Ok(());
225 }
226 tokio::time::sleep(retry_interval).await;
227 }
228
229 anyhow::bail!(
230 "Lore server on port {} did not become healthy within {:?} ({} attempts). \
231 Possible causes: server crash during startup, port conflict, \
232 configuration error, or insufficient resources. \
233 Check logs for details.",
234 port,
235 timeout,
236 attempt
237 );
238 }
239
240 pub fn log_path(&self) -> &Path {
242 &self.log_path
243 }
244
245 pub fn config_path(&self) -> &Path {
247 &self.config_path
248 }
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254 use tempfile::TempDir;
255
256 #[test]
257 fn test_process_manager_creation() {
258 let temp_dir = TempDir::new().unwrap();
259 let manager = LoreProcessManager::new(temp_dir.path());
260
261 assert_eq!(
262 manager.config_path(),
263 temp_dir
264 .path()
265 .join("lore")
266 .join("config")
267 .join("local.toml")
268 );
269 assert_eq!(
270 manager.log_path(),
271 temp_dir
272 .path()
273 .join("lore")
274 .join("logs")
275 .join("loreserver.log")
276 );
277 }
278
279 #[test]
280 fn test_is_running() {
281 let current_pid = std::process::id();
283 assert!(LoreProcessManager::is_running(current_pid));
284
285 assert!(!LoreProcessManager::is_running(999999));
287 }
288}