greppy/daemon/
client.rs

1//! Client for communicating with daemon
2
3use crate::core::config::Config;
4use crate::core::error::{Error, Result};
5use crate::daemon::protocol::{Method, Request, Response, ResponseResult};
6use std::io::{BufRead, BufReader, Write};
7use std::path::Path;
8use std::time::Duration;
9
10#[cfg(unix)]
11use std::os::unix::net::UnixStream;
12
13#[cfg(windows)]
14use std::net::TcpStream;
15
16/// Default timeout for daemon requests (30 seconds)
17const REQUEST_TIMEOUT: Duration = Duration::from_secs(30);
18
19/// Extended timeout for indexing operations (10 minutes)
20const INDEX_TIMEOUT: Duration = Duration::from_secs(600);
21
22/// Check if daemon is running (Unix: check socket file exists)
23#[cfg(unix)]
24pub fn is_running() -> Result<bool> {
25    let socket_path = Config::socket_path()?;
26    Ok(socket_path.exists())
27}
28
29/// Check if daemon is running (Windows: check port file exists)
30#[cfg(windows)]
31pub fn is_running() -> Result<bool> {
32    let port_path = Config::port_path()?;
33    Ok(port_path.exists())
34}
35
36/// Connect to the daemon (Unix: Unix socket)
37#[cfg(unix)]
38fn connect_with_timeout(read_timeout: Duration) -> Result<UnixStream> {
39    let socket_path = Config::socket_path()?;
40    let stream = UnixStream::connect(&socket_path).map_err(|e| Error::DaemonError {
41        message: format!("Failed to connect to daemon: {}", e),
42    })?;
43    stream
44        .set_read_timeout(Some(read_timeout))
45        .map_err(|e| Error::DaemonError {
46            message: format!("Failed to set read timeout: {}", e),
47        })?;
48    stream
49        .set_write_timeout(Some(Duration::from_secs(5)))
50        .map_err(|e| Error::DaemonError {
51            message: format!("Failed to set write timeout: {}", e),
52        })?;
53    Ok(stream)
54}
55
56/// Connect to the daemon (Windows: TCP on localhost)
57#[cfg(windows)]
58fn connect_with_timeout(read_timeout: Duration) -> Result<TcpStream> {
59    let port_path = Config::port_path()?;
60    let port_str = std::fs::read_to_string(&port_path).map_err(|e| Error::DaemonError {
61        message: format!("Failed to read daemon port file: {}", e),
62    })?;
63    let port: u16 = port_str.trim().parse().map_err(|e| Error::DaemonError {
64        message: format!("Invalid port in daemon port file: {}", e),
65    })?;
66
67    let addr = format!("127.0.0.1:{}", port);
68    let stream = TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_secs(5))
69        .map_err(|e| Error::DaemonError {
70            message: format!("Failed to connect to daemon at {}: {}", addr, e),
71        })?;
72    stream
73        .set_read_timeout(Some(read_timeout))
74        .map_err(|e| Error::DaemonError {
75            message: format!("Failed to set read timeout: {}", e),
76        })?;
77    stream
78        .set_write_timeout(Some(Duration::from_secs(5)))
79        .map_err(|e| Error::DaemonError {
80            message: format!("Failed to set write timeout: {}", e),
81        })?;
82    Ok(stream)
83}
84
85/// Send a request to the daemon
86fn send_request<S: Write + std::io::Read>(stream: &mut S, request: &Request) -> Result<Response> {
87    let json = serde_json::to_string(request).map_err(|e| Error::DaemonError {
88        message: format!("Failed to serialize request: {}", e),
89    })?;
90
91    stream
92        .write_all(json.as_bytes())
93        .map_err(|e| Error::DaemonError {
94            message: format!("Failed to send request: {}", e),
95        })?;
96    stream.write_all(b"\n").map_err(|e| Error::DaemonError {
97        message: format!("Failed to send newline: {}", e),
98    })?;
99    stream.flush().map_err(|e| Error::DaemonError {
100        message: format!("Failed to flush: {}", e),
101    })?;
102
103    let mut reader = BufReader::new(stream);
104    let mut response_line = String::new();
105    reader
106        .read_line(&mut response_line)
107        .map_err(|e| Error::DaemonError {
108            message: format!("Failed to read response: {}", e),
109        })?;
110
111    serde_json::from_str(&response_line).map_err(|e| Error::DaemonError {
112        message: format!("Invalid response from daemon: {}", e),
113    })
114}
115
116/// Send a search request to the daemon
117pub async fn search(
118    query: &str,
119    project: &Path,
120    limit: usize,
121) -> Result<crate::search::SearchResponse> {
122    let mut stream = connect_with_timeout(REQUEST_TIMEOUT)?;
123
124    let request = Request {
125        id: uuid::Uuid::new_v4().to_string(),
126        method: Method::Search {
127            query: query.to_string(),
128            project: project.to_string_lossy().to_string(),
129            limit,
130        },
131    };
132
133    let response = send_request(&mut stream, &request)?;
134
135    match response.result {
136        ResponseResult::Search(search_response) => Ok(search_response),
137        ResponseResult::Error { message } => Err(Error::DaemonError { message }),
138        _ => Err(Error::DaemonError {
139            message: "Unexpected response type".to_string(),
140        }),
141    }
142}
143
144/// Send an index request to the daemon (uses extended timeout)
145pub async fn index(project: &Path, force: bool) -> Result<(usize, usize, f64)> {
146    let mut stream = connect_with_timeout(INDEX_TIMEOUT)?;
147
148    let request = Request {
149        id: uuid::Uuid::new_v4().to_string(),
150        method: Method::Index {
151            project: project.to_string_lossy().to_string(),
152            force,
153        },
154    };
155
156    let response = send_request(&mut stream, &request)?;
157
158    match response.result {
159        ResponseResult::Index {
160            file_count,
161            chunk_count,
162            elapsed_ms,
163            ..
164        } => Ok((file_count, chunk_count, elapsed_ms)),
165        ResponseResult::Error { message } => Err(Error::DaemonError { message }),
166        _ => Err(Error::DaemonError {
167            message: "Unexpected response type".to_string(),
168        }),
169    }
170}
171
172/// Send a stop request to the daemon
173pub async fn stop() -> Result<bool> {
174    let mut stream = connect_with_timeout(REQUEST_TIMEOUT)?;
175
176    let request = Request {
177        id: uuid::Uuid::new_v4().to_string(),
178        method: Method::Stop,
179    };
180
181    let response = send_request(&mut stream, &request)?;
182
183    match response.result {
184        ResponseResult::Stop { success } => Ok(success),
185        ResponseResult::Error { message } => Err(Error::DaemonError { message }),
186        _ => Err(Error::DaemonError {
187            message: "Unexpected response type".to_string(),
188        }),
189    }
190}
191
192/// Get daemon status
193pub async fn status() -> Result<(bool, u32, Vec<crate::daemon::protocol::ProjectInfo>)> {
194    let mut stream = connect_with_timeout(REQUEST_TIMEOUT)?;
195
196    let request = Request {
197        id: uuid::Uuid::new_v4().to_string(),
198        method: Method::Status,
199    };
200
201    let response = send_request(&mut stream, &request)?;
202
203    match response.result {
204        ResponseResult::Status {
205            running,
206            pid,
207            projects,
208        } => Ok((running, pid, projects)),
209        ResponseResult::Error { message } => Err(Error::DaemonError { message }),
210        _ => Err(Error::DaemonError {
211            message: "Unexpected response type".to_string(),
212        }),
213    }
214}