use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use bronzite_types::{Query, QueryData, QueryResult, Request, Response};
#[cfg(unix)]
use std::os::unix::net::UnixStream;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Failed to connect to Bronzite daemon: {0}")]
ConnectionFailed(#[from] std::io::Error),
#[error("Failed to serialize request: {0}")]
SerializationError(#[from] serde_json::Error),
#[error("Daemon returned an error: {0}")]
DaemonError(String),
#[error("Response ID mismatch: expected {expected}, got {got}")]
ResponseMismatch { expected: u64, got: u64 },
#[error("Daemon is not running. Start it with: bronzite-daemon --ensure")]
DaemonNotRunning,
#[error("Unexpected response type")]
UnexpectedResponse,
#[error("Failed to start daemon: {0}")]
DaemonStartFailed(String),
#[error("Timeout waiting for daemon to start")]
DaemonStartTimeout,
}
pub type Result<T> = std::result::Result<T, Error>;
static REQUEST_ID: AtomicU64 = AtomicU64::new(1);
const DEFAULT_DAEMON_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)]
pub struct BronziteClient {
#[cfg(unix)]
stream: UnixStream,
#[cfg(windows)]
stream: std::net::TcpStream,
}
impl BronziteClient {
pub fn connect() -> Result<Self> {
let socket_path = bronzite_types::default_socket_path();
Self::connect_to(socket_path)
}
pub fn connect_for_workspace(workspace_root: &std::path::Path) -> Result<Self> {
let socket_path = bronzite_types::socket_path_for_workspace(workspace_root);
Self::connect_to(socket_path)
}
#[cfg(unix)]
pub fn connect_to(socket_path: PathBuf) -> Result<Self> {
if !socket_path.exists() {
return Err(Error::DaemonNotRunning);
}
let stream = UnixStream::connect(&socket_path)?;
Ok(Self { stream })
}
#[cfg(windows)]
pub fn connect_to(socket_path: PathBuf) -> Result<Self> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
socket_path.hash(&mut hasher);
let port = 10000 + (hasher.finish() % 50000) as u16;
let stream = std::net::TcpStream::connect(("127.0.0.1", port))?;
Ok(Self { stream })
}
pub fn query(&mut self, crate_name: &str, query: Query) -> Result<QueryData> {
let id = REQUEST_ID.fetch_add(1, Ordering::SeqCst);
let request = Request {
id,
crate_name: crate_name.to_string(),
query,
};
let mut request_json = serde_json::to_string(&request)?;
request_json.push('\n');
self.stream.write_all(request_json.as_bytes())?;
self.stream.flush()?;
let mut reader = BufReader::new(&self.stream);
let mut response_line = String::new();
reader.read_line(&mut response_line)?;
let response: Response = serde_json::from_str(&response_line)?;
if response.id != id {
return Err(Error::ResponseMismatch {
expected: id,
got: response.id,
});
}
match response.result {
QueryResult::Success { data } => Ok(data),
QueryResult::Error { message } => Err(Error::DaemonError(message)),
}
}
pub fn ping(&mut self) -> Result<bool> {
match self.query("", Query::Ping) {
Ok(QueryData::Pong) => Ok(true),
Ok(_) => Err(Error::UnexpectedResponse),
Err(e) => Err(e),
}
}
pub fn shutdown(&mut self) -> Result<()> {
match self.query("", Query::Shutdown) {
Ok(QueryData::ShuttingDown) => Ok(()),
Ok(_) => Err(Error::UnexpectedResponse),
Err(e) => Err(e),
}
}
pub fn list_items(&mut self, crate_name: &str) -> Result<Vec<bronzite_types::ItemInfo>> {
match self.query(crate_name, Query::ListItems)? {
QueryData::Items { items } => Ok(items),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_trait_impls(
&mut self,
crate_name: &str,
type_path: &str,
) -> Result<Vec<bronzite_types::TraitImplDetails>> {
let query = Query::GetTraitImpls {
type_path: type_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::TraitImpls { impls } => Ok(impls),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_inherent_impls(
&mut self,
crate_name: &str,
type_path: &str,
) -> Result<Vec<bronzite_types::InherentImplDetails>> {
let query = Query::GetInherentImpls {
type_path: type_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::InherentImpls { impls } => Ok(impls),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn check_impl(
&mut self,
crate_name: &str,
type_path: &str,
trait_path: &str,
) -> Result<(bool, Option<bronzite_types::TraitImplDetails>)> {
let query = Query::CheckImpl {
type_path: type_path.to_string(),
trait_path: trait_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::ImplCheck {
implements,
impl_info,
} => Ok((implements, impl_info)),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_fields(
&mut self,
crate_name: &str,
type_path: &str,
) -> Result<Vec<bronzite_types::FieldInfo>> {
let query = Query::GetFields {
type_path: type_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::Fields { fields } => Ok(fields),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_type(
&mut self,
crate_name: &str,
type_path: &str,
) -> Result<bronzite_types::TypeDetails> {
let query = Query::GetType {
path: type_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::TypeInfo(info) => Ok(info),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_traits(&mut self, crate_name: &str) -> Result<Vec<bronzite_types::TraitInfo>> {
match self.query(crate_name, Query::GetTraits)? {
QueryData::Traits { traits } => Ok(traits),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_trait(
&mut self,
crate_name: &str,
trait_path: &str,
) -> Result<bronzite_types::TraitDetails> {
let query = Query::GetTrait {
path: trait_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::TraitDetails(details) => Ok(details),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn find_types(
&mut self,
crate_name: &str,
pattern: &str,
) -> Result<Vec<bronzite_types::TypeSummary>> {
let query = Query::FindTypes {
pattern: pattern.to_string(),
};
match self.query(crate_name, query)? {
QueryData::Types { types } => Ok(types),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn resolve_alias(
&mut self,
crate_name: &str,
path: &str,
) -> Result<(String, String, Vec<String>)> {
let query = Query::ResolveAlias {
path: path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::ResolvedType {
original,
resolved,
chain,
} => Ok((original, resolved, chain)),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_implementors(
&mut self,
crate_name: &str,
trait_path: &str,
) -> Result<Vec<bronzite_types::TypeSummary>> {
let query = Query::GetImplementors {
trait_path: trait_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::Implementors { types } => Ok(types),
_ => Err(Error::UnexpectedResponse),
}
}
pub fn get_layout(
&mut self,
crate_name: &str,
type_path: &str,
) -> Result<bronzite_types::LayoutInfo> {
let query = Query::GetLayout {
type_path: type_path.to_string(),
};
match self.query(crate_name, query)? {
QueryData::Layout(layout) => Ok(layout),
_ => Err(Error::UnexpectedResponse),
}
}
}
pub fn connect() -> Result<BronziteClient> {
BronziteClient::connect()
}
pub fn connect_for_workspace(workspace_root: &std::path::Path) -> Result<BronziteClient> {
BronziteClient::connect_for_workspace(workspace_root)
}
pub fn is_daemon_running() -> bool {
is_daemon_running_at(&bronzite_types::default_socket_path())
}
#[cfg(unix)]
pub fn is_daemon_running_at(socket_path: &PathBuf) -> bool {
if !socket_path.exists() {
return false;
}
match UnixStream::connect(socket_path) {
Ok(mut stream) => {
let request = Request {
id: 0,
crate_name: String::new(),
query: Query::Ping,
};
if let Ok(json) = serde_json::to_string(&request) {
let msg = format!("{}\n", json);
if stream.write_all(msg.as_bytes()).is_ok() {
let _ = stream.flush();
let _ = stream.set_read_timeout(Some(Duration::from_secs(5)));
let mut reader = BufReader::new(&stream);
let mut response = String::new();
if reader.read_line(&mut response).is_ok() {
return response.contains("pong");
}
}
}
false
}
Err(_) => false,
}
}
#[cfg(windows)]
pub fn is_daemon_running_at(socket_path: &PathBuf) -> bool {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
socket_path.hash(&mut hasher);
let port = 10000 + (hasher.finish() % 50000) as u16;
std::net::TcpStream::connect(("127.0.0.1", port)).is_ok()
}
pub fn ensure_daemon_running(manifest_path: Option<&std::path::Path>) -> Result<()> {
ensure_daemon_running_with_timeout(manifest_path, DEFAULT_DAEMON_TIMEOUT)
}
pub fn ensure_daemon_running_with_timeout(
manifest_path: Option<&std::path::Path>,
timeout: Duration,
) -> Result<()> {
let socket_path = bronzite_types::default_socket_path();
if is_daemon_running_at(&socket_path) {
return Ok(());
}
let daemon_path = find_daemon_binary()?;
let mut cmd = Command::new(&daemon_path);
cmd.arg("--ensure");
cmd.arg("--ensure-timeout")
.arg(timeout.as_secs().to_string());
if let Some(path) = manifest_path {
cmd.arg("--manifest-path").arg(path);
}
let output = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map_err(|e| Error::DaemonStartFailed(format!("Failed to run bronzite-daemon: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(Error::DaemonStartFailed(format!(
"bronzite-daemon --ensure failed: {}",
stderr.trim()
)));
}
if !is_daemon_running_at(&socket_path) {
return Err(Error::DaemonStartTimeout);
}
Ok(())
}
fn find_daemon_binary() -> Result<PathBuf> {
if let Ok(output) = Command::new("which").arg("bronzite-daemon").output() {
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !path.is_empty() {
return Ok(PathBuf::from(path));
}
}
}
if let Ok(current_exe) = std::env::current_exe() {
if let Some(parent) = current_exe.parent() {
let daemon_path = parent.join("bronzite-daemon");
if daemon_path.exists() {
return Ok(daemon_path);
}
}
}
if let Ok(home) = std::env::var("CARGO_HOME") {
let daemon_path = PathBuf::from(home).join("bin").join("bronzite-daemon");
if daemon_path.exists() {
return Ok(daemon_path);
}
}
if let Ok(home) = std::env::var("HOME") {
let daemon_path = PathBuf::from(home)
.join(".cargo")
.join("bin")
.join("bronzite-daemon");
if daemon_path.exists() {
return Ok(daemon_path);
}
}
Ok(PathBuf::from("bronzite-daemon"))
}
pub fn connect_or_start(manifest_path: Option<&std::path::Path>) -> Result<BronziteClient> {
ensure_daemon_running(manifest_path)?;
connect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_daemon_running_when_not_running() {
assert!(!is_daemon_running() || true); }
#[test]
fn test_connect_fails_when_daemon_not_running() {
let fake_path = PathBuf::from("/tmp/bronzite-nonexistent-12345.sock");
let result = BronziteClient::connect_to(fake_path);
assert!(result.is_err());
}
#[test]
fn test_find_daemon_binary() {
let _ = find_daemon_binary();
}
}
pub mod reflection;
pub use reflection::{
Crate, EnumDef, Field, Item, Method, StructDef, TraitDef, TraitImpl, TraitMethod, TypeAliasDef,
UnionDef,
};