use std::os::unix::net::UnixStream;
use std::os::unix::process::CommandExt;
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
use crate::protocol;
fn runtime_dir() -> PathBuf {
if let Ok(test_dir) = std::env::var("EZPN_TEST_SOCKET_DIR") {
return PathBuf::from(test_dir);
}
std::env::var("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from("/tmp"))
}
pub fn socket_path(name: &str) -> PathBuf {
runtime_dir().join(format!("ezpn-session-{}.sock", name))
}
fn is_alive(path: &std::path::Path) -> bool {
let Ok(mut stream) = UnixStream::connect(path) else {
return false;
};
stream
.set_read_timeout(Some(Duration::from_millis(200)))
.ok();
stream
.set_write_timeout(Some(Duration::from_millis(200)))
.ok();
if protocol::write_msg(&mut stream, protocol::C_PING, &[]).is_err() {
return false;
}
matches!(protocol::read_msg(&mut stream), Ok((protocol::S_PONG, _)))
}
pub fn auto_name() -> String {
let base = std::env::current_dir()
.ok()
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "default".to_string());
let base: String = base
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '-' || c == '_' || c == '.' {
c
} else {
'_'
}
})
.collect();
if !socket_path(&base).exists() {
return base;
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let hhmm = format!("{:02}{:02}", (now / 3600) % 24, (now / 60) % 60);
let name = format!("{}-{}", base, hhmm);
if !socket_path(&name).exists() {
return name;
}
let name = format!("{}-{}{:02}", base, hhmm, now % 60);
if !socket_path(&name).exists() {
return name;
}
format!("{}-{}", base, std::process::id())
}
pub fn list() -> Vec<(String, PathBuf)> {
let dir = runtime_dir();
let mut sessions = Vec::new();
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let fname = entry.file_name().to_string_lossy().into_owned();
if let Some(name) = fname
.strip_prefix("ezpn-session-")
.and_then(|s| s.strip_suffix(".sock"))
{
let path = entry.path();
if is_alive(&path) {
sessions.push((name.to_string(), path));
} else {
let _ = std::fs::remove_file(&path);
}
}
}
}
sessions.sort_by(|a, b| {
let a_mtime = std::fs::metadata(&a.1).and_then(|m| m.modified()).ok();
let b_mtime = std::fs::metadata(&b.1).and_then(|m| m.modified()).ok();
b_mtime.cmp(&a_mtime)
});
sessions
}
pub fn find(name: Option<&str>) -> Option<(String, PathBuf)> {
if let Some(n) = name {
let path = socket_path(n);
if is_alive(&path) {
return Some((n.to_string(), path));
}
return None;
}
list().into_iter().next()
}
pub fn spawn_server(session_name: &str, original_args: &[String]) -> anyhow::Result<PathBuf> {
let exe = std::env::current_exe()?;
let sock = socket_path(session_name);
let mut cmd = Command::new(exe);
cmd.arg("--server").arg(session_name);
for arg in original_args {
cmd.arg(arg);
}
cmd.stdin(std::process::Stdio::null());
cmd.stdout(std::process::Stdio::null());
cmd.stderr(std::process::Stdio::null());
unsafe {
cmd.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
cmd.spawn()?;
let deadline = std::time::Instant::now() + Duration::from_secs(3);
while std::time::Instant::now() < deadline {
if is_alive(&sock) {
return Ok(sock);
}
std::thread::sleep(Duration::from_millis(50));
}
anyhow::bail!("server did not start within 3 seconds")
}
pub fn cleanup(name: &str) {
let _ = std::fs::remove_file(socket_path(name));
}