gardesk/garlock / 3ffcab8

Browse files

Add IPC module for daemon communication

Unix socket-based IPC at $XDG_RUNTIME_DIR/garlock.sock:
- JSON protocol with Command/Response/Event types
- Non-blocking server for daemon mode
- Client for sending commands to running daemon
- Socket cleanup on exit
Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
3ffcab8a731f1caa57e65ec1b0a2c0fc1ea93028
Parents
da1201b
Tree
a442031

3 changed files

StatusFile+-
A garlock/src/ipc/mod.rs 27 0
A garlock/src/ipc/protocol.rs 146 0
A garlock/src/ipc/server.rs 258 0
garlock/src/ipc/mod.rsadded
@@ -0,0 +1,27 @@
1
+//! IPC module for garlock
2
+//!
3
+//! Provides Unix socket-based communication for:
4
+//! - Daemon mode: Listen for lock commands
5
+//! - Client mode: Send commands to running daemon
6
+//!
7
+//! ## Socket Location
8
+//! `$XDG_RUNTIME_DIR/garlock.sock`
9
+//!
10
+//! ## Protocol
11
+//! JSON messages, one per line:
12
+//!
13
+//! Commands (client -> daemon):
14
+//! - `{"command":"lock"}` - Lock screen immediately
15
+//! - `{"command":"query-state"}` - Get current state
16
+//! - `{"command":"shutdown"}` - Shutdown daemon
17
+//!
18
+//! Responses (daemon -> client):
19
+//! - `{"status":"ok"}` - Success
20
+//! - `{"status":"state","locked":true,"failed_attempts":0}` - State info
21
+//! - `{"status":"error","message":"..."}` - Error
22
+
23
+mod protocol;
24
+mod server;
25
+
26
+pub use protocol::{Command, Event, Response};
27
+pub use server::{socket_path, CommandReceiver, IpcClient, IpcServer};
garlock/src/ipc/protocol.rsadded
@@ -0,0 +1,146 @@
1
+//! IPC protocol definitions for garlock
2
+//!
3
+//! JSON-based protocol for communication between garlock daemon and clients.
4
+
5
+use serde::{Deserialize, Serialize};
6
+
7
+/// Commands sent from clients to garlock daemon
8
+#[derive(Debug, Clone, Serialize, Deserialize)]
9
+#[serde(tag = "command", rename_all = "kebab-case")]
10
+pub enum Command {
11
+    /// Lock the screen immediately
12
+    Lock,
13
+    /// Query current lock state
14
+    QueryState,
15
+    /// Gracefully shutdown the daemon
16
+    Shutdown,
17
+}
18
+
19
+/// Response from garlock daemon to clients
20
+#[derive(Debug, Clone, Serialize, Deserialize)]
21
+#[serde(tag = "status", rename_all = "kebab-case")]
22
+pub enum Response {
23
+    /// Command executed successfully
24
+    Ok {
25
+        /// Optional message
26
+        #[serde(skip_serializing_if = "Option::is_none")]
27
+        message: Option<String>,
28
+    },
29
+    /// Current state information
30
+    State {
31
+        /// Whether screen is currently locked
32
+        locked: bool,
33
+        /// Number of failed authentication attempts (if locked)
34
+        #[serde(skip_serializing_if = "Option::is_none")]
35
+        failed_attempts: Option<u32>,
36
+        /// Whether in cooldown period
37
+        #[serde(skip_serializing_if = "Option::is_none")]
38
+        in_cooldown: Option<bool>,
39
+    },
40
+    /// Error occurred
41
+    Error {
42
+        /// Error message
43
+        message: String,
44
+    },
45
+}
46
+
47
+/// Events broadcast from garlock daemon to subscribers
48
+#[derive(Debug, Clone, Serialize, Deserialize)]
49
+#[serde(tag = "event", rename_all = "kebab-case")]
50
+pub enum Event {
51
+    /// Screen was locked
52
+    Locked,
53
+    /// Screen was unlocked (successful auth)
54
+    Unlocked,
55
+    /// Authentication attempt failed
56
+    AuthFailed {
57
+        /// Current attempt number
58
+        attempt: u32,
59
+    },
60
+    /// Cooldown started after too many failures
61
+    CooldownStarted {
62
+        /// Cooldown duration in seconds
63
+        seconds: u64,
64
+    },
65
+    /// Daemon is shutting down
66
+    Shutdown,
67
+}
68
+
69
+impl Response {
70
+    /// Create a success response
71
+    pub fn ok() -> Self {
72
+        Self::Ok { message: None }
73
+    }
74
+
75
+    /// Create a success response with message
76
+    pub fn ok_with_message(msg: impl Into<String>) -> Self {
77
+        Self::Ok {
78
+            message: Some(msg.into()),
79
+        }
80
+    }
81
+
82
+    /// Create an error response
83
+    pub fn error(msg: impl Into<String>) -> Self {
84
+        Self::Error {
85
+            message: msg.into(),
86
+        }
87
+    }
88
+
89
+    /// Create a state response
90
+    pub fn state(locked: bool, failed_attempts: Option<u32>, in_cooldown: Option<bool>) -> Self {
91
+        Self::State {
92
+            locked,
93
+            failed_attempts,
94
+            in_cooldown,
95
+        }
96
+    }
97
+}
98
+
99
+#[cfg(test)]
100
+mod tests {
101
+    use super::*;
102
+
103
+    #[test]
104
+    fn test_command_serialization() {
105
+        let cmd = Command::Lock;
106
+        let json = serde_json::to_string(&cmd).unwrap();
107
+        assert_eq!(json, r#"{"command":"lock"}"#);
108
+
109
+        let cmd = Command::QueryState;
110
+        let json = serde_json::to_string(&cmd).unwrap();
111
+        assert_eq!(json, r#"{"command":"query-state"}"#);
112
+    }
113
+
114
+    #[test]
115
+    fn test_command_deserialization() {
116
+        let cmd: Command = serde_json::from_str(r#"{"command":"lock"}"#).unwrap();
117
+        assert!(matches!(cmd, Command::Lock));
118
+
119
+        let cmd: Command = serde_json::from_str(r#"{"command":"query-state"}"#).unwrap();
120
+        assert!(matches!(cmd, Command::QueryState));
121
+    }
122
+
123
+    #[test]
124
+    fn test_response_serialization() {
125
+        let resp = Response::ok();
126
+        let json = serde_json::to_string(&resp).unwrap();
127
+        assert_eq!(json, r#"{"status":"ok"}"#);
128
+
129
+        let resp = Response::state(true, Some(2), Some(false));
130
+        let json = serde_json::to_string(&resp).unwrap();
131
+        assert!(json.contains(r#""locked":true"#));
132
+        assert!(json.contains(r#""failed_attempts":2"#));
133
+    }
134
+
135
+    #[test]
136
+    fn test_event_serialization() {
137
+        let event = Event::Locked;
138
+        let json = serde_json::to_string(&event).unwrap();
139
+        assert_eq!(json, r#"{"event":"locked"}"#);
140
+
141
+        let event = Event::AuthFailed { attempt: 3 };
142
+        let json = serde_json::to_string(&event).unwrap();
143
+        assert!(json.contains(r#""event":"auth-failed""#));
144
+        assert!(json.contains(r#""attempt":3"#));
145
+    }
146
+}
garlock/src/ipc/server.rsadded
@@ -0,0 +1,258 @@
1
+//! IPC server for garlock daemon mode
2
+//!
3
+//! Unix domain socket server that listens for commands from clients.
4
+
5
+use std::fs;
6
+use std::io::{BufRead, BufReader, Write};
7
+use std::os::unix::net::{UnixListener, UnixStream};
8
+use std::path::PathBuf;
9
+use std::sync::mpsc::{self, Receiver, Sender, TryRecvError};
10
+use std::thread;
11
+use std::time::Duration;
12
+
13
+use anyhow::{Context, Result};
14
+
15
+use super::protocol::{Command, Response};
16
+
17
+/// Get the socket path for garlock IPC
18
+pub fn socket_path() -> PathBuf {
19
+    std::env::var("XDG_RUNTIME_DIR")
20
+        .map(PathBuf::from)
21
+        .unwrap_or_else(|_| PathBuf::from("/tmp"))
22
+        .join("garlock.sock")
23
+}
24
+
25
+/// IPC server for daemon mode
26
+pub struct IpcServer {
27
+    listener: UnixListener,
28
+    socket_path: PathBuf,
29
+}
30
+
31
+impl IpcServer {
32
+    /// Create a new IPC server
33
+    ///
34
+    /// Binds to the socket path and starts listening.
35
+    pub fn new() -> Result<Self> {
36
+        let socket_path = socket_path();
37
+
38
+        // Remove existing socket if present
39
+        if socket_path.exists() {
40
+            fs::remove_file(&socket_path)
41
+                .with_context(|| format!("Failed to remove existing socket: {:?}", socket_path))?;
42
+        }
43
+
44
+        // Create parent directory if needed
45
+        if let Some(parent) = socket_path.parent() {
46
+            fs::create_dir_all(parent).ok();
47
+        }
48
+
49
+        let listener = UnixListener::bind(&socket_path)
50
+            .with_context(|| format!("Failed to bind to socket: {:?}", socket_path))?;
51
+
52
+        // Set non-blocking so we can poll
53
+        listener.set_nonblocking(true)?;
54
+
55
+        tracing::info!(?socket_path, "IPC server listening");
56
+
57
+        Ok(Self {
58
+            listener,
59
+            socket_path,
60
+        })
61
+    }
62
+
63
+    /// Get the socket path
64
+    pub fn socket_path(&self) -> &PathBuf {
65
+        &self.socket_path
66
+    }
67
+
68
+    /// Poll for incoming connections and commands
69
+    ///
70
+    /// Returns a command if one was received, None otherwise.
71
+    /// This is non-blocking.
72
+    pub fn poll(&self) -> Option<(Command, ClientConnection)> {
73
+        match self.listener.accept() {
74
+            Ok((stream, _addr)) => {
75
+                // Set blocking for the connection itself
76
+                stream.set_nonblocking(false).ok();
77
+                stream
78
+                    .set_read_timeout(Some(Duration::from_secs(5)))
79
+                    .ok();
80
+                stream
81
+                    .set_write_timeout(Some(Duration::from_secs(5)))
82
+                    .ok();
83
+
84
+                match Self::read_command(&stream) {
85
+                    Ok(cmd) => {
86
+                        tracing::debug!(?cmd, "Received IPC command");
87
+                        Some((cmd, ClientConnection { stream }))
88
+                    }
89
+                    Err(e) => {
90
+                        tracing::warn!("Failed to read IPC command: {}", e);
91
+                        None
92
+                    }
93
+                }
94
+            }
95
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => None,
96
+            Err(e) => {
97
+                tracing::warn!("Failed to accept connection: {}", e);
98
+                None
99
+            }
100
+        }
101
+    }
102
+
103
+    /// Read a command from a stream
104
+    fn read_command(stream: &UnixStream) -> Result<Command> {
105
+        let mut reader = BufReader::new(stream);
106
+        let mut line = String::new();
107
+        reader.read_line(&mut line)?;
108
+
109
+        let cmd: Command = serde_json::from_str(line.trim())
110
+            .with_context(|| format!("Failed to parse command: {}", line.trim()))?;
111
+
112
+        Ok(cmd)
113
+    }
114
+
115
+    /// Clean up the socket file
116
+    pub fn cleanup(&self) {
117
+        if self.socket_path.exists() {
118
+            if let Err(e) = fs::remove_file(&self.socket_path) {
119
+                tracing::warn!(?self.socket_path, "Failed to remove socket: {}", e);
120
+            } else {
121
+                tracing::debug!(?self.socket_path, "Socket removed");
122
+            }
123
+        }
124
+    }
125
+}
126
+
127
+impl Drop for IpcServer {
128
+    fn drop(&mut self) {
129
+        self.cleanup();
130
+    }
131
+}
132
+
133
+/// A connected client that can receive responses
134
+pub struct ClientConnection {
135
+    stream: UnixStream,
136
+}
137
+
138
+impl ClientConnection {
139
+    /// Send a response to the client
140
+    pub fn respond(&mut self, response: Response) -> Result<()> {
141
+        let json = serde_json::to_string(&response)?;
142
+        writeln!(self.stream, "{}", json)?;
143
+        self.stream.flush()?;
144
+        Ok(())
145
+    }
146
+}
147
+
148
+/// IPC client for sending commands to the daemon
149
+pub struct IpcClient {
150
+    stream: UnixStream,
151
+}
152
+
153
+impl IpcClient {
154
+    /// Connect to the garlock daemon
155
+    pub fn connect() -> Result<Self> {
156
+        let socket_path = socket_path();
157
+
158
+        let stream = UnixStream::connect(&socket_path)
159
+            .with_context(|| format!("Failed to connect to socket: {:?}", socket_path))?;
160
+
161
+        stream.set_read_timeout(Some(Duration::from_secs(5)))?;
162
+        stream.set_write_timeout(Some(Duration::from_secs(5)))?;
163
+
164
+        Ok(Self { stream })
165
+    }
166
+
167
+    /// Send a command and receive a response
168
+    pub fn send(&mut self, command: Command) -> Result<Response> {
169
+        // Send command
170
+        let json = serde_json::to_string(&command)?;
171
+        writeln!(self.stream, "{}", json)?;
172
+        self.stream.flush()?;
173
+
174
+        // Read response
175
+        let mut reader = BufReader::new(&self.stream);
176
+        let mut line = String::new();
177
+        reader.read_line(&mut line)?;
178
+
179
+        let response: Response = serde_json::from_str(line.trim())
180
+            .with_context(|| format!("Failed to parse response: {}", line.trim()))?;
181
+
182
+        Ok(response)
183
+    }
184
+
185
+    /// Send lock command
186
+    pub fn lock(&mut self) -> Result<Response> {
187
+        self.send(Command::Lock)
188
+    }
189
+
190
+    /// Query current state
191
+    pub fn query_state(&mut self) -> Result<Response> {
192
+        self.send(Command::QueryState)
193
+    }
194
+
195
+    /// Request daemon shutdown
196
+    pub fn shutdown(&mut self) -> Result<Response> {
197
+        self.send(Command::Shutdown)
198
+    }
199
+}
200
+
201
+/// Channel-based command receiver for integration with event loop
202
+pub struct CommandReceiver {
203
+    rx: Receiver<(Command, Sender<Response>)>,
204
+    _server_thread: thread::JoinHandle<()>,
205
+}
206
+
207
+impl CommandReceiver {
208
+    /// Start a background thread to handle IPC connections
209
+    pub fn start() -> Result<Self> {
210
+        let server = IpcServer::new()?;
211
+        let (tx, rx) = mpsc::channel();
212
+
213
+        let handle = thread::spawn(move || {
214
+            loop {
215
+                if let Some((cmd, mut client)) = server.poll() {
216
+                    let (resp_tx, resp_rx) = mpsc::channel();
217
+
218
+                    // Send command to main thread
219
+                    if tx.send((cmd, resp_tx)).is_err() {
220
+                        // Main thread closed, exit
221
+                        break;
222
+                    }
223
+
224
+                    // Wait for response from main thread
225
+                    match resp_rx.recv_timeout(Duration::from_secs(30)) {
226
+                        Ok(response) => {
227
+                            if let Err(e) = client.respond(response) {
228
+                                tracing::warn!("Failed to send response: {}", e);
229
+                            }
230
+                        }
231
+                        Err(_) => {
232
+                            let _ = client.respond(Response::error("Timeout waiting for response"));
233
+                        }
234
+                    }
235
+                }
236
+
237
+                // Small sleep to avoid busy loop
238
+                thread::sleep(Duration::from_millis(10));
239
+            }
240
+
241
+            server.cleanup();
242
+        });
243
+
244
+        Ok(Self {
245
+            rx,
246
+            _server_thread: handle,
247
+        })
248
+    }
249
+
250
+    /// Try to receive a command (non-blocking)
251
+    pub fn try_recv(&self) -> Option<(Command, Sender<Response>)> {
252
+        match self.rx.try_recv() {
253
+            Ok(cmd) => Some(cmd),
254
+            Err(TryRecvError::Empty) => None,
255
+            Err(TryRecvError::Disconnected) => None,
256
+        }
257
+    }
258
+}