1pub mod detached;
2pub mod process;
3pub mod runtime;
4
5#[cfg(unix)]
6mod unix;
7#[cfg(windows)]
8mod windows;
9#[cfg(windows)]
10pub use windows::NamedPipeListener;
11
12use std::path::PathBuf;
13
14use anyhow::Result;
15#[cfg(unix)]
16use anyhow::anyhow;
17#[cfg(unix)]
18use tokio::io::AsyncWriteExt;
19
20pub fn endpoint_dir() -> PathBuf {
25 crate::core::data_dir::lean_ctx_data_dir().unwrap_or_else(|_| legacy_endpoint_dir())
26}
27
28pub fn legacy_endpoint_dir() -> PathBuf {
32 dirs::data_local_dir()
33 .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share"))
34 .join("lean-ctx")
35}
36
37#[derive(Debug, Clone)]
39pub enum DaemonAddr {
40 #[cfg(unix)]
41 Unix(PathBuf),
42 #[cfg(windows)]
43 NamedPipe(String),
44}
45
46impl DaemonAddr {
47 pub fn default_for_current_os() -> Self {
48 #[cfg(unix)]
49 {
50 Self::Unix(unix::default_socket_path())
51 }
52 #[cfg(windows)]
53 {
54 Self::NamedPipe(windows::default_pipe_name())
55 }
56 }
57
58 pub fn display(&self) -> String {
59 match self {
60 #[cfg(unix)]
61 Self::Unix(p) => p.display().to_string(),
62 #[cfg(windows)]
63 Self::NamedPipe(n) => n.clone(),
64 }
65 }
66
67 pub fn is_listening(&self) -> bool {
69 match self {
70 #[cfg(unix)]
71 Self::Unix(p) => p.exists(),
72 #[cfg(windows)]
73 Self::NamedPipe(name) => windows::pipe_exists(name),
74 }
75 }
76}
77
78pub fn cleanup(addr: &DaemonAddr) {
80 match addr {
81 #[cfg(unix)]
82 DaemonAddr::Unix(p) => {
83 if p.exists() {
84 let _ = std::fs::remove_file(p);
85 }
86 }
87 #[cfg(windows)]
88 DaemonAddr::NamedPipe(_) => {
89 }
91 }
92}
93
94#[cfg(unix)]
96pub fn bind_listener(addr: &DaemonAddr) -> Result<tokio::net::UnixListener> {
97 match addr {
98 DaemonAddr::Unix(path) => unix::bind_listener(path),
99 }
100}
101
102#[cfg(unix)]
104pub async fn connect(addr: &DaemonAddr) -> Result<tokio::net::UnixStream> {
105 match addr {
106 DaemonAddr::Unix(path) => unix::connect(path).await,
107 }
108}
109
110#[cfg(unix)]
112pub async fn connect_enterprise(
113 path: &std::path::Path,
114 handshake: &lean_ctx_protocol::runtime_handshake::RuntimeHandshake,
115) -> Result<tokio::net::UnixStream> {
116 let mut stream = unix::connect(path).await?;
117 let frame = handshake.encode_frame().map_err(|error| anyhow!(error))?;
118 stream.write_all(&frame).await?;
119 stream.flush().await?;
120 Ok(stream)
121}
122
123#[cfg(windows)]
125pub fn bind_listener(addr: &DaemonAddr) -> Result<NamedPipeListener> {
126 match addr {
127 DaemonAddr::NamedPipe(name) => NamedPipeListener::bind(name),
128 }
129}
130
131#[cfg_attr(not(windows), allow(dead_code))]
137fn owner_only_pipe_sddl(sid: &str) -> String {
138 format!("O:{sid}D:P(A;;GA;;;{sid})")
139}
140
141#[cfg(windows)]
143pub async fn connect(
144 addr: &DaemonAddr,
145) -> Result<tokio::net::windows::named_pipe::NamedPipeClient> {
146 match addr {
147 DaemonAddr::NamedPipe(name) => windows::connect(name).await,
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 #[test]
156 fn daemon_addr_display_non_empty() {
157 let addr = DaemonAddr::default_for_current_os();
158 let display = addr.display();
159 assert!(!display.is_empty(), "`display` is empty");
160 }
161
162 #[test]
166 fn the_daemon_pipe_descriptor_grants_its_owner_alone() {
167 let sid = "S-1-5-21-1-2-3-1001";
168 let sddl = owner_only_pipe_sddl(sid);
169 assert_eq!(sddl, format!("O:{sid}D:P(A;;GA;;;{sid})"));
170 assert_eq!(sddl.matches("(A;").count(), 1, "one access entry");
171 for broad in ["WD", "AN", "AU", "BU", "IU"] {
172 assert!(!sddl.contains(&format!(";;;{broad})")), "{broad}: {sddl}");
173 }
174 }
175
176 #[test]
177 fn cleanup_nonexistent_does_not_panic() {
178 #[cfg(unix)]
179 {
180 let addr = DaemonAddr::Unix(std::path::PathBuf::from(
181 "/tmp/lean-ctx-test-nonexistent.sock",
182 ));
183 cleanup(&addr);
184 }
185 }
186
187 #[cfg(unix)]
188 #[tokio::test]
189 async fn enterprise_connect_sends_versioned_frame() {
190 let dir = tempfile::tempdir().unwrap();
191 let path = dir.path().join("enterprise.sock");
192 let listener = unix::bind_listener(&path).unwrap();
193 let handshake = lean_ctx_protocol::runtime_handshake::RuntimeHandshake::public([]);
194 let client = connect_enterprise(&path, &handshake);
195 let server = listener.accept();
196 let (client, accepted) = tokio::join!(client, server);
197 let _client = client.unwrap();
198 let (mut stream, _) = accepted.unwrap();
199 use tokio::io::AsyncReadExt;
200 let mut length = [0_u8; 4];
201 stream.read_exact(&mut length).await.unwrap();
202 let len = u32::from_be_bytes(length) as usize;
203 assert_eq!(len, handshake.encode_frame().unwrap().len() - 4);
204 }
205}