pub(crate) mod clipboard;
pub(crate) mod notify;
pub(crate) mod open_url;
use crate::tools::ToolExecError;
use crate::tools::context::ToolContext;
use crate::tools::ios_bridge::{IosToolBridge, IosToolRequest, ToolBridgeError};
use std::sync::Arc;
use std::time::Duration;
pub(crate) const IOS_GROUP: &str = "ios";
pub(crate) fn run_bridge_tool<T: serde::Serialize>(
bridge: &Arc<dyn IosToolBridge>,
tool_name: &'static str,
args: &T,
timeout: Duration,
ctx: Option<&ToolContext>,
) -> Result<serde_json::Value, ToolExecError> {
if let Some(ctx) = ctx
&& ctx.cancelled.load(std::sync::atomic::Ordering::Relaxed)
{
tracing::info!(
tool = tool_name,
session_id = ctx.session_id,
"ios tool canceled before dispatch"
);
return Err(ToolExecError("request was canceled".into()));
}
let args_json = serde_json::to_string(args).map_err(|e| {
ToolExecError(format!("failed to encode ios tool arguments: {e}"))
})?;
tracing::debug!(
tool = tool_name,
timeout_ms = timeout.as_millis() as u64,
"dispatching ios tool"
);
let pending = bridge
.dispatch(IosToolRequest {
request_id: 0,
name: tool_name.to_string(),
args_json,
})
.map_err(|e| bridge_err_to_tool_err(tool_name, e))?;
let request_id = pending.request_id;
let cancelled_flag = ctx.map(|c| Arc::clone(&c.cancelled));
let is_canceled = move || {
cancelled_flag
.as_ref()
.is_some_and(|f| f.load(std::sync::atomic::Ordering::Relaxed))
};
let reply = pending.wait(timeout, &is_canceled);
match reply {
Ok(value) => {
tracing::debug!(tool = tool_name, request_id, "ios tool reply received");
Ok(value)
}
Err(e) => {
if matches!(e, ToolBridgeError::Canceled) {
pending.cancel();
tracing::info!(tool = tool_name, request_id, "ios tool canceled");
} else {
tracing::warn!(tool = tool_name, request_id, error = %e, "ios tool failed");
}
Err(bridge_err_to_tool_err(tool_name, e))
}
}
}
fn bridge_err_to_tool_err(tool_name: &'static str, e: ToolBridgeError) -> ToolExecError {
match e {
ToolBridgeError::Canceled => ToolExecError("request was canceled".into()),
ToolBridgeError::Timeout => {
ToolExecError(format!("{tool_name} timed out waiting for the iOS host"))
}
ToolBridgeError::BridgeUnavailable => ToolExecError(format!(
"{tool_name} could not reach the iOS host (bridge unavailable)"
)),
ToolBridgeError::Platform(msg) => ToolExecError(msg),
}
}
#[cfg(test)]
pub(crate) mod test_util {
use super::*;
use crate::tools::ios_bridge::{MockBridge, MockResponse};
pub(crate) struct CancelDuringDispatchBridge {
pub inner: Arc<MockBridge>,
pub flag: Arc<std::sync::atomic::AtomicBool>,
}
impl CancelDuringDispatchBridge {
pub(crate) fn dispatched(&self) -> Vec<crate::tools::ios_bridge::IosToolRequest> {
self.inner.dispatched()
}
pub(crate) fn cancels(&self) -> Vec<u64> {
self.inner.cancels()
}
}
pub(crate) fn test_ctx() -> ToolContext {
let (tx, _rx) = std::sync::mpsc::channel();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("test.redb");
let db = Arc::new(redb::Database::create(&path).unwrap());
let _ = std::fs::remove_file(&path);
ToolContext::new(7, db, tx)
}
pub(crate) fn ctx_with_flag(flag: Arc<std::sync::atomic::AtomicBool>) -> ToolContext {
let mut c = test_ctx();
c.cancelled = flag;
c
}
pub(crate) fn cancel_race_fixture(
reply: crate::tools::ios_bridge::ToolBridgeReply,
) -> (
Arc<CancelDuringDispatchBridge>,
Arc<MockBridge>,
ToolContext,
) {
let inner = Arc::new(MockBridge::default());
inner.script(MockResponse::Reply(reply));
let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
let bridge = Arc::new(CancelDuringDispatchBridge {
inner: Arc::clone(&inner),
flag: Arc::clone(&flag),
});
(bridge, inner, ctx_with_flag(flag))
}
impl IosToolBridge for CancelDuringDispatchBridge {
fn dispatch(
&self,
request: IosToolRequest,
) -> Result<crate::tools::ios_bridge::IosToolPending, ToolBridgeError> {
self.flag.store(true, std::sync::atomic::Ordering::Relaxed);
self.inner.dispatch(request)
}
}
}