kanshou 0.1.2

Kanshou (観照) — live process introspection over Unix sockets. Every pleme-io binary exposes its typed AppState; MCP servers and operator tools forward queries through it.
Documentation
//! Canonical socket path resolution. Same algorithm consumed by the
//! server (binds) and the client (discovers).

use std::path::PathBuf;

/// Directory holding every kanshou socket on this host.
///
/// - macOS: `$HOME/Library/Application Support/kanshou`
/// - linux: `$XDG_RUNTIME_DIR/kanshou` if set, else `/tmp/kanshou-<uid>`
///
/// The directory is created on demand; existing dir + perms preserved.
#[must_use]
pub fn socket_dir() -> PathBuf {
    if cfg!(target_os = "macos") {
        let home = std::env::var_os("HOME").unwrap_or_default();
        let mut p = PathBuf::from(home);
        p.push("Library/Application Support/kanshou");
        p
    } else {
        if let Some(xdg) = std::env::var_os("XDG_RUNTIME_DIR") {
            let mut p = PathBuf::from(xdg);
            p.push("kanshou");
            return p;
        }
        // Fallback per-UID to avoid socket squatting on shared /tmp.
        let uid =
            unsafe { libc_geteuid() }.unwrap_or(0);
        PathBuf::from(format!("/tmp/kanshou-{uid}"))
    }
}

/// Canonical socket path for an app+pid pair.
#[must_use]
pub fn socket_path(app_name: &str, pid: u32) -> PathBuf {
    let mut p = socket_dir();
    p.push(format!("{app_name}-{pid}.sock"));
    p
}

/// Parse an app-name + PID out of a socket filename. Returns `None`
/// when the shape isn't `<name>-<pid>.sock`.
#[must_use]
pub fn parse_socket_name(name: &str) -> Option<(String, u32)> {
    let stem = name.strip_suffix(".sock")?;
    let dash = stem.rfind('-')?;
    let (app, pid_str) = stem.split_at(dash);
    let pid: u32 = pid_str.trim_start_matches('-').parse().ok()?;
    Some((app.to_string(), pid))
}

/// Best-effort `geteuid()` without pulling the `libc` crate. We only
/// need it on Linux's `/tmp` fallback path; macOS uses `$HOME` and
/// never reaches here. Returns `None` on Windows / unknown.
unsafe fn libc_geteuid() -> Option<u32> {
    #[cfg(unix)]
    {
        // `getuid` is signal-safe and always succeeds.
        // We deliberately don't depend on the libc crate — direct
        // FFI keeps the dep tree minimal for a substrate primitive.
        unsafe extern "C" {
            fn getuid() -> u32;
        }
        Some(unsafe { getuid() })
    }
    #[cfg(not(unix))]
    {
        None
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn socket_path_format() {
        let p = socket_path("mado", 12345);
        assert!(p.to_string_lossy().ends_with("mado-12345.sock"));
    }

    #[test]
    fn parse_basic() {
        assert_eq!(
            parse_socket_name("mado-12345.sock"),
            Some(("mado".into(), 12345))
        );
    }

    #[test]
    fn parse_dashed_app() {
        // App name itself may contain dashes — the LAST dash is the
        // PID separator. `blackmatter-cli-99.sock` → `blackmatter-cli`, 99.
        assert_eq!(
            parse_socket_name("blackmatter-cli-99.sock"),
            Some(("blackmatter-cli".into(), 99))
        );
    }

    #[test]
    fn parse_rejects_non_sock() {
        assert_eq!(parse_socket_name("mado-12345.log"), None);
    }

    #[test]
    fn parse_rejects_no_pid() {
        assert_eq!(parse_socket_name("mado.sock"), None);
    }

    #[test]
    fn parse_rejects_bad_pid() {
        assert_eq!(parse_socket_name("mado-abc.sock"), None);
    }
}