gate4agent_wake/grok_leader/
pipe.rs1use std::ffi::OsStr;
10use std::path::{Path, PathBuf};
11
12use thiserror::Error;
13
14const PIPE_KEY_0: u64 = 0x6772_6f6b_6c65_6164;
16const PIPE_KEY_1: u64 = 0x6572_5f70_6970_6521;
17
18#[derive(Debug, Error)]
19pub enum PushError {
20 #[error("leader-absent")]
21 LeaderAbsent,
22 #[error("leader-not-ready")]
23 LeaderNotReady,
24 #[error("leader-protocol: {0}")]
25 Protocol(String),
26 #[error("session-load-failed: {0}")]
27 SessionLoadFailed(String),
28 #[error("prompt-failed: {0}")]
29 PromptFailed(String),
30 #[error("frame-too-large")]
31 FrameTooLarge,
32}
33
34pub fn grok_home() -> Option<PathBuf> {
38 if let Some(env) = std::env::var_os("GROK_HOME").filter(|value| !value.is_empty()) {
39 return Some(PathBuf::from(env));
40 }
41 let home = std::env::var_os("USERPROFILE")
42 .or_else(|| std::env::var_os("HOME"))
43 .filter(|value| !value.is_empty())?;
44 let home = PathBuf::from(home);
45 Some(dunce::canonicalize(&home).unwrap_or(home).join(".grok"))
46}
47
48pub fn leader_socket(grok_home: &Path, override_socket: Option<&OsStr>) -> PathBuf {
52 if let Some(over) = override_socket.filter(|value| !value.is_empty()) {
53 return PathBuf::from(over);
54 }
55 grok_home.join("leader.sock")
56}
57
58pub fn leader_pipe_name(sock: &Path) -> String {
59 use std::hash::{Hash, Hasher};
60
61 use siphasher::sip::SipHasher13;
62
63 let mut hasher = SipHasher13::new_with_keys(PIPE_KEY_0, PIPE_KEY_1);
64 sock.hash(&mut hasher);
65 format!("grok-leader-{:016x}", hasher.finish())
66}
67
68#[cfg(windows)]
69pub fn leader_pipe_os_path(sock: &Path) -> PathBuf {
70 PathBuf::from(format!(r"\\.\pipe\{}", leader_pipe_name(sock)))
71}
72
73pub fn leader_is_listening(sock: &Path) -> bool {
76 #[cfg(windows)]
77 {
78 windows_pipe_ready(&leader_pipe_os_path(sock))
79 }
80 #[cfg(not(windows))]
81 {
82 unix_socket_ready(sock)
83 }
84}
85
86#[cfg(windows)]
87fn windows_pipe_ready(pipe: &Path) -> bool {
88 use std::os::windows::ffi::OsStrExt;
89
90 #[link(name = "kernel32")]
91 extern "system" {
92 fn WaitNamedPipeW(name: *const u16, timeout_ms: u32) -> i32;
93 fn GetLastError() -> u32;
94 }
95
96 const ERROR_FILE_NOT_FOUND: u32 = 2;
97 let mut wide: Vec<u16> = pipe.as_os_str().encode_wide().collect();
98 wide.push(0);
99 let ready = unsafe { WaitNamedPipeW(wide.as_ptr(), 1) };
100 if ready != 0 {
101 return true;
102 }
103 let err = unsafe { GetLastError() };
104 err != ERROR_FILE_NOT_FOUND
105}
106
107#[cfg(not(windows))]
108fn unix_socket_ready(sock: &Path) -> bool {
109 use std::os::unix::fs::FileTypeExt;
110 std::fs::metadata(sock).is_ok_and(|meta| meta.file_type().is_socket())
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn pipe_name_is_stable_and_shaped() {
119 let sock = Path::new(r"C:\grok\leader.sock");
120 let once = leader_pipe_name(sock);
121 let twice = leader_pipe_name(sock);
122 assert_eq!(once, twice);
123 assert!(once.starts_with("grok-leader-"));
124 assert_eq!(once.len(), "grok-leader-".len() + 16);
125 assert!(once.bytes().skip("grok-leader-".len()).all(|byte| byte.is_ascii_hexdigit()));
126 }
127
128 #[test]
129 fn a_different_path_hashes_differently() {
130 let home = leader_pipe_name(Path::new(r"C:\grok\leader.sock"));
131 let other = leader_pipe_name(Path::new(r"C:\grok\leader-other.sock"));
132 assert_ne!(home, other);
133 }
134
135 #[test]
136 fn socket_override_replaces_the_default() {
137 let home = Path::new(r"C:\grok");
138 assert_eq!(leader_socket(home, None), home.join("leader.sock"));
139 assert_eq!(
140 leader_socket(home, Some(OsStr::new(r"D:\custom.sock"))),
141 PathBuf::from(r"D:\custom.sock")
142 );
143 assert_eq!(leader_socket(home, Some(OsStr::new(""))), home.join("leader.sock"));
144 }
145}