use std::time::Duration;
use frust_devtools_protocol::{
InputScrollParams, InputTapParams, MetricsSnapshot, RpcError, ScreenshotResult, WidgetProps,
WidgetTreeDump,
};
use tokio::sync::{mpsc, oneshot};
use crate::backend::{BackendError, DevtoolsBackend};
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Call {
WidgetTree,
WidgetProps(u64),
MetricsSnapshot,
InputTap(InputTapParams),
InputScroll(InputScrollParams),
InputText(String),
Screenshot,
}
impl Call {
fn name(&self) -> &'static str {
match self {
Call::WidgetTree => "widget_tree",
Call::WidgetProps(_) => "widget_props",
Call::MetricsSnapshot => "metrics_snapshot",
Call::InputTap(_) => "input_tap",
Call::InputScroll(_) => "input_scroll",
Call::InputText(_) => "input_text",
Call::Screenshot => "screenshot",
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum CallOutcome {
WidgetTree(WidgetTreeDump),
WidgetProps(WidgetProps),
Metrics(MetricsSnapshot),
Ack,
Screenshot(ScreenshotResult),
Failed(RpcError),
}
struct BackendCall {
call: Call,
reply: oneshot::Sender<CallOutcome>,
}
pub(crate) struct BackendClient {
tx: mpsc::Sender<BackendCall>,
timeout: Duration,
}
impl BackendClient {
pub(crate) async fn call(&self, call: Call) -> CallOutcome {
let name = call.name();
let (reply, answer) = oneshot::channel();
let send_and_wait = async {
if self.tx.send(BackendCall { call, reply }).await.is_err() {
return None;
}
answer.await.ok()
};
match tokio::time::timeout(self.timeout, send_and_wait).await {
Ok(Some(outcome)) => outcome,
Ok(None) => {
log::warn!("frust-devtools: backend thread is gone, failing `{name}`");
CallOutcome::Failed(RpcError::new(
RpcError::INTERNAL_ERROR,
"devtools backend is no longer running",
))
}
Err(_) => {
let ms = self.timeout.as_millis();
log::warn!("frust-devtools: `{name}` timed out after {ms}ms");
CallOutcome::Failed(RpcError::new(
RpcError::INTERNAL_ERROR,
format!(
"`{name}` timed out after {ms}ms — the app's UI thread is busy or blocked"
),
))
}
}
}
}
pub(crate) fn spawn_backend_thread<B: DevtoolsBackend>(
backend: B,
queue_depth: usize,
timeout: Duration,
) -> BackendClient {
let (tx, mut rx) = mpsc::channel::<BackendCall>(queue_depth.max(1));
let spawned = std::thread::Builder::new()
.name("frust-devtools-backend".to_string())
.spawn(move || {
while let Some(BackendCall { call, reply }) = rx.blocking_recv() {
let _ = reply.send(execute(&backend, call));
}
});
if let Err(e) = spawned {
log::error!("frust-devtools: failed to spawn the backend thread: {e}");
}
BackendClient { tx, timeout }
}
fn execute<B: DevtoolsBackend>(backend: &B, call: Call) -> CallOutcome {
match call {
Call::WidgetTree => CallOutcome::WidgetTree(backend.widget_tree()),
Call::WidgetProps(id) => match backend.widget_props(id) {
Some(props) => CallOutcome::WidgetProps(props),
None => CallOutcome::Failed(
BackendError::invalid_request(format!("no widget with id {id}")).to_rpc_error(),
),
},
Call::MetricsSnapshot => CallOutcome::Metrics(backend.metrics_snapshot()),
Call::InputTap(params) => ack(backend.inject_tap(params)),
Call::InputScroll(params) => ack(backend.inject_scroll(params)),
Call::InputText(text) => ack(backend.inject_text(&text)),
Call::Screenshot => match backend.screenshot() {
Ok(shot) => CallOutcome::Screenshot(shot),
Err(e) => CallOutcome::Failed(e.to_rpc_error()),
},
}
}
fn ack(result: Result<(), BackendError>) -> CallOutcome {
match result {
Ok(()) => CallOutcome::Ack,
Err(e) => CallOutcome::Failed(e.to_rpc_error()),
}
}