use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Resolution {
Remote(String),
UnixSocket(PathBuf),
FileRead(PathBuf),
SpawnLocal,
}
pub fn resolve(
remote: Option<&str>,
socket: Option<&Path>,
status_file: Option<&Path>,
) -> Resolution {
if let Some(url) = remote {
return Resolution::Remote(url.to_string());
}
if let Some(sock) = socket {
return Resolution::UnixSocket(sock.to_path_buf());
}
if let Some(sf) = status_file {
return Resolution::FileRead(sf.to_path_buf());
}
Resolution::SpawnLocal
}
pub fn conventional_socket_path(repo_root: &Path) -> PathBuf {
let key = std::fs::canonicalize(repo_root).unwrap_or_else(|_| repo_root.to_path_buf());
let mut h = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut h);
let tag = format!("{:016x}", h.finish());
std::env::temp_dir().join(format!("cargoless-{tag}.sock"))
}
#[cfg(unix)]
fn socket_is_live(path: &Path) -> bool {
std::os::unix::net::UnixStream::connect(path).is_ok()
}
#[cfg(not(unix))]
fn socket_is_live(_path: &Path) -> bool {
false
}
pub fn discover(remote: Option<&str>, repo_root: &Path, status_file: &Path) -> Resolution {
if let Some(url) = remote {
return Resolution::Remote(url.to_string());
}
let sock = conventional_socket_path(repo_root);
let sock_opt = if socket_is_live(&sock) {
Some(sock.clone())
} else {
None
};
let sf_opt = if status_file.exists() {
Some(status_file.to_path_buf())
} else {
None
};
resolve(remote, sock_opt.as_deref(), sf_opt.as_deref())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn precedence_remote_beats_everything() {
assert_eq!(
resolve(
Some("http://host:8080"),
Some(Path::new("/tmp/x.sock")),
Some(Path::new("/p/.cargoless/cli-status")),
),
Resolution::Remote("http://host:8080".into())
);
}
#[test]
fn precedence_socket_beats_file_and_spawn() {
assert_eq!(
resolve(
None,
Some(Path::new("/tmp/x.sock")),
Some(Path::new("/p/.cargoless/cli-status")),
),
Resolution::UnixSocket(PathBuf::from("/tmp/x.sock"))
);
}
#[test]
fn precedence_file_when_no_socket_is_the_v0_fallback() {
assert_eq!(
resolve(None, None, Some(Path::new("/p/.cargoless/cli-status"))),
Resolution::FileRead(PathBuf::from("/p/.cargoless/cli-status"))
);
}
#[test]
fn precedence_spawn_local_when_nothing_present() {
assert_eq!(resolve(None, None, None), Resolution::SpawnLocal);
}
#[test]
fn conventional_socket_is_stable_and_repo_distinct() {
let a1 = conventional_socket_path(Path::new("/repos/alpha"));
let a2 = conventional_socket_path(Path::new("/repos/alpha"));
let b = conventional_socket_path(Path::new("/repos/beta"));
assert_eq!(a1, a2, "same repo path must yield the same socket");
assert_ne!(a1, b, "different repos must not collide");
let name = a1.file_name().unwrap().to_string_lossy();
assert!(
name.starts_with("cargoless-") && name.ends_with(".sock"),
"conventional name shape: {name}"
);
}
#[test]
fn discover_remote_short_circuits_without_probing() {
assert_eq!(
discover(
Some("http://h:1"),
Path::new("/nonexistent/repo"),
Path::new("/nonexistent/.cargoless/cli-status"),
),
Resolution::Remote("http://h:1".into())
);
}
#[test]
fn discover_spawn_local_when_clean_slate() {
let mut root = std::env::temp_dir();
root.push(format!("cargoless-disc-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).unwrap();
let sf = root.join(".cargoless").join("cli-status");
assert_eq!(
discover(None, &root, &sf),
Resolution::SpawnLocal,
"no socket + no status file ⇒ spawn local"
);
std::fs::create_dir_all(sf.parent().unwrap()).unwrap();
std::fs::write(&sf, "schema=2\nverdict=green\n").unwrap();
assert_eq!(discover(None, &root, &sf), Resolution::FileRead(sf.clone()));
let _ = std::fs::remove_dir_all(&root);
}
#[cfg(unix)]
fn unique_repo(tag: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!(
"cargoless-185-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&p).unwrap();
p
}
#[cfg(unix)]
#[test]
fn stale_socket_inode_falls_through_to_fileread_not_unix_socket() {
let repo = unique_repo("stale");
let sock = conventional_socket_path(&repo);
let _ = std::fs::remove_file(&sock);
{
let _l = std::os::unix::net::UnixListener::bind(&sock).expect("bind");
}
assert!(sock.exists(), "stale inode persists after listener drop");
let sf = repo.join(".cargoless").join("cli-status");
std::fs::create_dir_all(sf.parent().unwrap()).unwrap();
std::fs::write(&sf, "schema=2\nverdict=green\n").unwrap();
assert_eq!(
discover(None, &repo, &sf),
Resolution::FileRead(sf.clone()),
"stale socket (exists but no listener) must fall through to FileRead, \
never resolve a dead UnixSocket (#185 / #128-#129 class)"
);
let _ = std::fs::remove_file(&sock);
let _ = std::fs::remove_dir_all(&repo);
}
#[cfg(unix)]
#[test]
fn live_listener_resolves_unix_socket() {
let repo = unique_repo("live");
let sock = conventional_socket_path(&repo);
let _ = std::fs::remove_file(&sock);
let _l = std::os::unix::net::UnixListener::bind(&sock).expect("bind");
let sf = repo.join(".cargoless").join("cli-status");
std::fs::create_dir_all(sf.parent().unwrap()).unwrap();
std::fs::write(&sf, "schema=2\nverdict=green\n").unwrap();
assert_eq!(
discover(None, &repo, &sf),
Resolution::UnixSocket(sock.clone()),
"a live listener must still resolve UnixSocket (no happy-path regression)"
);
drop(_l);
let _ = std::fs::remove_file(&sock);
let _ = std::fs::remove_dir_all(&repo);
}
}