use std::io;
use std::path::PathBuf;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use super::{DAEMON_PORT_SCAN_SPAN, DEFAULT_DAEMON_BASE_PORT};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DaemonInfo {
pub pid: u32,
pub hyperd_endpoint: String,
pub health_port: u16,
pub started_at: String,
pub version: String,
}
pub fn state_dir() -> io::Result<PathBuf> {
if let Some(dir) = std::env::var_os("HYPERDB_STATE_DIR") {
return Ok(PathBuf::from(dir));
}
let home = home_dir().ok_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "cannot determine home directory")
})?;
Ok(home.join(".hyperdb"))
}
pub fn discovery_file_path() -> io::Result<PathBuf> {
Ok(state_dir()?.join("daemon.json"))
}
pub fn write_discovery_file(info: &DaemonInfo) -> io::Result<()> {
let dir = state_dir()?;
std::fs::create_dir_all(&dir)?;
let path = dir.join("daemon.json");
let tmp_path = dir.join("daemon.json.tmp");
let json = serde_json::to_string_pretty(info).map_err(|e| io::Error::other(e.to_string()))?;
std::fs::write(&tmp_path, json.as_bytes())?;
let _ = std::fs::remove_file(&path);
std::fs::rename(&tmp_path, &path)?;
Ok(())
}
pub fn discover() -> Option<DaemonInfo> {
let path = discovery_file_path().ok()?;
let contents = std::fs::read_to_string(&path).ok()?;
let info: DaemonInfo = serde_json::from_str(&contents).ok()?;
if is_daemon_alive(info.health_port) {
Some(info)
} else {
let _ = std::fs::remove_file(&path);
None
}
}
pub fn remove_discovery_file() {
if let Ok(path) = discovery_file_path() {
let _ = std::fs::remove_file(&path);
}
}
fn is_daemon_alive(port: u16) -> bool {
super::health::ping_identified(port, Duration::from_millis(300), Duration::from_millis(300))
.is_some()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PortScan {
pub base: u16,
pub span: u16,
}
pub fn resolve_port_scan() -> PortScan {
if let Some(port) = std::env::var(super::ENV_DAEMON_PORT)
.ok()
.and_then(|v| v.parse::<u16>().ok())
{
PortScan {
base: port,
span: 1,
}
} else {
PortScan {
base: DEFAULT_DAEMON_BASE_PORT,
span: DAEMON_PORT_SCAN_SPAN,
}
}
}
pub fn resolve_port() -> u16 {
resolve_port_scan().base
}
fn home_dir() -> Option<PathBuf> {
std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.map(PathBuf::from)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProbeResult {
OurDaemon(Box<DaemonInfo>),
Camped,
Refused,
}
fn probe_port(port: u16) -> ProbeResult {
let ping_timeout = Duration::from_millis(300);
if let Some(_version) = super::health::ping_identified(port, ping_timeout, ping_timeout) {
match super::health::send_command_with_timeout(port, "STATUS", ping_timeout, ping_timeout) {
Ok(response) => {
if let Ok(info) = serde_json::from_str::<DaemonInfo>(response.trim()) {
ProbeResult::OurDaemon(Box::new(info))
} else {
ProbeResult::Camped
}
}
Err(_) => ProbeResult::Camped,
}
} else {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
match std::net::TcpStream::connect_timeout(&addr, ping_timeout) {
Ok(_) => ProbeResult::Camped, Err(_) => ProbeResult::Refused, }
}
}
#[derive(Debug)]
pub enum ScanOutcome {
Found(Box<DaemonInfo>),
FreePort(u16),
AllOccupied,
}
pub fn scan_for_daemon(scan: PortScan) -> ScanOutcome {
let mut first_free: Option<u16> = None;
for offset in 0..scan.span {
let Some(port) = scan.base.checked_add(offset) else {
break; };
match probe_port(port) {
ProbeResult::OurDaemon(info) => {
return ScanOutcome::Found(info);
}
ProbeResult::Refused => {
if first_free.is_none() {
first_free = Some(port);
}
}
ProbeResult::Camped => {
}
}
}
match first_free {
Some(port) => ScanOutcome::FreePort(port),
None => ScanOutcome::AllOccupied,
}
}
pub fn find_running_daemon() -> Option<DaemonInfo> {
discover().or_else(|| match scan_for_daemon(resolve_port_scan()) {
ScanOutcome::Found(info) => Some(*info),
_ => None,
})
}