solverforge_service/
service.rs1use 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 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 if let Some(ref home) = java_home {
65 cmd.env("JAVA_HOME", home);
66 }
67
68 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 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 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 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 std::thread::sleep(Duration::from_millis(100));
153
154 info!("Waiting for service health check at {}", health_url);
155
156 loop {
157 if start.elapsed() > Duration::from_millis(500) {
159 let running = service.is_running();
160 debug!("Process running check: {}", running);
161 if !running {
162 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}