use std::path::{Path, PathBuf};
use std::time::Duration;
use blitz_control_protocol::{
AgentControlRequest, DebugDescriptor, DebugProtocolError, DebugResponse, DiagnosticsRequest,
decode_response, decode_rpc, encode_agent_request, encode_diagnostics_request, encode_rpc,
};
use endpoint_libs::libs::ws::mcp_wire::{
JsonRpcId, JsonRpcMessage, JsonRpcRequest, MCP_PROTOCOL_VERSION,
};
use endpoint_libs::libs::ws::transport::{TransportStream, framed_json};
use endpoint_libs::libs::ws::{MessageStream, WireMessage};
use eyre::{Context, Result, bail, eyre};
use tokio::net::UnixStream;
use tokio::time::timeout;
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
pub struct Descriptor {
pub path: PathBuf,
pub descriptor: DebugDescriptor,
pub raw: serde_json::Value,
}
impl Descriptor {
pub fn socket_path(&self) -> PathBuf {
match self.descriptor.address.strip_prefix("unix://") {
Some(path) => PathBuf::from(path),
None => self.path.with_extension("sock"),
}
}
pub fn warn_if_stale(&self) {
if !pid_is_live(self.descriptor.pid) {
eprintln!(
"warning: descriptor {} names pid {}, which is not running; \
numbers read from it would describe a dead instance",
self.path.display(),
self.descriptor.pid
);
}
}
}
fn pid_is_live(pid: u32) -> bool {
std::process::Command::new("ps")
.args(["-p", &pid.to_string()])
.output()
.map(|out| out.status.success())
.unwrap_or(true)
}
pub fn discover(explicit: Option<&str>) -> Result<Descriptor> {
let mut candidates: Vec<PathBuf> = Vec::new();
if let Some(path) = explicit {
candidates.push(PathBuf::from(path));
}
if let Ok(path) = std::env::var("TAURI_BLITZ_CONTROL_DESCRIPTOR") {
candidates.push(PathBuf::from(path));
}
for path in candidates {
if path.exists() {
return read_descriptor(&path);
}
}
let pinned = PathBuf::from("target/blitz-control.json");
if pinned.exists()
&& let Ok(descriptor) = read_descriptor(&pinned)
&& pid_is_live(descriptor.descriptor.pid)
{
return Ok(descriptor);
}
let root = PathBuf::from(std::env::var("TMPDIR").unwrap_or_else(|_| "/tmp".into()))
.join("tauri-blitz-agent");
let mut found: Vec<(std::time::SystemTime, PathBuf)> = std::fs::read_dir(&root)
.into_iter()
.flatten()
.flatten()
.map(|entry| entry.path())
.filter(|path| path.extension().is_some_and(|ext| ext == "json"))
.filter_map(|path| Some((path.metadata().ok()?.modified().ok()?, path)))
.collect();
found.sort();
let mut newest: Option<Descriptor> = None;
for (_, path) in found.iter().rev() {
let Ok(descriptor) = read_descriptor(path) else {
continue;
};
if pid_is_live(descriptor.descriptor.pid) {
return Ok(descriptor);
}
if newest.is_none() {
newest = Some(descriptor);
}
}
match newest {
Some(descriptor) => Ok(descriptor),
None => bail!(
"no inspector descriptor found; is a diagnostics build running?\n\
looked at $TAURI_BLITZ_CONTROL_DESCRIPTOR and {}",
root.display()
),
}
}
fn read_descriptor(path: &Path) -> Result<Descriptor> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading descriptor {}", path.display()))?;
let raw: serde_json::Value = serde_json::from_str(&text)
.with_context(|| format!("parsing descriptor {}", path.display()))?;
let descriptor: DebugDescriptor = serde_json::from_value(raw.clone())
.with_context(|| format!("descriptor {} is not a DebugDescriptor", path.display()))?;
Ok(Descriptor {
path: path.to_path_buf(),
descriptor,
raw,
})
}
pub struct Client {
stream: Box<dyn MessageStream>,
next_id: i64,
}
impl Client {
pub async fn connect(socket: &Path) -> Result<Self> {
let stream = UnixStream::connect(socket)
.await
.with_context(|| format!("connecting to {}", socket.display()))?;
Ok(Self {
stream: Box::new(TransportStream::new(framed_json(stream))),
next_id: 0,
})
}
fn next_id(&mut self) -> JsonRpcId {
self.next_id += 1;
JsonRpcId::Number(self.next_id)
}
async fn exchange(&mut self, request: WireMessage, id: &JsonRpcId) -> Result<WireMessage> {
self.stream
.send(request)
.await
.map_err(|error| eyre!("sending to the inspector failed: {error}"))?;
loop {
let message = timeout(REQUEST_TIMEOUT, self.stream.recv())
.await
.map_err(|_| eyre!("the inspector did not answer within {REQUEST_TIMEOUT:?}"))?
.ok_or_else(|| eyre!("the inspector closed the connection"))?
.map_err(|error| eyre!("reading from the inspector failed: {error}"))?;
if response_id(&message).as_ref() == Some(id) {
return Ok(message);
}
}
}
pub async fn raw_request(
&mut self,
method: &str,
params: serde_json::Value,
) -> Result<serde_json::Value> {
let id = self.next_id();
let request = encode_rpc(JsonRpcMessage::Request(JsonRpcRequest::call(
id.clone(),
method,
params,
)))
.map_err(protocol_error)?;
let message = self.exchange(request, &id).await?;
envelope(&message)
}
pub async fn initialize(&mut self) -> Result<serde_json::Value> {
self.raw_request(
"initialize",
serde_json::json!({"protocolVersion": MCP_PROTOCOL_VERSION}),
)
.await
}
pub async fn tools_list(&mut self) -> Result<serde_json::Value> {
self.raw_request("tools/list", serde_json::json!({})).await
}
pub async fn agent(&mut self, request: &AgentControlRequest) -> Result<Answer> {
let id = self.next_id();
let frame = encode_agent_request(id.clone(), request).map_err(protocol_error)?;
let message = self.exchange(frame, &id).await?;
Answer::new(message)
}
pub async fn diagnostics(&mut self, request: &DiagnosticsRequest) -> Result<Answer> {
let id = self.next_id();
let frame = encode_diagnostics_request(id.clone(), request).map_err(protocol_error)?;
let message = self.exchange(frame, &id).await?;
Answer::new(message)
}
pub async fn diagnostics_envelope(
&mut self,
request: &DiagnosticsRequest,
) -> Result<serde_json::Value> {
let id = self.next_id();
let frame = encode_diagnostics_request(id.clone(), request).map_err(protocol_error)?;
let message = self.exchange(frame, &id).await?;
envelope(&message)
}
pub async fn drain(&mut self, seconds: f64) -> Result<Vec<serde_json::Value>> {
let deadline = tokio::time::Instant::now() + Duration::from_secs_f64(seconds);
let mut out = Vec::new();
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return Ok(out);
}
match timeout(remaining, self.stream.recv()).await {
Err(_) => return Ok(out),
Ok(None) => return Ok(out),
Ok(Some(Ok(message))) => out.push(envelope(&message)?),
Ok(Some(Err(error))) => bail!("reading from the inspector failed: {error}"),
}
}
}
}
pub struct Answer {
pub envelope: serde_json::Value,
pub response: DebugResponse,
}
impl Answer {
fn new(message: WireMessage) -> Result<Self> {
let envelope = envelope(&message)?;
let (_, response) = decode_response(message).map_err(protocol_error)?;
if let DebugResponse::Error(error) = &response {
bail!("inspector returned {}: {}", error.code, error.message);
}
Ok(Self { envelope, response })
}
}
fn envelope(message: &WireMessage) -> Result<serde_json::Value> {
match message {
WireMessage::Text(text) => {
serde_json::from_str(text).context("the inspector sent a non-JSON text frame")
}
_ => bail!("the inspector sent a non-text frame"),
}
}
fn response_id(message: &WireMessage) -> Option<JsonRpcId> {
match decode_rpc(message.clone()) {
Ok(JsonRpcMessage::Response(response)) => response.id,
_ => None,
}
}
fn protocol_error(error: DebugProtocolError) -> eyre::Report {
eyre!("{error}")
}