solverforge_service/
service.rs1use 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 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 if let Some(ref home) = java_home {
57 cmd.env("JAVA_HOME", home);
58 }
59
60 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 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 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}