Skip to main content

quincy_gui/
ipc.rs

1use ipnet::IpNet;
2use quincy::{QuincyError, Result};
3use serde::{Deserialize, Serialize};
4use std::env;
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
8use tracing::{debug, info};
9
10use crate::gui::GuiError;
11
12#[cfg(unix)]
13use std::fs;
14
15#[cfg(unix)]
16use std::os::unix::fs::PermissionsExt;
17
18#[cfg(unix)]
19use tokio::io::{ReadHalf, WriteHalf};
20
21#[cfg(unix)]
22use tokio::net::{UnixListener, UnixStream};
23
24#[cfg(windows)]
25use tokio::net::windows::named_pipe::{
26    ClientOptions, NamedPipeClient, NamedPipeServer, ServerOptions,
27};
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ConnectionMetrics {
31    pub bytes_sent: u64,
32    pub bytes_received: u64,
33    pub packets_sent: u64,
34    pub packets_received: u64,
35    pub connection_duration: Duration,
36    pub client_address: Option<IpNet>,
37    pub server_address: Option<IpNet>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub enum ConnectionStatus {
42    Disconnected,
43    Connecting,
44    Connected,
45    Error(GuiError),
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ClientStatus {
50    pub status: ConnectionStatus,
51    pub metrics: Option<ConnectionMetrics>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub enum IpcMessage {
56    StartClient { config_path: PathBuf },
57    StopClient,
58    GetStatus,
59    StatusUpdate(ClientStatus),
60    Error(GuiError),
61    Shutdown,
62}
63
64pub struct IpcServer {
65    #[cfg(unix)]
66    listener: UnixListener,
67    #[cfg(windows)]
68    pipe_path: String,
69}
70
71impl IpcServer {
72    pub fn new(socket_path: &Path) -> Result<Self> {
73        #[cfg(unix)]
74        {
75            // Ensure the parent directory exists
76            if let Some(parent) = socket_path.parent()
77                && !parent.exists()
78            {
79                fs::create_dir_all(parent)?;
80            }
81
82            // Remove existing socket file if it exists
83            if socket_path.exists() {
84                fs::remove_file(socket_path)?;
85            }
86
87            let listener = UnixListener::bind(socket_path)?;
88
89            // Set socket file permissions to 0600 (user read/write only)
90            let permissions = fs::Permissions::from_mode(0o600);
91            fs::set_permissions(socket_path, permissions)?;
92
93            info!("IPC server listening on: {:?}", socket_path);
94            Ok(Self { listener })
95        }
96
97        #[cfg(windows)]
98        {
99            let pipe_path = format!(
100                r"\\.\pipe\{}",
101                socket_path.file_name().unwrap().to_string_lossy()
102            );
103            info!("IPC server will listen on: {}", pipe_path);
104            Ok(Self { pipe_path })
105        }
106    }
107
108    pub async fn accept(&self) -> Result<IpcConnection> {
109        #[cfg(unix)]
110        {
111            let (stream, _) = self.listener.accept().await?;
112            Ok(IpcConnection::new_unix(stream))
113        }
114
115        #[cfg(windows)]
116        {
117            let server = ServerOptions::new()
118                .first_pipe_instance(true)
119                .access_inbound(true)
120                .reject_remote_clients(true)
121                .create(&self.pipe_path)?;
122            server.connect().await?;
123            Ok(IpcConnection::new_windows_server(server))
124        }
125    }
126}
127
128pub struct IpcClient {
129    connection: IpcConnection,
130}
131
132impl IpcClient {
133    pub async fn connect(socket_path: &Path) -> Result<Self> {
134        #[cfg(unix)]
135        {
136            debug!("Attempting to connect to IPC socket at: {:?}", socket_path);
137            let stream = UnixStream::connect(socket_path).await.map_err(|e| {
138                debug!(
139                    "Failed to connect to Unix socket at {:?}: {}",
140                    socket_path, e
141                );
142                e
143            })?;
144            let connection = IpcConnection::new_unix(stream);
145            debug!("Successfully connected to IPC socket");
146            Ok(Self { connection })
147        }
148
149        #[cfg(windows)]
150        {
151            let pipe_path = format!(
152                r"\\.\pipe\{}",
153                socket_path.file_name().unwrap().to_string_lossy()
154            );
155            let stream = ClientOptions::new().open(&pipe_path)?;
156            let connection = IpcConnection::new_windows_client(stream);
157            Ok(Self { connection })
158        }
159    }
160
161    pub async fn send(&mut self, message: &IpcMessage) -> Result<()> {
162        self.connection.send(message).await
163    }
164
165    pub async fn recv(&mut self) -> Result<IpcMessage> {
166        self.connection.recv().await
167    }
168}
169
170/// IPC connection with properly buffered reader for reliable message reception.
171#[cfg(unix)]
172pub struct IpcConnection {
173    reader: BufReader<ReadHalf<UnixStream>>,
174    writer: WriteHalf<UnixStream>,
175}
176
177#[cfg(windows)]
178pub struct IpcConnection {
179    inner: WindowsPipeConnection,
180}
181
182#[cfg(windows)]
183enum WindowsPipeConnection {
184    Server {
185        reader: BufReader<tokio::io::ReadHalf<NamedPipeServer>>,
186        writer: tokio::io::WriteHalf<NamedPipeServer>,
187    },
188    Client {
189        reader: BufReader<tokio::io::ReadHalf<NamedPipeClient>>,
190        writer: tokio::io::WriteHalf<NamedPipeClient>,
191    },
192}
193
194impl IpcConnection {
195    /// Creates a new IPC connection from a Unix stream.
196    #[cfg(unix)]
197    pub fn new_unix(stream: UnixStream) -> Self {
198        let (read_half, write_half) = tokio::io::split(stream);
199        Self {
200            reader: BufReader::new(read_half),
201            writer: write_half,
202        }
203    }
204
205    /// Connects to an IPC server at the given path.
206    #[cfg(unix)]
207    pub async fn connect(path: &Path) -> Result<Self> {
208        let stream = UnixStream::connect(path).await?;
209        Ok(Self::new_unix(stream))
210    }
211
212    /// Creates a new IPC connection from a Windows named pipe server.
213    #[cfg(windows)]
214    pub fn new_windows_server(stream: NamedPipeServer) -> Self {
215        let (read_half, write_half) = tokio::io::split(stream);
216        Self {
217            inner: WindowsPipeConnection::Server {
218                reader: BufReader::new(read_half),
219                writer: write_half,
220            },
221        }
222    }
223
224    /// Creates a new IPC connection from a Windows named pipe client.
225    #[cfg(windows)]
226    pub fn new_windows_client(stream: NamedPipeClient) -> Self {
227        let (read_half, write_half) = tokio::io::split(stream);
228        Self {
229            inner: WindowsPipeConnection::Client {
230                reader: BufReader::new(read_half),
231                writer: write_half,
232            },
233        }
234    }
235
236    /// Connects to an IPC server at the given path.
237    #[cfg(windows)]
238    pub async fn connect(path: &Path) -> Result<Self> {
239        let pipe_name = path.to_string_lossy();
240        let client = ClientOptions::new().open(&*pipe_name)?;
241        Ok(Self::new_windows_client(client))
242    }
243
244    #[cfg(unix)]
245    pub async fn send(&mut self, message: &IpcMessage) -> Result<()> {
246        let json = serde_json::to_string(message)?;
247        debug!("Sending IPC message: {}", json);
248
249        self.writer.write_all(json.as_bytes()).await?;
250        self.writer.write_all(b"\n").await?;
251        self.writer.flush().await?;
252
253        Ok(())
254    }
255
256    #[cfg(windows)]
257    pub async fn send(&mut self, message: &IpcMessage) -> Result<()> {
258        let json = serde_json::to_string(message)?;
259        debug!("Sending IPC message: {}", json);
260
261        match &mut self.inner {
262            WindowsPipeConnection::Server { writer, .. } => {
263                writer.write_all(json.as_bytes()).await?;
264                writer.write_all(b"\n").await?;
265                writer.flush().await?;
266            }
267            WindowsPipeConnection::Client { writer, .. } => {
268                writer.write_all(json.as_bytes()).await?;
269                writer.write_all(b"\n").await?;
270                writer.flush().await?;
271            }
272        }
273
274        Ok(())
275    }
276
277    #[cfg(unix)]
278    pub async fn recv(&mut self) -> Result<IpcMessage> {
279        let mut line = String::new();
280        self.reader.read_line(&mut line).await?;
281
282        if line.is_empty() {
283            return Err(QuincyError::system("Connection closed"));
284        }
285
286        debug!("Received IPC message: {}", line.trim());
287        let message: IpcMessage = serde_json::from_str(line.trim())?;
288        Ok(message)
289    }
290
291    #[cfg(windows)]
292    pub async fn recv(&mut self) -> Result<IpcMessage> {
293        let mut line = String::new();
294
295        match &mut self.inner {
296            WindowsPipeConnection::Server { reader, .. } => {
297                reader.read_line(&mut line).await?;
298            }
299            WindowsPipeConnection::Client { reader, .. } => {
300                reader.read_line(&mut line).await?;
301            }
302        }
303
304        if line.is_empty() {
305            return Err(QuincyError::system("Connection closed"));
306        }
307
308        debug!("Received IPC message: {}", line.trim());
309        let message: IpcMessage = serde_json::from_str(line.trim())?;
310        Ok(message)
311    }
312}
313
314pub fn get_ipc_socket_path(instance_name: &str) -> PathBuf {
315    #[cfg(unix)]
316    {
317        // Use XDG_RUNTIME_DIR if available and the directory exists
318        if let Ok(runtime_dir) = env::var("XDG_RUNTIME_DIR") {
319            let runtime_path = PathBuf::from(&runtime_dir);
320            if runtime_path.exists() && runtime_path.is_dir() {
321                return runtime_path.join(format!("quincy-{instance_name}.sock"));
322            }
323        }
324        // Fall back to temp_dir if XDG_RUNTIME_DIR is not available
325        env::temp_dir().join(format!("quincy-{instance_name}.sock"))
326    }
327
328    #[cfg(not(unix))]
329    {
330        env::temp_dir().join(format!("quincy-{instance_name}.sock"))
331    }
332}
333
334pub fn get_log_file_path(instance_name: &str) -> PathBuf {
335    #[cfg(unix)]
336    {
337        // Use XDG_STATE_HOME (~/.local/state) for logs, or fall back to XDG_RUNTIME_DIR
338        if let Ok(state_dir) = env::var("XDG_STATE_HOME") {
339            let state_path = PathBuf::from(&state_dir);
340            if state_path.exists() && state_path.is_dir() {
341                return state_path.join(format!("quincy-{instance_name}.log"));
342            }
343        }
344        if let Ok(runtime_dir) = env::var("XDG_RUNTIME_DIR") {
345            let runtime_path = PathBuf::from(&runtime_dir);
346            if runtime_path.exists() && runtime_path.is_dir() {
347                return runtime_path.join(format!("quincy-{instance_name}.log"));
348            }
349        }
350        env::temp_dir().join(format!("quincy-{instance_name}.log"))
351    }
352
353    #[cfg(not(unix))]
354    {
355        env::temp_dir().join(format!("quincy-{instance_name}.log"))
356    }
357}