use std::io::{self, ErrorKind, Write};
use std::path::Path;
use std::time::{Duration, SystemTime};
use std::env;
const STALE_PORT_PROBE_ATTEMPTS: usize = 3;
const STALE_PORT_CONNECT_TIMEOUT: Duration = Duration::from_millis(100);
const STALE_PORT_RETRY_DELAY: Duration = Duration::from_millis(25);
const STALE_PORT_AUTH_READ_TIMEOUT: Duration = Duration::from_millis(120);
const BOOT_TIME_MARGIN: Duration = Duration::from_secs(10);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum PortProbeResult {
Alive,
Stale,
Inconclusive,
}
pub fn is_warm_session(base: &str) -> bool {
base == "__warm__" || base.ends_with("____warm__")
}
pub fn next_session_name(ns_prefix: Option<&str>) -> String {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return "0".to_string(),
};
let psmux_dir = format!("{}\\.psmux", home);
let mut used: std::collections::HashSet<u32> = std::collections::HashSet::new();
if let Ok(entries) = std::fs::read_dir(&psmux_dir) {
for entry in entries.flatten() {
if let Some(fname) = entry.file_name().to_str() {
if let Some((base, ext)) = fname.rsplit_once('.') {
if ext != "port" { continue; }
if is_warm_session(base) { continue; }
let session_part = if let Some(pfx) = ns_prefix {
let full_pfx = format!("{}__", pfx);
if base.starts_with(&full_pfx) {
&base[full_pfx.len()..]
} else {
continue; }
} else {
if base.contains("__") { continue; } base
};
if let Ok(n) = session_part.parse::<u32>() {
used.insert(n);
}
}
}
}
}
let mut id = 0u32;
while used.contains(&id) {
id += 1;
}
id.to_string()
}
pub fn allocate_session_id() -> usize {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let counter_path = format!("{}\\.psmux\\next_session_id", home);
let current = std::fs::read_to_string(&counter_path)
.ok()
.and_then(|s| s.trim().parse::<usize>().ok())
.unwrap_or(0);
let _ = std::fs::write(&counter_path, (current + 1).to_string());
current
}
pub fn write_session_id_file(port_file_base: &str, session_id: usize) {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let sid_path = format!("{}\\.psmux\\{}.sid", home, port_file_base);
let _ = std::fs::write(&sid_path, session_id.to_string());
}
pub fn remove_session_id_file(port_file_base: &str) {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let sid_path = format!("{}\\.psmux\\{}.sid", home, port_file_base);
let _ = std::fs::remove_file(&sid_path);
}
pub fn resolve_session_by_id(id: usize) -> Option<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let psmux_dir = format!("{}\\.psmux", home);
if let Ok(entries) = std::fs::read_dir(&psmux_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "sid").unwrap_or(false) {
if let Ok(content) = std::fs::read_to_string(&path) {
if let Ok(file_id) = content.trim().parse::<usize>() {
if file_id == id {
if let Some(base) = path.file_stem().and_then(|s| s.to_str()) {
let port_path = format!("{}\\.psmux\\{}.port", home, base);
if std::path::Path::new(&port_path).exists() {
return Some(base.to_string());
}
}
}
}
}
}
}
}
None
}
pub fn cleanup_stale_port_files() {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return,
};
let psmux_dir = format!("{}\\.psmux", home);
cleanup_stale_port_files_in(Path::new(&psmux_dir));
}
fn cleanup_stale_port_files_in(psmux_dir: &Path) {
cleanup_stale_port_files_in_with(psmux_dir, probe_session_for_cleanup);
}
fn read_key_for_port_path(port_path: &Path) -> String {
let key_path = port_path.with_extension("key");
std::fs::read_to_string(&key_path)
.map(|s| s.trim().to_string())
.unwrap_or_default()
}
#[cfg(windows)]
fn system_boot_time() -> Option<SystemTime> {
#[link(name = "kernel32")]
extern "system" {
fn GetTickCount64() -> u64;
}
let uptime_ms = unsafe { GetTickCount64() };
SystemTime::now().checked_sub(Duration::from_millis(uptime_ms))
}
#[cfg(not(windows))]
fn system_boot_time() -> Option<SystemTime> {
None
}
fn is_pre_boot(mtime: SystemTime, boot: SystemTime, margin: Duration) -> bool {
match boot.checked_sub(margin) {
Some(cutoff) => mtime < cutoff,
None => false,
}
}
fn cleanup_stale_port_files_in_with<F>(psmux_dir: &Path, mut probe: F)
where
F: FnMut(&str, u16) -> PortProbeResult,
{
let boot = system_boot_time();
if let Ok(entries) = std::fs::read_dir(psmux_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "port").unwrap_or(false) {
if let Some(boot) = boot {
if let Some(mtime) = entry.metadata().ok().and_then(|m| m.modified().ok()) {
if is_pre_boot(mtime, boot, BOOT_TIME_MARGIN) {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("cleanup", &format!(
"reaping '{}': port file predates last boot (server died on restart)",
registry_base(&path)));
}
remove_session_registry_files(&path);
continue;
}
}
}
if let Ok(port_str) = std::fs::read_to_string(&path) {
if let Ok(port) = port_str.trim().parse::<u16>() {
let key = read_key_for_port_path(&path);
if probe(&key, port) == PortProbeResult::Stale {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("cleanup", &format!(
"reaping '{}' (port {}): no psmux server authenticated as ours (stale)",
registry_base(&path), port));
}
remove_session_registry_files(&path);
}
} else {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("cleanup", &format!(
"reaping '{}': unparseable port value {:?}",
registry_base(&path), port_str.trim()));
}
remove_session_registry_files(&path);
}
}
}
}
}
}
fn registry_base(port_path: &Path) -> &str {
port_path.file_stem().and_then(|s| s.to_str()).unwrap_or("?")
}
fn remove_session_registry_files(port_path: &Path) {
let _ = std::fs::remove_file(port_path);
let key_path = port_path.with_extension("key");
let _ = std::fs::remove_file(&key_path);
let sid_path = port_path.with_extension("sid");
let _ = std::fs::remove_file(&sid_path);
}
#[derive(Clone, Copy, PartialEq)]
enum AuthProbe {
Authenticated,
Rejected,
Unknown,
}
fn probe_auth_identity(addr: std::net::SocketAddr, key: &str) -> Result<AuthProbe, ErrorKind> {
let mut s = std::net::TcpStream::connect_timeout(&addr, STALE_PORT_CONNECT_TIMEOUT)
.map_err(|e| e.kind())?;
let key = match validate_auth_key(key) {
Some(k) => k,
None => return Ok(AuthProbe::Unknown),
};
let _ = s.set_read_timeout(Some(STALE_PORT_AUTH_READ_TIMEOUT));
let _ = s.set_nodelay(true);
if write!(s, "AUTH {}\n", key).is_err() {
return Ok(AuthProbe::Unknown);
}
let _ = s.flush();
let mut br = std::io::BufReader::new(std::io::Read::take(s, 4096));
let mut line = String::new();
match std::io::BufRead::read_line(&mut br, &mut line) {
Ok(0) => Ok(AuthProbe::Unknown),
Ok(_) => {
let t = line.trim();
if t == "OK" {
Ok(AuthProbe::Authenticated)
} else if t.starts_with("ERROR") {
Ok(AuthProbe::Rejected)
} else {
Ok(AuthProbe::Unknown)
}
}
Err(_) => Ok(AuthProbe::Unknown),
}
}
fn probe_session_for_cleanup(key: &str, port: u16) -> PortProbeResult {
let addr = std::net::SocketAddr::from(([127, 0, 0, 1], port));
let mut saw_refused = false;
let mut saw_inconclusive = false;
for attempt in 0..STALE_PORT_PROBE_ATTEMPTS {
match probe_auth_identity(addr, key) {
Ok(AuthProbe::Authenticated) => {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("probe",
&format!("port {}: AUTH accepted -> alive", port));
}
return PortProbeResult::Alive;
}
Ok(AuthProbe::Rejected) => {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("probe", &format!(
"port {}: AUTH rejected by a different server (reused port) -> stale", port));
}
return PortProbeResult::Stale;
}
Ok(AuthProbe::Unknown) => saw_inconclusive = true,
Err(ErrorKind::ConnectionRefused) => saw_refused = true,
Err(_) => saw_inconclusive = true,
}
if attempt + 1 < STALE_PORT_PROBE_ATTEMPTS {
std::thread::sleep(STALE_PORT_RETRY_DELAY);
}
}
if saw_refused && !saw_inconclusive {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("probe",
&format!("port {}: connection refused on all attempts -> stale", port));
}
PortProbeResult::Stale
} else {
if crate::debug_log::session_log_enabled() {
crate::debug_log::session_log("probe",
&format!("port {}: no definitive answer -> inconclusive (kept)", port));
}
PortProbeResult::Inconclusive
}
}
pub fn read_session_key(session: &str) -> io::Result<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let keypath = format!("{}\\.psmux\\{}.key", home, session);
std::fs::read_to_string(&keypath).map(|s| s.trim().to_string())
}
pub const MAX_AUTHED_RESPONSE_BYTES: u64 = 256 * 1024;
pub fn validate_auth_key(key: &str) -> Option<&str> {
let k = key.trim_matches(|c: char| c == '\r' || c == '\n');
if k.is_empty() {
return None;
}
if k.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0) {
return None;
}
Some(k)
}
pub fn send_auth_cmd(addr: &str, key: &str, cmd: &[u8]) -> io::Result<()> {
let key = match validate_auth_key(key) {
Some(k) => k,
None => return Ok(()),
};
let sock_addr: std::net::SocketAddr = addr.parse().map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
if let Ok(mut s) = std::net::TcpStream::connect_timeout(&sock_addr, Duration::from_millis(50)) {
let _ = s.set_nodelay(true);
let _ = write!(s, "AUTH {}\n", key);
let _ = std::io::Write::write_all(&mut s, cmd);
let _ = s.flush();
}
Ok(())
}
pub fn send_auth_cmd_response(addr: &str, key: &str, cmd: &[u8]) -> io::Result<String> {
let key = match validate_auth_key(key) {
Some(k) => k,
None => return Err(io::Error::new(io::ErrorKind::InvalidInput, "invalid session key")),
};
let mut s = std::net::TcpStream::connect(addr)?;
let _ = s.set_nodelay(true);
let _ = s.set_read_timeout(Some(Duration::from_millis(500)));
let _ = write!(s, "AUTH {}\n", key);
let _ = std::io::Write::write_all(&mut s, cmd);
let _ = s.flush();
let mut br = std::io::BufReader::new(std::io::Read::take(&mut s, MAX_AUTHED_RESPONSE_BYTES));
let mut auth_line = String::new();
let _ = std::io::BufRead::read_line(&mut br, &mut auth_line);
let mut buf = String::new();
let _ = std::io::Read::read_to_string(&mut br, &mut buf);
Ok(buf)
}
fn open_authed(
addr: &str,
key: &str,
cmd: &[u8],
connect_timeout: Duration,
read_timeout: Duration,
) -> Option<std::io::BufReader<std::io::Take<std::net::TcpStream>>> {
let key = validate_auth_key(key)?;
let sock_addr: std::net::SocketAddr = addr.parse().ok()?;
let mut s = std::net::TcpStream::connect_timeout(&sock_addr, connect_timeout).ok()?;
s.set_read_timeout(Some(read_timeout)).ok()?;
let _ = s.set_nodelay(true);
write!(s, "AUTH {}\n", key).ok()?;
s.write_all(cmd).ok()?;
if !cmd.ends_with(b"\n") {
s.write_all(b"\n").ok()?;
}
let _ = s.flush();
Some(std::io::BufReader::new(std::io::Read::take(s, MAX_AUTHED_RESPONSE_BYTES)))
}
fn read_authed_line<R: std::io::BufRead>(br: &mut R) -> Option<String> {
let mut line = String::new();
if std::io::BufRead::read_line(br, &mut line).ok()? == 0 {
return None;
}
let trimmed = line.trim();
if trimmed == "OK" {
line.clear();
if std::io::BufRead::read_line(br, &mut line).ok()? == 0 {
return None;
}
}
let trimmed = line.trim();
if trimmed.is_empty() || trimmed == "OK" || trimmed.starts_with("ERROR:") {
None
} else {
Some(trimmed.to_string())
}
}
fn read_authed_all<R: std::io::Read>(rd: &mut R) -> Option<String> {
let mut buf = String::new();
std::io::Read::read_to_string(rd, &mut buf).ok()?;
let body = buf.strip_prefix("OK\n").or_else(|| buf.strip_prefix("OK\r\n")).unwrap_or(&buf);
let trimmed = body.trim();
if trimmed.is_empty() || trimmed.starts_with("ERROR:") {
None
} else {
Some(trimmed.to_string())
}
}
pub fn fetch_authed_response(
addr: &str,
key: &str,
cmd: &[u8],
connect_timeout: Duration,
read_timeout: Duration,
) -> Option<String> {
let mut br = open_authed(addr, key, cmd, connect_timeout, read_timeout)?;
read_authed_line(&mut br)
}
pub fn fetch_authed_response_multi(
addr: &str,
key: &str,
cmd: &[u8],
connect_timeout: Duration,
read_timeout: Duration,
) -> Option<String> {
let mut br = open_authed(addr, key, cmd, connect_timeout, read_timeout)?;
read_authed_all(&mut br)
}
pub fn fetch_session_info(
addr: &str,
key: &str,
connect_timeout: Duration,
read_timeout: Duration,
) -> Option<String> {
fetch_authed_response(addr, key, b"session-info\n", connect_timeout, read_timeout)
}
#[allow(dead_code)]
pub fn fetch_session_infos_parallel<F>(
inputs: Vec<(String, String, String)>,
connect_timeout: Duration,
read_timeout: Duration,
fallback: F,
) -> Vec<(String, String)>
where
F: Fn(&str) -> String + Send + Sync,
{
if inputs.is_empty() {
return Vec::new();
}
if inputs.len() == 1 {
let (label, addr, key) = &inputs[0];
let info = fetch_session_info(addr, key, connect_timeout, read_timeout)
.unwrap_or_else(|| fallback(label));
return vec![(label.clone(), info)];
}
let results: Vec<(String, String)> = std::thread::scope(|scope| {
let fallback_ref = &fallback;
let handles: Vec<_> = inputs
.iter()
.map(|(label, addr, key)| {
let label = label.clone();
let addr = addr.clone();
let key = key.clone();
scope.spawn(move || {
let info = fetch_session_info(&addr, &key, connect_timeout, read_timeout)
.unwrap_or_else(|| fallback_ref(&label));
(label, info)
})
})
.collect();
handles.into_iter().filter_map(|h| h.join().ok()).collect()
});
results
}
#[derive(Clone, Debug, PartialEq)]
pub enum SessionLiveness {
Alive(String),
Dead,
Unreachable,
}
fn probe_session_liveness(
addr: &str,
key: &str,
connect_timeout: Duration,
read_timeout: Duration,
) -> SessionLiveness {
let sock: std::net::SocketAddr = match addr.parse() {
Ok(a) => a,
Err(_) => return SessionLiveness::Dead,
};
let key = match validate_auth_key(key) {
Some(k) => k,
None => return SessionLiveness::Unreachable,
};
let mut s = match std::net::TcpStream::connect_timeout(&sock, connect_timeout) {
Ok(s) => s,
Err(_) => return SessionLiveness::Dead,
};
let _ = s.set_read_timeout(Some(read_timeout));
let _ = s.set_nodelay(true);
if write!(s, "AUTH {}\n", key).is_err() || s.write_all(b"session-info\n").is_err() {
return SessionLiveness::Dead;
}
let _ = s.flush();
let mut br = std::io::BufReader::new(std::io::Read::take(s, MAX_AUTHED_RESPONSE_BYTES));
let mut line = String::new();
match std::io::BufRead::read_line(&mut br, &mut line) {
Ok(0) => SessionLiveness::Dead,
Ok(_) => {
let t = line.trim();
if t.starts_with("ERROR") {
return SessionLiveness::Dead;
}
if t == "OK" {
line.clear();
match std::io::BufRead::read_line(&mut br, &mut line) {
Ok(0) => SessionLiveness::Dead,
Ok(_) => {
let t2 = line.trim();
if t2.is_empty() || t2 == "OK" || t2.starts_with("ERROR") {
SessionLiveness::Dead
} else {
SessionLiveness::Alive(t2.to_string())
}
}
Err(_) => SessionLiveness::Dead,
}
} else {
SessionLiveness::Alive(t.to_string())
}
}
Err(_) => SessionLiveness::Dead,
}
}
pub fn classify_sessions_parallel(
inputs: Vec<(String, String, String)>,
connect_timeout: Duration,
read_timeout: Duration,
) -> Vec<(String, SessionLiveness)> {
if inputs.is_empty() {
return Vec::new();
}
if inputs.len() == 1 {
let (label, addr, key) = &inputs[0];
let v = probe_session_liveness(addr, key, connect_timeout, read_timeout);
return vec![(label.clone(), v)];
}
std::thread::scope(|scope| {
let handles: Vec<_> = inputs
.iter()
.map(|(label, addr, key)| {
let label = label.clone();
let addr = addr.clone();
let key = key.clone();
scope.spawn(move || {
let v = probe_session_liveness(&addr, &key, connect_timeout, read_timeout);
(label, v)
})
})
.collect();
handles.into_iter().filter_map(|h| h.join().ok()).collect()
})
}
pub fn remove_session_registry(base: &str) {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return,
};
let port_path = format!("{}\\.psmux\\{}.port", home, base);
remove_session_registry_files(Path::new(&port_path));
}
pub fn send_control(line: String) -> io::Result<()> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let mut target = env::var("PSMUX_TARGET_SESSION").ok().unwrap_or_else(|| "default".to_string());
if is_warm_session(&target) {
let ns = target.strip_suffix("____warm__").map(|s| s.to_string());
target = resolve_last_session_name_ns(ns.as_deref()).unwrap_or_else(|| "default".to_string());
}
let full_target = env::var("PSMUX_TARGET_FULL").ok();
let path = format!("{}\\.psmux\\{}.port", home, target);
let port = std::fs::read_to_string(&path).ok().and_then(|s| s.trim().parse::<u16>().ok()).ok_or_else(|| io::Error::new(io::ErrorKind::Other, format!("no server running on session '{}'", target)))?.clone();
let session_key = read_session_key(&target).unwrap_or_default();
let addr: std::net::SocketAddr = format!("127.0.0.1:{}", port).parse().unwrap();
let mut stream = std::net::TcpStream::connect_timeout(&addr, Duration::from_millis(100))?;
let _ = stream.set_nodelay(true);
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
let _ = write!(stream, "AUTH {}\n", session_key);
if let Some(ref ft) = full_target {
let _ = write!(stream, "TARGET {}\n", ft);
}
let _ = write!(stream, "{}", line);
let _ = stream.flush();
let mut buf = [0u8; 64];
let _ = std::io::Read::read(&mut stream, &mut buf);
Ok(())
}
pub fn send_control_with_response(line: String) -> io::Result<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).unwrap_or_default();
let mut target = env::var("PSMUX_TARGET_SESSION").ok().unwrap_or_else(|| "default".to_string());
if is_warm_session(&target) {
let ns = target.strip_suffix("____warm__").map(|s| s.to_string());
target = resolve_last_session_name_ns(ns.as_deref()).unwrap_or_else(|| "default".to_string());
}
let full_target = env::var("PSMUX_TARGET_FULL").ok();
let path = format!("{}\\.psmux\\{}.port", home, target);
let port = std::fs::read_to_string(&path).ok().and_then(|s| s.trim().parse::<u16>().ok()).ok_or_else(|| io::Error::new(io::ErrorKind::Other, format!("no server running on session '{}'", target)))?.clone();
let session_key = read_session_key(&target).unwrap_or_default();
let addr = format!("127.0.0.1:{}", port);
let mut stream = std::net::TcpStream::connect(&addr)?;
let _ = stream.set_nodelay(true);
let _ = stream.set_read_timeout(Some(Duration::from_millis(2000)));
let _ = write!(stream, "AUTH {}\n", session_key);
if let Some(ref ft) = full_target {
let _ = write!(stream, "TARGET {}\n", ft);
}
let _ = write!(stream, "{}", line);
let _ = stream.flush();
let mut buf = Vec::new();
let mut temp = [0u8; 4096];
loop {
match std::io::Read::read(&mut stream, &mut temp) {
Ok(0) => break,
Ok(n) => buf.extend_from_slice(&temp[..n]),
Err(e) if e.kind() == io::ErrorKind::WouldBlock || e.kind() == io::ErrorKind::TimedOut => break,
Err(_) => break,
}
}
let result = String::from_utf8_lossy(&buf).to_string();
let result = if result.starts_with("OK\n") {
result[3..].to_string()
} else if result.starts_with("OK\r\n") {
result[4..].to_string()
} else {
result
};
Ok(result)
}
pub fn send_control_to_port(port: u16, msg: &str, session_key: &str) -> io::Result<()> {
let addr = format!("127.0.0.1:{}", port);
if let Ok(mut stream) = std::net::TcpStream::connect(&addr) {
let _ = stream.set_nodelay(true);
let _ = write!(stream, "AUTH {}\n", session_key);
let _ = stream.write_all(msg.as_bytes());
let _ = stream.flush();
let mut buf = [0u8; 64];
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
let _ = std::io::Read::read(&mut stream, &mut buf);
}
Ok(())
}
pub fn resolve_last_session_name() -> Option<String> {
resolve_last_session_name_ns(None)
}
pub fn resolve_last_session_name_ns(ns: Option<&str>) -> Option<String> {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let dir = format!("{}\\.psmux", home);
let last = std::fs::read_to_string(format!("{}\\last_session", dir)).ok();
if let Some(name) = last {
let name = name.trim().to_string();
let ns_ok = match ns {
Some(n) => name.starts_with(&format!("{}__", n)),
None => !name.contains("__"),
};
if ns_ok {
let p = format!("{}\\{}.port", dir, name);
if std::path::Path::new(&p).exists() { return Some(name); }
}
}
let mut picks: Vec<(String, std::time::SystemTime)> = Vec::new();
if let Ok(rd) = std::fs::read_dir(&dir) {
for e in rd.flatten() {
if let Some(fname) = e.file_name().to_str() {
if let Some((base, ext)) = fname.rsplit_once('.') {
if ext == "port" { if let Ok(md) = e.metadata() { picks.push((base.to_string(), md.modified().unwrap_or(std::time::SystemTime::UNIX_EPOCH))); } }
}
}
}
}
picks.retain(|(n, _)| !is_warm_session(n));
picks.retain(|(n, _)| match ns {
Some(prefix) => n.starts_with(&format!("{}__", prefix)),
None => !n.contains("__"),
});
picks.sort_by_key(|(_, t)| *t);
picks.last().map(|(n, _)| n.clone())
}
pub fn resolve_default_session_name() -> Option<String> {
if let Ok(name) = env::var("PSMUX_DEFAULT_SESSION") {
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let p = format!("{}\\.psmux\\{}.port", home, name);
if std::path::Path::new(&p).exists() { return Some(name); }
}
let home = env::var("USERPROFILE").or_else(|_| env::var("HOME")).ok()?;
let candidates = [format!("{}\\.psmuxrc", home), format!("{}\\.psmux\\pmuxrc", home)];
for cfg in candidates.iter() {
if let Ok(text) = std::fs::read_to_string(cfg) {
let line = text.lines().find(|l| !l.trim().is_empty())?;
let name = if let Some(rest) = line.strip_prefix("default-session ") { rest.trim().to_string() } else { line.trim().to_string() };
let p = format!("{}\\.psmux\\{}.port", home, name);
if std::path::Path::new(&p).exists() { return Some(name); }
}
}
None
}
pub fn reap_children_placeholder() -> io::Result<bool> { Ok(false) }
pub fn list_session_names() -> Vec<String> {
list_session_names_ns(None)
}
pub fn list_session_names_ns(ns: Option<&str>) -> Vec<String> {
let home = std::env::var("USERPROFILE").or_else(|_| std::env::var("HOME")).unwrap_or_default();
let dir = format!("{}\\.psmux", home);
let mut names = Vec::new();
if let Ok(entries) = std::fs::read_dir(&dir) {
for e in entries.flatten() {
if let Some(fname) = e.file_name().to_str().map(|s| s.to_string()) {
if let Some((base, ext)) = fname.rsplit_once('.') {
if ext == "port" {
if is_warm_session(base) { continue; }
match ns {
Some(prefix) => {
if !base.starts_with(&format!("{}__", prefix)) { continue; }
}
None => {
if base.contains("__") { continue; }
}
}
names.push(base.to_string());
}
}
}
}
}
names.sort();
names
}
#[derive(Clone, Debug)]
pub struct TreeEntry {
pub session_name: String,
pub session_port: u16,
pub is_session_header: bool,
pub window_index: Option<usize>,
pub window_name: String,
pub window_panes: usize,
pub window_size: String,
pub is_current_session: bool,
pub is_active_window: bool,
}
pub fn list_all_sessions_tree(current_session: &str, current_windows: &[(String, usize, String, bool)]) -> Vec<TreeEntry> {
let home = match env::var("USERPROFILE").or_else(|_| env::var("HOME")) {
Ok(h) => h,
Err(_) => return vec![],
};
let psmux_dir = format!("{}\\.psmux", home);
let mut sessions: Vec<(String, u16, std::time::SystemTime)> = Vec::new();
if let Ok(entries) = std::fs::read_dir(&psmux_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().map(|e| e == "port").unwrap_or(false) {
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
if is_warm_session(stem) { continue; }
if let Ok(port_str) = std::fs::read_to_string(&path) {
if let Ok(port) = port_str.trim().parse::<u16>() {
let mtime = entry.metadata()
.and_then(|m| m.modified())
.unwrap_or(std::time::SystemTime::UNIX_EPOCH);
sessions.push((stem.to_string(), port, mtime));
}
}
}
}
}
}
sessions.sort_by_key(|(name, _, _)| name.clone());
let mut tree = Vec::new();
for (name, port, _) in &sessions {
let is_current = name == current_session;
tree.push(TreeEntry {
session_name: name.clone(),
session_port: *port,
is_session_header: true,
window_index: None,
window_name: String::new(),
window_panes: 0,
window_size: String::new(),
is_current_session: is_current,
is_active_window: false,
});
if is_current {
for (i, (wname, panes, size, is_active)) in current_windows.iter().enumerate() {
tree.push(TreeEntry {
session_name: name.clone(),
session_port: *port,
is_session_header: false,
window_index: Some(i),
window_name: wname.clone(),
window_panes: *panes,
window_size: size.clone(),
is_current_session: true,
is_active_window: *is_active,
});
}
} else {
let key = read_session_key(name).unwrap_or_default();
let addr = format!("127.0.0.1:{}", port);
if let Ok(resp) = send_auth_cmd_response(&addr, &key, b"list-windows -F \"#{window_index}:#{window_name}:#{window_panes}:#{window_width}x#{window_height}:#{window_active}\"\n") {
for line in resp.lines() {
let line = line.trim();
if line.is_empty() { continue; }
let parts: Vec<&str> = line.splitn(5, ':').collect();
if parts.len() >= 5 {
let wi = parts[0].parse::<usize>().unwrap_or(0);
let wn = parts[1].to_string();
let wp = parts[2].parse::<usize>().unwrap_or(1);
let ws = parts[3].to_string();
let wa = parts[4] == "1";
tree.push(TreeEntry {
session_name: name.clone(),
session_port: *port,
is_session_header: false,
window_index: Some(wi),
window_name: wn,
window_panes: wp,
window_size: ws,
is_current_session: false,
is_active_window: wa,
});
}
}
}
}
}
tree
}
#[cfg(windows)]
pub fn kill_remaining_server_processes() {
const TH32CS_SNAPPROCESS: u32 = 0x00000002;
const PROCESS_TERMINATE: u32 = 0x0001;
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
const INVALID_HANDLE: isize = -1;
#[repr(C)]
struct PROCESSENTRY32W {
dw_size: u32,
cnt_usage: u32,
th32_process_id: u32,
th32_default_heap_id: usize,
th32_module_id: u32,
cnt_threads: u32,
th32_parent_process_id: u32,
pc_pri_class_base: i32,
dw_flags: u32,
sz_exe_file: [u16; 260],
}
#[link(name = "kernel32")]
extern "system" {
fn CreateToolhelp32Snapshot(dw_flags: u32, th32_process_id: u32) -> isize;
fn Process32FirstW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn Process32NextW(h_snapshot: isize, lppe: *mut PROCESSENTRY32W) -> i32;
fn OpenProcess(desired_access: u32, inherit_handle: i32, process_id: u32) -> isize;
fn TerminateProcess(h_process: isize, exit_code: u32) -> i32;
fn CloseHandle(handle: isize) -> i32;
}
let my_pid = std::process::id();
unsafe {
let snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if snap == INVALID_HANDLE || snap == 0 { return; }
let mut pe: PROCESSENTRY32W = std::mem::zeroed();
pe.dw_size = std::mem::size_of::<PROCESSENTRY32W>() as u32;
let target_names: &[&str] = &["psmux.exe", "pmux.exe", "tmux.exe"];
let mut pids_to_kill: Vec<u32> = Vec::new();
if Process32FirstW(snap, &mut pe) != 0 {
loop {
let pid = pe.th32_process_id;
if pid != my_pid {
let len = pe.sz_exe_file.iter().position(|&c| c == 0).unwrap_or(260);
let name = String::from_utf16_lossy(&pe.sz_exe_file[..len]);
let name_lower = name.to_lowercase();
for target in target_names {
if name_lower == *target || name_lower.ends_with(&format!("\\{}", target)) {
pids_to_kill.push(pid);
break;
}
}
}
if Process32NextW(snap, &mut pe) == 0 { break; }
}
}
CloseHandle(snap);
for pid in &pids_to_kill {
let h = OpenProcess(PROCESS_TERMINATE | PROCESS_QUERY_LIMITED_INFORMATION, 0, *pid);
if h != 0 && h != INVALID_HANDLE {
let _ = TerminateProcess(h, 1);
CloseHandle(h);
}
}
}
}
#[cfg(not(windows))]
pub fn kill_remaining_server_processes() {
let _ = std::process::Command::new("pkill")
.args(&["-f", "psmux|pmux"])
.status();
}
#[cfg(test)]
#[path = "../tests-rs/test_session.rs"]
mod tests;
#[cfg(test)]
#[path = "../tests-rs/test_issue250_root_cause.rs"]
mod tests_issue250_root_cause;