1use 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 #[cfg(unix)]
42 {
43 use std::os::unix::fs::FileTypeExt;
44 let path = std::path::Path::new(socket_path);
45 if std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_socket()) {
46 let _ = std::fs::remove_file(path);
47 }
48 }
49 #[cfg(not(unix))]
50 let _ = socket_path;
51}
52
53pub(crate) struct SocketIdentity {
55 #[cfg(unix)]
56 path: String,
57 #[cfg(unix)]
58 inode: (u64, u64),
59}
60impl SocketIdentity {
61 pub(crate) fn remove(&self) {
62 #[cfg(unix)]
63 {
64 use std::os::unix::fs::{FileTypeExt, MetadataExt};
65 if std::fs::symlink_metadata(&self.path)
66 .is_ok_and(|m| m.file_type().is_socket() && (m.dev(), m.ino()) == self.inode)
67 {
68 let _ = std::fs::remove_file(&self.path);
69 }
70 }
71 }
72}
73
74#[cfg(unix)]
75pub(crate) type SessionListener = UnixListener;
76#[cfg(windows)]
77pub(crate) type SessionListener = tokio::net::windows::named_pipe::NamedPipeServer;
78
79pub(crate) fn bind_session_socket(
80 path: &str,
81) -> std::io::Result<(SessionListener, SocketIdentity)> {
82 #[cfg(unix)]
83 {
84 use std::os::unix::fs::{MetadataExt, PermissionsExt};
85 let listener = UnixListener::bind(path)?;
87 let meta = std::fs::symlink_metadata(path)?;
88 let identity = SocketIdentity {
89 path: path.into(),
90 inode: (meta.dev(), meta.ino()),
91 };
92 if let Err(e) = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) {
93 identity.remove();
94 return Err(e);
95 }
96 Ok((listener, identity))
97 }
98 #[cfg(windows)]
99 {
100 let server = tokio::net::windows::named_pipe::ServerOptions::new()
101 .first_pipe_instance(true)
102 .create(pipe_name_for(path))?;
103 Ok((server, SocketIdentity {}))
104 }
105}
106
107pub fn listen_session_socket(
110 path: String,
111 queue: Arc<EventQueue>,
112 shutdown: Arc<AtomicBool>,
113) -> tokio::task::JoinHandle<()> {
114 match bind_session_socket(&path) {
115 Ok((listener, identity)) => serve_session_socket_inner(
116 listener,
117 path,
118 queue,
119 shutdown,
120 Some(LegacyCleanup(identity)),
121 ),
122 Err(e) => {
123 tracing::error!("socket: bind failed: {}", e);
124 tokio::spawn(async {})
125 }
126 }
127}
128
129struct LegacyCleanup(SocketIdentity);
130impl Drop for LegacyCleanup {
131 fn drop(&mut self) {
132 #[cfg(unix)]
133 {
134 let _lock = std::path::Path::new(&self.0.path)
138 .parent()
139 .and_then(|dir| super::registry::lock_dir(dir).ok());
140 self.0.remove();
141 }
142 }
143}
144
145pub(crate) fn serve_session_socket(
146 listener: SessionListener,
147 path: String,
148 queue: Arc<EventQueue>,
149 shutdown: Arc<AtomicBool>,
150) -> tokio::task::JoinHandle<()> {
151 serve_session_socket_inner(listener, path, queue, shutdown, None)
152}
153
154#[cfg(unix)]
160fn serve_session_socket_inner(
161 listener: SessionListener,
162 socket_path: String,
163 queue: Arc<EventQueue>,
164 shutdown: Arc<AtomicBool>,
165 cleanup: Option<LegacyCleanup>,
166) -> tokio::task::JoinHandle<()> {
167 tokio::spawn(async move {
168 let _cleanup = cleanup;
169 tracing::info!("socket: listening on {}", socket_path);
170
171 loop {
172 if shutdown.load(Ordering::Acquire) {
173 break;
174 }
175
176 let accept =
178 tokio::time::timeout(std::time::Duration::from_millis(500), listener.accept());
179
180 match accept.await {
181 Ok(Ok((mut stream, _addr))) => {
182 let queue = queue.clone();
183 tokio::spawn(async move {
184 let _ = tokio::time::timeout(
186 std::time::Duration::from_secs(5),
187 handle_connection(&mut stream, &queue),
188 )
189 .await;
190 });
191 }
192 Ok(Err(e)) => {
193 tracing::warn!("socket: accept error: {}", e);
194 }
195 Err(_) => {
196 }
198 }
199 }
200
201 tracing::info!("socket: shut down {}", socket_path);
202 })
203}
204
205#[cfg(windows)]
209fn serve_session_socket_inner(
210 mut server: SessionListener,
211 socket_path: String,
212 queue: Arc<EventQueue>,
213 shutdown: Arc<AtomicBool>,
214 cleanup: Option<LegacyCleanup>,
215) -> tokio::task::JoinHandle<()> {
216 use tokio::net::windows::named_pipe::ServerOptions;
217 tokio::spawn(async move {
218 let _cleanup = cleanup;
219 let pipe_name = pipe_name_for(&socket_path);
220 tracing::info!("socket: listening on {}", pipe_name);
221
222 loop {
223 if shutdown.load(Ordering::Acquire) {
224 break;
225 }
226
227 let accept =
231 tokio::time::timeout(std::time::Duration::from_millis(500), server.connect());
232
233 match accept.await {
234 Ok(Ok(())) => {
235 let next = match ServerOptions::new().create(&pipe_name) {
238 Ok(s) => s,
239 Err(e) => {
240 tracing::error!("socket: failed to recreate pipe: {}", e);
241 break;
242 }
243 };
244 let mut connected = std::mem::replace(&mut server, next);
245 let queue = queue.clone();
246 tokio::spawn(async move {
247 let _ = tokio::time::timeout(
249 std::time::Duration::from_secs(5),
250 handle_connection(&mut connected, &queue),
251 )
252 .await;
253 });
254 }
255 Ok(Err(e)) => {
256 tracing::warn!("socket: accept error: {}", e);
257 }
258 Err(_) => {
259 }
261 }
262 }
263
264 tracing::info!("socket: shut down {}", pipe_name);
265 })
266}
267
268async fn handle_connection<S: tokio::io::AsyncRead + Unpin>(stream: &mut S, queue: &EventQueue) {
269 let mut buf = Vec::with_capacity(4096);
271 let mut chunk = [0u8; 8192];
272
273 loop {
274 match stream.read(&mut chunk).await {
275 Ok(0) => break, Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => break,
278 Ok(n) => {
279 if buf.len() + n > MAX_PAYLOAD {
280 tracing::warn!(
281 "socket: payload exceeds {}KB limit, dropping connection",
282 MAX_PAYLOAD / 1024
283 );
284 return;
285 }
286 buf.extend_from_slice(&chunk[..n]);
287 }
288 Err(e) => {
289 tracing::warn!("socket: read error: {}", e);
290 return;
291 }
292 }
293 }
294
295 if buf.is_empty() {
296 return;
297 }
298
299 match serde_json::from_slice::<Event>(&buf) {
300 Ok(event) => {
301 tracing::info!(
302 "socket: event {} from {}",
303 event.id,
304 event.source.source_type
305 );
306 if let Err(e) = queue.push(event) {
307 tracing::warn!("socket: queue push failed: {}", e);
308 }
309 }
310 Err(e) => {
311 tracing::warn!("socket: invalid JSON payload: {}", e);
312 }
313 }
314}
315
316#[cfg(all(test, unix))]
317mod tests {
318 use super::*;
319 use crate::events::{Event, Severity};
320 use std::sync::atomic::AtomicBool;
321 use tokio::io::AsyncWriteExt;
322 use tokio::net::UnixStream;
323
324 async fn wait_for_socket(path: &str) {
325 for _ in 0..50 {
326 if std::path::Path::new(path).exists() {
327 return;
328 }
329 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
330 }
331 panic!("socket never appeared at {}", path);
332 }
333
334 #[tokio::test]
335 async fn legacy_abort_before_first_poll_closes_listener_and_cleans_inode() {
336 let tmp = tempfile::tempdir().unwrap();
337 let path = tmp
338 .path()
339 .join("session.sock")
340 .to_string_lossy()
341 .into_owned();
342 let handle = listen_session_socket(
343 path.clone(),
344 Arc::new(EventQueue::new(10)),
345 Arc::new(AtomicBool::new(false)),
346 );
347 assert!(std::path::Path::new(&path).exists());
348 handle.abort();
349 let _ = handle.await;
350 assert!(!std::path::Path::new(&path).exists());
351 assert!(UnixStream::connect(&path).await.is_err());
352 }
353
354 #[tokio::test]
355 async fn legacy_abort_cleans_inode_even_while_registry_lock_is_held() {
356 let tmp = tempfile::tempdir().unwrap();
357 let path = tmp
358 .path()
359 .join("contended.sock")
360 .to_string_lossy()
361 .into_owned();
362 let handle = listen_session_socket(
363 path.clone(),
364 Arc::new(EventQueue::new(10)),
365 Arc::new(AtomicBool::new(false)),
366 );
367 let lock = crate::events::registry::lock_dir(tmp.path()).unwrap();
368 handle.abort();
369 let _ = handle.await;
370 assert!(!std::path::Path::new(&path).exists());
371 let rebound = tokio::net::UnixListener::bind(&path).unwrap();
373 drop(rebound);
374 cleanup_socket(&path);
375 drop(lock);
376 }
377
378 #[tokio::test]
379 async fn legacy_old_listener_exit_keeps_replacement_inode_connectable() {
380 let tmp = tempfile::tempdir().unwrap();
381 let path = tmp
382 .path()
383 .join("session.sock")
384 .to_string_lossy()
385 .into_owned();
386 let queue = Arc::new(EventQueue::new(10));
387 let handle = listen_session_socket(
388 path.clone(),
389 queue.clone(),
390 Arc::new(AtomicBool::new(false)),
391 );
392 std::fs::remove_file(&path).unwrap();
393 let replacement =
394 listen_session_socket(path.clone(), queue, Arc::new(AtomicBool::new(false)));
395 handle.abort();
396 let _ = handle.await;
397 let _client = UnixStream::connect(&path).await.unwrap();
398 replacement.abort();
399 let _ = replacement.await;
400 }
401
402 #[tokio::test]
403 async fn delivers_event_to_queue() {
404 let tmp = tempfile::tempdir().unwrap();
405 let path = tmp
406 .path()
407 .join("session.sock")
408 .to_string_lossy()
409 .into_owned();
410 let queue = Arc::new(EventQueue::new(10));
411 let shutdown = Arc::new(AtomicBool::new(false));
412
413 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
414 wait_for_socket(&path).await;
415
416 let event = Event::simple("test", "hello socket", Some(Severity::High));
417 let json = serde_json::to_vec(&event).unwrap();
418
419 let mut client = UnixStream::connect(&path).await.unwrap();
420 client.write_all(&json).await.unwrap();
421 client.shutdown().await.unwrap();
422
423 for _ in 0..50 {
425 if !queue.is_empty() {
426 break;
427 }
428 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
429 }
430
431 shutdown.store(true, Ordering::Release);
432 handle.await.unwrap();
433
434 let popped = queue.pop().expect("event should be in queue");
435 assert_eq!(popped.content.text, "hello socket");
436 assert_eq!(popped.source.source_type, "test");
437 }
438
439 #[tokio::test]
440 async fn rejects_oversized_payload() {
441 let tmp = tempfile::tempdir().unwrap();
442 let path = tmp
443 .path()
444 .join("session.sock")
445 .to_string_lossy()
446 .into_owned();
447 let queue = Arc::new(EventQueue::new(10));
448 let shutdown = Arc::new(AtomicBool::new(false));
449
450 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
451 wait_for_socket(&path).await;
452
453 let oversized = vec![b'x'; MAX_PAYLOAD + 1024];
455 let mut client = UnixStream::connect(&path).await.unwrap();
456 client.write_all(&oversized).await.unwrap();
457 client.shutdown().await.unwrap();
458
459 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
460
461 shutdown.store(true, Ordering::Release);
462 handle.await.unwrap();
463
464 assert_eq!(queue.len(), 0, "oversized payload should not reach queue");
465 }
466
467 #[tokio::test]
468 async fn invalid_json_does_not_crash() {
469 let tmp = tempfile::tempdir().unwrap();
470 let path = tmp
471 .path()
472 .join("session.sock")
473 .to_string_lossy()
474 .into_owned();
475 let queue = Arc::new(EventQueue::new(10));
476 let shutdown = Arc::new(AtomicBool::new(false));
477
478 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
479 wait_for_socket(&path).await;
480
481 let mut client = UnixStream::connect(&path).await.unwrap();
482 client.write_all(b"this is not json at all").await.unwrap();
483 client.shutdown().await.unwrap();
484
485 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
486
487 let event = Event::simple("test", "still alive", None);
489 let json = serde_json::to_vec(&event).unwrap();
490 let mut client2 = UnixStream::connect(&path).await.unwrap();
491 client2.write_all(&json).await.unwrap();
492 client2.shutdown().await.unwrap();
493
494 for _ in 0..50 {
495 if !queue.is_empty() {
496 break;
497 }
498 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
499 }
500
501 shutdown.store(true, Ordering::Release);
502 handle.await.unwrap();
503
504 assert_eq!(queue.len(), 1);
505 assert_eq!(queue.pop().unwrap().content.text, "still alive");
506 }
507
508 #[tokio::test]
509 async fn regular_file_is_not_removed_on_startup() {
510 let tmp = tempfile::tempdir().unwrap();
511 let path = tmp
512 .path()
513 .join("session.sock")
514 .to_string_lossy()
515 .into_owned();
516 std::fs::write(&path, b"stale").unwrap();
518 assert!(std::path::Path::new(&path).exists());
519
520 let queue = Arc::new(EventQueue::new(10));
521 let shutdown = Arc::new(AtomicBool::new(false));
522
523 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
525 wait_for_socket(&path).await;
526
527 shutdown.store(true, Ordering::Release);
528 handle.await.unwrap();
529 assert_eq!(std::fs::read(&path).unwrap(), b"stale");
530 std::fs::remove_file(path).unwrap();
531 }
532}