use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::{Duration, Instant};
pub(crate) const DAEMON_QUERY_DEADLINE: Duration = Duration::from_secs(2);
pub(crate) use crate::session_picker_mux::{
MuxDirectory, MuxSessionRow, MuxWorkspaceRow, daemon_name_of,
};
pub(crate) fn default_socket_dir() -> Option<PathBuf> {
par_term_emu_core_rust::mux::ipc::default_socket_path("x")
.parent()
.map(Path::to_path_buf)
}
pub(crate) fn socket_files(dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut sockets: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| daemon_name_of(p).is_some())
.collect();
sockets.sort();
sockets
}
fn query_daemon(socket: &Path) -> Result<MuxDirectory, String> {
let daemon = daemon_name_of(socket).unwrap_or_default();
let mut client = par_term_mux::MuxSessionClient::connect(socket)
.map_err(|e| format!("par-mux daemon '{daemon}': {e}"))?;
let workspaces = match client.list_workspaces() {
Ok(rows) => rows,
Err(e) if is_unknown_command(&e.to_string()) => Vec::new(),
Err(e) => return Err(format!("par-mux daemon '{daemon}': {e}")),
};
let workspaces = workspaces
.into_iter()
.map(|w| MuxWorkspaceRow {
socket: socket.to_path_buf(),
daemon: daemon.clone(),
id: w.id,
name: w.name,
active: w.active,
})
.collect();
let sessions = client
.list_sessions()
.map_err(|e| format!("par-mux daemon '{daemon}': {e}"))?
.into_iter()
.map(|s| MuxSessionRow {
socket: socket.to_path_buf(),
daemon: daemon.clone(),
id: s.id,
name: s.name,
workspace_name: s.workspace_name,
})
.collect();
Ok(MuxDirectory {
sessions,
workspaces,
errors: Vec::new(),
})
}
pub(crate) fn scan(dir: &Path, deadline: Duration) -> MuxDirectory {
let sockets = socket_files(dir);
let (tx, rx) = mpsc::channel();
for socket in &sockets {
let tx = tx.clone();
let socket = socket.clone();
let spawned = std::thread::Builder::new()
.name("mux-directory".into())
.spawn(move || {
let _ = tx.send((socket.clone(), query_daemon(&socket)));
});
if spawned.is_err() {
log::warn!("mux directory: could not start a query thread");
}
}
drop(tx);
let mut dir_out = MuxDirectory::default();
let mut pending: Vec<PathBuf> = sockets.clone();
let end = Instant::now() + deadline;
while !pending.is_empty() {
let left = end.saturating_duration_since(Instant::now());
match rx.recv_timeout(left) {
Ok((socket, result)) => {
pending.retain(|p| p != &socket);
match result {
Ok(rows) => {
dir_out.sessions.extend(rows.sessions);
dir_out.workspaces.extend(rows.workspaces);
}
Err(e) if is_dead_socket(&e) => {}
Err(e) => dir_out.errors.push(e),
}
}
Err(_) => break,
}
}
for socket in pending {
let daemon = daemon_name_of(&socket).unwrap_or_default();
dir_out
.errors
.push(format!("par-mux daemon '{daemon}' did not answer"));
}
dir_out
.sessions
.sort_by(|a, b| (&a.daemon, &a.name).cmp(&(&b.daemon, &b.name)));
dir_out
}
fn is_dead_socket(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
lower.contains("connection refused")
|| lower.contains("no such file")
|| lower.contains("not found")
}
fn is_unknown_command(message: &str) -> bool {
message.contains("unknown command:")
}
pub(crate) struct PendingScan {
rx: mpsc::Receiver<MuxDirectory>,
}
impl PendingScan {
pub(crate) fn start(dir: PathBuf) -> Option<Self> {
let (tx, rx) = mpsc::channel();
std::thread::Builder::new()
.name("mux-directory-scan".into())
.spawn(move || {
let _ = tx.send(scan(&dir, DAEMON_QUERY_DEADLINE));
})
.ok()?;
Some(Self { rx })
}
pub(crate) fn poll(&self) -> Option<Result<MuxDirectory, ()>> {
match self.rx.try_recv() {
Ok(dir) => Some(Ok(dir)),
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => Some(Err(())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_missing_directory_scans_empty() {
let dir = std::env::temp_dir().join(format!("par-term-no-mux-{}", std::process::id()));
assert_eq!(
scan(&dir, Duration::from_millis(200)),
MuxDirectory::default()
);
}
}