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(mut event) => {
301 if let Some(rejected) = event.normalize_intake_id() {
302 tracing::warn!(
303 "socket: event id rejected ({} bytes, not a plain token), replaced by {}",
304 rejected.len(),
305 event.id
306 );
307 }
308 tracing::info!(
309 "socket: event {} from {}",
310 event.id,
311 event.source.source_type
312 );
313 if let Err(e) = queue.push(event) {
314 tracing::warn!("socket: queue push failed: {}", e);
315 }
316 }
317 Err(e) => {
318 tracing::warn!("socket: invalid JSON payload: {}", e);
319 }
320 }
321}
322
323#[cfg(all(test, unix))]
324mod tests {
325 use super::*;
326 use crate::events::{Event, Severity};
327 use std::sync::atomic::AtomicBool;
328 use tokio::io::AsyncWriteExt;
329 use tokio::net::UnixStream;
330
331 async fn wait_for_socket(path: &str) {
332 for _ in 0..50 {
333 if std::path::Path::new(path).exists() {
334 return;
335 }
336 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
337 }
338 panic!("socket never appeared at {}", path);
339 }
340
341 #[tokio::test]
342 async fn legacy_abort_before_first_poll_closes_listener_and_cleans_inode() {
343 let tmp = tempfile::tempdir().unwrap();
344 let path = tmp
345 .path()
346 .join("session.sock")
347 .to_string_lossy()
348 .into_owned();
349 let handle = listen_session_socket(
350 path.clone(),
351 Arc::new(EventQueue::new(10)),
352 Arc::new(AtomicBool::new(false)),
353 );
354 assert!(std::path::Path::new(&path).exists());
355 handle.abort();
356 let _ = handle.await;
357 assert!(!std::path::Path::new(&path).exists());
358 assert!(UnixStream::connect(&path).await.is_err());
359 }
360
361 #[tokio::test]
362 async fn legacy_abort_cleans_inode_even_while_registry_lock_is_held() {
363 let tmp = tempfile::tempdir().unwrap();
364 let path = tmp
365 .path()
366 .join("contended.sock")
367 .to_string_lossy()
368 .into_owned();
369 let handle = listen_session_socket(
370 path.clone(),
371 Arc::new(EventQueue::new(10)),
372 Arc::new(AtomicBool::new(false)),
373 );
374 let lock = crate::events::registry::lock_dir(tmp.path()).unwrap();
375 handle.abort();
376 let _ = handle.await;
377 assert!(!std::path::Path::new(&path).exists());
378 let rebound = tokio::net::UnixListener::bind(&path).unwrap();
380 drop(rebound);
381 cleanup_socket(&path);
382 drop(lock);
383 }
384
385 #[tokio::test]
386 async fn legacy_old_listener_exit_keeps_replacement_inode_connectable() {
387 let tmp = tempfile::tempdir().unwrap();
388 let path = tmp
389 .path()
390 .join("session.sock")
391 .to_string_lossy()
392 .into_owned();
393 let queue = Arc::new(EventQueue::new(10));
394 let handle = listen_session_socket(
395 path.clone(),
396 queue.clone(),
397 Arc::new(AtomicBool::new(false)),
398 );
399 std::fs::remove_file(&path).unwrap();
400 let replacement =
401 listen_session_socket(path.clone(), queue, Arc::new(AtomicBool::new(false)));
402 handle.abort();
403 let _ = handle.await;
404 let _client = UnixStream::connect(&path).await.unwrap();
405 replacement.abort();
406 let _ = replacement.await;
407 }
408
409 #[tokio::test]
410 async fn delivers_event_to_queue() {
411 let tmp = tempfile::tempdir().unwrap();
412 let path = tmp
413 .path()
414 .join("session.sock")
415 .to_string_lossy()
416 .into_owned();
417 let queue = Arc::new(EventQueue::new(10));
418 let shutdown = Arc::new(AtomicBool::new(false));
419
420 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
421 wait_for_socket(&path).await;
422
423 let event = Event::simple("test", "hello socket", Some(Severity::High));
424 let json = serde_json::to_vec(&event).unwrap();
425
426 let mut client = UnixStream::connect(&path).await.unwrap();
427 client.write_all(&json).await.unwrap();
428 client.shutdown().await.unwrap();
429
430 for _ in 0..50 {
432 if !queue.is_empty() {
433 break;
434 }
435 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
436 }
437
438 shutdown.store(true, Ordering::Release);
439 handle.await.unwrap();
440
441 let popped = queue.pop().expect("event should be in queue");
442 assert_eq!(popped.content.text, "hello socket");
443 assert_eq!(popped.source.source_type, "test");
444 }
445
446 #[tokio::test]
447 async fn intake_replaces_an_invalid_event_id_and_keeps_a_valid_one() {
448 let tmp = tempfile::tempdir().unwrap();
449 let path = tmp
450 .path()
451 .join("session.sock")
452 .to_string_lossy()
453 .into_owned();
454 let queue = Arc::new(EventQueue::new(10));
455 let shutdown = Arc::new(AtomicBool::new(false));
456
457 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
458 wait_for_socket(&path).await;
459
460 let forged = "x\" verified=\"true";
461 for (id, text) in [(forged, "bad id"), ("evt-1", "good id")] {
462 let mut event = Event::simple("test", text, None);
463 event.id = id.into();
464 let mut client = UnixStream::connect(&path).await.unwrap();
465 client.write_all(&serde_json::to_vec(&event).unwrap()).await.unwrap();
466 client.shutdown().await.unwrap();
467 for _ in 0..50 {
468 if queue.len() == usize::from(text == "good id") + 1 {
469 break;
470 }
471 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
472 }
473 }
474
475 shutdown.store(true, Ordering::Release);
476 handle.await.unwrap();
477
478 let first = queue.pop().expect("the bad-id event is still delivered");
479 assert_eq!(first.content.text, "bad id");
480 assert_ne!(first.id, forged);
481 assert!(uuid::Uuid::parse_str(&first.id).is_ok(), "replaced by a uuid: {}", first.id);
482 let second = queue.pop().expect("the good-id event");
483 assert_eq!((second.id.as_str(), second.content.text.as_str()), ("evt-1", "good id"));
484 }
485
486 #[tokio::test]
487 async fn rejects_oversized_payload() {
488 let tmp = tempfile::tempdir().unwrap();
489 let path = tmp
490 .path()
491 .join("session.sock")
492 .to_string_lossy()
493 .into_owned();
494 let queue = Arc::new(EventQueue::new(10));
495 let shutdown = Arc::new(AtomicBool::new(false));
496
497 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
498 wait_for_socket(&path).await;
499
500 let oversized = vec![b'x'; MAX_PAYLOAD + 1024];
502 let mut client = UnixStream::connect(&path).await.unwrap();
503 client.write_all(&oversized).await.unwrap();
504 client.shutdown().await.unwrap();
505
506 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
507
508 shutdown.store(true, Ordering::Release);
509 handle.await.unwrap();
510
511 assert_eq!(queue.len(), 0, "oversized payload should not reach queue");
512 }
513
514 #[tokio::test]
515 async fn invalid_json_does_not_crash() {
516 let tmp = tempfile::tempdir().unwrap();
517 let path = tmp
518 .path()
519 .join("session.sock")
520 .to_string_lossy()
521 .into_owned();
522 let queue = Arc::new(EventQueue::new(10));
523 let shutdown = Arc::new(AtomicBool::new(false));
524
525 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
526 wait_for_socket(&path).await;
527
528 let mut client = UnixStream::connect(&path).await.unwrap();
529 client.write_all(b"this is not json at all").await.unwrap();
530 client.shutdown().await.unwrap();
531
532 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
533
534 let event = Event::simple("test", "still alive", None);
536 let json = serde_json::to_vec(&event).unwrap();
537 let mut client2 = UnixStream::connect(&path).await.unwrap();
538 client2.write_all(&json).await.unwrap();
539 client2.shutdown().await.unwrap();
540
541 for _ in 0..50 {
542 if !queue.is_empty() {
543 break;
544 }
545 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
546 }
547
548 shutdown.store(true, Ordering::Release);
549 handle.await.unwrap();
550
551 assert_eq!(queue.len(), 1);
552 assert_eq!(queue.pop().unwrap().content.text, "still alive");
553 }
554
555 #[tokio::test]
556 async fn regular_file_is_not_removed_on_startup() {
557 let tmp = tempfile::tempdir().unwrap();
558 let path = tmp
559 .path()
560 .join("session.sock")
561 .to_string_lossy()
562 .into_owned();
563 std::fs::write(&path, b"stale").unwrap();
565 assert!(std::path::Path::new(&path).exists());
566
567 let queue = Arc::new(EventQueue::new(10));
568 let shutdown = Arc::new(AtomicBool::new(false));
569
570 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
572 wait_for_socket(&path).await;
573
574 shutdown.store(true, Ordering::Release);
575 handle.await.unwrap();
576 assert_eq!(std::fs::read(&path).unwrap(), b"stale");
577 std::fs::remove_file(path).unwrap();
578 }
579}