Skip to main content

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};
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        Self::start_with_retry(config, true)
23    }
24
25    fn start_with_retry(config: ServiceConfig, allow_retry: bool) -> ServiceResult<Self> {
26        let port = if config.port == 0 {
27            find_available_port()?
28        } else {
29            config.port
30        };
31
32        let java = find_java(config.java_home.as_deref())?;
33
34        // Derive JAVA_HOME from the java path for Maven
35        let java_home = java
36            .parent()
37            .and_then(|bin| bin.parent())
38            .map(|home| home.to_path_buf());
39
40        let jar_manager = if let Some(ref submodule_dir) = config.submodule_dir {
41            let cache_dir = config
42                .cache_dir
43                .clone()
44                .unwrap_or_else(crate::util::get_cache_dir);
45            JarManager::with_paths(submodule_dir.clone(), cache_dir)
46                .with_java_home(java_home.as_deref())
47        } else {
48            JarManager::new().with_java_home(java_home.as_deref())
49        };
50
51        let jar_path = jar_manager.ensure_jar()?;
52        let working_dir = jar_manager.cache_dir();
53
54        info!("Starting embedded solver service on port {}", port);
55        debug!("Using JAR: {}", jar_path.display());
56        debug!("Using Java: {}", java.display());
57
58        let shutdown_flag = Arc::new(AtomicBool::new(false));
59        let shutdown_flag_clone = shutdown_flag.clone();
60
61        let mut cmd = Command::new(&java);
62
63        // Set JAVA_HOME for the subprocess to ensure it uses the correct Java
64        if let Some(ref home) = java_home {
65            cmd.env("JAVA_HOME", home);
66        }
67
68        // JVM options must come before -jar
69        cmd.arg(format!("-Dquarkus.http.port={}", port));
70
71        for opt in &config.java_opts {
72            cmd.arg(opt);
73        }
74
75        cmd.arg("-jar")
76            .arg(&jar_path)
77            .current_dir(working_dir)
78            .stdout(Stdio::piped())
79            .stderr(Stdio::piped());
80
81        let mut process = cmd.spawn().map_err(|e| {
82            ServiceError::StartFailed(format!(
83                "Failed to start Java process: {}. Is Java installed?",
84                e
85            ))
86        })?;
87
88        // Capture stdout (solver metrics)
89        if let Some(stdout) = process.stdout.take() {
90            let shutdown = shutdown_flag_clone.clone();
91            thread::spawn(move || {
92                let reader = BufReader::new(stdout);
93                for line in reader.lines() {
94                    if shutdown.load(Ordering::Relaxed) {
95                        break;
96                    }
97                    if let Ok(line) = line {
98                        if line.contains("ERROR") {
99                            error!("[solver] {}", line);
100                        } else if line.contains("WARN") {
101                            warn!("[solver] {}", line);
102                        } else if line.contains("INFO") {
103                            info!("[solver] {}", line);
104                        } else {
105                            debug!("[solver] {}", line);
106                        }
107                    }
108                }
109            });
110        }
111
112        // Capture stderr (JVM warnings, errors)
113        if let Some(stderr) = process.stderr.take() {
114            let shutdown = shutdown_flag_clone;
115            thread::spawn(move || {
116                let reader = BufReader::new(stderr);
117                for line in reader.lines() {
118                    if shutdown.load(Ordering::Relaxed) {
119                        break;
120                    }
121                    if let Ok(line) = line {
122                        if line.contains("ERROR") {
123                            error!("[solver-service] {}", line);
124                        } else if line.contains("WARN") {
125                            warn!("[solver-service] {}", line);
126                        } else if line.contains("INFO") {
127                            info!("[solver-service] {}", line);
128                        } else {
129                            debug!("[solver-service] {}", line);
130                        }
131                    }
132                }
133            });
134        }
135
136        let mut service = EmbeddedService {
137            process: Some(process),
138            port,
139            shutdown_flag,
140        };
141
142        let health_url = format!("http://localhost:{}/health/ready", port);
143
144        // Wait for service to be ready, checking if process crashes
145        let start = std::time::Instant::now();
146        let client = reqwest::blocking::Client::builder()
147            .timeout(Duration::from_secs(2))
148            .build()
149            .map_err(|e| ServiceError::Http(e.to_string()))?;
150
151        // Give process a moment to start before checking
152        std::thread::sleep(Duration::from_millis(100));
153
154        info!("Waiting for service health check at {}", health_url);
155
156        loop {
157            // Check if process crashed (only after initial startup window)
158            if start.elapsed() > Duration::from_millis(500) {
159                let running = service.is_running();
160                debug!("Process running check: {}", running);
161                if !running {
162                    // Get exit status for diagnostics
163                    if let Some(ref mut proc) = service.process {
164                        if let Ok(Some(status)) = proc.try_wait() {
165                            error!("Java process exited with status: {:?}", status);
166                        }
167                    }
168                    if allow_retry {
169                        warn!("Java process crashed during startup, deleting cached JAR and retrying...");
170                        if let Err(e) = std::fs::remove_file(&jar_path) {
171                            warn!("Failed to delete cached JAR: {}", e);
172                        }
173                        return Self::start_with_retry(config, false);
174                    }
175                    return Err(ServiceError::StartFailed(
176                        "Java process crashed during startup. Check logs or try: rm ~/.cache/solverforge/*.jar".to_string(),
177                    ));
178                }
179            }
180
181            if start.elapsed() > config.startup_timeout {
182                return Err(ServiceError::Unhealthy(format!(
183                    "Service did not become ready within {:?}",
184                    config.startup_timeout
185                )));
186            }
187
188            match client.get(&health_url).send() {
189                Ok(response) if response.status().is_success() => {
190                    debug!("Service is ready after {:?}", start.elapsed());
191                    break;
192                }
193                Ok(response) => {
194                    debug!("Health check returned {}", response.status());
195                }
196                Err(e) => {
197                    debug!("Service not ready yet: {}", e);
198                }
199            }
200
201            std::thread::sleep(Duration::from_millis(500));
202        }
203
204        info!("Solver service is ready on port {}", port);
205
206        Ok(service)
207    }
208
209    pub fn url(&self) -> String {
210        format!("http://localhost:{}", self.port)
211    }
212
213    pub fn port(&self) -> u16 {
214        self.port
215    }
216
217    pub fn is_running(&mut self) -> bool {
218        if let Some(ref mut process) = self.process {
219            match process.try_wait() {
220                Ok(None) => true,
221                Ok(Some(_)) => false,
222                Err(_) => false,
223            }
224        } else {
225            false
226        }
227    }
228
229    pub fn stop(&mut self) -> ServiceResult<()> {
230        self.shutdown_flag.store(true, Ordering::Relaxed);
231
232        if let Some(mut process) = self.process.take() {
233            info!("Stopping embedded solver service");
234
235            #[cfg(unix)]
236            {
237                unsafe {
238                    libc::kill(process.id() as i32, libc::SIGTERM);
239                }
240            }
241
242            #[cfg(not(unix))]
243            {
244                process.kill().ok();
245            }
246
247            thread::sleep(Duration::from_secs(2));
248
249            if let Ok(None) = process.try_wait() {
250                warn!("Service didn't stop gracefully, forcing termination");
251                process.kill().ok();
252            }
253
254            process.wait().ok();
255            info!("Solver service stopped");
256        }
257
258        Ok(())
259    }
260
261    pub fn solver_service(&self) -> HttpSolverService {
262        HttpSolverService::new(self.url())
263    }
264}
265
266impl Drop for EmbeddedService {
267    fn drop(&mut self) {
268        if self.process.is_some() {
269            if let Err(e) = self.stop() {
270                error!("Failed to stop embedded service on drop: {}", e);
271            }
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[test]
281    fn test_url_generation() {
282        let service = EmbeddedService {
283            process: None,
284            port: 8080,
285            shutdown_flag: Arc::new(AtomicBool::new(false)),
286        };
287        assert_eq!(service.url(), "http://localhost:8080");
288    }
289
290    #[test]
291    fn test_port_getter() {
292        let service = EmbeddedService {
293            process: None,
294            port: 9999,
295            shutdown_flag: Arc::new(AtomicBool::new(false)),
296        };
297        assert_eq!(service.port(), 9999);
298    }
299
300    #[test]
301    fn test_is_running_no_process() {
302        let mut service = EmbeddedService {
303            process: None,
304            port: 8080,
305            shutdown_flag: Arc::new(AtomicBool::new(false)),
306        };
307        assert!(!service.is_running());
308    }
309}