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 if let Some(parent) = socket_path.parent()
77 && !parent.exists()
78 {
79 fs::create_dir_all(parent)?;
80 }
81
82 if socket_path.exists() {
84 fs::remove_file(socket_path)?;
85 }
86
87 let listener = UnixListener::bind(socket_path)?;
88
89 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#[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 #[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 #[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 #[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 #[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 #[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 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 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 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}