agent_engine/events/
socket.rs1use std::sync::{
9 atomic::{AtomicBool, Ordering},
10 Arc,
11};
12
13use tokio::io::AsyncReadExt;
14#[cfg(unix)]
15use tokio::net::UnixListener;
16
17use super::queue::EventQueue;
18use super::types::Event;
19
20const MAX_PAYLOAD: usize = 256 * 1024; #[cfg(windows)]
28pub fn pipe_name_for(socket_path: &str) -> String {
29 use sha2::{Digest, Sha256};
30 let digest = Sha256::digest(socket_path.as_bytes());
31 let hex: String = digest[..16].iter().map(|b| format!("{:02x}", b)).collect();
33 format!(r"\\.\pipe\synaps-session-{}", hex)
34}
35
36pub fn cleanup_socket(socket_path: &str) {
41 let path = std::path::Path::new(socket_path);
42 #[cfg(unix)]
43 {
44 if let Ok(meta) = std::fs::symlink_metadata(path) {
45 if meta.file_type().is_symlink() {
46 tracing::warn!("socket: refusing to remove symlink at {}", socket_path);
47 return;
48 }
49 }
50 }
51 match std::fs::remove_file(path) {
52 Ok(()) => {}
53 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
54 Err(e) => tracing::warn!("socket: failed to remove {}: {}", socket_path, e),
55 }
56}
57
58#[cfg(unix)]
64pub fn listen_session_socket(
65 socket_path: String,
66 queue: Arc<EventQueue>,
67 shutdown: Arc<AtomicBool>,
68) -> tokio::task::JoinHandle<()> {
69 tokio::spawn(async move {
70 cleanup_socket(&socket_path);
72
73 let listener = match UnixListener::bind(&socket_path) {
74 Ok(l) => l,
75 Err(e) => {
76 tracing::error!("socket: failed to bind {}: {}", socket_path, e);
77 return;
78 }
79 };
80
81 #[cfg(unix)]
83 {
84 use std::os::unix::fs::PermissionsExt;
85 if let Ok(meta) = std::fs::metadata(&socket_path) {
86 let mut perms = meta.permissions();
87 perms.set_mode(0o600);
88 let _ = std::fs::set_permissions(&socket_path, perms);
89 }
90 }
91
92 tracing::info!("socket: listening on {}", socket_path);
93
94 loop {
95 if shutdown.load(Ordering::Acquire) {
96 break;
97 }
98
99 let accept =
101 tokio::time::timeout(std::time::Duration::from_millis(500), listener.accept());
102
103 match accept.await {
104 Ok(Ok((mut stream, _addr))) => {
105 let queue = queue.clone();
106 tokio::spawn(async move {
107 let _ = tokio::time::timeout(
109 std::time::Duration::from_secs(5),
110 handle_connection(&mut stream, &queue),
111 )
112 .await;
113 });
114 }
115 Ok(Err(e)) => {
116 tracing::warn!("socket: accept error: {}", e);
117 }
118 Err(_) => {
119 }
121 }
122 }
123
124 cleanup_socket(&socket_path);
125 tracing::info!("socket: shut down, removed {}", socket_path);
126 })
127}
128
129#[cfg(windows)]
133pub fn listen_session_socket(
134 socket_path: String,
135 queue: Arc<EventQueue>,
136 shutdown: Arc<AtomicBool>,
137) -> tokio::task::JoinHandle<()> {
138 use tokio::net::windows::named_pipe::ServerOptions;
139 tokio::spawn(async move {
140 let pipe_name = pipe_name_for(&socket_path);
141 let mut server = match ServerOptions::new()
142 .first_pipe_instance(true)
143 .create(&pipe_name)
144 {
145 Ok(s) => s,
146 Err(e) => {
147 tracing::error!("socket: failed to create pipe {}: {}", pipe_name, e);
148 return;
149 }
150 };
151
152 tracing::info!("socket: listening on {}", pipe_name);
153
154 loop {
155 if shutdown.load(Ordering::Acquire) {
156 break;
157 }
158
159 let accept =
163 tokio::time::timeout(std::time::Duration::from_millis(500), server.connect());
164
165 match accept.await {
166 Ok(Ok(())) => {
167 let next = match ServerOptions::new().create(&pipe_name) {
170 Ok(s) => s,
171 Err(e) => {
172 tracing::error!("socket: failed to recreate pipe: {}", e);
173 break;
174 }
175 };
176 let mut connected = std::mem::replace(&mut server, next);
177 let queue = queue.clone();
178 tokio::spawn(async move {
179 let _ = tokio::time::timeout(
181 std::time::Duration::from_secs(5),
182 handle_connection(&mut connected, &queue),
183 )
184 .await;
185 });
186 }
187 Ok(Err(e)) => {
188 tracing::warn!("socket: accept error: {}", e);
189 }
190 Err(_) => {
191 }
193 }
194 }
195
196 tracing::info!("socket: shut down {}", pipe_name);
197 })
198}
199
200async fn handle_connection<S: tokio::io::AsyncRead + Unpin>(stream: &mut S, queue: &EventQueue) {
201 let mut buf = Vec::with_capacity(4096);
203 let mut chunk = [0u8; 8192];
204
205 loop {
206 match stream.read(&mut chunk).await {
207 Ok(0) => break, Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => break,
210 Ok(n) => {
211 if buf.len() + n > MAX_PAYLOAD {
212 tracing::warn!(
213 "socket: payload exceeds {}KB limit, dropping connection",
214 MAX_PAYLOAD / 1024
215 );
216 return;
217 }
218 buf.extend_from_slice(&chunk[..n]);
219 }
220 Err(e) => {
221 tracing::warn!("socket: read error: {}", e);
222 return;
223 }
224 }
225 }
226
227 if buf.is_empty() {
228 return;
229 }
230
231 match serde_json::from_slice::<Event>(&buf) {
232 Ok(event) => {
233 tracing::info!(
234 "socket: event {} from {}",
235 event.id,
236 event.source.source_type
237 );
238 if let Err(e) = queue.push(event) {
239 tracing::warn!("socket: queue push failed: {}", e);
240 }
241 }
242 Err(e) => {
243 tracing::warn!("socket: invalid JSON payload: {}", e);
244 }
245 }
246}
247
248#[cfg(all(test, unix))]
249mod tests {
250 use super::*;
251 use crate::events::{Event, Severity};
252 use std::sync::atomic::AtomicBool;
253 use tokio::io::AsyncWriteExt;
254 use tokio::net::UnixStream;
255
256 fn tmp_socket_path() -> String {
257 format!(
258 "/tmp/test-session-socket-{}.sock",
259 uuid::Uuid::new_v4().simple()
260 )
261 }
262
263 async fn wait_for_socket(path: &str) {
264 for _ in 0..50 {
265 if std::path::Path::new(path).exists() {
266 return;
267 }
268 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
269 }
270 panic!("socket never appeared at {}", path);
271 }
272
273 #[tokio::test]
274 async fn delivers_event_to_queue() {
275 let path = tmp_socket_path();
276 let queue = Arc::new(EventQueue::new(10));
277 let shutdown = Arc::new(AtomicBool::new(false));
278
279 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
280 wait_for_socket(&path).await;
281
282 let event = Event::simple("test", "hello socket", Some(Severity::High));
283 let json = serde_json::to_vec(&event).unwrap();
284
285 let mut client = UnixStream::connect(&path).await.unwrap();
286 client.write_all(&json).await.unwrap();
287 client.shutdown().await.unwrap();
288
289 for _ in 0..50 {
291 if !queue.is_empty() {
292 break;
293 }
294 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
295 }
296
297 shutdown.store(true, Ordering::Release);
298 handle.await.unwrap();
299
300 let popped = queue.pop().expect("event should be in queue");
301 assert_eq!(popped.content.text, "hello socket");
302 assert_eq!(popped.source.source_type, "test");
303 }
304
305 #[tokio::test]
306 async fn rejects_oversized_payload() {
307 let path = tmp_socket_path();
308 let queue = Arc::new(EventQueue::new(10));
309 let shutdown = Arc::new(AtomicBool::new(false));
310
311 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
312 wait_for_socket(&path).await;
313
314 let oversized = vec![b'x'; MAX_PAYLOAD + 1024];
316 let mut client = UnixStream::connect(&path).await.unwrap();
317 client.write_all(&oversized).await.unwrap();
318 client.shutdown().await.unwrap();
319
320 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
321
322 shutdown.store(true, Ordering::Release);
323 handle.await.unwrap();
324
325 assert_eq!(queue.len(), 0, "oversized payload should not reach queue");
326 }
327
328 #[tokio::test]
329 async fn invalid_json_does_not_crash() {
330 let path = tmp_socket_path();
331 let queue = Arc::new(EventQueue::new(10));
332 let shutdown = Arc::new(AtomicBool::new(false));
333
334 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
335 wait_for_socket(&path).await;
336
337 let mut client = UnixStream::connect(&path).await.unwrap();
338 client.write_all(b"this is not json at all").await.unwrap();
339 client.shutdown().await.unwrap();
340
341 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
342
343 let event = Event::simple("test", "still alive", None);
345 let json = serde_json::to_vec(&event).unwrap();
346 let mut client2 = UnixStream::connect(&path).await.unwrap();
347 client2.write_all(&json).await.unwrap();
348 client2.shutdown().await.unwrap();
349
350 for _ in 0..50 {
351 if !queue.is_empty() {
352 break;
353 }
354 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
355 }
356
357 shutdown.store(true, Ordering::Release);
358 handle.await.unwrap();
359
360 assert_eq!(queue.len(), 1);
361 assert_eq!(queue.pop().unwrap().content.text, "still alive");
362 }
363
364 #[tokio::test]
365 async fn stale_socket_removed_on_startup() {
366 let path = tmp_socket_path();
367 std::fs::write(&path, b"stale").unwrap();
369 assert!(std::path::Path::new(&path).exists());
370
371 let queue = Arc::new(EventQueue::new(10));
372 let shutdown = Arc::new(AtomicBool::new(false));
373
374 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
376 wait_for_socket(&path).await;
377
378 shutdown.store(true, Ordering::Release);
379 handle.await.unwrap();
380 }
381}