gardesk/tarmac / 75c9882

Browse files

harden border spawn and ipc delivery

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
75c9882ba1bed9e52020e73bd6faba629e7fedf3
Parents
fde3b8b
Tree
1f65086

2 changed files

StatusFile+-
M tarmac/src/ipc/events.rs 47 2
M tarmac/src/platform/border.rs 69 19
tarmac/src/ipc/events.rsmodified
@@ -1,6 +1,6 @@
11
 use serde::Serialize;
22
 use std::sync::Mutex;
3
-use std::sync::mpsc;
3
+use std::sync::mpsc::{self, TrySendError};
44
 
55
 /// Events emitted by the window manager for IPC subscribers.
66
 #[derive(Debug, Clone, Serialize)]
@@ -55,6 +55,51 @@ impl EventBus {
5555
     /// Disconnected subscribers are automatically removed.
5656
     pub fn publish(&self, event: WmEvent) {
5757
         let mut subs = self.subscribers.lock().unwrap();
58
-        subs.retain(|tx| tx.try_send(event.clone()).is_ok());
58
+        subs.retain(|tx| match tx.try_send(event.clone()) {
59
+            Ok(()) | Err(TrySendError::Full(_)) => true,
60
+            Err(TrySendError::Disconnected(_)) => false,
61
+        });
62
+    }
63
+}
64
+
65
+#[cfg(test)]
66
+mod tests {
67
+    use super::{EventBus, WmEvent};
68
+    use std::time::Duration;
69
+
70
+    #[test]
71
+    fn full_subscriber_is_not_dropped() {
72
+        let bus = EventBus::new();
73
+        let rx = bus.subscribe();
74
+
75
+        for window_id in 0..256 {
76
+            bus.publish(WmEvent::WindowClosed { window_id });
77
+        }
78
+
79
+        bus.publish(WmEvent::WindowClosed { window_id: 999 });
80
+
81
+        for _ in 0..256 {
82
+            rx.recv_timeout(Duration::from_millis(50))
83
+                .expect("buffered event missing");
84
+        }
85
+
86
+        bus.publish(WmEvent::WindowClosed { window_id: 1000 });
87
+        let event = rx
88
+            .recv_timeout(Duration::from_millis(50))
89
+            .expect("subscriber should still be registered");
90
+        assert!(matches!(event, WmEvent::WindowClosed { window_id: 1000 }));
91
+    }
92
+
93
+    #[test]
94
+    fn disconnected_subscriber_is_removed() {
95
+        let bus = EventBus::new();
96
+        let rx = bus.subscribe();
97
+        drop(rx);
98
+
99
+        bus.publish(WmEvent::WindowClosed { window_id: 1 });
100
+
101
+        let mut subscribers = bus.subscribers.lock().unwrap();
102
+        assert!(subscribers.is_empty());
103
+        subscribers.clear();
59104
     }
60105
 }
tarmac/src/platform/border.rsmodified
@@ -2,6 +2,7 @@
22
 //! ers is a standalone border renderer that handles its own window events,
33
 //! focus detection, and rendering. Tarmac just spawns and manages the process.
44
 
5
+use std::path::PathBuf;
56
 use std::process::{Child, Command};
67
 
78
 /// RGBA color for border configuration.
@@ -17,15 +18,34 @@ impl BorderColor {
1718
     /// Parse a hex color string (#RRGGBB or #RRGGBBAA).
1819
     pub fn from_hex(hex: &str) -> Self {
1920
         let hex = hex.trim_start_matches('#');
20
-        let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(0) as f64 / 255.0;
21
-        let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(0) as f64 / 255.0;
22
-        let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(0) as f64 / 255.0;
23
-        let a = if hex.len() >= 8 {
24
-            u8::from_str_radix(&hex[6..8], 16).unwrap_or(255) as f64 / 255.0
21
+        match Self::try_from_hex(hex) {
22
+            Some(color) => color,
23
+            None => {
24
+                tracing::warn!(input = hex, "invalid border color, falling back to black");
25
+                Self {
26
+                    r: 0.0,
27
+                    g: 0.0,
28
+                    b: 0.0,
29
+                    a: 1.0,
30
+                }
31
+            }
32
+        }
33
+    }
34
+
35
+    fn try_from_hex(hex: &str) -> Option<Self> {
36
+        if hex.len() != 6 && hex.len() != 8 {
37
+            return None;
38
+        }
39
+
40
+        let r = u8::from_str_radix(hex.get(0..2)?, 16).ok()? as f64 / 255.0;
41
+        let g = u8::from_str_radix(hex.get(2..4)?, 16).ok()? as f64 / 255.0;
42
+        let b = u8::from_str_radix(hex.get(4..6)?, 16).ok()? as f64 / 255.0;
43
+        let a = if hex.len() == 8 {
44
+            u8::from_str_radix(hex.get(6..8)?, 16).ok()? as f64 / 255.0
2545
         } else {
2646
             1.0
2747
         };
28
-        Self { r, g, b, a }
48
+        Some(Self { r, g, b, a })
2949
     }
3050
 
3151
     fn hex_string(self) -> String {
@@ -78,24 +98,34 @@ impl BorderManager {
7898
             .ok()
7999
             .and_then(|p| p.parent().map(|d| d.join("ers")))
80100
             .filter(|p| p.exists())
81
-            .map(|p| p.to_string_lossy().to_string())
82
-            .unwrap_or_else(|| "ers".to_string());
83
-
84
-        let cmd = format!(
85
-            "{} --active-only --width {} --radius {} --color '{}' --inactive '{}'",
86
-            ers_bin,
87
-            self.border_width,
88
-            self.radius,
89
-            self.focused_color.hex_string(),
90
-            self.unfocused_color.hex_string(),
101
+            .unwrap_or_else(|| PathBuf::from("ers"));
102
+
103
+        tracing::debug!(
104
+            ers_bin = ?ers_bin,
105
+            border_width = self.border_width,
106
+            radius = self.radius,
107
+            focused = %self.focused_color.hex_string(),
108
+            unfocused = %self.unfocused_color.hex_string(),
109
+            "spawning ers"
91110
         );
92
-        tracing::debug!(cmd, "spawning ers");
93
-        match Command::new("/bin/sh").args(["-c", &cmd]).spawn() {
111
+
112
+        match Command::new(&ers_bin)
113
+            .arg("--active-only")
114
+            .arg("--width")
115
+            .arg(self.border_width.to_string())
116
+            .arg("--radius")
117
+            .arg(self.radius.to_string())
118
+            .arg("--color")
119
+            .arg(self.focused_color.hex_string())
120
+            .arg("--inactive")
121
+            .arg(self.unfocused_color.hex_string())
122
+            .spawn()
123
+        {
94124
             Ok(child) => {
95125
                 self.child = Some(child);
96126
             }
97127
             Err(e) => {
98
-                tracing::warn!(err = %e, "failed to spawn ers");
128
+                tracing::warn!(err = %e, ers_bin = ?ers_bin, "failed to spawn ers");
99129
             }
100130
         }
101131
     }
@@ -132,3 +162,23 @@ impl Drop for BorderManager {
132162
         self.kill();
133163
     }
134164
 }
165
+
166
+#[cfg(test)]
167
+mod tests {
168
+    use super::BorderColor;
169
+
170
+    #[test]
171
+    fn malformed_hex_falls_back_without_panicking() {
172
+        let color = BorderColor::from_hex("#fff");
173
+        assert_eq!(color.r, 0.0);
174
+        assert_eq!(color.g, 0.0);
175
+        assert_eq!(color.b, 0.0);
176
+        assert_eq!(color.a, 1.0);
177
+    }
178
+
179
+    #[test]
180
+    fn parses_rgba_hex() {
181
+        let color = BorderColor::from_hex("#11223380");
182
+        assert_eq!(color.hex_string(), "#11223380");
183
+    }
184
+}