agent_engine/events/
socket.rs1use std::sync::{
5 atomic::{AtomicBool, Ordering},
6 Arc,
7};
8
9use tokio::io::AsyncReadExt;
10use tokio::net::UnixListener;
11
12use super::queue::EventQueue;
13use super::types::Event;
14
15const MAX_PAYLOAD: usize = 256 * 1024; pub fn cleanup_socket(socket_path: &str) {
22 let path = std::path::Path::new(socket_path);
23 #[cfg(unix)]
24 {
25 if let Ok(meta) = std::fs::symlink_metadata(path) {
26 if meta.file_type().is_symlink() {
27 tracing::warn!("socket: refusing to remove symlink at {}", socket_path);
28 return;
29 }
30 }
31 }
32 match std::fs::remove_file(path) {
33 Ok(()) => {}
34 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
35 Err(e) => tracing::warn!("socket: failed to remove {}: {}", socket_path, e),
36 }
37}
38
39pub fn listen_session_socket(
45 socket_path: String,
46 queue: Arc<EventQueue>,
47 shutdown: Arc<AtomicBool>,
48) -> tokio::task::JoinHandle<()> {
49 tokio::spawn(async move {
50 cleanup_socket(&socket_path);
52
53 let listener = match UnixListener::bind(&socket_path) {
54 Ok(l) => l,
55 Err(e) => {
56 tracing::error!("socket: failed to bind {}: {}", socket_path, e);
57 return;
58 }
59 };
60
61 #[cfg(unix)]
63 {
64 use std::os::unix::fs::PermissionsExt;
65 if let Ok(meta) = std::fs::metadata(&socket_path) {
66 let mut perms = meta.permissions();
67 perms.set_mode(0o600);
68 let _ = std::fs::set_permissions(&socket_path, perms);
69 }
70 }
71
72 tracing::info!("socket: listening on {}", socket_path);
73
74 loop {
75 if shutdown.load(Ordering::Acquire) {
76 break;
77 }
78
79 let accept =
81 tokio::time::timeout(std::time::Duration::from_millis(500), listener.accept());
82
83 match accept.await {
84 Ok(Ok((mut stream, _addr))) => {
85 let queue = queue.clone();
86 tokio::spawn(async move {
87 let _ = tokio::time::timeout(
89 std::time::Duration::from_secs(5),
90 handle_connection(&mut stream, &queue),
91 )
92 .await;
93 });
94 }
95 Ok(Err(e)) => {
96 tracing::warn!("socket: accept error: {}", e);
97 }
98 Err(_) => {
99 }
101 }
102 }
103
104 cleanup_socket(&socket_path);
105 tracing::info!("socket: shut down, removed {}", socket_path);
106 })
107}
108
109async fn handle_connection(stream: &mut tokio::net::UnixStream, queue: &EventQueue) {
110 let mut buf = Vec::with_capacity(4096);
112 let mut chunk = [0u8; 8192];
113
114 loop {
115 match stream.read(&mut chunk).await {
116 Ok(0) => break, Ok(n) => {
118 if buf.len() + n > MAX_PAYLOAD {
119 tracing::warn!(
120 "socket: payload exceeds {}KB limit, dropping connection",
121 MAX_PAYLOAD / 1024
122 );
123 return;
124 }
125 buf.extend_from_slice(&chunk[..n]);
126 }
127 Err(e) => {
128 tracing::warn!("socket: read error: {}", e);
129 return;
130 }
131 }
132 }
133
134 if buf.is_empty() {
135 return;
136 }
137
138 match serde_json::from_slice::<Event>(&buf) {
139 Ok(event) => {
140 tracing::info!(
141 "socket: event {} from {}",
142 event.id,
143 event.source.source_type
144 );
145 if let Err(e) = queue.push(event) {
146 tracing::warn!("socket: queue push failed: {}", e);
147 }
148 }
149 Err(e) => {
150 tracing::warn!("socket: invalid JSON payload: {}", e);
151 }
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use crate::events::{Event, Severity};
159 use std::sync::atomic::AtomicBool;
160 use tokio::io::AsyncWriteExt;
161 use tokio::net::UnixStream;
162
163 fn tmp_socket_path() -> String {
164 format!(
165 "/tmp/test-session-socket-{}.sock",
166 uuid::Uuid::new_v4().simple()
167 )
168 }
169
170 async fn wait_for_socket(path: &str) {
171 for _ in 0..50 {
172 if std::path::Path::new(path).exists() {
173 return;
174 }
175 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
176 }
177 panic!("socket never appeared at {}", path);
178 }
179
180 #[tokio::test]
181 async fn delivers_event_to_queue() {
182 let path = tmp_socket_path();
183 let queue = Arc::new(EventQueue::new(10));
184 let shutdown = Arc::new(AtomicBool::new(false));
185
186 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
187 wait_for_socket(&path).await;
188
189 let event = Event::simple("test", "hello socket", Some(Severity::High));
190 let json = serde_json::to_vec(&event).unwrap();
191
192 let mut client = UnixStream::connect(&path).await.unwrap();
193 client.write_all(&json).await.unwrap();
194 client.shutdown().await.unwrap();
195
196 for _ in 0..50 {
198 if !queue.is_empty() {
199 break;
200 }
201 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
202 }
203
204 shutdown.store(true, Ordering::Release);
205 handle.await.unwrap();
206
207 let popped = queue.pop().expect("event should be in queue");
208 assert_eq!(popped.content.text, "hello socket");
209 assert_eq!(popped.source.source_type, "test");
210 }
211
212 #[tokio::test]
213 async fn rejects_oversized_payload() {
214 let path = tmp_socket_path();
215 let queue = Arc::new(EventQueue::new(10));
216 let shutdown = Arc::new(AtomicBool::new(false));
217
218 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
219 wait_for_socket(&path).await;
220
221 let oversized = vec![b'x'; MAX_PAYLOAD + 1024];
223 let mut client = UnixStream::connect(&path).await.unwrap();
224 client.write_all(&oversized).await.unwrap();
225 client.shutdown().await.unwrap();
226
227 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
228
229 shutdown.store(true, Ordering::Release);
230 handle.await.unwrap();
231
232 assert_eq!(queue.len(), 0, "oversized payload should not reach queue");
233 }
234
235 #[tokio::test]
236 async fn invalid_json_does_not_crash() {
237 let path = tmp_socket_path();
238 let queue = Arc::new(EventQueue::new(10));
239 let shutdown = Arc::new(AtomicBool::new(false));
240
241 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
242 wait_for_socket(&path).await;
243
244 let mut client = UnixStream::connect(&path).await.unwrap();
245 client.write_all(b"this is not json at all").await.unwrap();
246 client.shutdown().await.unwrap();
247
248 tokio::time::sleep(std::time::Duration::from_millis(200)).await;
249
250 let event = Event::simple("test", "still alive", None);
252 let json = serde_json::to_vec(&event).unwrap();
253 let mut client2 = UnixStream::connect(&path).await.unwrap();
254 client2.write_all(&json).await.unwrap();
255 client2.shutdown().await.unwrap();
256
257 for _ in 0..50 {
258 if !queue.is_empty() {
259 break;
260 }
261 tokio::time::sleep(std::time::Duration::from_millis(20)).await;
262 }
263
264 shutdown.store(true, Ordering::Release);
265 handle.await.unwrap();
266
267 assert_eq!(queue.len(), 1);
268 assert_eq!(queue.pop().unwrap().content.text, "still alive");
269 }
270
271 #[tokio::test]
272 async fn stale_socket_removed_on_startup() {
273 let path = tmp_socket_path();
274 std::fs::write(&path, b"stale").unwrap();
276 assert!(std::path::Path::new(&path).exists());
277
278 let queue = Arc::new(EventQueue::new(10));
279 let shutdown = Arc::new(AtomicBool::new(false));
280
281 let handle = listen_session_socket(path.clone(), queue.clone(), shutdown.clone());
283 wait_for_socket(&path).await;
284
285 shutdown.store(true, Ordering::Release);
286 handle.await.unwrap();
287 }
288}