gardesk/gar / 0d07d9e

Browse files

Fix i3 IPC FD leak with timeout-based stale client cleanup

Clients that connect but never send messages or subscribe to events
would accumulate indefinitely, eventually exhausting file descriptors.

Add tracking for client creation time and last activity timestamp.
Clients are now cleaned up if they have been idle for 60+ seconds
without subscribing to any events or sending any messages.

This approach is safer than treating UnexpectedEof as disconnect,
which caused issues with polybar's connection handling.
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
0d07d9e1f5cbe2f4ed340fa2779199322463179e
Parents
d122620
Tree
ffa3995

1 changed file

StatusFile+-
M gar/src/ipc/i3_server.rs 42 7
gar/src/ipc/i3_server.rsmodified
@@ -12,6 +12,7 @@ use std::collections::HashSet;
12
 use std::io::{BufReader, BufWriter};
12
 use std::io::{BufReader, BufWriter};
13
 use std::os::unix::net::{UnixListener, UnixStream};
13
 use std::os::unix::net::{UnixListener, UnixStream};
14
 use std::path::PathBuf;
14
 use std::path::PathBuf;
15
+use std::time::{Duration, Instant};
15
 
16
 
16
 use super::i3_compat::{read_message, write_event, write_response, EventType, I3Message};
17
 use super::i3_compat::{read_message, write_event, write_response, EventType, I3Message};
17
 
18
 
@@ -22,12 +23,19 @@ enum ReadResult {
22
     Disconnected,
23
     Disconnected,
23
 }
24
 }
24
 
25
 
26
+/// Timeout for clients that have never sent a message and have no subscriptions.
27
+const IDLE_CLIENT_TIMEOUT: Duration = Duration::from_secs(60);
28
+
25
 /// A connected i3 IPC client.
29
 /// A connected i3 IPC client.
26
 struct I3Client {
30
 struct I3Client {
27
     stream: UnixStream,
31
     stream: UnixStream,
28
     reader: BufReader<UnixStream>,
32
     reader: BufReader<UnixStream>,
29
     writer: BufWriter<UnixStream>,
33
     writer: BufWriter<UnixStream>,
30
     subscriptions: HashSet<String>,
34
     subscriptions: HashSet<String>,
35
+    /// When this client connected.
36
+    created_at: Instant,
37
+    /// When we last received a message from this client.
38
+    last_activity: Option<Instant>,
31
 }
39
 }
32
 
40
 
33
 impl I3Client {
41
 impl I3Client {
@@ -40,12 +48,33 @@ impl I3Client {
40
             reader,
48
             reader,
41
             writer,
49
             writer,
42
             subscriptions: HashSet::new(),
50
             subscriptions: HashSet::new(),
51
+            created_at: Instant::now(),
52
+            last_activity: None,
43
         })
53
         })
44
     }
54
     }
45
 
55
 
56
+    /// Check if this client is stale and should be cleaned up.
57
+    /// A client is stale if it has never sent a message, has no subscriptions,
58
+    /// and has been connected for longer than IDLE_CLIENT_TIMEOUT.
59
+    fn is_stale(&self) -> bool {
60
+        // Clients with subscriptions are waiting for events - keep them
61
+        if !self.subscriptions.is_empty() {
62
+            return false;
63
+        }
64
+        // Clients that have sent messages are active - keep them
65
+        if self.last_activity.is_some() {
66
+            return false;
67
+        }
68
+        // New clients that haven't done anything yet - check timeout
69
+        self.created_at.elapsed() > IDLE_CLIENT_TIMEOUT
70
+    }
71
+
46
     fn read_message(&mut self) -> ReadResult {
72
     fn read_message(&mut self) -> ReadResult {
47
         match read_message(&mut self.reader) {
73
         match read_message(&mut self.reader) {
48
-            Ok(Some(msg)) => ReadResult::Message(msg),
74
+            Ok(Some(msg)) => {
75
+                self.last_activity = Some(Instant::now());
76
+                ReadResult::Message(msg)
77
+            }
49
             Ok(None) => ReadResult::WouldBlock,
78
             Ok(None) => ReadResult::WouldBlock,
50
             Err(e) => {
79
             Err(e) => {
51
                 tracing::debug!("i3 IPC client read error: {}", e);
80
                 tracing::debug!("i3 IPC client read error: {}", e);
@@ -147,21 +176,27 @@ impl I3IpcServer {
147
 
176
 
148
     /// Process incoming requests from all clients.
177
     /// Process incoming requests from all clients.
149
     /// Returns a list of (client_index, message) pairs.
178
     /// Returns a list of (client_index, message) pairs.
179
+    /// Also cleans up stale/disconnected clients.
150
     pub fn poll_requests(&mut self) -> Vec<(usize, I3Message)> {
180
     pub fn poll_requests(&mut self) -> Vec<(usize, I3Message)> {
151
         let mut requests = Vec::new();
181
         let mut requests = Vec::new();
152
-        let mut disconnected = Vec::new();
182
+        let mut to_remove = Vec::new();
153
 
183
 
154
         for (i, client) in self.clients.iter_mut().enumerate() {
184
         for (i, client) in self.clients.iter_mut().enumerate() {
155
             match client.read_message() {
185
             match client.read_message() {
156
                 ReadResult::Message(msg) => requests.push((i, msg)),
186
                 ReadResult::Message(msg) => requests.push((i, msg)),
157
-                ReadResult::Disconnected => disconnected.push(i),
187
+                ReadResult::Disconnected => to_remove.push(i),
158
-                ReadResult::WouldBlock => {}
188
+                ReadResult::WouldBlock => {
189
+                    // Check if this client is stale (never sent anything, no subscriptions)
190
+                    if client.is_stale() {
191
+                        tracing::debug!("Cleaning up stale i3 IPC client (no activity for {:?})", IDLE_CLIENT_TIMEOUT);
192
+                        to_remove.push(i);
193
+                    }
194
+                }
159
             }
195
             }
160
         }
196
         }
161
 
197
 
162
-        // Remove disconnected clients (in reverse order to preserve indices)
198
+        // Remove disconnected/stale clients (in reverse order to preserve indices)
163
-        for i in disconnected.into_iter().rev() {
199
+        for i in to_remove.into_iter().rev() {
164
-            tracing::debug!("i3 IPC client disconnected");
165
             self.clients.remove(i);
200
             self.clients.remove(i);
166
         }
201
         }
167
 
202