fix: share stop state across Unix endpoint aliases

refs #4291
This commit is contained in:
Jonathan Liebig
2026-10-28 02:11:51 +01:00
parent c6130f3344
commit c7afa20122
4 changed files with 82 additions and 22 deletions
+5 -5
View File
@@ -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<Vec<u8>> {
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<u8>, std::path::PathBuf)> {
let path = std::path::absolute(path)?;
Ok((path.as_os_str().as_encoded_bytes().to_vec(), path))
}
#[cfg(unix)]
+31 -2
View File
@@ -493,9 +493,38 @@ pub(crate) fn create_remote_ssh_config_file(path: &Path) -> std::io::Result<std:
.open(path)
}
pub(crate) fn server_lifecycle_identity(path: &Path) -> std::io::Result<Vec<u8>> {
pub(crate) fn server_lifecycle_identity(path: &Path) -> std::io::Result<(Vec<u8>, 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<()> {
+7 -2
View File
@@ -3466,7 +3466,9 @@ impl Drop for InputSourceRestore {
}
}
pub(crate) fn server_lifecycle_identity(path: &std::path::Path) -> std::io::Result<Vec<u8>> {
pub(crate) fn server_lifecycle_identity(
path: &std::path::Path,
) -> std::io::Result<(Vec<u8>, 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)]
+39 -13
View File
@@ -26,9 +26,8 @@ pub(super) fn receipt_path(api_path: &Path) -> io::Result<PathBuf> {
}
fn sidecar_path(api_path: &Path, suffix: &str) -> io::Result<PathBuf> {
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();
}
}