use std::fs::File;
use std::io;
use std::path::PathBuf;
use std::time::{Duration, Instant};
use crate::platform::ipc::Listener;
const STALE_RECOVERY_LOCK_TIMEOUT: Duration = Duration::from_secs(1);
const STALE_RECOVERY_LOCK_POLL: Duration = Duration::from_millis(10);
pub fn resolve_socket_path(bare_name: &str) -> Result<String, String> {
crate::platform::ipc::broker_endpoint_name(bare_name, false).map_err(|error| error.to_string())
}
pub fn resolve_path_scoped_socket_path(bare_name: &str) -> Result<String, String> {
crate::platform::ipc::broker_endpoint_name(bare_name, true).map_err(|error| error.to_string())
}
pub fn is_already_bound_error(err: &io::Error) -> bool {
matches!(
err.kind(),
io::ErrorKind::AddrInUse | io::ErrorKind::WouldBlock | io::ErrorKind::PermissionDenied,
)
}
pub fn unix_socket_path_is_stale(socket_path: &str) -> bool {
crate::platform::ipc::Endpoint::new(socket_path.to_owned())
.map(|endpoint| endpoint.is_stale())
.unwrap_or(false)
}
pub fn wrap_socket_name(socket_path: &str) -> Result<interprocess::local_socket::Name<'_>, String> {
running_process_platform_internal::legacy_ipc_name(socket_path)
}
#[derive(Debug)]
pub enum BindSingletonError {
InvalidName(String),
AlreadyBound(io::Error),
Other(io::Error),
}
pub fn bind_singleton(socket_path: &str) -> Result<Listener, BindSingletonError> {
let endpoint = crate::platform::ipc::Endpoint::new(socket_path.to_owned())
.map_err(|error| BindSingletonError::InvalidName(error.to_string()))?;
bind_singleton_with_endpoint(&endpoint, || Listener::bind(&endpoint))
}
pub fn bind_singleton_with<T, F>(socket_path: &str, bind: F) -> Result<T, BindSingletonError>
where
F: FnMut() -> io::Result<T>,
{
let endpoint = crate::platform::ipc::Endpoint::new(socket_path.to_owned())
.map_err(|error| BindSingletonError::InvalidName(error.to_string()))?;
bind_singleton_with_endpoint(&endpoint, bind)
}
fn bind_singleton_with_endpoint<T, F>(
endpoint: &crate::platform::ipc::Endpoint,
mut bind: F,
) -> Result<T, BindSingletonError>
where
F: FnMut() -> io::Result<T>,
{
endpoint
.ensure_parent_exists()
.map_err(BindSingletonError::Other)?;
let mut listener_result = bind();
if let Err(err) = &listener_result {
if is_already_bound_error(err) && endpoint.is_stale() {
listener_result = recover_stale_endpoint(endpoint, &mut bind);
}
}
listener_result.map_err(|err| {
if is_already_bound_error(&err) {
BindSingletonError::AlreadyBound(err)
} else {
BindSingletonError::Other(err)
}
})
}
fn recover_stale_endpoint<T, F>(
endpoint: &crate::platform::ipc::Endpoint,
bind: &mut F,
) -> io::Result<T>
where
F: FnMut() -> io::Result<T>,
{
let _guard = StaleRecoveryLock::acquire(endpoint.display())?;
match bind() {
Ok(listener) => Ok(listener),
Err(error) if is_already_bound_error(&error) && endpoint.is_stale() => {
endpoint.retire()?;
bind()
}
Err(error) => Err(error),
}
}
struct StaleRecoveryLock(File);
impl StaleRecoveryLock {
fn acquire(socket_path: &str) -> io::Result<Self> {
let lock_path = stale_recovery_lock_path(socket_path);
let file = crate::platform::fs::open_lock_file(&lock_path)?;
let deadline = Instant::now() + STALE_RECOVERY_LOCK_TIMEOUT;
loop {
match crate::platform::fs::try_lock_exclusive(&file) {
Ok(()) => return Ok(Self(file)),
Err(error)
if crate::platform::fs::is_lock_conflict(&error)
&& Instant::now() < deadline =>
{
std::thread::sleep(STALE_RECOVERY_LOCK_POLL);
}
Err(error) => return Err(error),
}
}
}
}
impl Drop for StaleRecoveryLock {
fn drop(&mut self) {
let _ = crate::platform::fs::unlock(&self.0);
}
}
fn stale_recovery_lock_path(socket_path: &str) -> PathBuf {
let mut lock_path = std::ffi::OsString::from(socket_path);
lock_path.push(".bind.lock");
PathBuf::from(lock_path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_socket_path_produces_a_nonempty_path() {
let path = resolve_socket_path("rpb-v2-test-singleton-bind").expect("resolve");
assert!(!path.is_empty());
}
#[test]
fn path_scoped_socket_does_not_add_a_user_runtime_directory() {
let first = resolve_path_scoped_socket_path("rpb-v2-program-0123456789abcdef-0")
.expect("resolve path-scoped endpoint");
let again = resolve_path_scoped_socket_path("rpb-v2-program-0123456789abcdef-0")
.expect("resolve stable endpoint");
assert_eq!(first, again);
if crate::platform::ipc::endpoint_is_filesystem_backed() {
assert_eq!(
std::path::Path::new(&first).parent(),
Some(std::path::Path::new("/tmp"))
);
}
}
#[test]
fn is_already_bound_error_classifies_expected_kinds() {
assert!(is_already_bound_error(&io::Error::from(
io::ErrorKind::AddrInUse
)));
assert!(is_already_bound_error(&io::Error::from(
io::ErrorKind::WouldBlock
)));
assert!(is_already_bound_error(&io::Error::from(
io::ErrorKind::PermissionDenied
)));
assert!(!is_already_bound_error(&io::Error::from(
io::ErrorKind::NotFound
)));
}
#[test]
fn bind_singleton_binds_once_and_refuses_a_second_bind() {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let socket_path = resolve_socket_path(&format!(
"rpb-v2-test-singleton-bind-{:010x}",
nonce & 0xFF_FFFF_FFFF
))
.expect("resolve");
let _first = bind_singleton(&socket_path).expect("first bind must succeed");
let second = bind_singleton(&socket_path);
assert!(
matches!(second, Err(BindSingletonError::AlreadyBound(_))),
"second bind at the same path must be refused as AlreadyBound, got {second:?}"
);
}
#[test]
fn stale_endpoint_n_way_recovery_has_exactly_one_winner() {
use std::sync::{mpsc, Arc, Barrier};
const CONTENDERS: usize = 16;
if !crate::platform::ipc::endpoint_is_filesystem_backed() {
return;
}
let temp = tempfile::tempdir().expect("tempdir");
let socket_path = temp.path().join("stale.sock");
let socket_path = socket_path.to_string_lossy().into_owned();
let endpoint = crate::platform::ipc::Endpoint::new(socket_path.clone())
.expect("construct stale endpoint");
let mut stale_listener = Listener::bind(&endpoint).expect("seed stale endpoint");
stale_listener.do_not_reclaim_name_on_drop();
drop(stale_listener);
assert!(endpoint.is_stale(), "seeded endpoint must be stale");
let start = Arc::new(Barrier::new(CONTENDERS));
let release = Arc::new(Barrier::new(CONTENDERS + 1));
let (send, receive) = mpsc::channel();
let threads: Vec<_> = (0..CONTENDERS)
.map(|_| {
let socket_path = socket_path.clone();
let start = Arc::clone(&start);
let release = Arc::clone(&release);
let send = send.clone();
std::thread::spawn(move || {
start.wait();
let listener = bind_singleton_with(&socket_path, || {
let endpoint = crate::platform::ipc::Endpoint::new(socket_path.clone())?;
Listener::bind(&endpoint)
});
send.send(listener.is_ok()).expect("send bind result");
release.wait();
drop(listener);
})
})
.collect();
drop(send);
let results: Vec<_> = receive.iter().take(CONTENDERS).collect();
assert_eq!(
results.iter().filter(|won| **won).count(),
1,
"stale recovery must not unlink a newly bound winner: {results:?}"
);
release.wait();
for thread in threads {
thread.join().expect("bind contender");
}
}
}