use std::path::PathBuf;
fn victauri_base_dir() -> PathBuf {
std::env::temp_dir().join("victauri")
}
#[cfg(unix)]
fn dir_is_trusted(path: &std::path::Path) -> bool {
use std::os::unix::fs::{MetadataExt, PermissionsExt};
let Ok(meta) = std::fs::symlink_metadata(path) else {
return false;
};
if !meta.file_type().is_dir() {
return false; }
let Some(euid) = current_euid() else {
return false; };
meta.uid() == euid && (meta.permissions().mode() & 0o022) == 0
}
#[cfg(unix)]
fn current_euid() -> Option<u32> {
use std::os::unix::fs::MetadataExt;
let probe = std::env::temp_dir().join(format!(".victauri_uidprobe_{}", std::process::id()));
std::fs::write(&probe, b"").ok()?;
let uid = match std::fs::metadata(&probe) {
Ok(m) => Some(m.uid()),
Err(_) => None,
};
let _ = std::fs::remove_file(&probe);
uid
}
#[cfg(not(unix))]
fn dir_is_trusted(_path: &std::path::Path) -> bool {
true
}
pub fn scan_discovery_dirs_for_port() -> Option<u16> {
let servers = find_live_servers();
if servers.len() == 1 {
return Some(servers[0].port);
}
None
}
pub fn scan_discovery_dirs_for_token() -> Option<String> {
let servers = find_live_servers();
if servers.len() == 1 {
return servers[0].token.clone();
}
None
}
#[derive(Debug, Clone)]
pub enum DiscoveryStatus {
Live,
Stale {
stale: Vec<(u32, u16)>,
},
None,
}
impl DiscoveryStatus {
#[must_use]
pub fn hint(&self) -> Option<String> {
match self {
Self::Live => None,
Self::Stale { stale } => {
let detail = stale
.iter()
.map(|(pid, port)| format!("PID {pid} on port {port}"))
.collect::<Vec<_>>()
.join(", ");
Some(format!(
"A Victauri app was running ({detail}) but its server is now unreachable — \
the app process has exited. It most likely crashed, was closed, or its \
backend is mid-rebuild. Victauri runs inside the app, so it cannot report \
build/compile status itself: check your build or dev-server terminal, then \
relaunch the app and retry."
))
}
Self::None => Some(
"No Victauri server discovery files were found. Either the app is not running, \
or it is a release build (Victauri is enabled only in debug builds via \
#[cfg(debug_assertions)]). Start the app in a debug/dev build and retry."
.to_string(),
),
}
}
}
#[must_use]
pub fn diagnose_discovery() -> DiscoveryStatus {
let base = victauri_base_dir();
let Ok(entries) = std::fs::read_dir(&base) else {
return DiscoveryStatus::None;
};
let mut stale = Vec::new();
let mut any_dir = false;
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(pid) = path
.file_name()
.and_then(|n| n.to_str())
.and_then(|s| s.parse::<u32>().ok())
else {
continue;
};
if !dir_is_trusted(&path) {
continue;
}
let Ok(port_str) = std::fs::read_to_string(path.join("port")) else {
continue;
};
let Ok(port) = port_str.trim().parse::<u16>() else {
continue;
};
any_dir = true;
if std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], port)),
std::time::Duration::from_millis(100),
)
.is_ok()
{
return DiscoveryStatus::Live;
}
stale.push((pid, port));
}
if any_dir {
DiscoveryStatus::Stale { stale }
} else {
DiscoveryStatus::None
}
}
struct DiscoveredServer {
port: u16,
token: Option<String>,
}
fn find_live_servers() -> Vec<DiscoveredServer> {
let base = victauri_base_dir();
let Ok(entries) = std::fs::read_dir(&base) else {
return Vec::new();
};
let mut servers = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(pid_str) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
if pid_str.parse::<u32>().is_err() {
continue;
}
if !dir_is_trusted(&path) {
continue;
}
let port_path = path.join("port");
let Ok(port_str) = std::fs::read_to_string(&port_path) else {
continue;
};
let Ok(port) = port_str.trim().parse::<u16>() else {
continue;
};
if std::net::TcpStream::connect_timeout(
&std::net::SocketAddr::from(([127, 0, 0, 1], port)),
std::time::Duration::from_millis(100),
)
.is_err()
{
let _ = std::fs::remove_dir_all(&path);
continue;
}
let token = std::fs::read_to_string(path.join("token"))
.ok()
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty());
servers.push(DiscoveredServer { port, token });
}
servers
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn live_status_has_no_hint() {
assert!(DiscoveryStatus::Live.hint().is_none());
}
#[test]
fn stale_status_names_pid_and_port() {
let hint = DiscoveryStatus::Stale {
stale: vec![(1234, 7374)],
}
.hint()
.expect("stale has a hint");
assert!(hint.contains("1234"), "hint names the PID: {hint}");
assert!(hint.contains("7374"), "hint names the port: {hint}");
assert!(
hint.contains("crashed") || hint.contains("rebuild"),
"hint explains the likely cause: {hint}"
);
}
#[test]
fn none_status_mentions_debug_build() {
let hint = DiscoveryStatus::None.hint().expect("none has a hint");
assert!(
hint.contains("debug") || hint.contains("not running"),
"hint explains app-not-running / release-build: {hint}"
);
}
}