debugger/ipc/
transport.rs

1//! Cross-platform IPC transport layer
2//!
3//! Abstracts Unix domain sockets (Unix/macOS) and named pipes (Windows)
4//! using the interprocess crate.
5
6use std::io;
7use tokio::io::{AsyncReadExt, AsyncWriteExt};
8
9use crate::common::paths;
10
11/// Maximum message size (10 MB)
12const MAX_MESSAGE_SIZE: u32 = 10 * 1024 * 1024;
13
14// Platform-specific imports and type aliases
15#[cfg(unix)]
16pub mod platform {
17    pub use interprocess::local_socket::tokio::{
18        prelude::*,
19        Listener, Stream,
20    };
21    pub use interprocess::local_socket::{
22        GenericFilePath, ListenerOptions,
23    };
24}
25
26#[cfg(windows)]
27pub mod platform {
28    pub use interprocess::local_socket::tokio::{
29        prelude::*,
30        Listener, Stream,
31    };
32    pub use interprocess::local_socket::{
33        GenericNamespaced, ListenerOptions,
34    };
35}
36
37use platform::*;
38
39/// Re-export Stream for use in other modules
40pub use platform::Stream;
41
42/// Create a listener for incoming IPC connections
43pub async fn create_listener() -> io::Result<Listener> {
44    // Ensure socket directory exists (Unix) and clean up stale socket
45    paths::ensure_socket_dir()?;
46    paths::remove_socket()?;
47
48    let name = paths::socket_name();
49
50    #[cfg(unix)]
51    let listener = {
52        let name = name.to_fs_name::<GenericFilePath>()?;
53        ListenerOptions::new()
54            .name(name)
55            .create_tokio()?
56    };
57
58    #[cfg(windows)]
59    let listener = {
60        let name = name.to_ns_name::<GenericNamespaced>()?;
61        ListenerOptions::new()
62            .name(name)
63            .create_tokio()?
64    };
65
66    // Set socket permissions on Unix
67    #[cfg(unix)]
68    {
69        use std::os::unix::fs::PermissionsExt;
70        let path = paths::socket_path();
71        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
72    }
73
74    Ok(listener)
75}
76
77/// Connect to the daemon's IPC socket
78pub async fn connect() -> io::Result<Stream> {
79    let name = paths::socket_name();
80
81    #[cfg(unix)]
82    let stream = {
83        let name = name.to_fs_name::<GenericFilePath>()?;
84        Stream::connect(name).await?
85    };
86
87    #[cfg(windows)]
88    let stream = {
89        let name = name.to_ns_name::<GenericNamespaced>()?;
90        Stream::connect(name).await?
91    };
92
93    Ok(stream)
94}
95
96/// Send a length-prefixed message
97pub async fn send_message<W: AsyncWriteExt + Unpin>(
98    writer: &mut W,
99    data: &[u8],
100) -> io::Result<()> {
101    if data.len() > MAX_MESSAGE_SIZE as usize {
102        return Err(io::Error::new(
103            io::ErrorKind::InvalidInput,
104            "Message too large",
105        ));
106    }
107
108    let len = data.len() as u32;
109    writer.write_all(&len.to_le_bytes()).await?;
110    writer.write_all(data).await?;
111    writer.flush().await?;
112    Ok(())
113}
114
115/// Receive a length-prefixed message
116pub async fn recv_message<R: AsyncReadExt + Unpin>(reader: &mut R) -> io::Result<Vec<u8>> {
117    let mut len_buf = [0u8; 4];
118    reader.read_exact(&mut len_buf).await?;
119    let len = u32::from_le_bytes(len_buf);
120
121    if len > MAX_MESSAGE_SIZE {
122        return Err(io::Error::new(
123            io::ErrorKind::InvalidData,
124            format!("Message too large: {} bytes", len),
125        ));
126    }
127
128    let mut data = vec![0u8; len as usize];
129    reader.read_exact(&mut data).await?;
130    Ok(data)
131}
132
133/// Check if the daemon socket exists
134pub fn socket_exists() -> bool {
135    #[cfg(unix)]
136    {
137        paths::socket_path().exists()
138    }
139
140    #[cfg(windows)]
141    {
142        // On Windows, we can't easily check if a named pipe exists
143        // We'll rely on connection attempts instead
144        true
145    }
146}