mirror of
https://github.com/encounter/ghidra-cli.git
synced 2026-07-10 03:18:56 -07:00
create a socket for each project
This commit is contained in:
@@ -21,7 +21,8 @@ A high-performance Rust CLI for automating Ghidra reverse engineering tasks, des
|
|||||||
```
|
```
|
||||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
│ CLI Command │────▶│ Daemon (IPC) │────▶│ GhidraBridge │
|
│ CLI Command │────▶│ Daemon (IPC) │────▶│ GhidraBridge │
|
||||||
│ ghidra ... │ │ Unix socket │ │ TCP to Ghidra │
|
│ ghidra ... │ │ Per-project │ │ TCP to Ghidra │
|
||||||
|
│ --project X │ │ Unix socket │ │ │
|
||||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -35,6 +36,7 @@ All commands go through the daemon, which maintains a persistent connection to G
|
|||||||
- **Consistent state** - Single Ghidra process for all operations
|
- **Consistent state** - Single Ghidra process for all operations
|
||||||
- **Fast queries** - No JVM startup overhead per command
|
- **Fast queries** - No JVM startup overhead per command
|
||||||
- **Auto-start** - Daemon starts automatically when needed
|
- **Auto-start** - Daemon starts automatically when needed
|
||||||
|
- **Per-project isolation** - Each project gets its own daemon and socket, enabling concurrent analysis of multiple binaries
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -181,24 +183,40 @@ The daemon keeps Ghidra loaded in memory. It starts automatically when needed, b
|
|||||||
ghidra daemon start --project myproject --program mybinary
|
ghidra daemon start --project myproject --program mybinary
|
||||||
|
|
||||||
# Check daemon status
|
# Check daemon status
|
||||||
ghidra daemon status
|
ghidra daemon status --project myproject
|
||||||
|
|
||||||
# All commands use the daemon automatically
|
# All commands use the daemon automatically
|
||||||
ghidra function list # Fast!
|
ghidra function list --project myproject # Fast!
|
||||||
ghidra decompile main # Fast!
|
ghidra decompile main --project myproject # Fast!
|
||||||
|
|
||||||
# Stop daemon
|
# Stop daemon
|
||||||
ghidra daemon stop
|
ghidra daemon stop --project myproject
|
||||||
|
|
||||||
# Restart with different program
|
# Restart with different program
|
||||||
ghidra daemon restart --project myproject --program otherbinary
|
ghidra daemon restart --project myproject --program otherbinary
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Multi-Project Support
|
||||||
|
|
||||||
|
Each project gets its own daemon process and socket, allowing concurrent analysis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Work on multiple projects simultaneously
|
||||||
|
ghidra quick ./binary_a --project projA
|
||||||
|
ghidra quick ./binary_b --project projB
|
||||||
|
|
||||||
|
# Query each independently
|
||||||
|
ghidra function list --project projA
|
||||||
|
ghidra function list --project projB
|
||||||
|
```
|
||||||
|
|
||||||
## Output Formats
|
## Output Formats
|
||||||
|
|
||||||
Default output adapts to context:
|
Default output is human-readable in all contexts. Use flags to request machine formats:
|
||||||
- **Interactive (TTY)**: Compact human-readable format
|
|
||||||
- **Piped/scripted**: Compact JSON for machine parsing
|
- **Default**: Compact human-readable format (designed for both humans and AI agents)
|
||||||
|
- **--json**: Compact JSON for machine parsing
|
||||||
|
- **--pretty**: Pretty-printed JSON (indented, multi-line)
|
||||||
|
|
||||||
Override with flags:
|
Override with flags:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -18,12 +18,13 @@ A high-performance Rust CLI for automating Ghidra reverse engineering tasks. Des
|
|||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
ghidra-cli uses a **daemon-only architecture**:
|
ghidra-cli uses a **daemon-only architecture** with **per-project isolation**:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
|
||||||
│ CLI Command │────▶│ Daemon (IPC) │────▶│ GhidraBridge │
|
│ CLI Command │────▶│ Daemon (IPC) │────▶│ GhidraBridge │
|
||||||
│ ghidra ... │ │ Unix socket │ │ TCP to Ghidra │
|
│ ghidra ... │ │ Per-project │ │ TCP to Ghidra │
|
||||||
|
│ --project X │ │ Unix socket │ │ │
|
||||||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -37,7 +38,8 @@ ghidra-cli uses a **daemon-only architecture**:
|
|||||||
- **Daemon**: Background process managing IPC and Ghidra bridge
|
- **Daemon**: Background process managing IPC and Ghidra bridge
|
||||||
- **Bridge**: Python script running inside Ghidra, executing commands
|
- **Bridge**: Python script running inside Ghidra, executing commands
|
||||||
- **Auto-start**: Daemon starts automatically when needed (import, analyze, quick)
|
- **Auto-start**: Daemon starts automatically when needed (import, analyze, quick)
|
||||||
- **One daemon per project**: Each project gets its own daemon instance
|
- **Per-project sockets**: Each project gets its own socket at `~/.local/share/ghidra-cli/ghidra-cli-{hash}.sock`
|
||||||
|
- **One daemon per project**: Multiple agents can work on different projects concurrently without conflicts
|
||||||
- **One program per daemon**: Daemon loads a single program for queries
|
- **One program per daemon**: Daemon loads a single program for queries
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
@@ -529,9 +531,34 @@ ghidra daemon start --project myproject --program mybinary --foreground
|
|||||||
### Reset State
|
### Reset State
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Stop all daemons
|
# Stop daemon for a specific project
|
||||||
ghidra daemon stop --project myproject
|
ghidra daemon stop --project myproject
|
||||||
|
|
||||||
# Remove lock files if needed
|
# Remove lock files if needed (per-project, named by hash)
|
||||||
rm ~/.local/share/ghidra-cli/daemon-*.lock
|
rm ~/.local/share/ghidra-cli/daemon-*.lock
|
||||||
|
|
||||||
|
# Remove sockets if needed (per-project, named by hash)
|
||||||
|
rm /run/user/$UID/ghidra-cli/ghidra-cli-*.sock
|
||||||
|
# Or on systems without XDG_RUNTIME_DIR:
|
||||||
|
rm /tmp/ghidra-cli/ghidra-cli-*.sock
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Multi-Project Support
|
||||||
|
|
||||||
|
ghidra-cli supports concurrent analysis of multiple projects. Each project gets:
|
||||||
|
- Its own daemon process (identified by lock file)
|
||||||
|
- Its own Unix socket (named by project path hash)
|
||||||
|
|
||||||
|
This allows multiple agents or terminals to work on different binaries without conflicts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1: Work on project A
|
||||||
|
ghidra quick ./binary_a --project projectA
|
||||||
|
ghidra function list --project projectA
|
||||||
|
|
||||||
|
# Terminal 2: Work on project B (concurrently)
|
||||||
|
ghidra quick ./binary_b --project projectB
|
||||||
|
ghidra decompile main --project projectB
|
||||||
|
```
|
||||||
|
|
||||||
|
Both daemons run independently and don't interfere with each other.
|
||||||
|
|||||||
+15
-1
@@ -7,7 +7,8 @@ The daemon is the central execution authority for ghidra-cli. All commands route
|
|||||||
```
|
```
|
||||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||||
│ CLI Client │────▶│ IPC Server │────▶│ Handler │────▶│ GhidraBridge│
|
│ CLI Client │────▶│ IPC Server │────▶│ Handler │────▶│ GhidraBridge│
|
||||||
│ (DaemonCli) │ │ (Unix sock) │ │ (Routing) │ │ (TCP→Ghidra)│
|
│ (DaemonCli) │ │ Per-project │ │ (Routing) │ │ (TCP→Ghidra)│
|
||||||
|
│ │ │ Unix socket │ │ │ │ │
|
||||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||||
│
|
│
|
||||||
▼
|
▼
|
||||||
@@ -17,6 +18,16 @@ The daemon is the central execution authority for ghidra-cli. All commands route
|
|||||||
└─────────────┘
|
└─────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Per-Project Socket Isolation
|
||||||
|
|
||||||
|
Each project gets its own Unix socket to enable concurrent daemon operation:
|
||||||
|
|
||||||
|
- **Socket naming**: `ghidra-cli-{hash}.sock` where `{hash}` is MD5 of project path
|
||||||
|
- **Socket location**: `$XDG_RUNTIME_DIR/ghidra-cli/` or `/tmp/ghidra-cli/`
|
||||||
|
- **Lock file naming**: `daemon-{hash}.lock` (same hash for consistency)
|
||||||
|
|
||||||
|
This allows multiple agents or terminals to work on different projects without conflicts.
|
||||||
|
|
||||||
## Key Components
|
## Key Components
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
@@ -56,8 +67,11 @@ Import, Analyze, and Quick commands auto-start the daemon:
|
|||||||
- **One program per daemon** - Daemon loads a single program
|
- **One program per daemon** - Daemon loads a single program
|
||||||
- **Graceful shutdown** - Handles SIGTERM, SIGINT, IPC shutdown command
|
- **Graceful shutdown** - Handles SIGTERM, SIGINT, IPC shutdown command
|
||||||
- **Lock files** - Located at `~/.local/share/ghidra-cli/daemon-{hash}.lock`
|
- **Lock files** - Located at `~/.local/share/ghidra-cli/daemon-{hash}.lock`
|
||||||
|
- **Socket files** - Located at `$XDG_RUNTIME_DIR/ghidra-cli/ghidra-cli-{hash}.sock`
|
||||||
- **Logs** - Located at `~/.local/share/ghidra-cli/daemon.log`
|
- **Logs** - Located at `~/.local/share/ghidra-cli/daemon.log`
|
||||||
|
|
||||||
|
The `{hash}` is computed as `MD5(project_path_string)` ensuring each project has unique socket and lock file names.
|
||||||
|
|
||||||
## Handlers
|
## Handlers
|
||||||
|
|
||||||
Specialized handlers in `handlers/` directory:
|
Specialized handlers in `handlers/` directory:
|
||||||
|
|||||||
@@ -2,9 +2,12 @@
|
|||||||
//!
|
//!
|
||||||
//! Uses local sockets (Unix domain sockets / Windows named pipes) with
|
//! Uses local sockets (Unix domain sockets / Windows named pipes) with
|
||||||
//! the new IPC layer instead of TCP.
|
//! the new IPC layer instead of TCP.
|
||||||
|
//!
|
||||||
|
//! Each project gets its own socket for concurrent daemon operation.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
@@ -108,16 +111,17 @@ impl IpcServer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the IPC server.
|
/// Run the IPC server for a specific project.
|
||||||
pub async fn run_ipc_server(
|
pub async fn run_ipc_server(
|
||||||
bridge: Arc<Mutex<Option<GhidraBridge>>>,
|
bridge: Arc<Mutex<Option<GhidraBridge>>>,
|
||||||
shutdown_tx: broadcast::Sender<()>,
|
shutdown_tx: broadcast::Sender<()>,
|
||||||
|
project_path: &Path,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// Create the IPC listener
|
// Create the IPC listener for this project
|
||||||
let listener = transport::create_listener().await
|
let listener = transport::create_listener_for_project(project_path).await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to create IPC listener: {}", e))?;
|
.map_err(|e| anyhow::anyhow!("Failed to create IPC listener: {}", e))?;
|
||||||
|
|
||||||
info!("IPC server listening on {}", transport::socket_name());
|
info!("IPC server listening on {}", transport::socket_name_for_project(project_path));
|
||||||
|
|
||||||
let server = Arc::new(IpcServer::new(bridge, shutdown_tx.clone()));
|
let server = Arc::new(IpcServer::new(bridge, shutdown_tx.clone()));
|
||||||
let mut shutdown_rx = shutdown_tx.subscribe();
|
let mut shutdown_rx = shutdown_tx.subscribe();
|
||||||
@@ -155,8 +159,8 @@ pub async fn run_ipc_server(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up socket
|
// Clean up socket for this project
|
||||||
transport::remove_socket().ok();
|
transport::remove_socket_for_project(project_path).ok();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -86,8 +86,9 @@ pub async fn run(config: DaemonConfig) -> Result<()> {
|
|||||||
// Start IPC server task
|
// Start IPC server task
|
||||||
let ipc_bridge = bridge.clone();
|
let ipc_bridge = bridge.clone();
|
||||||
let ipc_shutdown_tx = shutdown_tx.clone();
|
let ipc_shutdown_tx = shutdown_tx.clone();
|
||||||
|
let ipc_project_path = config.project_path.clone();
|
||||||
let ipc_handle = tokio::spawn(async move {
|
let ipc_handle = tokio::spawn(async move {
|
||||||
if let Err(e) = ipc_server::run_ipc_server(ipc_bridge, ipc_shutdown_tx).await {
|
if let Err(e) = ipc_server::run_ipc_server(ipc_bridge, ipc_shutdown_tx, &ipc_project_path).await {
|
||||||
error!("IPC server error: {}", e);
|
error!("IPC server error: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+9
-7
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use tokio::io::{ReadHalf, WriteHalf};
|
use tokio::io::{ReadHalf, WriteHalf};
|
||||||
|
|
||||||
@@ -16,13 +18,13 @@ pub struct DaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DaemonClient {
|
impl DaemonClient {
|
||||||
/// Connect to the running daemon.
|
/// Connect to the running daemon for a specific project.
|
||||||
pub async fn connect() -> Result<Self> {
|
pub async fn connect(project_path: &Path) -> Result<Self> {
|
||||||
let stream = transport::connect().await.map_err(|e| {
|
let stream = transport::connect_for_project(project_path).await.map_err(|e| {
|
||||||
if e.kind() == std::io::ErrorKind::NotFound
|
if e.kind() == std::io::ErrorKind::NotFound
|
||||||
|| e.kind() == std::io::ErrorKind::ConnectionRefused
|
|| e.kind() == std::io::ErrorKind::ConnectionRefused
|
||||||
{
|
{
|
||||||
anyhow::anyhow!("Daemon not running")
|
anyhow::anyhow!("Daemon not running for project: {}", project_path.display())
|
||||||
} else {
|
} else {
|
||||||
anyhow::anyhow!("Failed to connect to daemon: {}", e)
|
anyhow::anyhow!("Failed to connect to daemon: {}", e)
|
||||||
}
|
}
|
||||||
@@ -182,7 +184,7 @@ impl DaemonClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if daemon is running (without establishing a full connection).
|
/// Check if daemon is running for a specific project (without establishing a full connection).
|
||||||
pub fn daemon_available() -> bool {
|
pub fn daemon_available(project_path: &Path) -> bool {
|
||||||
transport::socket_exists()
|
transport::socket_exists_for_project(project_path)
|
||||||
}
|
}
|
||||||
|
|||||||
+44
-27
@@ -2,18 +2,21 @@
|
|||||||
//!
|
//!
|
||||||
//! Abstracts Unix domain sockets (Unix/macOS) and named pipes (Windows)
|
//! Abstracts Unix domain sockets (Unix/macOS) and named pipes (Windows)
|
||||||
//! using the interprocess crate. Uses length-prefixed message framing.
|
//! using the interprocess crate. Uses length-prefixed message framing.
|
||||||
|
//!
|
||||||
|
//! Socket paths are per-project to allow concurrent daemons for different
|
||||||
|
//! projects without conflicts. Socket names use MD5 hash of the project path.
|
||||||
|
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
|
||||||
/// Maximum message size (10 MB)
|
/// Maximum message size (10 MB)
|
||||||
const MAX_MESSAGE_SIZE: u32 = 10 * 1024 * 1024;
|
const MAX_MESSAGE_SIZE: u32 = 10 * 1024 * 1024;
|
||||||
|
|
||||||
/// Socket name for the daemon
|
/// Socket name prefix for the daemon
|
||||||
const SOCKET_NAME: &str = "ghidra-cli.sock";
|
const SOCKET_PREFIX: &str = "ghidra-cli";
|
||||||
|
|
||||||
// Platform-specific imports and type aliases
|
// Platform-specific imports and type aliases
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -30,6 +33,12 @@ pub mod platform {
|
|||||||
|
|
||||||
pub use platform::*;
|
pub use platform::*;
|
||||||
|
|
||||||
|
/// Compute MD5 hash of project path for socket naming.
|
||||||
|
/// Uses same hashing approach as lock files for consistency.
|
||||||
|
fn project_hash(project_path: &Path) -> String {
|
||||||
|
format!("{:x}", md5::compute(project_path.to_string_lossy().as_bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the socket directory path.
|
/// Get the socket directory path.
|
||||||
fn socket_dir() -> io::Result<PathBuf> {
|
fn socket_dir() -> io::Result<PathBuf> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -49,36 +58,42 @@ fn socket_dir() -> io::Result<PathBuf> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the socket path.
|
/// Get the socket path for a specific project.
|
||||||
///
|
///
|
||||||
/// Checks GHIDRA_CLI_SOCKET env var first (used for testing), then falls back to default.
|
/// Checks GHIDRA_CLI_SOCKET env var first (used for testing), then falls back to
|
||||||
pub fn socket_path() -> io::Result<PathBuf> {
|
/// project-specific socket using MD5 hash of project path.
|
||||||
|
pub fn socket_path_for_project(project_path: &Path) -> io::Result<PathBuf> {
|
||||||
if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") {
|
if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") {
|
||||||
return Ok(PathBuf::from(path));
|
return Ok(PathBuf::from(path));
|
||||||
}
|
}
|
||||||
let dir = socket_dir()?;
|
let dir = socket_dir()?;
|
||||||
Ok(dir.join(SOCKET_NAME))
|
let hash = project_hash(project_path);
|
||||||
|
Ok(dir.join(format!("{}-{}.sock", SOCKET_PREFIX, hash)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the socket name for interprocess.
|
/// Get the socket name for interprocess, for a specific project.
|
||||||
///
|
///
|
||||||
/// On Unix, respects GHIDRA_CLI_SOCKET env var for test isolation.
|
/// On Unix, respects GHIDRA_CLI_SOCKET env var for test isolation.
|
||||||
pub fn socket_name() -> String {
|
pub fn socket_name_for_project(project_path: &Path) -> String {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
// Check env var first (used for testing)
|
// Check env var first (used for testing)
|
||||||
if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") {
|
if let Ok(path) = std::env::var("GHIDRA_CLI_SOCKET") {
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
socket_path()
|
socket_path_for_project(project_path)
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
.map(|p| p.to_string_lossy().to_string())
|
||||||
.unwrap_or_else(|_| format!("/tmp/ghidra-cli/{}", SOCKET_NAME))
|
.unwrap_or_else(|_| {
|
||||||
|
let hash = project_hash(project_path);
|
||||||
|
format!("/tmp/ghidra-cli/{}-{}.sock", SOCKET_PREFIX, hash)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
// Windows uses named pipe namespace
|
// Windows uses named pipe namespace with project hash
|
||||||
format!("ghidra-cli-{}", std::process::id())
|
let hash = project_hash(project_path);
|
||||||
|
format!("{}-{}", SOCKET_PREFIX, hash)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,11 +114,11 @@ pub fn ensure_socket_dir() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove the socket file if it exists.
|
/// Remove the socket file for a specific project if it exists.
|
||||||
pub fn remove_socket() -> io::Result<()> {
|
pub fn remove_socket_for_project(project_path: &Path) -> io::Result<()> {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let path = socket_path()?;
|
let path = socket_path_for_project(project_path)?;
|
||||||
if path.exists() {
|
if path.exists() {
|
||||||
std::fs::remove_file(&path)?;
|
std::fs::remove_file(&path)?;
|
||||||
}
|
}
|
||||||
@@ -112,32 +127,34 @@ pub fn remove_socket() -> io::Result<()> {
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
|
let _ = project_path; // unused on Windows
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the socket exists.
|
/// Check if the socket for a specific project exists.
|
||||||
pub fn socket_exists() -> bool {
|
pub fn socket_exists_for_project(project_path: &Path) -> bool {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
socket_path().map(|p| p.exists()).unwrap_or(false)
|
socket_path_for_project(project_path).map(|p| p.exists()).unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
// On Windows, we can't easily check if a named pipe exists
|
// On Windows, we can't easily check if a named pipe exists
|
||||||
// We'll rely on connection attempts instead
|
// We'll rely on connection attempts instead
|
||||||
|
let _ = project_path;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a listener for incoming IPC connections.
|
/// Create a listener for incoming IPC connections for a specific project.
|
||||||
pub async fn create_listener() -> io::Result<Listener> {
|
pub async fn create_listener_for_project(project_path: &Path) -> io::Result<Listener> {
|
||||||
// Ensure socket directory exists and clean up stale socket
|
// Ensure socket directory exists and clean up stale socket
|
||||||
ensure_socket_dir()?;
|
ensure_socket_dir()?;
|
||||||
remove_socket()?;
|
remove_socket_for_project(project_path)?;
|
||||||
|
|
||||||
let name = socket_name();
|
let name = socket_name_for_project(project_path);
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let listener = {
|
let listener = {
|
||||||
@@ -155,16 +172,16 @@ pub async fn create_listener() -> io::Result<Listener> {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let path = socket_path()?;
|
let path = socket_path_for_project(project_path)?;
|
||||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
|
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(listener)
|
Ok(listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connect to the daemon's IPC socket.
|
/// Connect to the daemon's IPC socket for a specific project.
|
||||||
pub async fn connect() -> io::Result<Stream> {
|
pub async fn connect_for_project(project_path: &Path) -> io::Result<Stream> {
|
||||||
let name = socket_name();
|
let name = socket_name_for_project(project_path);
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
let stream = {
|
let stream = {
|
||||||
|
|||||||
+5
-5
@@ -127,7 +127,7 @@ async fn run_with_daemon_check(cli: Cli) -> anyhow::Result<()> {
|
|||||||
|
|
||||||
ensure_daemon_running(&project_path).await?;
|
ensure_daemon_running(&project_path).await?;
|
||||||
|
|
||||||
match ipc::client::DaemonClient::connect().await {
|
match ipc::client::DaemonClient::connect(&project_path).await {
|
||||||
Ok(mut client) => {
|
Ok(mut client) => {
|
||||||
info!("Connected to daemon via IPC");
|
info!("Connected to daemon via IPC");
|
||||||
let output = execute_via_daemon(&mut client, &cli.command, cli.json, cli.pretty).await?;
|
let output = execute_via_daemon(&mut client, &cli.command, cli.json, cli.pretty).await?;
|
||||||
@@ -430,8 +430,8 @@ async fn handle_daemon_stop(project: Option<String>) -> anyhow::Result<()> {
|
|||||||
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
|
if let Some(daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
|
||||||
println!("Stopping daemon (PID: {})...", daemon_info.pid);
|
println!("Stopping daemon (PID: {})...", daemon_info.pid);
|
||||||
|
|
||||||
// Connect via IPC and send shutdown
|
// Connect via IPC and send shutdown (using project path for socket)
|
||||||
let mut client = ipc::client::DaemonClient::connect().await?;
|
let mut client = ipc::client::DaemonClient::connect(&project_path).await?;
|
||||||
client.shutdown().await?;
|
client.shutdown().await?;
|
||||||
|
|
||||||
println!("Daemon stopped successfully");
|
println!("Daemon stopped successfully");
|
||||||
@@ -468,7 +468,7 @@ async fn handle_daemon_status(project: Option<String>) -> anyhow::Result<()> {
|
|||||||
println!(" Log file: {}", daemon_info.log_file.display());
|
println!(" Log file: {}", daemon_info.log_file.display());
|
||||||
|
|
||||||
// Try to get detailed status from daemon via IPC
|
// Try to get detailed status from daemon via IPC
|
||||||
if let Ok(mut client) = ipc::client::DaemonClient::connect().await {
|
if let Ok(mut client) = ipc::client::DaemonClient::connect(&project_path).await {
|
||||||
if let Ok(status) = client.status().await {
|
if let Ok(status) = client.status().await {
|
||||||
if let Some(bridge_running) = status.get("bridge_running").and_then(|v| v.as_bool()) {
|
if let Some(bridge_running) = status.get("bridge_running").and_then(|v| v.as_bool()) {
|
||||||
println!(" Bridge: {}", if bridge_running { "running" } else { "stopped" });
|
println!(" Bridge: {}", if bridge_running { "running" } else { "stopped" });
|
||||||
@@ -489,7 +489,7 @@ async fn handle_daemon_ping(project: Option<String>) -> anyhow::Result<()> {
|
|||||||
let project_path = resolve_project_path(&project, &config)?;
|
let project_path = resolve_project_path(&project, &config)?;
|
||||||
|
|
||||||
if let Some(_daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
|
if let Some(_daemon_info) = get_running_daemon_info(&data_dir, &project_path)? {
|
||||||
let mut client = ipc::client::DaemonClient::connect().await?;
|
let mut client = ipc::client::DaemonClient::connect(&project_path).await?;
|
||||||
client.ping().await?;
|
client.ping().await?;
|
||||||
println!("Daemon is responsive");
|
println!("Daemon is responsive");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+4
-1
@@ -191,8 +191,11 @@ impl DaemonTestHarness {
|
|||||||
// Set GHIDRA_CLI_SOCKET for this process so client connects to the right socket
|
// Set GHIDRA_CLI_SOCKET for this process so client connects to the right socket
|
||||||
// SAFETY: Tests run single-threaded (--test-threads=1), so no data race.
|
// SAFETY: Tests run single-threaded (--test-threads=1), so no data race.
|
||||||
unsafe { std::env::set_var("GHIDRA_CLI_SOCKET", &self.socket_path); }
|
unsafe { std::env::set_var("GHIDRA_CLI_SOCKET", &self.socket_path); }
|
||||||
|
// When GHIDRA_CLI_SOCKET is set, the project path is ignored
|
||||||
|
// but we still need to pass one for the function signature
|
||||||
|
let project_path = std::path::Path::new(&self.project);
|
||||||
self.runtime.block_on(async {
|
self.runtime.block_on(async {
|
||||||
ghidra_cli::ipc::client::DaemonClient::connect().await
|
ghidra_cli::ipc::client::DaemonClient::connect(project_path).await
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user