Skip to main content

ghidra_cli/ipc/
client.rs

1//! CLI-side IPC client for communicating with the daemon.
2
3#![allow(dead_code)]
4
5use std::path::Path;
6
7use anyhow::{Context, Result};
8use tokio::io::{ReadHalf, WriteHalf};
9
10use super::protocol::{Command, Request, Response};
11use super::transport::{self, Stream};
12
13/// Client for communicating with the Ghidra daemon.
14pub struct DaemonClient {
15    reader: ReadHalf<Stream>,
16    writer: WriteHalf<Stream>,
17    next_id: u64,
18}
19
20impl DaemonClient {
21    /// Connect to the running daemon for a specific project.
22    pub async fn connect(project_path: &Path) -> Result<Self> {
23        let stream = transport::connect_for_project(project_path)
24            .await
25            .map_err(|e| {
26                if e.kind() == std::io::ErrorKind::NotFound
27                    || e.kind() == std::io::ErrorKind::ConnectionRefused
28                {
29                    anyhow::anyhow!("Daemon not running for project: {}", project_path.display())
30                } else {
31                    anyhow::anyhow!("Failed to connect to daemon: {}", e)
32                }
33            })?;
34
35        let (reader, writer) = tokio::io::split(stream);
36
37        Ok(Self {
38            reader,
39            writer,
40            next_id: 1,
41        })
42    }
43
44    /// Send a command and wait for the response.
45    pub async fn send_command(&mut self, command: Command) -> Result<serde_json::Value> {
46        let id = self.next_id;
47        self.next_id += 1;
48
49        let request = Request::new(id, command);
50        let json = serde_json::to_vec(&request).context("Failed to serialize request")?;
51
52        transport::send_message(&mut self.writer, &json)
53            .await
54            .context("Failed to send message to daemon")?;
55
56        let response_data = transport::recv_message(&mut self.reader)
57            .await
58            .context("Failed to receive message from daemon")?;
59
60        let response: Response =
61            serde_json::from_slice(&response_data).context("Failed to parse daemon response")?;
62
63        if response.id != id {
64            anyhow::bail!("Response ID mismatch: expected {}, got {}", id, response.id);
65        }
66
67        if response.success {
68            Ok(response.result.unwrap_or(serde_json::json!({})))
69        } else {
70            let error = response
71                .error
72                .unwrap_or_else(|| "Unknown error".to_string());
73            anyhow::bail!("{}", error)
74        }
75    }
76
77    /// Check if daemon is responding.
78    pub async fn ping(&mut self) -> Result<bool> {
79        match self.send_command(Command::Ping).await {
80            Ok(_) => Ok(true),
81            Err(e) if e.to_string().contains("not running") => Ok(false),
82            Err(e) => Err(e),
83        }
84    }
85
86    /// Get daemon status.
87    pub async fn status(&mut self) -> Result<serde_json::Value> {
88        self.send_command(Command::Status).await
89    }
90
91    /// Shutdown the daemon.
92    pub async fn shutdown(&mut self) -> Result<()> {
93        self.send_command(Command::Shutdown).await?;
94        Ok(())
95    }
96
97    /// Clear the result cache.
98    pub async fn clear_cache(&mut self) -> Result<()> {
99        self.send_command(Command::ClearCache).await?;
100        Ok(())
101    }
102
103    /// List functions.
104    pub async fn list_functions(
105        &mut self,
106        limit: Option<usize>,
107        filter: Option<String>,
108    ) -> Result<serde_json::Value> {
109        self.send_command(Command::ListFunctions { limit, filter })
110            .await
111    }
112
113    /// Decompile a function.
114    pub async fn decompile(&mut self, address: String) -> Result<serde_json::Value> {
115        self.send_command(Command::Decompile { address }).await
116    }
117
118    /// List strings.
119    pub async fn list_strings(&mut self, limit: Option<usize>) -> Result<serde_json::Value> {
120        self.send_command(Command::ListStrings { limit }).await
121    }
122
123    /// List imports.
124    pub async fn list_imports(&mut self) -> Result<serde_json::Value> {
125        self.send_command(Command::ListImports).await
126    }
127
128    /// List exports.
129    pub async fn list_exports(&mut self) -> Result<serde_json::Value> {
130        self.send_command(Command::ListExports).await
131    }
132
133    /// Get memory map.
134    pub async fn memory_map(&mut self) -> Result<serde_json::Value> {
135        self.send_command(Command::MemoryMap).await
136    }
137
138    /// Get program info.
139    pub async fn program_info(&mut self) -> Result<serde_json::Value> {
140        self.send_command(Command::ProgramInfo).await
141    }
142
143    /// Get cross-references to an address.
144    pub async fn xrefs_to(&mut self, address: String) -> Result<serde_json::Value> {
145        self.send_command(Command::XRefsTo { address }).await
146    }
147
148    /// Get cross-references from an address.
149    pub async fn xrefs_from(&mut self, address: String) -> Result<serde_json::Value> {
150        self.send_command(Command::XRefsFrom { address }).await
151    }
152
153    /// Execute a CLI command through the daemon (takes pre-serialized JSON).
154    pub async fn execute_cli_json(&mut self, command_json: String) -> Result<serde_json::Value> {
155        self.send_command(Command::ExecuteCli { command_json })
156            .await
157    }
158
159    /// Import a binary into a project.
160    pub async fn import_binary(
161        &mut self,
162        binary_path: &str,
163        project: &str,
164        program: Option<&str>,
165    ) -> Result<serde_json::Value> {
166        self.send_command(Command::Import {
167            binary_path: binary_path.to_string(),
168            project: project.to_string(),
169            program: program.map(|s| s.to_string()),
170        })
171        .await
172    }
173
174    /// Analyze a program in a project.
175    pub async fn analyze_program(
176        &mut self,
177        project: &str,
178        program: &str,
179    ) -> Result<serde_json::Value> {
180        self.send_command(Command::Analyze {
181            project: project.to_string(),
182            program: program.to_string(),
183        })
184        .await
185    }
186}
187
188/// Check if daemon is running for a specific project (without establishing a full connection).
189pub fn daemon_available(project_path: &Path) -> bool {
190    transport::socket_exists_for_project(project_path)
191}