Skip to main content

weft_core/process/
mod.rs

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// ---------- Windows Job Object: kill all child processes when core exits ----------
15#[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    /// A Win32 Job Object that kills all assigned processes when dropped.
27    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                // Configure: kill all processes in the job when the handle is closed.
42                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        /// Assign a child process (by pid) to this job.
59        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    /// No-op on non-Windows platforms (Unix uses process groups / PR_SET_PDEATHSIG).
84    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// ---------- End Job Object ----------
91
92#[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 Object: all child processes are assigned here; when core exits
131    /// (or this is dropped), the OS kills them automatically.
132    _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    /// Register a service from config (does not start it).
154    pub async fn register(&self, config: ServiceConfig) {
155        self.register_sync(config);
156    }
157
158    /// Sync version of register, callable from non-async contexts.
159    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    /// Start a registered service.
176    pub async fn start(&self, name: &str) -> Result<()> {
177        self.start_sync(name)
178    }
179
180    /// Sync version of start, callable from non-async contexts (e.g. JS ops).
181    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        // Windows 上 npx/npm/uvx 等是 .cmd 批处理。Rust 的 Command::new
194        // 通过 CreateProcessW 间接调用 cmd.exe 启动它们,stdin/stdout 管道正常。
195        // 不要用 cmd /c 包装(cmd /c 会搞乱 stdin/stdout 管道,导致 MCP stdio 超时)。
196        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        // Windows: 隐藏子进程 console 窗口(FFI 嵌入模式下 parent 是 GUI app,
212        // 不加这个每个 service 都弹黑框)。
213        #[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        // Assign to Job Object so the child dies when core exits.
233        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    /// Stop a running service.
298    pub async fn stop(&self, name: &str) -> Result<()> {
299        self.stop_sync(name)
300    }
301
302    /// Sync version of stop.
303    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    /// Restart a service.
337    pub async fn restart(&self, name: &str) -> Result<()> {
338        self.stop_sync(name).ok();
339        self.start_sync(name)
340    }
341
342    /// Get status of all services.
343    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    /// Get status of one service.
356    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    /// Start all services that have auto_start = true.
408    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    /// Stop all running services.
515    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    /// Run health checks for all services with health_url configured.
527    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}