rmux-server 0.9.0

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use std::path::PathBuf;

#[cfg(unix)]
use std::path::Path;

#[cfg(unix)]
use crate::unix_socket::SocketFileIdentity;

#[cfg(unix)]
pub(crate) struct SocketCleanup {
    socket_path: PathBuf,
    socket_identity: Option<SocketFileIdentity>,
    armed: bool,
}

#[cfg(unix)]
impl SocketCleanup {
    pub(crate) fn new(socket_path: PathBuf, socket_identity: Option<SocketFileIdentity>) -> Self {
        let socket_identity =
            socket_identity.or_else(|| crate::unix_socket::socket_file_identity(&socket_path).ok());
        Self {
            socket_path,
            socket_identity,
            armed: true,
        }
    }

    pub(crate) fn socket_identity(&self) -> Option<SocketFileIdentity> {
        self.socket_identity
    }

    pub(crate) fn update_socket_identity(&mut self, socket_identity: Option<SocketFileIdentity>) {
        self.socket_identity = socket_identity;
        self.armed = true;
    }

    pub(crate) fn cleanup_now(&mut self) {
        if !self.armed {
            return;
        }
        self.armed = false;
        cleanup_socket_artifacts(&self.socket_path, self.socket_identity.take());
    }
}

#[cfg(unix)]
impl Drop for SocketCleanup {
    fn drop(&mut self) {
        self.cleanup_now();
    }
}

#[cfg(windows)]
pub(crate) struct SocketCleanup;

#[cfg(windows)]
impl SocketCleanup {
    pub(crate) fn new(_socket_path: PathBuf) -> Self {
        Self
    }

    pub(crate) fn cleanup_now(&mut self) {}
}

#[cfg(unix)]
fn cleanup_socket_artifacts(socket_path: &Path, socket_identity: Option<SocketFileIdentity>) {
    if !release_socket_generation(socket_path, socket_identity) {
        return;
    }
    for lock_path in startup_lock_paths(socket_path) {
        let _ = remove_regular_file_if_present(&lock_path);
    }
    crate::tmux_shim::cleanup_tmux_shim(socket_path);
}

#[cfg(unix)]
fn release_socket_generation(
    socket_path: &Path,
    socket_identity: Option<SocketFileIdentity>,
) -> bool {
    let Some(socket_identity) = socket_identity else {
        return socket_path.as_os_str().is_empty() || socket_path_is_absent(socket_path);
    };

    match crate::unix_socket::remove_socket_file_if_identity_matches(socket_path, socket_identity) {
        Ok(true) => true,
        Ok(false) => socket_path_is_absent(socket_path),
        Err(_) => false,
    }
}

#[cfg(unix)]
fn socket_path_is_absent(socket_path: &Path) -> bool {
    matches!(
        std::fs::symlink_metadata(socket_path),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound
    )
}

#[cfg(unix)]
fn startup_lock_paths(socket_path: &Path) -> Vec<PathBuf> {
    let Some(parent) = socket_path.parent() else {
        return Vec::new();
    };
    let Some(file_name) = socket_path.file_name() else {
        return Vec::new();
    };

    let mut startup_lock_name = file_name.to_os_string();
    startup_lock_name.push(".startup-lock");
    let mut legacy_lock_name = file_name.to_os_string();
    legacy_lock_name.push(".lock");

    vec![
        parent.join(startup_lock_name),
        parent.join(legacy_lock_name),
    ]
}

#[cfg(unix)]
fn remove_regular_file_if_present(path: &Path) -> std::io::Result<()> {
    match std::fs::symlink_metadata(path) {
        Ok(metadata) if metadata.file_type().is_file() => std::fs::remove_file(path),
        Ok(_) => Ok(()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(error),
    }
}

#[cfg(all(test, unix))]
mod tests {
    use super::*;
    use std::os::unix::net::{UnixListener as StdUnixListener, UnixStream};
    use std::sync::atomic::{AtomicUsize, Ordering};

    static UNIQUE_ID: AtomicUsize = AtomicUsize::new(0);

    #[tokio::test]
    async fn drop_preserves_recreated_foreign_socket() {
        let _env_lock = crate::test_env::lock_async().await;
        let socket_path = unique_socket_path();
        let bound = crate::unix_socket::bind_unix_listener_at(&socket_path).expect("bind socket");
        let cleanup = SocketCleanup::new(socket_path.clone(), bound.identity);
        std::fs::remove_file(&socket_path).expect("unlink original socket path");
        let foreign = StdUnixListener::bind(&socket_path).expect("bind foreign replacement");
        let lock_paths = startup_lock_paths(&socket_path);
        for lock_path in &lock_paths {
            std::fs::write(lock_path, b"replacement").expect("write replacement lock");
        }
        let shim_path = crate::tmux_shim::test_shim_path(&socket_path);
        std::fs::create_dir_all(shim_path.parent().expect("shim parent"))
            .expect("create replacement shim directory");
        std::fs::write(&shim_path, b"replacement").expect("write replacement shim");

        drop(cleanup);

        assert!(
            UnixStream::connect(&socket_path).is_ok(),
            "cleanup must not remove a different socket inode"
        );
        for lock_path in &lock_paths {
            assert!(
                lock_path.is_file(),
                "cleanup must preserve replacement startup locks"
            );
        }
        assert!(
            shim_path.is_file(),
            "cleanup must preserve the replacement tmux shim"
        );
        drop(foreign);
        for lock_path in lock_paths {
            let _ = std::fs::remove_file(lock_path);
        }
        crate::tmux_shim::cleanup_tmux_shim(&socket_path);
        drop(bound.listener);
        cleanup_socket_dir(&socket_path);
    }

    #[tokio::test]
    async fn cleanup_now_disarms_drop_for_recreated_generation_artifacts() {
        let _env_lock = crate::test_env::lock_async().await;
        let socket_path = unique_socket_path();
        let bound = crate::unix_socket::bind_unix_listener_at(&socket_path).expect("bind socket");
        let mut cleanup = SocketCleanup::new(socket_path.clone(), bound.identity);

        cleanup.cleanup_now();
        assert!(
            !socket_path.exists(),
            "the original socket should be removed by explicit cleanup"
        );

        let lock_paths = startup_lock_paths(&socket_path);
        for lock_path in &lock_paths {
            std::fs::write(lock_path, b"replacement").expect("write replacement lock");
        }
        let shim_path = crate::tmux_shim::test_shim_path(&socket_path);
        std::fs::create_dir_all(shim_path.parent().expect("shim parent"))
            .expect("create replacement shim directory");
        std::fs::write(&shim_path, b"replacement").expect("write replacement shim");

        drop(cleanup);

        assert!(
            !socket_path.exists(),
            "drop must not recreate or reclean the released socket path"
        );
        for lock_path in &lock_paths {
            assert!(
                lock_path.is_file(),
                "drop must preserve replacement startup locks"
            );
        }
        assert!(
            shim_path.is_file(),
            "drop must preserve the replacement tmux shim"
        );

        for lock_path in lock_paths {
            let _ = std::fs::remove_file(lock_path);
        }
        crate::tmux_shim::cleanup_tmux_shim(&socket_path);
        drop(bound.listener);
        cleanup_socket_dir(&socket_path);
    }

    fn unique_socket_path() -> PathBuf {
        let unique_id = UNIQUE_ID.fetch_add(1, Ordering::Relaxed);
        PathBuf::from(format!("/tmp/rmxcl{}{}", std::process::id(), unique_id)).join("s")
    }

    fn cleanup_socket_dir(socket_path: &Path) {
        let _ = std::fs::remove_file(socket_path);
        if let Some(parent) = socket_path.parent() {
            let _ = std::fs::remove_dir_all(parent);
        }
    }
}