1use std::io::{BufRead, BufReader, Write};
21use std::path::PathBuf;
22use std::process::{Command, Stdio};
23use std::sync::atomic::{AtomicU64, Ordering};
24use std::time::Duration;
25
26use bronzite_types::{Query, QueryData, QueryResult, Request, Response};
27
28#[cfg(unix)]
29use std::os::unix::net::UnixStream;
30
31#[derive(Debug, thiserror::Error)]
33pub enum Error {
34 #[error("Failed to connect to Bronzite daemon: {0}")]
35 ConnectionFailed(#[from] std::io::Error),
36
37 #[error("Failed to serialize request: {0}")]
38 SerializationError(#[from] serde_json::Error),
39
40 #[error("Daemon returned an error: {0}")]
41 DaemonError(String),
42
43 #[error("Response ID mismatch: expected {expected}, got {got}")]
44 ResponseMismatch { expected: u64, got: u64 },
45
46 #[error("Daemon is not running. Start it with: bronzite-daemon --ensure")]
47 DaemonNotRunning,
48
49 #[error("Unexpected response type")]
50 UnexpectedResponse,
51
52 #[error("Failed to start daemon: {0}")]
53 DaemonStartFailed(String),
54
55 #[error("Timeout waiting for daemon to start")]
56 DaemonStartTimeout,
57}
58
59pub type Result<T> = std::result::Result<T, Error>;
61
62static REQUEST_ID: AtomicU64 = AtomicU64::new(1);
64
65const DEFAULT_DAEMON_TIMEOUT: Duration = Duration::from_secs(30);
67
68pub struct BronziteClient {
70 #[cfg(unix)]
71 stream: UnixStream,
72 #[cfg(windows)]
73 stream: std::net::TcpStream,
74}
75
76impl BronziteClient {
77 pub fn connect() -> Result<Self> {
79 let socket_path = bronzite_types::default_socket_path();
80 Self::connect_to(socket_path)
81 }
82
83 pub fn connect_for_workspace(workspace_root: &std::path::Path) -> Result<Self> {
85 let socket_path = bronzite_types::socket_path_for_workspace(workspace_root);
86 Self::connect_to(socket_path)
87 }
88
89 #[cfg(unix)]
91 pub fn connect_to(socket_path: PathBuf) -> Result<Self> {
92 if !socket_path.exists() {
93 return Err(Error::DaemonNotRunning);
94 }
95
96 let stream = UnixStream::connect(&socket_path)?;
97 Ok(Self { stream })
98 }
99
100 #[cfg(windows)]
102 pub fn connect_to(socket_path: PathBuf) -> Result<Self> {
103 use std::collections::hash_map::DefaultHasher;
106 use std::hash::{Hash, Hasher};
107
108 let mut hasher = DefaultHasher::new();
109 socket_path.hash(&mut hasher);
110 let port = 10000 + (hasher.finish() % 50000) as u16;
111
112 let stream = std::net::TcpStream::connect(("127.0.0.1", port))?;
113 Ok(Self { stream })
114 }
115
116 pub fn query(&mut self, crate_name: &str, query: Query) -> Result<QueryData> {
118 let id = REQUEST_ID.fetch_add(1, Ordering::SeqCst);
119
120 let request = Request {
121 id,
122 crate_name: crate_name.to_string(),
123 query,
124 };
125
126 let mut request_json = serde_json::to_string(&request)?;
128 request_json.push('\n');
129 self.stream.write_all(request_json.as_bytes())?;
130 self.stream.flush()?;
131
132 let mut reader = BufReader::new(&self.stream);
134 let mut response_line = String::new();
135 reader.read_line(&mut response_line)?;
136
137 let response: Response = serde_json::from_str(&response_line)?;
138
139 if response.id != id {
141 return Err(Error::ResponseMismatch {
142 expected: id,
143 got: response.id,
144 });
145 }
146
147 match response.result {
149 QueryResult::Success { data } => Ok(data),
150 QueryResult::Error { message } => Err(Error::DaemonError(message)),
151 }
152 }
153
154 pub fn ping(&mut self) -> Result<bool> {
156 match self.query("", Query::Ping) {
157 Ok(QueryData::Pong) => Ok(true),
158 Ok(_) => Err(Error::UnexpectedResponse),
159 Err(e) => Err(e),
160 }
161 }
162
163 pub fn shutdown(&mut self) -> Result<()> {
165 match self.query("", Query::Shutdown) {
166 Ok(QueryData::ShuttingDown) => Ok(()),
167 Ok(_) => Err(Error::UnexpectedResponse),
168 Err(e) => Err(e),
169 }
170 }
171
172 pub fn list_items(&mut self, crate_name: &str) -> Result<Vec<bronzite_types::ItemInfo>> {
174 match self.query(crate_name, Query::ListItems)? {
175 QueryData::Items { items } => Ok(items),
176 _ => Err(Error::UnexpectedResponse),
177 }
178 }
179
180 pub fn get_trait_impls(
182 &mut self,
183 crate_name: &str,
184 type_path: &str,
185 ) -> Result<Vec<bronzite_types::TraitImplDetails>> {
186 let query = Query::GetTraitImpls {
187 type_path: type_path.to_string(),
188 };
189
190 match self.query(crate_name, query)? {
191 QueryData::TraitImpls { impls } => Ok(impls),
192 _ => Err(Error::UnexpectedResponse),
193 }
194 }
195
196 pub fn get_inherent_impls(
198 &mut self,
199 crate_name: &str,
200 type_path: &str,
201 ) -> Result<Vec<bronzite_types::InherentImplDetails>> {
202 let query = Query::GetInherentImpls {
203 type_path: type_path.to_string(),
204 };
205
206 match self.query(crate_name, query)? {
207 QueryData::InherentImpls { impls } => Ok(impls),
208 _ => Err(Error::UnexpectedResponse),
209 }
210 }
211
212 pub fn check_impl(
214 &mut self,
215 crate_name: &str,
216 type_path: &str,
217 trait_path: &str,
218 ) -> Result<(bool, Option<bronzite_types::TraitImplDetails>)> {
219 let query = Query::CheckImpl {
220 type_path: type_path.to_string(),
221 trait_path: trait_path.to_string(),
222 };
223
224 match self.query(crate_name, query)? {
225 QueryData::ImplCheck {
226 implements,
227 impl_info,
228 } => Ok((implements, impl_info)),
229 _ => Err(Error::UnexpectedResponse),
230 }
231 }
232
233 pub fn get_fields(
235 &mut self,
236 crate_name: &str,
237 type_path: &str,
238 ) -> Result<Vec<bronzite_types::FieldInfo>> {
239 let query = Query::GetFields {
240 type_path: type_path.to_string(),
241 };
242
243 match self.query(crate_name, query)? {
244 QueryData::Fields { fields } => Ok(fields),
245 _ => Err(Error::UnexpectedResponse),
246 }
247 }
248
249 pub fn get_type(
251 &mut self,
252 crate_name: &str,
253 type_path: &str,
254 ) -> Result<bronzite_types::TypeDetails> {
255 let query = Query::GetType {
256 path: type_path.to_string(),
257 };
258
259 match self.query(crate_name, query)? {
260 QueryData::TypeInfo(info) => Ok(info),
261 _ => Err(Error::UnexpectedResponse),
262 }
263 }
264
265 pub fn get_traits(&mut self, crate_name: &str) -> Result<Vec<bronzite_types::TraitInfo>> {
267 match self.query(crate_name, Query::GetTraits)? {
268 QueryData::Traits { traits } => Ok(traits),
269 _ => Err(Error::UnexpectedResponse),
270 }
271 }
272
273 pub fn get_trait(
275 &mut self,
276 crate_name: &str,
277 trait_path: &str,
278 ) -> Result<bronzite_types::TraitDetails> {
279 let query = Query::GetTrait {
280 path: trait_path.to_string(),
281 };
282
283 match self.query(crate_name, query)? {
284 QueryData::TraitDetails(details) => Ok(details),
285 _ => Err(Error::UnexpectedResponse),
286 }
287 }
288
289 pub fn find_types(
291 &mut self,
292 crate_name: &str,
293 pattern: &str,
294 ) -> Result<Vec<bronzite_types::TypeSummary>> {
295 let query = Query::FindTypes {
296 pattern: pattern.to_string(),
297 };
298
299 match self.query(crate_name, query)? {
300 QueryData::Types { types } => Ok(types),
301 _ => Err(Error::UnexpectedResponse),
302 }
303 }
304
305 pub fn resolve_alias(
307 &mut self,
308 crate_name: &str,
309 path: &str,
310 ) -> Result<(String, String, Vec<String>)> {
311 let query = Query::ResolveAlias {
312 path: path.to_string(),
313 };
314
315 match self.query(crate_name, query)? {
316 QueryData::ResolvedType {
317 original,
318 resolved,
319 chain,
320 } => Ok((original, resolved, chain)),
321 _ => Err(Error::UnexpectedResponse),
322 }
323 }
324
325 pub fn get_implementors(
327 &mut self,
328 crate_name: &str,
329 trait_path: &str,
330 ) -> Result<Vec<bronzite_types::TypeSummary>> {
331 let query = Query::GetImplementors {
332 trait_path: trait_path.to_string(),
333 };
334
335 match self.query(crate_name, query)? {
336 QueryData::Implementors { types } => Ok(types),
337 _ => Err(Error::UnexpectedResponse),
338 }
339 }
340
341 pub fn get_layout(
343 &mut self,
344 crate_name: &str,
345 type_path: &str,
346 ) -> Result<bronzite_types::LayoutInfo> {
347 let query = Query::GetLayout {
348 type_path: type_path.to_string(),
349 };
350
351 match self.query(crate_name, query)? {
352 QueryData::Layout(layout) => Ok(layout),
353 _ => Err(Error::UnexpectedResponse),
354 }
355 }
356}
357
358pub fn connect() -> Result<BronziteClient> {
363 BronziteClient::connect()
364}
365
366pub fn connect_for_workspace(workspace_root: &std::path::Path) -> Result<BronziteClient> {
368 BronziteClient::connect_for_workspace(workspace_root)
369}
370
371pub fn is_daemon_running() -> bool {
373 is_daemon_running_at(&bronzite_types::default_socket_path())
374}
375
376#[cfg(unix)]
378pub fn is_daemon_running_at(socket_path: &PathBuf) -> bool {
379 if !socket_path.exists() {
380 return false;
381 }
382
383 match UnixStream::connect(socket_path) {
385 Ok(mut stream) => {
386 let request = Request {
388 id: 0,
389 crate_name: String::new(),
390 query: Query::Ping,
391 };
392
393 if let Ok(json) = serde_json::to_string(&request) {
394 let msg = format!("{}\n", json);
395 if stream.write_all(msg.as_bytes()).is_ok() {
396 let _ = stream.flush();
397
398 let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
400
401 let mut reader = BufReader::new(&stream);
403 let mut response = String::new();
404 if reader.read_line(&mut response).is_ok() {
405 return response.contains("pong");
406 }
407 }
408 }
409 false
410 }
411 Err(_) => false,
412 }
413}
414
415#[cfg(windows)]
416pub fn is_daemon_running_at(socket_path: &PathBuf) -> bool {
417 use std::collections::hash_map::DefaultHasher;
418 use std::hash::{Hash, Hasher};
419
420 let mut hasher = DefaultHasher::new();
421 socket_path.hash(&mut hasher);
422 let port = 10000 + (hasher.finish() % 50000) as u16;
423
424 std::net::TcpStream::connect(("127.0.0.1", port)).is_ok()
425}
426
427pub fn ensure_daemon_running(manifest_path: Option<&std::path::Path>) -> Result<()> {
451 ensure_daemon_running_with_timeout(manifest_path, DEFAULT_DAEMON_TIMEOUT)
452}
453
454pub fn ensure_daemon_running_with_timeout(
456 manifest_path: Option<&std::path::Path>,
457 timeout: Duration,
458) -> Result<()> {
459 let socket_path = bronzite_types::default_socket_path();
460
461 if is_daemon_running_at(&socket_path) {
463 return Ok(());
464 }
465
466 let daemon_path = find_daemon_binary()?;
468
469 let mut cmd = Command::new(&daemon_path);
471 cmd.arg("--ensure");
472 cmd.arg("--ensure-timeout")
473 .arg(timeout.as_secs().to_string());
474
475 if let Some(path) = manifest_path {
476 cmd.arg("--manifest-path").arg(path);
477 }
478
479 let output = cmd
481 .stdin(Stdio::null())
482 .stdout(Stdio::piped())
483 .stderr(Stdio::piped())
484 .output()
485 .map_err(|e| Error::DaemonStartFailed(format!("Failed to run bronzite-daemon: {}", e)))?;
486
487 if !output.status.success() {
488 let stderr = String::from_utf8_lossy(&output.stderr);
489 return Err(Error::DaemonStartFailed(format!(
490 "bronzite-daemon --ensure failed: {}",
491 stderr.trim()
492 )));
493 }
494
495 if !is_daemon_running_at(&socket_path) {
497 return Err(Error::DaemonStartTimeout);
498 }
499
500 Ok(())
501}
502
503fn find_daemon_binary() -> Result<PathBuf> {
505 if let Ok(output) = Command::new("which").arg("bronzite-daemon").output() {
507 if output.status.success() {
508 let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
509 if !path.is_empty() {
510 return Ok(PathBuf::from(path));
511 }
512 }
513 }
514
515 if let Ok(current_exe) = std::env::current_exe() {
517 if let Some(parent) = current_exe.parent() {
518 let daemon_path = parent.join("bronzite-daemon");
519 if daemon_path.exists() {
520 return Ok(daemon_path);
521 }
522 }
523 }
524
525 if let Ok(home) = std::env::var("CARGO_HOME") {
527 let daemon_path = PathBuf::from(home).join("bin").join("bronzite-daemon");
528 if daemon_path.exists() {
529 return Ok(daemon_path);
530 }
531 }
532
533 if let Ok(home) = std::env::var("HOME") {
535 let daemon_path = PathBuf::from(home)
536 .join(".cargo")
537 .join("bin")
538 .join("bronzite-daemon");
539 if daemon_path.exists() {
540 return Ok(daemon_path);
541 }
542 }
543
544 Ok(PathBuf::from("bronzite-daemon"))
546}
547
548pub fn connect_or_start(manifest_path: Option<&std::path::Path>) -> Result<BronziteClient> {
559 ensure_daemon_running(manifest_path)?;
560 connect()
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_is_daemon_running_when_not_running() {
569 assert!(!is_daemon_running() || true); }
573
574 #[test]
575 fn test_connect_fails_when_daemon_not_running() {
576 let fake_path = PathBuf::from("/tmp/bronzite-nonexistent-12345.sock");
578 let result = BronziteClient::connect_to(fake_path);
579 assert!(result.is_err());
580 }
581
582 #[test]
583 fn test_find_daemon_binary() {
584 let _ = find_daemon_binary();
586 }
587}