solverforge_service/
service.rs

1use crate::config::ServiceConfig;
2use crate::error::{ServiceError, ServiceResult};
3use crate::jar::JarManager;
4use crate::util::{find_available_port, find_java, wait_for_ready};
5use log::{debug, error, info, warn};
6use solverforge_core::HttpSolverService;
7use std::io::{BufRead, BufReader};
8use std::process::{Child, Command, Stdio};
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::thread;
12use std::time::Duration;
13
14pub struct EmbeddedService {
15    process: Option<Child>,
16    port: u16,
17    shutdown_flag: Arc<AtomicBool>,
18}
19
20impl EmbeddedService {
21    pub fn start(config: ServiceConfig) -> ServiceResult<Self> {
22        let port = if config.port == 0 {
23            find_available_port()?
24        } else {
25            config.port
26        };
27
28        let java = find_java(config.java_home.as_deref())?;
29
30        // Derive JAVA_HOME from the java path for Maven
31        let java_home = java
32            .parent()
33            .and_then(|bin| bin.parent())
34            .map(|home| home.to_path_buf());
35
36        let jar_manager = if let Some(submodule_dir) = config.submodule_dir {
37            let cache_dir = config.cache_dir.unwrap_or_else(crate::util::get_cache_dir);
38            JarManager::with_paths(submodule_dir, cache_dir).with_java_home(java_home.as_deref())
39        } else {
40            JarManager::new()?.with_java_home(java_home.as_deref())
41        };
42
43        let jar_path = jar_manager.ensure_jar()?;
44        let working_dir = jar_manager.cache_dir();
45
46        info!("Starting embedded solver service on port {}", port);
47        debug!("Using JAR: {}", jar_path.display());
48        debug!("Using Java: {}", java.display());
49
50        let shutdown_flag = Arc::new(AtomicBool::new(false));
51        let shutdown_flag_clone = shutdown_flag.clone();
52
53        let mut cmd = Command::new(&java);
54
55        // Set JAVA_HOME for the subprocess to ensure it uses the correct Java
56        if let Some(ref home) = java_home {
57            cmd.env("JAVA_HOME", home);
58        }
59
60        // JVM options must come before -jar
61        cmd.arg(format!("-Dquarkus.http.port={}", port));
62
63        for opt in &config.java_opts {
64            cmd.arg(opt);
65        }
66
67        cmd.arg("-jar")
68            .arg(&jar_path)
69            .current_dir(working_dir)
70            .stdout(Stdio::piped())
71            .stderr(Stdio::piped());
72
73        let mut process = cmd.spawn().map_err(|e| {
74            ServiceError::StartFailed(format!(
75                "Failed to start Java process: {}. Is Java installed?",
76                e
77            ))
78        })?;
79
80        // Capture stdout (solver metrics)
81        if let Some(stdout) = process.stdout.take() {
82            let shutdown = shutdown_flag_clone.clone();
83            thread::spawn(move || {
84                let reader = BufReader::new(stdout);
85                for line in reader.lines() {
86                    if shutdown.load(Ordering::Relaxed) {
87                        break;
88                    }
89                    if let Ok(line) = line {
90                        if line.contains("ERROR") {
91                            error!("[solver] {}", line);
92                        } else if line.contains("WARN") {
93                            warn!("[solver] {}", line);
94                        } else if line.contains("INFO") {
95                            info!("[solver] {}", line);
96                        } else {
97                            debug!("[solver] {}", line);
98                        }
99                    }
100                }
101            });
102        }
103
104        // Capture stderr (JVM warnings, errors)
105        if let Some(stderr) = process.stderr.take() {
106            let shutdown = shutdown_flag_clone;
107            thread::spawn(move || {
108                let reader = BufReader::new(stderr);
109                for line in reader.lines() {
110                    if shutdown.load(Ordering::Relaxed) {
111                        break;
112                    }
113                    if let Ok(line) = line {
114                        if line.contains("ERROR") {
115                            error!("[solver-service] {}", line);
116                        } else if line.contains("WARN") {
117                            warn!("[solver-service] {}", line);
118                        } else if line.contains("INFO") {
119                            info!("[solver-service] {}", line);
120                        } else {
121                            debug!("[solver-service] {}", line);
122                        }
123                    }
124                }
125            });
126        }
127
128        let service = EmbeddedService {
129            process: Some(process),
130            port,
131            shutdown_flag,
132        };
133
134        let health_url = format!("http://localhost:{}/health/ready", port);
135        wait_for_ready(&health_url, config.startup_timeout)?;
136
137        info!("Solver service is ready on port {}", port);
138
139        Ok(service)
140    }
141
142    pub fn url(&self) -> String {
143        format!("http://localhost:{}", self.port)
144    }
145
146    pub fn port(&self) -> u16 {
147        self.port
148    }
149
150    pub fn is_running(&mut self) -> bool {
151        if let Some(ref mut process) = self.process {
152            match process.try_wait() {
153                Ok(None) => true,
154                Ok(Some(_)) => false,
155                Err(_) => false,
156            }
157        } else {
158            false
159        }
160    }
161
162    pub fn stop(&mut self) -> ServiceResult<()> {
163        self.shutdown_flag.store(true, Ordering::Relaxed);
164
165        if let Some(mut process) = self.process.take() {
166            info!("Stopping embedded solver service");
167
168            #[cfg(unix)]
169            {
170                unsafe {
171                    libc::kill(process.id() as i32, libc::SIGTERM);
172                }
173            }
174
175            #[cfg(not(unix))]
176            {
177                process.kill().ok();
178            }
179
180            thread::sleep(Duration::from_secs(2));
181
182            if let Ok(None) = process.try_wait() {
183                warn!("Service didn't stop gracefully, forcing termination");
184                process.kill().ok();
185            }
186
187            process.wait().ok();
188            info!("Solver service stopped");
189        }
190
191        Ok(())
192    }
193
194    pub fn solver_service(&self) -> HttpSolverService {
195        HttpSolverService::new(self.url())
196    }
197}
198
199impl Drop for EmbeddedService {
200    fn drop(&mut self) {
201        if self.process.is_some() {
202            if let Err(e) = self.stop() {
203                error!("Failed to stop embedded service on drop: {}", e);
204            }
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_url_generation() {
215        let service = EmbeddedService {
216            process: None,
217            port: 8080,
218            shutdown_flag: Arc::new(AtomicBool::new(false)),
219        };
220        assert_eq!(service.url(), "http://localhost:8080");
221    }
222
223    #[test]
224    fn test_port_getter() {
225        let service = EmbeddedService {
226            process: None,
227            port: 9999,
228            shutdown_flag: Arc::new(AtomicBool::new(false)),
229        };
230        assert_eq!(service.port(), 9999);
231    }
232
233    #[test]
234    fn test_is_running_no_process() {
235        let mut service = EmbeddedService {
236            process: None,
237            port: 8080,
238            shutdown_flag: Arc::new(AtomicBool::new(false)),
239        };
240        assert!(!service.is_running());
241    }
242}