1use crate::util::paths;
7use std::io::{Read, Write};
8use std::os::unix::net::{UnixListener, UnixStream};
9use std::path::PathBuf;
10use std::sync::mpsc::{self, Receiver, Sender};
11use std::thread;
12use std::time::Duration;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum IpcCommand {
17 CycleForward,
19 CycleBackward,
21 Ping,
23}
24
25impl IpcCommand {
26 fn to_byte(self) -> u8 {
27 match self {
28 IpcCommand::CycleForward => b'F',
29 IpcCommand::CycleBackward => b'B',
30 IpcCommand::Ping => b'P',
31 }
32 }
33
34 fn from_byte(byte: u8) -> Option<Self> {
35 match byte {
36 b'F' => Some(IpcCommand::CycleForward),
37 b'B' => Some(IpcCommand::CycleBackward),
38 b'P' => Some(IpcCommand::Ping),
39 _ => None,
40 }
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum IpcResponse {
47 Ok,
49 Pong,
51 Error,
53}
54
55impl IpcResponse {
56 fn to_byte(self) -> u8 {
57 match self {
58 IpcResponse::Ok => b'K',
59 IpcResponse::Pong => b'O',
60 IpcResponse::Error => b'E',
61 }
62 }
63
64 fn from_byte(byte: u8) -> Option<Self> {
65 match byte {
66 b'K' => Some(IpcResponse::Ok),
67 b'O' => Some(IpcResponse::Pong),
68 b'E' => Some(IpcResponse::Error),
69 _ => None,
70 }
71 }
72}
73
74pub struct IpcServer {
84 receiver: Receiver<IpcCommand>,
85 _listener_thread: thread::JoinHandle<()>,
86 socket_path: PathBuf,
87}
88
89impl IpcServer {
90 pub fn start() -> std::io::Result<Self> {
92 let socket_path = Self::socket_path();
93
94 if socket_path.exists() {
96 std::fs::remove_file(&socket_path).ok();
97 }
98
99 if let Some(parent) = socket_path.parent() {
101 std::fs::create_dir_all(parent)?;
102 }
103
104 let listener = UnixListener::bind(&socket_path)?;
105 listener.set_nonblocking(true)?;
106
107 tracing::info!("IPC server listening on {:?}", socket_path);
108
109 let (sender, receiver) = mpsc::channel();
110 let path_clone = socket_path.clone();
111
112 let listener_thread = thread::spawn(move || {
113 Self::listener_loop(listener, sender, path_clone);
114 });
115
116 Ok(Self {
117 receiver,
118 _listener_thread: listener_thread,
119 socket_path,
120 })
121 }
122
123 pub fn try_recv(&self) -> Option<IpcCommand> {
125 self.receiver.try_recv().ok()
126 }
127
128 fn socket_path() -> PathBuf {
130 match paths::cache_dir() {
131 Ok(dir) => dir.join("ipc.sock"),
132 Err(_) => {
133 let uid = unsafe { libc::getuid() };
134 PathBuf::from(format!("/run/user/{}/open-sesame.sock", uid))
135 }
136 }
137 }
138
139 fn listener_loop(listener: UnixListener, sender: Sender<IpcCommand>, _path: PathBuf) {
155 loop {
156 match listener.accept() {
157 Ok((mut stream, _)) => {
158 stream
160 .set_read_timeout(Some(Duration::from_millis(100)))
161 .ok();
162
163 let mut buf = [0u8; 1];
164 if stream.read_exact(&mut buf).is_ok()
165 && let Some(cmd) = IpcCommand::from_byte(buf[0])
166 {
167 tracing::debug!("IPC received command: {:?}", cmd);
168
169 let response = if cmd == IpcCommand::Ping {
171 IpcResponse::Pong
172 } else {
173 if sender.send(cmd).is_ok() {
175 IpcResponse::Ok
176 } else {
177 IpcResponse::Error
178 }
179 };
180
181 stream.write_all(&[response.to_byte()]).ok();
182 }
183 }
184 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
185 thread::sleep(Duration::from_millis(10));
187 }
188 Err(e) => {
189 tracing::error!("IPC accept error: {}", e);
190 thread::sleep(Duration::from_millis(100));
191 }
192 }
193 }
194 }
195}
196
197impl Drop for IpcServer {
198 fn drop(&mut self) {
199 std::fs::remove_file(&self.socket_path).ok();
201 }
202}
203
204pub struct IpcClient;
206
207impl IpcClient {
208 pub fn send(cmd: IpcCommand) -> std::io::Result<IpcResponse> {
210 let socket_path = IpcServer::socket_path();
211
212 let mut stream = UnixStream::connect(&socket_path)?;
213 stream.set_read_timeout(Some(Duration::from_millis(500)))?;
214 stream.set_write_timeout(Some(Duration::from_millis(500)))?;
215
216 stream.write_all(&[cmd.to_byte()])?;
218
219 let mut buf = [0u8; 1];
221 stream.read_exact(&mut buf)?;
222
223 IpcResponse::from_byte(buf[0]).ok_or_else(|| {
224 std::io::Error::new(std::io::ErrorKind::InvalidData, "Invalid IPC response")
225 })
226 }
227
228 pub fn is_instance_running() -> bool {
230 Self::send(IpcCommand::Ping).is_ok()
231 }
232
233 pub fn signal_cycle_forward() -> bool {
235 match Self::send(IpcCommand::CycleForward) {
236 Ok(IpcResponse::Ok) => {
237 tracing::info!("IPC: cycle forward acknowledged");
238 true
239 }
240 Ok(resp) => {
241 tracing::warn!("IPC: unexpected response {:?}", resp);
242 false
243 }
244 Err(e) => {
245 tracing::error!("IPC: failed to send cycle forward: {}", e);
246 false
247 }
248 }
249 }
250
251 pub fn signal_cycle_backward() -> bool {
253 match Self::send(IpcCommand::CycleBackward) {
254 Ok(IpcResponse::Ok) => {
255 tracing::info!("IPC: cycle backward acknowledged");
256 true
257 }
258 Ok(resp) => {
259 tracing::warn!("IPC: unexpected response {:?}", resp);
260 false
261 }
262 Err(e) => {
263 tracing::error!("IPC: failed to send cycle backward: {}", e);
264 false
265 }
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_command_byte_roundtrip() {
276 for cmd in [
277 IpcCommand::CycleForward,
278 IpcCommand::CycleBackward,
279 IpcCommand::Ping,
280 ] {
281 let byte = cmd.to_byte();
282 let decoded = IpcCommand::from_byte(byte);
283 assert_eq!(decoded, Some(cmd));
284 }
285 }
286
287 #[test]
288 fn test_response_byte_roundtrip() {
289 for resp in [IpcResponse::Ok, IpcResponse::Pong, IpcResponse::Error] {
290 let byte = resp.to_byte();
291 let decoded = IpcResponse::from_byte(byte);
292 assert_eq!(decoded, Some(resp));
293 }
294 }
295
296 #[test]
297 fn test_invalid_bytes() {
298 assert_eq!(IpcCommand::from_byte(0), None);
299 assert_eq!(IpcCommand::from_byte(255), None);
300 assert_eq!(IpcResponse::from_byte(0), None);
301 assert_eq!(IpcResponse::from_byte(255), None);
302 }
303}