mi6_otel_server/
lifecycle.rs

1//! Process lifecycle management for the OTel server.
2//!
3//! This module provides functions to start, stop, and check the status
4//! of the mi6 OTel server process.
5
6use std::net::TcpStream;
7use std::process::{Command, Stdio};
8use std::time::Duration;
9
10use anyhow::{Context, Result};
11use mi6_core::OtelMode;
12
13/// Default port for the OTel server.
14pub const DEFAULT_PORT: u16 = 4318;
15
16/// Check if a server is running on the given port.
17pub 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
25/// Check if the server on this port is a mi6 OTel server.
26///
27/// Returns `true` if it's our server, `false` if it's something else or not running.
28pub 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    // Set read timeout
39    let _ = stream.set_read_timeout(Some(Duration::from_millis(500)));
40
41    // Send HTTP GET /health request
42    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    // Read response - check for our specific service identifier
48    let mut response = [0u8; 256];
49    if stream.read(&mut response).is_err() {
50        return false;
51    }
52
53    // Check for our specific response containing "mi6-otel"
54    let response_str = String::from_utf8_lossy(&response);
55    response_str.contains("mi6-otel")
56}
57
58/// Kill the mi6 OTel server running on the given port.
59#[cfg(unix)]
60fn kill_mi6_server(port: u16) -> Result<bool> {
61    // Use lsof to find the PID of the process listening on this port
62    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                // Use kill command to send SIGTERM
74                let _ = Command::new("kill").args(["-TERM", pid]).output();
75                killed = true;
76            }
77        }
78        if killed {
79            // Wait a moment for the process to terminate
80            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
92/// Stop the mi6 OTel server running on the given port.
93pub 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/// Status information for the OTel server.
113#[derive(Debug)]
114pub struct OtelServerStatus {
115    /// Whether a server is running on the port
116    pub running: bool,
117    /// Whether the running server is a mi6 server
118    pub is_mi6: bool,
119    /// Port number
120    pub port: u16,
121    /// Path to the mi6 CLI binary (used for relay mode)
122    pub cli_binary: Option<String>,
123    /// Version of the CLI binary
124    pub cli_version: Option<String>,
125    /// Processing mode from config (note: running server may use different mode)
126    pub mode: OtelMode,
127}
128
129impl OtelServerStatus {
130    /// Get detailed status of the OTel server.
131    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        // Get CLI binary path (look up "mi6" on PATH for relay mode)
138        let cli_binary = which::which("mi6").ok().map(|p| p.display().to_string());
139
140        // Get CLI version by running `mi6 --version`
141        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
154/// Get the version of the mi6 CLI binary on PATH.
155fn 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        // Parse "mi6 0.1.0" -> "0.1.0"
161        stdout.trim().strip_prefix("mi6 ").map(String::from)
162    } else {
163        None
164    }
165}
166
167/// Get the status of the OTel server.
168pub 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
201/// Ensure the OTel server is running, starting it if necessary.
202///
203/// # Arguments
204/// * `port` - The port to run the server on
205/// * `restart` - If true, restart the server even if it's already running
206/// * `mode` - Processing mode (None uses config default)
207///
208/// # Returns
209/// `Ok(true)` if the server is confirmed running, `Ok(false)` if it may still be starting.
210pub fn ensure_running(port: u16, restart: bool, mode: Option<OtelMode>) -> Result<bool> {
211    if is_server_running(port) {
212        // Something is running on this port - check if it's our server
213        if is_mi6_server(port) {
214            if restart {
215                // Kill and restart
216                kill_mi6_server(port)?;
217            } else {
218                return Ok(true); // Our server is already running
219            }
220        } else {
221            // Port is in use by something else
222            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    // Find the binary path
232    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    // Platform-specific spawning
237    #[cfg(unix)]
238    {
239        // Use nohup to make process immune to SIGHUP and parent death
240        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    // Wait briefly for server to start
275    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    // Server may still be starting - warn user but don't fail
283    eprintln!(
284        "Warning: OTel server not yet responding on port {}; it may still be starting",
285        port
286    );
287    Ok(false)
288}
289
290/// Get the default OTel port.
291pub fn default_port() -> u16 {
292    DEFAULT_PORT
293}
294
295/// Find an available port starting from the given port.
296///
297/// Scans up to 100 ports from `start_port`, returning the first port that is
298/// either not in use or already running a mi6 OTel server.
299pub fn find_available_port(start_port: u16) -> u16 {
300    for port in start_port..start_port + 100 {
301        // Port is usable if nothing is running or if our server is already there
302        if !is_server_running(port) || is_mi6_server(port) {
303            return port;
304        }
305    }
306    // Fallback to original port
307    start_port
308}