1pub mod health;
2
3use crate::config::ServiceConfig;
4use anyhow::{bail, Context, Result};
5use std::collections::HashMap;
6use std::io::{BufRead, BufReader, Read, Write};
7use std::process::{Child, Command, Stdio};
8use std::sync::{Arc, Mutex, RwLock};
9use std::time::{Duration, Instant};
10
11const AUTO_START_READY_TIMEOUT: Duration = Duration::from_secs(30);
12const AUTO_START_READY_RECOVERY_TIMEOUT: Duration = Duration::from_secs(300);
13
14#[cfg(windows)]
16mod job {
17 use windows_sys::Win32::Foundation::{CloseHandle, HANDLE};
18 use windows_sys::Win32::System::JobObjects::{
19 AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
20 SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
21 JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
22 };
23 use windows_sys::Win32::System::Threading::OpenProcess;
24 use windows_sys::Win32::System::Threading::PROCESS_ALL_ACCESS;
25
26 pub struct JobObject {
28 handle: HANDLE,
29 }
30
31 unsafe impl Send for JobObject {}
32 unsafe impl Sync for JobObject {}
33
34 impl JobObject {
35 pub fn new() -> Option<Self> {
36 unsafe {
37 let handle = CreateJobObjectW(std::ptr::null(), std::ptr::null());
38 if handle.is_null() {
39 return None;
40 }
41 let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
43 info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
44 let ok = SetInformationJobObject(
45 handle,
46 JobObjectExtendedLimitInformation,
47 &info as *const _ as *const _,
48 std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
49 );
50 if ok == 0 {
51 CloseHandle(handle);
52 return None;
53 }
54 Some(Self { handle })
55 }
56 }
57
58 pub fn assign_process(&self, pid: u32) -> bool {
60 unsafe {
61 let proc_handle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
62 if proc_handle.is_null() {
63 return false;
64 }
65 let ok = AssignProcessToJobObject(self.handle, proc_handle);
66 CloseHandle(proc_handle);
67 ok != 0
68 }
69 }
70 }
71
72 impl Drop for JobObject {
73 fn drop(&mut self) {
74 unsafe {
75 CloseHandle(self.handle);
76 }
77 }
78 }
79}
80
81#[cfg(not(windows))]
82mod job {
83 pub struct JobObject;
85 impl JobObject {
86 pub fn new() -> Option<Self> { Some(Self) }
87 pub fn assign_process(&self, _pid: u32) -> bool { true }
88 }
89}
90#[derive(Debug, Clone, PartialEq)]
93pub enum ServiceStatus {
94 Stopped,
95 Starting,
96 Running,
97 Unhealthy,
98 Crashed,
99}
100
101impl std::fmt::Display for ServiceStatus {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 match self {
104 Self::Stopped => write!(f, "stopped"),
105 Self::Starting => write!(f, "starting"),
106 Self::Running => write!(f, "running"),
107 Self::Unhealthy => write!(f, "unhealthy"),
108 Self::Crashed => write!(f, "crashed"),
109 }
110 }
111}
112
113impl serde::Serialize for ServiceStatus {
114 fn serialize<S: serde::Serializer>(&self, s: S) -> std::result::Result<S::Ok, S::Error> {
115 s.serialize_str(&self.to_string())
116 }
117}
118
119struct ManagedService {
120 config: ServiceConfig,
121 child: Option<Child>,
122 status: ServiceStatus,
123 stdin: Option<std::process::ChildStdin>,
124 stdout_buffer: Arc<Mutex<Vec<u8>>>,
125 stdout_base_offset: Arc<Mutex<usize>>,
126}
127
128pub struct ProcessManager {
129 services: RwLock<HashMap<String, ManagedService>>,
130 _job: Option<job::JobObject>,
133}
134
135impl Default for ProcessManager {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141impl ProcessManager {
142 pub fn new() -> Self {
143 let _job = job::JobObject::new();
144 if _job.is_none() {
145 tracing::warn!("Failed to create Job Object — child processes may outlive core");
146 }
147 Self {
148 services: RwLock::new(HashMap::new()),
149 _job,
150 }
151 }
152
153 pub async fn register(&self, config: ServiceConfig) {
155 self.register_sync(config);
156 }
157
158 pub fn register_sync(&self, config: ServiceConfig) {
160 let name = config.name.clone();
161 let mut services = self.services.write().unwrap();
162 services.insert(
163 name,
164 ManagedService {
165 config,
166 child: None,
167 status: ServiceStatus::Stopped,
168 stdin: None,
169 stdout_buffer: Arc::new(Mutex::new(Vec::new())),
170 stdout_base_offset: Arc::new(Mutex::new(0)),
171 },
172 );
173 }
174
175 pub async fn start(&self, name: &str) -> Result<()> {
177 self.start_sync(name)
178 }
179
180 pub fn start_sync(&self, name: &str) -> Result<()> {
182 let mut services = self.services.write().unwrap();
183 let svc = services
184 .get_mut(name)
185 .ok_or_else(|| anyhow::anyhow!("Service '{}' not registered", name))?;
186
187 if svc.child.is_some() {
188 bail!("Service '{}' is already running", name);
189 }
190
191 svc.status = ServiceStatus::Starting;
192
193 let mut cmd = Command::new(&svc.config.command);
197 cmd.args(&svc.config.args);
198
199 if let Some(ref workdir) = svc.config.workdir {
200 cmd.current_dir(workdir);
201 }
202
203 for (k, v) in &svc.config.env {
204 cmd.env(k, v);
205 }
206
207 cmd.stdin(Stdio::piped());
208 cmd.stdout(Stdio::piped());
209 cmd.stderr(Stdio::piped());
210
211 #[cfg(windows)]
214 {
215 use std::os::windows::process::CommandExt;
216 const CREATE_NO_WINDOW: u32 = 0x08000000;
217 cmd.creation_flags(CREATE_NO_WINDOW);
218 }
219
220 tracing::info!(
221 "Starting service '{}' with command='{}' args={:?} cwd={:?}",
222 name,
223 svc.config.command,
224 svc.config.args,
225 svc.config.workdir
226 );
227
228 let child = cmd
229 .spawn()
230 .with_context(|| format!("Failed to start service '{}'", name))?;
231
232 if let Some(ref job) = self._job {
234 let pid = child.id();
235 if !job.assign_process(pid) {
236 tracing::warn!("Failed to assign service '{}' (pid {}) to Job Object", name, pid);
237 }
238 }
239
240 tracing::info!("Started service '{}' (pid: {:?})", name, child.id());
241 let mut child = child;
242 svc.stdin = child.stdin.take();
243 if let Ok(mut buffer) = svc.stdout_buffer.lock() {
244 buffer.clear();
245 }
246 if let Ok(mut base_offset) = svc.stdout_base_offset.lock() {
247 *base_offset = 0;
248 }
249
250 if let Some(stdout) = child.stdout.take() {
251 let service_name = name.to_string();
252 let stdout_buffer = svc.stdout_buffer.clone();
253 let stdout_base_offset = svc.stdout_base_offset.clone();
254 std::thread::spawn(move || {
255 let mut reader = BufReader::new(stdout);
256 let mut chunk = [0u8; 4096];
257 loop {
258 let read = match reader.read(&mut chunk) {
259 Ok(0) => break,
260 Ok(size) => size,
261 Err(_) => break,
262 };
263 if let Ok(mut buffer) = stdout_buffer.lock() {
264 buffer.extend_from_slice(&chunk[..read]);
265 if buffer.len() > 1024 * 1024 {
266 let overflow = buffer.len() - (1024 * 1024);
267 buffer.drain(0..overflow);
268 if let Ok(mut base_offset) = stdout_base_offset.lock() {
269 *base_offset += overflow;
270 }
271 }
272 }
273 let text = String::from_utf8_lossy(&chunk[..read]);
274 for line in text.lines() {
275 tracing::info!("[service:{} stdout] {}", service_name, line);
276 }
277 }
278 });
279 }
280
281 if let Some(stderr) = child.stderr.take() {
282 let service_name = name.to_string();
283 std::thread::spawn(move || {
284 let reader = BufReader::new(stderr);
285 for line in reader.lines().map_while(Result::ok) {
286 tracing::warn!("[service:{} stderr] {}", service_name, line);
287 }
288 });
289 }
290
291 svc.child = Some(child);
292 svc.status = ServiceStatus::Running;
293
294 Ok(())
295 }
296
297 pub async fn stop(&self, name: &str) -> Result<()> {
299 self.stop_sync(name)
300 }
301
302 pub fn stop_sync(&self, name: &str) -> Result<()> {
304 let mut services = self.services.write().unwrap();
305 let svc = services
306 .get_mut(name)
307 .ok_or_else(|| anyhow::anyhow!("Service '{}' not registered", name))?;
308
309 if let Some(ref mut child) = svc.child {
310 tracing::info!("Stopping service '{}'", name);
311 #[cfg(windows)]
312 {
313 use std::os::windows::process::CommandExt;
314 const CREATE_NO_WINDOW: u32 = 0x08000000;
315 let pid = child.id();
316 let _ = Command::new("taskkill")
317 .args(["/PID", &pid.to_string(), "/T", "/F"])
318 .stdout(Stdio::null())
319 .stderr(Stdio::null())
320 .creation_flags(CREATE_NO_WINDOW)
321 .status();
322 }
323 #[cfg(not(windows))]
324 {
325 child.kill().ok();
326 }
327 child.wait().ok();
328 svc.child = None;
329 svc.stdin = None;
330 svc.status = ServiceStatus::Stopped;
331 }
332
333 Ok(())
334 }
335
336 pub async fn restart(&self, name: &str) -> Result<()> {
338 self.stop_sync(name).ok();
339 self.start_sync(name)
340 }
341
342 pub async fn all_statuses(&self) -> HashMap<String, ServiceStatus> {
344 self.all_statuses_sync()
345 }
346
347 pub fn all_statuses_sync(&self) -> HashMap<String, ServiceStatus> {
348 let services = self.services.read().unwrap();
349 services
350 .iter()
351 .map(|(name, svc)| (name.clone(), svc.status.clone()))
352 .collect()
353 }
354
355 pub async fn status(&self, name: &str) -> Option<ServiceStatus> {
357 self.status_sync(name)
358 }
359
360 pub fn status_sync(&self, name: &str) -> Option<ServiceStatus> {
361 let services = self.services.read().unwrap();
362 services.get(name).map(|s| s.status.clone())
363 }
364
365 pub fn service_config_sync(&self, name: &str) -> Option<ServiceConfig> {
366 let services = self.services.read().unwrap();
367 services.get(name).map(|svc| svc.config.clone())
368 }
369
370 pub async fn service_config(&self, name: &str) -> Option<ServiceConfig> {
371 self.service_config_sync(name)
372 }
373
374 pub fn write_stdin_sync(&self, name: &str, input: &str) -> Result<()> {
375 let mut services = self.services.write().unwrap();
376 let svc = services
377 .get_mut(name)
378 .ok_or_else(|| anyhow::anyhow!("Service '{}' not registered", name))?;
379 let stdin = svc
380 .stdin
381 .as_mut()
382 .ok_or_else(|| anyhow::anyhow!("Service '{}' has no stdin", name))?;
383 stdin.write_all(input.as_bytes())?;
384 stdin.flush()?;
385 Ok(())
386 }
387
388 pub fn read_stdout_since_sync(&self, name: &str, offset: usize) -> Result<(usize, Vec<u8>)> {
389 let services = self.services.read().unwrap();
390 let svc = services
391 .get(name)
392 .ok_or_else(|| anyhow::anyhow!("Service '{}' not registered", name))?;
393 let base_offset = *svc.stdout_base_offset.lock().unwrap();
394 let buffer = svc.stdout_buffer.lock().unwrap();
395 if offset < base_offset {
396 bail!(
397 "stdout buffer for service '{}' truncated before offset {} (current base offset {})",
398 name,
399 offset,
400 base_offset
401 );
402 }
403 let start = (offset - base_offset).min(buffer.len());
404 Ok((base_offset + buffer.len(), buffer[start..].to_vec()))
405 }
406
407 pub async fn start_auto(self: &Arc<Self>) {
409 let names: Vec<String> = {
410 let services = self.services.read().unwrap();
411 services
412 .iter()
413 .filter(|(_, svc)| svc.config.auto_start)
414 .map(|(name, _)| name.clone())
415 .collect()
416 };
417
418 for name in names {
419 if let Err(e) = self.start_sync(&name) {
420 tracing::error!("Failed to auto-start service '{}': {}", name, e);
421 continue;
422 }
423
424 if let Err(error) = self
425 .wait_for_service_ready(&name, AUTO_START_READY_TIMEOUT)
426 .await
427 {
428 tracing::error!(
429 "Service '{}' did not become ready after auto-start: {}",
430 name,
431 error
432 );
433
434 self.spawn_readiness_recovery(name.clone());
435 }
436 }
437 }
438
439 fn spawn_readiness_recovery(self: &Arc<Self>, name: String) {
440 let process_manager = Arc::clone(self);
441 tokio::spawn(async move {
442 if let Err(error) = process_manager
443 .recover_service_readiness(name.clone(), AUTO_START_READY_RECOVERY_TIMEOUT)
444 .await
445 {
446 tracing::warn!(
447 "Service '{}' never recovered readiness after startup timeout: {}",
448 name,
449 error
450 );
451 }
452 });
453 }
454
455 async fn wait_for_service_ready(&self, name: &str, timeout: Duration) -> Result<()> {
456 let health_url = {
457 let services = self.services.read().unwrap();
458 services
459 .get(name)
460 .and_then(|svc| svc.config.health_url.clone())
461 };
462
463 let Some(health_url) = health_url else {
464 return Ok(());
465 };
466
467 let deadline = Instant::now() + timeout;
468 loop {
469 if health::check_health(&health_url, Duration::from_secs(2)).await? {
470 let mut services = self.services.write().unwrap();
471 if let Some(svc) = services.get_mut(name) {
472 svc.status = ServiceStatus::Running;
473 }
474 return Ok(());
475 }
476
477 if Instant::now() >= deadline {
478 let mut services = self.services.write().unwrap();
479 if let Some(svc) = services.get_mut(name) {
480 svc.status = ServiceStatus::Unhealthy;
481 }
482 bail!("healthcheck never passed for {}", health_url);
483 }
484
485 tokio::time::sleep(Duration::from_millis(200)).await;
486 }
487 }
488
489 async fn recover_service_readiness(&self, name: String, timeout: Duration) -> Result<()> {
490 let deadline = Instant::now() + timeout;
491
492 loop {
493 match self.status_sync(&name) {
494 Some(ServiceStatus::Running) => return Ok(()),
495 Some(ServiceStatus::Stopped | ServiceStatus::Crashed) | None => {
496 bail!("service stopped before readiness recovered")
497 }
498 Some(ServiceStatus::Starting | ServiceStatus::Unhealthy) => {}
499 }
500
501 match self
502 .wait_for_service_ready(&name, Duration::from_secs(5))
503 .await
504 {
505 Ok(()) => return Ok(()),
506 Err(_) if Instant::now() < deadline => {
507 tokio::time::sleep(Duration::from_millis(500)).await;
508 }
509 Err(error) => return Err(error),
510 }
511 }
512 }
513
514 pub async fn stop_all(&self) {
516 let names: Vec<String> = {
517 let services = self.services.read().unwrap();
518 services.keys().cloned().collect()
519 };
520
521 for name in names {
522 self.stop_sync(&name).ok();
523 }
524 }
525
526 pub async fn run_health_checks(&self) {
528 let configs: Vec<(String, Option<String>)> = {
529 let services = self.services.read().unwrap();
530 services
531 .iter()
532 .filter(|(_, svc)| svc.child.is_some())
533 .map(|(name, svc)| (name.clone(), svc.config.health_url.clone()))
534 .collect()
535 };
536
537 for (name, health_url) in configs {
538 if let Some(url) = health_url {
539 let healthy = health::check_health(&url, std::time::Duration::from_secs(5))
540 .await
541 .unwrap_or(false);
542
543 let mut services = self.services.write().unwrap();
544 if let Some(svc) = services.get_mut(&name) {
545 svc.status = if healthy {
546 ServiceStatus::Running
547 } else {
548 ServiceStatus::Unhealthy
549 };
550 }
551 }
552 }
553 }
554}