use std::path::PathBuf;
use std::time::Duration;
pub fn daemon_ws_url() -> String {
std::env::var("CAR_DAEMON_URL").unwrap_or_else(|_| "ws://127.0.0.1:9100".to_string())
}
pub fn daemon_port() -> u16 {
parse_port(&daemon_ws_url()).unwrap_or(9100)
}
pub fn parse_host_port(url: &str) -> Option<String> {
let after_scheme = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
let host_port = after_scheme.split('/').next().unwrap_or("");
if host_port.starts_with('[') {
if host_port.contains("]:") {
Some(host_port.to_string())
} else {
None
}
} else if host_port.contains(':') {
Some(host_port.to_string())
} else {
None
}
}
pub fn parse_port(url: &str) -> Option<u16> {
let host_port = parse_host_port(url)?;
let port_str = if host_port.starts_with('[') {
host_port.rsplit("]:").next()?
} else {
host_port.rsplit(':').next()?
};
port_str.parse().ok()
}
pub fn probe_daemon_port(timeout: Duration) -> bool {
let url = daemon_ws_url();
let host_port = match parse_host_port(&url) {
Some(hp) => hp,
None => return false,
};
let addrs: Vec<_> = match std::net::ToSocketAddrs::to_socket_addrs(&host_port) {
Ok(it) => it.collect(),
Err(_) => return false,
};
addrs
.into_iter()
.any(|addr| std::net::TcpStream::connect_timeout(&addr, timeout).is_ok())
}
pub fn try_spawn_daemon() -> Result<(), String> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
candidates.push(dir.join("car-server"));
}
}
candidates.push(PathBuf::from("car-server"));
let port = daemon_port().to_string();
let mut last_err: Option<std::io::Error> = None;
for candidate in candidates {
let mut cmd = std::process::Command::new(&candidate);
cmd.args(["--port", &port])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null());
match cmd.spawn() {
Ok(_) => return Ok(()),
Err(e) => last_err = Some(e),
}
}
Err(match last_err {
Some(e) if e.kind() == std::io::ErrorKind::NotFound => {
"car-server binary not found".to_string()
}
Some(e) => format!("car-server spawn failed: {e}"),
None => unreachable!("candidates is never empty"),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_host_port_basic() {
assert_eq!(
parse_host_port("ws://127.0.0.1:9100").as_deref(),
Some("127.0.0.1:9100")
);
assert_eq!(
parse_host_port("wss://other:1234/json-rpc").as_deref(),
Some("other:1234")
);
assert!(parse_host_port("ws://localhost").is_none());
assert_eq!(
parse_host_port("127.0.0.1:9100").as_deref(),
Some("127.0.0.1:9100")
);
assert_eq!(
parse_host_port("wss://[::1]:9100/").as_deref(),
Some("[::1]:9100")
);
assert!(parse_host_port("ws://[::1]").is_none());
}
#[test]
fn parse_port_basic() {
assert_eq!(parse_port("ws://127.0.0.1:9100"), Some(9100));
assert_eq!(parse_port("wss://other:1234"), Some(1234));
assert_eq!(parse_port("ws://[::1]:9101"), Some(9101));
assert_eq!(parse_port("ws://localhost"), None);
assert_eq!(parse_port("ws://[::1]"), None);
}
#[test]
fn probe_dead_port_returns_false() {
let prev = std::env::var("CAR_DAEMON_URL").ok();
std::env::set_var("CAR_DAEMON_URL", "ws://127.0.0.1:1");
assert!(!probe_daemon_port(Duration::from_millis(100)));
match prev {
Some(v) => std::env::set_var("CAR_DAEMON_URL", v),
None => std::env::remove_var("CAR_DAEMON_URL"),
}
}
}