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 {
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 const READY_FD_ENV: &str = "EZPN_READY_FD";
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 fds = [0i32; 2];
let pipe_ok = unsafe { libc::pipe(fds.as_mut_ptr()) } == 0;
let (read_fd, write_fd) = if pipe_ok { (fds[0], fds[1]) } else { (-1, -1) };
if pipe_ok {
unsafe {
let flags = libc::fcntl(read_fd, libc::F_GETFD);
if flags >= 0 {
libc::fcntl(read_fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
}
}
}
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());
if pipe_ok {
cmd.env(READY_FD_ENV, write_fd.to_string());
}
let captured_write_fd = if pipe_ok { write_fd } else { -1 };
unsafe {
cmd.pre_exec(move || {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
if captured_write_fd >= 0 {
let flags = libc::fcntl(captured_write_fd, libc::F_GETFD);
if flags >= 0 {
libc::fcntl(captured_write_fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC);
}
}
Ok(())
});
}
let _child = cmd.spawn()?;
if pipe_ok {
unsafe {
libc::close(write_fd);
}
}
if pipe_ok {
let mut pfd = libc::pollfd {
fd: read_fd,
events: libc::POLLIN,
revents: 0,
};
let rc = unsafe { libc::poll(&mut pfd, 1, 3000) };
if rc > 0 && (pfd.revents & libc::POLLIN) != 0 {
let mut buf = [0u8; 8];
unsafe {
libc::read(read_fd, buf.as_mut_ptr() as *mut _, buf.len());
libc::close(read_fd);
}
if is_alive(&sock) {
return Ok(sock);
}
} else {
unsafe { libc::close(read_fd) };
}
}
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));
}