diff --git a/src/platform/fallback.rs b/src/platform/fallback.rs index a04b7f182..6493b1f72 100644 --- a/src/platform/fallback.rs +++ b/src/platform/fallback.rs @@ -3,11 +3,11 @@ use std::process::Command; use super::{ClipboardImage, ForegroundJob, Signal}; -pub(crate) fn server_lifecycle_identity(path: &std::path::Path) -> std::io::Result> { - Ok(std::path::absolute(path)? - .as_os_str() - .as_encoded_bytes() - .to_vec()) +pub(crate) fn server_lifecycle_identity( + path: &std::path::Path, +) -> std::io::Result<(Vec, std::path::PathBuf)> { + let path = std::path::absolute(path)?; + Ok((path.as_os_str().as_encoded_bytes().to_vec(), path)) } #[cfg(unix)] diff --git a/src/platform/unix_common.rs b/src/platform/unix_common.rs index 5ec63f6ad..ea1586768 100644 --- a/src/platform/unix_common.rs +++ b/src/platform/unix_common.rs @@ -493,9 +493,38 @@ pub(crate) fn create_remote_ssh_config_file(path: &Path) -> std::io::Result std::io::Result> { +pub(crate) fn server_lifecycle_identity(path: &Path) -> std::io::Result<(Vec, PathBuf)> { let path = std::path::absolute(path)?; - Ok(path.as_os_str().as_encoded_bytes().to_vec()) + let name = path.file_name().ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "invalid server endpoint path", + ) + })?; + let mut resolved = PathBuf::new(); + for component in path.parent().into_iter().flat_map(Path::components) { + if component == std::path::Component::ParentDir { + resolved.pop(); + continue; + } + resolved.push(component.as_os_str()); + match resolved.canonicalize() { + Ok(path) => resolved = path, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + match std::fs::symlink_metadata(&resolved) { + Ok(metadata) if metadata.file_type().is_symlink() => return Err(error), + Err(error) if error.kind() != std::io::ErrorKind::NotFound => { + return Err(error) + } + _ => {} + } + } + Err(error) => return Err(error), + } + } + // Keep the literal socket name stable after shutdown unlinks it. + resolved.push(name); + Ok((resolved.as_os_str().as_encoded_bytes().to_vec(), resolved)) } pub(crate) fn create_remote_private_dir(path: &Path) -> std::io::Result<()> { diff --git a/src/platform/windows.rs b/src/platform/windows.rs index a9337a661..3494a233f 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -3466,7 +3466,9 @@ impl Drop for InputSourceRestore { } } -pub(crate) fn server_lifecycle_identity(path: &std::path::Path) -> std::io::Result> { +pub(crate) fn server_lifecycle_identity( + path: &std::path::Path, +) -> std::io::Result<(Vec, std::path::PathBuf)> { use std::os::windows::ffi::OsStrExt as _; use windows_sys::Win32::Globalization::{ LCMapStringEx, LCMAP_UPPERCASE, LOCALE_NAME_INVARIANT, @@ -3507,7 +3509,10 @@ pub(crate) fn server_lifecycle_identity(path: &std::path::Path) -> std::io::Resu if written == 0 { return Err(std::io::Error::last_os_error()); } - Ok(normalized.into_iter().flat_map(u16::to_le_bytes).collect()) + Ok(( + normalized.into_iter().flat_map(u16::to_le_bytes).collect(), + std::path::absolute(path)?, + )) } #[cfg(test)] diff --git a/src/server/lifecycle.rs b/src/server/lifecycle.rs index 4bb465400..ed10231cf 100644 --- a/src/server/lifecycle.rs +++ b/src/server/lifecycle.rs @@ -26,9 +26,8 @@ pub(super) fn receipt_path(api_path: &Path) -> io::Result { } fn sidecar_path(api_path: &Path, suffix: &str) -> io::Result { - let identity = crate::platform::server_lifecycle_identity(api_path)?; + let (identity, absolute) = crate::platform::server_lifecycle_identity(api_path)?; let key = format!("{:x}", Sha256::digest(identity)); - let absolute = std::path::absolute(api_path)?; let parent = absolute.parent().ok_or_else(|| { io::Error::new(io::ErrorKind::InvalidInput, "invalid server endpoint path") })?; @@ -185,26 +184,36 @@ mod tests { .unwrap() .as_nanos() )); + #[cfg(unix)] + { + std::fs::create_dir_all(base.join("hop")).unwrap(); + std::os::unix::fs::symlink(&base, base.join("link")).unwrap(); + } for path in [ base.join("server.sock"), base.join("sessions/named/server.sock"), ] { #[cfg(windows)] - let alias = PathBuf::from(path.to_string_lossy().to_uppercase()); + let aliases = vec![PathBuf::from(path.to_string_lossy().to_uppercase())]; #[cfg(not(windows))] - let alias = path - .parent() - .unwrap() - .join(".") - .join(path.file_name().unwrap()); + let aliases = vec![ + path.parent() + .unwrap() + .join(".") + .join(path.file_name().unwrap()), + base.join("link/hop/..") + .join(path.strip_prefix(&base).unwrap()), + ]; let mut guard = lock(&path).unwrap(); guard.commit(false).unwrap(); - assert_eq!( - lock(&alias).err().unwrap().kind(), - io::ErrorKind::WouldBlock - ); + for alias in &aliases { + assert_eq!(lock(alias).err().unwrap().kind(), io::ErrorKind::WouldBlock); + assert_eq!(read(alias).unwrap().epoch, guard.receipt.epoch); + #[cfg(unix)] + assert_eq!(receipt_path(alias).unwrap(), receipt_path(&path).unwrap()); + } let waiter = std::thread::spawn({ - let alias = alias.clone(); + let alias = aliases.last().unwrap().clone(); move || lock_for_start(&alias).unwrap() }); guard.commit(true).unwrap(); @@ -213,6 +222,14 @@ mod tests { assert!(admitted.receipt.stopped); assert!(admitted.require_epoch(1).is_err()); drop(admitted); + if path.parent().unwrap().file_name().unwrap() == "named" { + let receipt = receipt_path(&path).unwrap(); + assert!(!path.parent().unwrap().exists()); + std::fs::create_dir(path.parent().unwrap()).unwrap(); + assert_eq!(receipt_path(&path).unwrap(), receipt); + std::fs::remove_dir(path.parent().unwrap()).unwrap(); + assert_eq!(receipt_path(&path).unwrap(), receipt); + } let starts = std::thread::scope(|scope| { let attempt = || { let mut guard = lock_for_start(&path).unwrap(); @@ -225,6 +242,15 @@ mod tests { assert_eq!(starts.iter().filter(|result| result.is_ok()).count(), 1); remove_test_receipt(&path); } + #[cfg(unix)] + { + std::os::unix::fs::symlink(base.join("absent"), base.join("dangling")).unwrap(); + std::os::unix::fs::symlink("loop", base.join("loop")).unwrap(); + for directory in ["dangling", "loop"] { + assert!(lock(&base.join(directory).join("server.sock")).is_err()); + } + assert!(!base.join("absent").exists()); + } std::fs::remove_dir_all(base).unwrap(); } }