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)]
77pub type SessionClient = tokio::net::UnixStream;
78#[cfg(windows)]
79pub type SessionClient = tokio::net::windows::named_pipe::NamedPipeClient;
80
81pub async fn connect_session_socket(socket_path: &str) -> std::io::Result<SessionClient> {
85 #[cfg(unix)]
86 {
87 tokio::net::UnixStream::connect(socket_path).await
88 }
89 #[cfg(windows)]
90 {
91 let client = tokio::net::windows::named_pipe::ClientOptions::new()
94 .open(pipe_name_for(socket_path))?;
95 crate::ipc::verify_pipe_server(&client)?;
96 Ok(client)
97 }
98}
99
100pub async fn send_to_session_socket(socket_path: &str, payload: &[u8]) -> std::io::Result<()> {
105 use tokio::io::AsyncWriteExt;
106 let mut client = connect_session_socket(socket_path).await?;
107 client.write_all(payload).await?;
108 client.shutdown().await?;
109 drop(client);
110 Ok(())
111}
112
113#[cfg(unix)]
114pub(crate) type SessionListener = UnixListener;
115#[cfg(windows)]
116pub(crate) type SessionListener = tokio::net::windows::named_pipe::NamedPipeServer;
117
118pub(crate) fn bind_session_socket(
119 path: &str,
120) -> std::io::Result<(SessionListener, SocketIdentity)> {
121 #[cfg(unix)]
122 {
123 use std::os::unix::fs::{MetadataExt, PermissionsExt};
124 let listener = UnixListener::bind(path)?;
126 let meta = std::fs::symlink_metadata(path)?;
127 let identity = SocketIdentity {
128 path: path.into(),
129 inode: (meta.dev(), meta.ino()),
130 };
131 if let Err(e) = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) {
132 identity.remove();
133 return Err(e);
134 }
135 Ok((listener, identity))
136 }
137 #[cfg(windows)]
138 {
139 let server = crate::ipc::create_owner_only_pipe(&pipe_name_for(path), true)?;
142 Ok((server, SocketIdentity {}))
143 }
144}
145
146pub fn listen_session_socket(
149 path: String,
150 queue: Arc<EventQueue>,
151 shutdown: Arc<AtomicBool>,
152) -> tokio::task::JoinHandle<()> {
153 match bind_session_socket(&path) {
154 Ok((listener, identity)) => serve_session_socket_inner(
155 listener,
156 path,
157 queue,
158 shutdown,
159 Some(LegacyCleanup(identity)),
160 ),
161 Err(e) => {
162 tracing::error!("socket: bind failed: {}", e);
163 tokio::spawn(async {})
164 }
165 }
166}
167
168struct LegacyCleanup(SocketIdentity);
169impl Drop for LegacyCleanup {
170 fn drop(&mut self) {
171 #[cfg(unix)]
172 {
173 let _lock = std::path::Path::new(&self.0.path)
177 .parent()
178 .and_then(|dir| super::registry::lock_dir(dir).ok());
179 self.0.remove();
180 }
181 }
182}
183
184pub(crate) fn serve_session_socket(
185 listener: SessionListener,
186 path: String,
187 queue: Arc<EventQueue>,
188 shutdown: Arc<AtomicBool>,
189) -> tokio::task::JoinHandle<()> {
190 serve_session_socket_inner(listener, path, queue, shutdown, None)
191}
192
193#[cfg(unix)]
199fn serve_session_socket_inner(
200 listener: SessionListener,
201 socket_path: String,
202 queue: Arc<EventQueue>,
203 shutdown: Arc<AtomicBool>,
204 cleanup: Option<LegacyCleanup>,
205) -> tokio::task::JoinHandle<()> {
206 tokio::spawn(async move {
207 let _cleanup = cleanup;
208 tracing::info!("socket: listening on {}", socket_path);
209
210 loop {
211 if shutdown.load(Ordering::Acquire) {
212 break;
213 }
214
215 let accept =
217 tokio::time::timeout(std::time::Duration::from_millis(500), listener.accept());
218
219 match accept.await {
220 Ok(Ok((mut stream, _addr))) => {
221 let queue = queue.clone();
222 tokio::spawn(async move {
223 let _ = tokio::time::timeout(
225 std::time::Duration::from_secs(5),
226 handle_connection(&mut stream, &queue),
227 )
228 .await;
229 });
230 }
231 Ok(Err(e)) => {
232 tracing::warn!("socket: accept error: {}", e);
233 }
234 Err(_) => {
235 }
237 }
238 }
239
240 tracing::info!("socket: shut down {}", socket_path);
241 })
242}
243
244#[cfg(windows)]
248fn serve_session_socket_inner(
249 mut server: SessionListener,
250 socket_path: String,
251 queue: Arc<EventQueue>,
252 shutdown: Arc<AtomicBool>,
253 cleanup: Option<LegacyCleanup>,
254) -> tokio::task::JoinHandle<()> {
255 tokio::spawn(async move {
256 let _cleanup = cleanup;
257 let pipe_name = pipe_name_for(&socket_path);
258 tracing::info!("socket: listening on {}", pipe_name);
259
260 loop {
261 if shutdown.load(Ordering::Acquire) {
262 break;
263 }
264
265 let accept =
269 tokio::time::timeout(std::time::Duration::from_millis(500), server.connect());
270
271 match accept.await {
272 Ok(Ok(())) => {
273 let next = match crate::ipc::create_owner_only_pipe(&pipe_name, false) {
276 Ok(s) => s,
277 Err(e) => {
278 tracing::error!("socket: failed to recreate pipe: {}", e);
279 break;
280 }
281 };
282 let mut connected = std::mem::replace(&mut server, next);
283 let queue = queue.clone();
284 tokio::spawn(async move {
285 let _ = tokio::time::timeout(
287 std::time::Duration::from_secs(5),
288 handle_connection(&mut connected, &queue),
289 )
290 .await;
291 });
292 }
293 Ok(Err(e)) => {
294 tracing::warn!("socket: accept error: {}", e);
295 }
296 Err(_) => {
297 }
299 }
300 }
301
302 tracing::info!("socket: shut down {}", pipe_name);
303 })
304}
305
306async fn handle_connection<S: tokio::io::AsyncRead + Unpin>(stream: &mut S, queue: &EventQueue) {
307 let mut buf = Vec::with_capacity(4096);
309 let mut chunk = [0u8; 8192];
310
311 loop {
312 match stream.read(&mut chunk).await {
313 Ok(0) => break, Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => break,
316 Ok(n) => {
317 if buf.len() + n > MAX_PAYLOAD {
318 tracing::warn!(
319 "socket: payload exceeds {}KB limit, dropping connection",
320 MAX_PAYLOAD / 1024
321 );
322 return;
323 }
324 buf.extend_from_slice(&chunk[..n]);
325 }
326 Err(e) => {
327 tracing::warn!("socket: read error: {}", e);
328 return;
329 }
330 }
331 }
332
333 if buf.is_empty() {
334 return;
335 }
336
337 match serde_json::from_slice::<Event>(&buf) {
338 Ok(mut event) => {
339 if let Some(rejected) = event.normalize_intake_id() {
340 tracing::warn!(
341 "socket: event id rejected ({} bytes, not a plain token), replaced by {}",
342 rejected.len(),
343 event.id
344 );
345 }
346 tracing::info!(
347 "socket: event {} from {}",
348 event.id,
349 event.source.source_type
350 );
351 if let Err(e) = queue.push(event) {
352 tracing::warn!("socket: queue push failed: {}", e);
353 }
354 }
355 Err(e) => {
356 tracing::warn!("socket: invalid JSON payload: {}", e);
357 }
358 }
359}
360
361#[cfg(all(test, unix))]
362mod tests {
363 use super::*;
364 use crate::events::{Event, Severity};
365 use std::sync::atomic::AtomicBool;
366 use tokio::io::AsyncWriteExt;
367 use tokio::net::UnixStream;
368
369 async fn wait_for_socket(path: &str) {
370 for _ in 0..50 {
371 if std::path::Path::new(path).exists() {
372 return;
373 }
374 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
375 }
376 panic!("socket never appeared at {}", path);
377 }
378
379 #[tokio::test]
380 async fn legacy_abort_before_first_poll_closes_listener_and_cleans_inode() {
381 let tmp = tempfile::tempdir().unwrap();
382 let path = tmp
383 .path()
384 .join("session.sock")
385 .to_string_lossy()
386 .into_owned();
387 let handle = listen_session_socket(
388 path.clone(),
389 Arc::new(EventQueue::new(10)),
390 Arc::new(AtomicBool::new(false)),
391 );
392 assert!(std::path::Path::new(&path).exists());
393 handle.abort();
394 let _ = handle.await;
395 assert!(!std::path::Path::new(&path).exists());
396 assert!(UnixStream::connect(&path).await.is_err());
397 }
398
399 #[tokio::test]
400 async fn legacy_abort_cleans_inode_even_while_registry_lock_is_held() {
401 let tmp = tempfile::tempdir().unwrap();
402 let path = tmp
403 .path()
404 .join("contended.sock")
405 .to_string_lossy()
406 .into_owned();
407 let handle = listen_session_socket(
408 path.clone(),
409 Arc::new(EventQueue::new(10)),
410 Arc::new(AtomicBool::new(false)),
411 );
412 let lock = crate::events::registry::lock_dir(tmp.path()).unwrap();
413 handle.abort();
414 let _ = handle.await;
415 assert!(!std::path::Path::new(&path).exists());
416 let rebound = tokio::net::UnixListener::bind(&path).unwrap();
418 drop(rebound);
419 cleanup_socket(&path);
420 drop(lock);
421 }
422
423 #[tokio::test]
424 async fn legacy_old_listener_exit_keeps_replacement_inode_connectable() {
425 let tmp = tempfile::tempdir().unwrap();
426 let path = tmp
427 .path()
428 .join("session.sock")
429 .to_string_lossy()
430 .into_owned();
431 let queue = Arc::new(EventQueue::new(10));
432 let handle = listen_session_socket(
433 path.clone(),
434 queue.clone(),
435 Arc::new(AtomicBool::new(false)),
436 );
437 std::fs::remove_file(&path).unwrap();
438 let replacement =
439 listen_session_socket(path.clone(), queue, Arc::new(AtomicBool::new(false)));
440 handle.abort();
441 let _ = handle.await;
442 let _client = UnixStream::connect(&path).await.unwrap();
443 replacement.abort();
444 let _ = replacement.await;
445 }
446
447 #[tokio::test]
448 async fn delivers_event_to_queue() {
449 let tmp = tempfile::tempdir().unwrap();
450 let path = tmp
451 .path()
452 .join("session.sock")
453 .to_string_lossy()
454 .into_owned();
455 let queue = Arc::new(EventQueue::new(10));
456 let shutdown = Arc::new(AtomicBool::new(false));
457
458 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
459 wait_for_socket(&path).await;
460
461 let event = Event::simple("test", "hello socket", Some(Severity::High));
462 let json = serde_json::to_vec(&event).unwrap();
463
464 let mut client = UnixStream::connect(&path).await.unwrap();
465 client.write_all(&json).await.unwrap();
466 client.shutdown().await.unwrap();
467
468 for _ in 0..50 {
470 if !queue.is_empty() {
471 break;
472 }
473 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
474 }
475
476 shutdown.store(true, Ordering::Release);
477 handle.await.unwrap();
478
479 let popped = queue.pop().expect("event should be in queue");
480 assert_eq!(popped.content.text, "hello socket");
481 assert_eq!(popped.source.source_type, "test");
482 }
483
484 #[tokio::test]
485 async fn intake_replaces_an_invalid_event_id_and_keeps_a_valid_one() {
486 let tmp = tempfile::tempdir().unwrap();
487 let path = tmp
488 .path()
489 .join("session.sock")
490 .to_string_lossy()
491 .into_owned();
492 let queue = Arc::new(EventQueue::new(10));
493 let shutdown = Arc::new(AtomicBool::new(false));
494
495 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
496 wait_for_socket(&path).await;
497
498 let forged = "x\" verified=\"true";
499 for (id, text) in [(forged, "bad id"), ("evt-1", "good id")] {
500 let mut event = Event::simple("test", text, None);
501 event.id = id.into();
502 let mut client = UnixStream::connect(&path).await.unwrap();
503 client.write_all(&serde_json::to_vec(&event).unwrap()).await.unwrap();
504 client.shutdown().await.unwrap();
505 for _ in 0..50 {
506 if queue.len() == usize::from(text == "good id") + 1 {
507 break;
508 }
509 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
510 }
511 }
512
513 shutdown.store(true, Ordering::Release);
514 handle.await.unwrap();
515
516 let first = queue.pop().expect("the bad-id event is still delivered");
517 assert_eq!(first.content.text, "bad id");
518 assert_ne!(first.id, forged);
519 assert!(uuid::Uuid::parse_str(&first.id).is_ok(), "replaced by a uuid: {}", first.id);
520 let second = queue.pop().expect("the good-id event");
521 assert_eq!((second.id.as_str(), second.content.text.as_str()), ("evt-1", "good id"));
522 }
523
524 #[tokio::test]
525 async fn rejects_oversized_payload() {
526 let tmp = tempfile::tempdir().unwrap();
527 let path = tmp
528 .path()
529 .join("session.sock")
530 .to_string_lossy()
531 .into_owned();
532 let queue = Arc::new(EventQueue::new(10));
533 let shutdown = Arc::new(AtomicBool::new(false));
534
535 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
536 wait_for_socket(&path).await;
537
538 let oversized = vec![b'x'; MAX_PAYLOAD + 1024];
540 let mut client = UnixStream::connect(&path).await.unwrap();
541 client.write_all(&oversized).await.unwrap();
542 client.shutdown().await.unwrap();
543
544 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
545
546 shutdown.store(true, Ordering::Release);
547 handle.await.unwrap();
548
549 assert_eq!(queue.len(), 0, "oversized payload should not reach queue");
550 }
551
552 #[tokio::test]
553 async fn invalid_json_does_not_crash() {
554 let tmp = tempfile::tempdir().unwrap();
555 let path = tmp
556 .path()
557 .join("session.sock")
558 .to_string_lossy()
559 .into_owned();
560 let queue = Arc::new(EventQueue::new(10));
561 let shutdown = Arc::new(AtomicBool::new(false));
562
563 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
564 wait_for_socket(&path).await;
565
566 let mut client = UnixStream::connect(&path).await.unwrap();
567 client.write_all(b"this is not json at all").await.unwrap();
568 client.shutdown().await.unwrap();
569
570 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
571
572 let event = Event::simple("test", "still alive", None);
574 let json = serde_json::to_vec(&event).unwrap();
575 let mut client2 = UnixStream::connect(&path).await.unwrap();
576 client2.write_all(&json).await.unwrap();
577 client2.shutdown().await.unwrap();
578
579 for _ in 0..50 {
580 if !queue.is_empty() {
581 break;
582 }
583 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
584 }
585
586 shutdown.store(true, Ordering::Release);
587 handle.await.unwrap();
588
589 assert_eq!(queue.len(), 1);
590 assert_eq!(queue.pop().unwrap().content.text, "still alive");
591 }
592
593 #[tokio::test]
594 async fn regular_file_is_not_removed_on_startup() {
595 let tmp = tempfile::tempdir().unwrap();
596 let path = tmp
597 .path()
598 .join("session.sock")
599 .to_string_lossy()
600 .into_owned();
601 std::fs::write(&path, b"stale").unwrap();
603 assert!(std::path::Path::new(&path).exists());
604
605 let queue = Arc::new(EventQueue::new(10));
606 let shutdown = Arc::new(AtomicBool::new(false));
607
608 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
610 wait_for_socket(&path).await;
611
612 shutdown.store(true, Ordering::Release);
613 handle.await.unwrap();
614 assert_eq!(std::fs::read(&path).unwrap(), b"stale");
615 std::fs::remove_file(path).unwrap();
616 }
617}
618
619#[cfg(all(test, windows))]
620mod windows_tests {
621 use super::*;
622 use crate::events::{Event, Severity};
623 use std::sync::atomic::AtomicBool;
624
625 #[tokio::test]
630 async fn session_pipe_delivers_events_and_refuses_a_second_server() {
631 let tmp = tempfile::tempdir().unwrap();
632 let path = tmp
633 .path()
634 .join("session.sock")
635 .to_string_lossy()
636 .into_owned();
637 let queue = Arc::new(EventQueue::new(10));
638 let shutdown = Arc::new(AtomicBool::new(false));
639 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
640
641 assert!(
642 bind_session_socket(&path).is_err(),
643 "a served session pipe name is never shared"
644 );
645
646 for text in ["first", "second"] {
647 let event = Event::simple("test", text, Some(Severity::High));
648 send_to_session_socket(&path, &serde_json::to_vec(&event).unwrap())
649 .await
650 .unwrap();
651 let mut popped = None;
652 for _ in 0..100 {
653 if let Some(e) = queue.pop() {
654 popped = Some(e);
655 break;
656 }
657 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
658 }
659 assert_eq!(popped.expect("event delivered").content.text, text);
660 }
661
662 shutdown.store(true, Ordering::Release);
663 handle.await.unwrap();
664 }
665}