mi6_otel_server/
lifecycle.rs1use std::net::TcpStream;
7use std::process::{Command, Stdio};
8use std::time::Duration;
9
10use anyhow::{Context, Result};
11use mi6_core::OtelMode;
12
13pub const DEFAULT_PORT: u16 = 4318;
15
16pub fn is_server_running(port: u16) -> bool {
18 TcpStream::connect_timeout(
19 &std::net::SocketAddr::from(([127, 0, 0, 1], port)),
20 Duration::from_millis(100),
21 )
22 .is_ok()
23}
24
25pub fn is_mi6_server(port: u16) -> bool {
29 use std::io::{Read, Write};
30
31 let Ok(mut stream) = TcpStream::connect_timeout(
32 &std::net::SocketAddr::from(([127, 0, 0, 1], port)),
33 Duration::from_millis(500),
34 ) else {
35 return false;
36 };
37
38 let _ = stream.set_read_timeout(Some(Duration::from_millis(500)));
40
41 let request = "GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n";
43 if stream.write_all(request.as_bytes()).is_err() {
44 return false;
45 }
46
47 let mut response = [0u8; 256];
49 if stream.read(&mut response).is_err() {
50 return false;
51 }
52
53 let response_str = String::from_utf8_lossy(&response);
55 response_str.contains("mi6-otel")
56}
57
58#[cfg(unix)]
60fn kill_mi6_server(port: u16) -> Result<bool> {
61 let output = Command::new("lsof")
63 .args(["-ti", &format!(":{}", port)])
64 .output()
65 .context("failed to run lsof")?;
66
67 if output.status.success() {
68 let pids = String::from_utf8_lossy(&output.stdout);
69 let mut killed = false;
70 for pid in pids.lines() {
71 let pid = pid.trim();
72 if !pid.is_empty() {
73 let _ = Command::new("kill").args(["-TERM", pid]).output();
75 killed = true;
76 }
77 }
78 if killed {
79 std::thread::sleep(Duration::from_millis(500));
81 return Ok(true);
82 }
83 }
84 Ok(false)
85}
86
87#[cfg(not(unix))]
88fn kill_mi6_server(_port: u16) -> Result<bool> {
89 anyhow::bail!("stop is not supported on this platform")
90}
91
92pub fn stop_server(port: u16) -> Result<()> {
94 if !is_server_running(port) {
95 eprintln!("No server running on port {}", port);
96 return Ok(());
97 }
98
99 if !is_mi6_server(port) {
100 anyhow::bail!(
101 "port {} is in use by another service, not a mi6 OTel server",
102 port
103 );
104 }
105
106 if kill_mi6_server(port)? {
107 eprintln!("Stopped OTel server on port {}", port);
108 }
109 Ok(())
110}
111
112#[derive(Debug)]
114pub struct OtelServerStatus {
115 pub running: bool,
117 pub is_mi6: bool,
119 pub port: u16,
121 pub cli_binary: Option<String>,
123 pub cli_version: Option<String>,
125 pub mode: OtelMode,
127}
128
129impl OtelServerStatus {
130 pub fn get(port: u16) -> Self {
132 let running = is_server_running(port);
133 let is_mi6 = running && is_mi6_server(port);
134 let config = mi6_core::Config::load().unwrap_or_default();
135 let mode = config.otel.mode;
136
137 let cli_binary = which::which("mi6").ok().map(|p| p.display().to_string());
139
140 let cli_version = get_cli_version();
142
143 Self {
144 running,
145 is_mi6,
146 port,
147 cli_binary,
148 cli_version,
149 mode,
150 }
151 }
152}
153
154fn get_cli_version() -> Option<String> {
156 let output = Command::new("mi6").arg("--version").output().ok()?;
157
158 if output.status.success() {
159 let stdout = String::from_utf8_lossy(&output.stdout);
160 stdout.trim().strip_prefix("mi6 ").map(String::from)
162 } else {
163 None
164 }
165}
166
167pub fn get_status(port: u16) -> Result<()> {
169 let status = OtelServerStatus::get(port);
170
171 if !status.running {
172 eprintln!("OTel server is not running on port {}", port);
173 return Ok(());
174 }
175
176 if !status.is_mi6 {
177 eprintln!(
178 "Port {} is in use by another service (not a mi6 OTel server)",
179 port
180 );
181 return Ok(());
182 }
183
184 eprintln!("OTel Server Status");
185 eprintln!(" Running: yes");
186 eprintln!(" Port: {}", status.port);
187 eprintln!(" Mode: {} (from config)", status.mode);
188
189 if let Some(ref path) = status.cli_binary {
190 eprintln!(" CLI Binary: {}", path);
191 if let Some(ref v) = status.cli_version {
192 eprintln!(" CLI Version: {}", v);
193 }
194 } else {
195 eprintln!(" CLI Binary: not found on PATH");
196 }
197
198 Ok(())
199}
200
201pub fn ensure_running(port: u16, restart: bool, mode: Option<OtelMode>) -> Result<bool> {
211 if is_server_running(port) {
212 if is_mi6_server(port) {
214 if restart {
215 kill_mi6_server(port)?;
217 } else {
218 return Ok(true); }
220 } else {
221 anyhow::bail!(
223 "port {} is in use by another service; \
224 cannot start mi6 OTel server; \
225 consider changing the port in your settings",
226 port
227 );
228 }
229 }
230
231 let binary_path = std::env::current_exe().context("failed to determine binary path")?;
233 let port_str = port.to_string();
234 let mode_str = mode.map(|m| m.to_string());
235
236 #[cfg(unix)]
238 {
239 let mut cmd = Command::new("nohup");
241 cmd.arg(&binary_path)
242 .args(["otel", "run", "--port", &port_str]);
243
244 if let Some(ref m) = mode_str {
245 cmd.args(["--mode", m]);
246 }
247
248 cmd.stdin(Stdio::null())
249 .stdout(Stdio::null())
250 .stderr(Stdio::null());
251
252 use std::os::unix::process::CommandExt;
253 cmd.process_group(0);
254
255 cmd.spawn().context("failed to spawn otel server")?;
256 }
257
258 #[cfg(not(unix))]
259 {
260 let mut cmd = Command::new(&binary_path);
261 cmd.args(["otel", "run", "--port", &port_str]);
262
263 if let Some(ref m) = mode_str {
264 cmd.args(["--mode", m]);
265 }
266
267 cmd.stdin(Stdio::null())
268 .stdout(Stdio::null())
269 .stderr(Stdio::null())
270 .spawn()
271 .context("failed to spawn otel server")?;
272 }
273
274 for _ in 0..10 {
276 std::thread::sleep(Duration::from_millis(50));
277 if is_server_running(port) {
278 return Ok(true);
279 }
280 }
281
282 eprintln!(
284 "Warning: OTel server not yet responding on port {}; it may still be starting",
285 port
286 );
287 Ok(false)
288}
289
290pub fn default_port() -> u16 {
292 DEFAULT_PORT
293}
294
295pub fn find_available_port(start_port: u16) -> u16 {
300 for port in start_port..start_port + 100 {
301 if !is_server_running(port) || is_mi6_server(port) {
303 return port;
304 }
305 }
306 start_port
308}