Skip to main content

tauri_plugin_py_runtime/bridge/
invoke.rs

1//! 调用路由:把前端调用转换为 `request` 帧,并把 Python 返回的帧分发回对应的等待者。
2//!
3//! 架构位置:前端 -> Tauri 命令 -> **Bridge** -> Writer/FrameCodec -> Python 进程。
4//! [`Bridge`] 不持有连接:发送通过调用方传入的 [`Writer`],接收由
5//! [`crate::runtime::Supervisor`] 的读循环调用 [`Bridge::handle`]。
6//!
7//! 不变量:
8//! - 每个调用 id 只会被完成一次(响应、错误、超时、取消或 [`Bridge::fail_all`] 中先到者生效),
9//!   之后迟到的帧被静默丢弃;
10//! - 调用完成时同时注销其 channel 与客户端 id 映射,因此调用返回后到达的 `channel_data` 不会再投递;
11//! - 客户端调用 id 按窗口隔离,见 [`CallOptions::client_id`]。
12
13use std::{
14  collections::{BTreeMap, HashMap},
15  sync::{
16    atomic::{AtomicU64, Ordering},
17    Arc, Mutex,
18  },
19  time::Duration,
20};
21
22use tokio::sync::oneshot;
23
24use crate::{
25  bridge::{
26    protocol::{Frame, Message, Payload, ProtocolError},
27    transport::Writer,
28  },
29  error::{codes, Error, Result},
30};
31
32/// 接收某个 channel 数据的回调,通常把载荷转发给前端的 `Channel`。
33///
34/// 在 Python 读循环中同步调用,不应阻塞。
35pub type ChannelSink = Arc<dyn Fn(Payload) + Send + Sync>;
36
37/// 接收 Python 发往前端的事件与进程退出通知。
38///
39/// 插件内的实现把事件转发为 Tauri 事件 `py://<event>`;测试中可替换为记录器。
40pub trait EventSink: Send + Sync + 'static {
41  /// 转发 Python 发出的事件;`target` 为目标 webview 标签,`None` 表示广播。
42  fn emit(&self, event: &str, target: Option<&str>, payload: Payload);
43  /// 通知 Python 进程意外退出,`code` 为退出码(被信号终止时为 `None`)。
44  fn exited(&self, code: Option<i32>);
45}
46
47/// 单次调用的可选参数。
48#[derive(Debug, Clone, Default)]
49pub struct CallOptions {
50  /// 前端生成的调用 id,用于 [`Bridge::cancel`]。
51  ///
52  /// 以 `(窗口标签, client_id)` 为键,因此不同窗口可以使用相同 id,且一个 webview 只能取消
53  /// 自己发起的调用;同一窗口内仍在进行的 id 不能重复使用。
54  pub client_id: Option<String>,
55  /// 本次调用的超时时间;为 `None` 时使用默认超时,指定值最多为默认超时的 [`MAX_TIMEOUT_FACTOR`] 倍。
56  pub timeout: Option<Duration>,
57  /// 发起调用的 webview 标签,会随 `request` 帧传给 Python(`Command.window`)。
58  pub window: Option<String>,
59}
60
61/// 前端指定的单次超时最多为默认超时的此倍数。
62pub const MAX_TIMEOUT_FACTOR: u32 = 10;
63
64/// 客户端调用 id 按窗口隔离:`(窗口标签, 客户端 id)`。
65type ClientKey = (String, String);
66
67struct Pending {
68  tx: oneshot::Sender<Result<Payload>>,
69  channels: Vec<u64>,
70  client: Option<ClientKey>,
71}
72
73/// 进行中调用、客户端 id 与 channel 的注册表,在多次进程重启之间复用。
74///
75/// 内部使用多把互斥锁,任何时刻最多持有其中一把,且锁内不会调用外部回调。
76#[derive(Default)]
77pub struct Bridge {
78  next_id: AtomicU64,
79  next_channel: AtomicU64,
80  pending: Mutex<HashMap<u64, Pending>>,
81  clients: Mutex<HashMap<ClientKey, u64>>,
82  channels: Mutex<HashMap<u64, ChannelSink>>,
83}
84
85fn cancel_frame(id: u64, reason: &str) -> Frame {
86  Frame::new(Message::Cancel { id, reason: reason.into() })
87}
88
89impl Bridge {
90  /// 创建空的注册表。
91  pub fn new() -> Self {
92    Self::default()
93  }
94
95  /// 通过 `writer` 发送一次命令调用,并等待 Python 的响应。
96  ///
97  /// `channels` 中的每个回调会分配一个新的 channel id,随请求以“参数名 → channel id”发送,
98  /// 调用结束后即注销。超时时间为 `opts.timeout`(上限为 `default_timeout` 的
99  /// [`MAX_TIMEOUT_FACTOR`] 倍)或 `default_timeout`;超时后会向 Python 发送
100  /// `reason: "timeout"` 的 `cancel` 帧。
101  ///
102  /// # Errors
103  ///
104  /// - `invalid_args`:同一窗口内该客户端调用 id 仍在进行中;
105  /// - `frame_too_large` / `runtime_unavailable`:请求帧无法发送(见 [`Writer::send`]);
106  /// - `timeout`:超过超时时间;
107  /// - `cancelled`:被 [`Bridge::cancel`] 取消;
108  /// - `runtime_exited` 等:被 [`Bridge::fail_all`] 以指定错误码终止;
109  /// - Python 返回的错误(如 `command_not_found`、`invalid_args`、`internal` 或自定义错误码),
110  ///   以及响应载荷不一致时的 `internal`。
111  pub async fn call(
112    &self,
113    writer: &Writer,
114    cmd: &str,
115    args: Payload,
116    channels: Vec<(String, ChannelSink)>,
117    opts: CallOptions,
118    default_timeout: Duration,
119  ) -> Result<Payload> {
120    let id = self.next_id.fetch_add(1, Ordering::Relaxed) + 1;
121    let client = opts
122      .client_id
123      .as_ref()
124      .map(|client_id| (opts.window.clone().unwrap_or_default(), client_id.clone()));
125    if let Some(key) = &client {
126      let mut clients = self.clients.lock().unwrap();
127      if clients.contains_key(key) {
128        return Err(Error::new(
129          codes::INVALID_ARGS,
130          format!("call id '{}' is already pending in this window", key.1),
131        ));
132      }
133      clients.insert(key.clone(), id);
134    }
135    let mut channel_ids = BTreeMap::new();
136    let mut registered = Vec::with_capacity(channels.len());
137    {
138      let mut map = self.channels.lock().unwrap();
139      for (name, sink) in channels {
140        let channel_id = self.next_channel.fetch_add(1, Ordering::Relaxed) + 1;
141        map.insert(channel_id, sink);
142        channel_ids.insert(name, channel_id);
143        registered.push(channel_id);
144      }
145    }
146
147    let (tx, rx) = oneshot::channel();
148    self.pending.lock().unwrap().insert(id, Pending { tx, channels: registered, client });
149
150    let (payload, body) = args.split();
151    let message = Message::Request { id, cmd: cmd.into(), channels: channel_ids, window: opts.window.clone(), payload };
152    if let Err(e) = writer.send(Frame { message, body }) {
153      self.finish(id);
154      return Err(e);
155    }
156
157    let timeout = opts
158      .timeout
159      .map_or(default_timeout, |t| t.min(default_timeout.saturating_mul(MAX_TIMEOUT_FACTOR)));
160    match tokio::time::timeout(timeout, rx).await {
161      Ok(Ok(result)) => result,
162      Ok(Err(_)) => Err(Error::new(codes::RUNTIME_EXITED, "python runtime exited")),
163      Err(_) => {
164        // 超时与响应可能同时到达:只有成功注销调用的一方才发送 cancel,避免取消已完成的调用。
165        if self.finish(id).is_some() {
166          let _ = writer.send(cancel_frame(id, "timeout"));
167        }
168        Err(Error::new(
169          codes::TIMEOUT,
170          format!("command '{cmd}' timed out after {}ms", timeout.as_millis()),
171        ))
172      }
173    }
174  }
175
176  /// 注销调用及其 channel 与客户端 id 映射;返回 `None` 表示该调用已被其他路径完成。
177  fn finish(&self, id: u64) -> Option<Pending> {
178    let pending = self.pending.lock().unwrap().remove(&id)?;
179    if let Some(key) = &pending.client {
180      let mut clients = self.clients.lock().unwrap();
181      // 该客户端 id 可能已被同窗口的新调用重新占用,只移除仍指向本调用的映射。
182      if clients.get(key) == Some(&id) {
183        clients.remove(key);
184      }
185    }
186    let mut channels = self.channels.lock().unwrap();
187    for channel_id in &pending.channels {
188      channels.remove(channel_id);
189    }
190    Some(pending)
191  }
192
193  /// 取消 `window` 窗口中客户端 id 为 `client_id` 的进行中调用。
194  ///
195  /// 等待者立即以 `cancelled` 失败,不等待 Python 确认;若提供了 `writer`,还会向 Python 发送
196  /// `reason: "client"` 的 `cancel` 帧(发送失败被忽略)。返回是否找到并取消了该调用。
197  pub fn cancel(&self, writer: Option<&Writer>, window: &str, client_id: &str) -> bool {
198    let key = (window.to_string(), client_id.to_string());
199    let id = { self.clients.lock().unwrap().get(&key).copied() };
200    let Some(id) = id else { return false };
201    let Some(pending) = self.finish(id) else { return false };
202    if let Some(writer) = writer {
203      let _ = writer.send(cancel_frame(id, "client"));
204    }
205    let _ = pending.tx.send(Err(Error::new(codes::CANCELLED, "call was cancelled")));
206    true
207  }
208
209  /// 处理一帧来自 Python 的消息:完成对应调用、转发事件或投递 channel 数据。
210  ///
211  /// 未知调用 id 或 channel 的帧被静默丢弃;载荷不一致或方向错误的帧记录警告后丢弃,
212  /// 不影响连接。
213  pub fn handle(&self, frame: Frame, sink: &dyn EventSink) {
214    if let Err(e) = self.dispatch(frame, sink) {
215      log::warn!(target: "py-runtime", "dropped frame from python: {e}");
216    }
217  }
218
219  fn dispatch(&self, frame: Frame, sink: &dyn EventSink) -> std::result::Result<(), ProtocolError> {
220    let Frame { message, body } = frame;
221    match message {
222      Message::Response { id, payload } => {
223        if let Some(pending) = self.finish(id) {
224          let _ = pending.tx.send(payload.join(body).map_err(Error::from));
225        }
226      }
227      Message::Error { id, code, message, data } => {
228        if let Some(pending) = self.finish(id) {
229          let code = code.unwrap_or_else(|| codes::INTERNAL.into());
230          let _ = pending.tx.send(Err(Error { code, message: message.unwrap_or_default(), data }));
231        }
232      }
233      Message::Event { event, target, payload, .. } => sink.emit(&event, target.as_deref(), payload.join(body)?),
234      Message::ChannelData { channel, payload } => {
235        let channel = self.channels.lock().unwrap().get(&channel).cloned();
236        if let Some(channel) = channel {
237          channel(payload.join(body)?);
238        }
239      }
240      Message::ChannelClose { channel } => {
241        self.channels.lock().unwrap().remove(&channel);
242      }
243      other => log::warn!(target: "py-runtime", "unexpected {} frame from python", <&str>::from(&other)),
244    }
245    Ok(())
246  }
247
248  /// 以给定错误码终止所有进行中的调用,并清空客户端 id 与 channel 注册表。
249  ///
250  /// 在 Python 进程退出、停止或启动失败时调用。
251  pub fn fail_all(&self, code: &str, message: &str) {
252    let drained: Vec<Pending> = self.pending.lock().unwrap().drain().map(|(_, p)| p).collect();
253    self.clients.lock().unwrap().clear();
254    self.channels.lock().unwrap().clear();
255    for pending in drained {
256      let _ = pending.tx.send(Err(Error::new(code, message)));
257    }
258  }
259}
260
261#[cfg(test)]
262mod tests {
263  use super::*;
264  use crate::bridge::protocol::{Inline, DEFAULT_MAX_FRAME_SIZE};
265  use bytes::Bytes;
266  use serde_json::{json, Value};
267  use tokio::sync::mpsc::UnboundedReceiver;
268
269  struct NullSink;
270  impl EventSink for NullSink {
271    fn emit(&self, _: &str, _: Option<&str>, _: Payload) {}
272    fn exited(&self, _: Option<i32>) {}
273  }
274
275  fn setup() -> (Arc<Bridge>, Writer, UnboundedReceiver<Bytes>) {
276    let (writer, rx) = Writer::channel(DEFAULT_MAX_FRAME_SIZE);
277    (Arc::new(Bridge::new()), writer, rx)
278  }
279
280  fn spawn_call(
281    bridge: &Arc<Bridge>,
282    writer: &Writer,
283    cmd: &str,
284    opts: CallOptions,
285    channels: Vec<(String, ChannelSink)>,
286  ) -> tokio::task::JoinHandle<Result<Payload>> {
287    let (bridge, writer, cmd) = (bridge.clone(), writer.clone(), cmd.to_string());
288    tokio::spawn(async move {
289      bridge.call(&writer, &cmd, Payload::None, channels, opts, Duration::from_secs(5)).await
290    })
291  }
292
293  /// 读取下一帧发往 Python 的消息,并转为 JSON。
294  async fn recv(rx: &mut UnboundedReceiver<Bytes>) -> Value {
295    let frame = Frame::decode(&rx.recv().await.unwrap()).unwrap();
296    serde_json::to_value(frame.message).unwrap()
297  }
298
299  fn response(id: u64, payload: Payload) -> Frame {
300    let (payload, body) = payload.split();
301    Frame { message: Message::Response { id, payload }, body }
302  }
303
304  fn message(value: Value) -> Frame {
305    Frame::new(serde_json::from_value(value).unwrap())
306  }
307
308  #[tokio::test]
309  async fn response_resolves_call() {
310    let (bridge, writer, mut rx) = setup();
311    let call = spawn_call(&bridge, &writer, "hello", CallOptions { window: Some("main".into()), ..Default::default() }, vec![]);
312    let req = recv(&mut rx).await;
313    assert_eq!(req, json!({"type": "request", "id": 1, "cmd": "hello", "window": "main"}));
314    bridge.handle(response(1, Payload::Json(json!("hi"))), &NullSink);
315    assert_eq!(call.await.unwrap().unwrap(), Payload::Json(json!("hi")));
316  }
317
318  #[tokio::test]
319  async fn out_of_order_responses() {
320    let (bridge, writer, mut rx) = setup();
321    let first = spawn_call(&bridge, &writer, "a", CallOptions::default(), vec![]);
322    let id_a = recv(&mut rx).await["id"].as_u64().unwrap();
323    let second = spawn_call(&bridge, &writer, "b", CallOptions::default(), vec![]);
324    let id_b = recv(&mut rx).await["id"].as_u64().unwrap();
325    bridge.handle(response(id_b, Payload::Json(json!(2))), &NullSink);
326    assert_eq!(second.await.unwrap().unwrap(), Payload::Json(json!(2)));
327    bridge.handle(response(id_a, Payload::Json(json!(1))), &NullSink);
328    assert_eq!(first.await.unwrap().unwrap(), Payload::Json(json!(1)));
329  }
330
331  #[tokio::test]
332  async fn response_with_inconsistent_payload_fails_call() {
333    let (bridge, writer, mut rx) = setup();
334    let call = spawn_call(&bridge, &writer, "x", CallOptions::default(), vec![]);
335    let id = recv(&mut rx).await["id"].as_u64().unwrap();
336    let mut bad = response(id, Payload::Json(json!(1)));
337    bad.body = vec![1];
338    bridge.handle(bad, &NullSink);
339    assert_eq!(call.await.unwrap().unwrap_err().code, codes::INTERNAL);
340  }
341
342  #[tokio::test]
343  async fn error_frame_maps_to_error() {
344    let (bridge, writer, mut rx) = setup();
345    let call = spawn_call(&bridge, &writer, "x", CallOptions::default(), vec![]);
346    let id = recv(&mut rx).await["id"].as_u64().unwrap();
347    bridge.handle(
348      message(json!({"type": "error", "id": id, "code": "custom", "message": "bad", "data": {"k": 1}})),
349      &NullSink,
350    );
351    let err = call.await.unwrap().unwrap_err();
352    assert_eq!(err, Error::new("custom", "bad").with_data(json!({"k": 1})));
353  }
354
355  #[tokio::test]
356  async fn timeout_sends_cancel() {
357    let (bridge, writer, mut rx) = setup();
358    let opts = CallOptions { timeout: Some(Duration::from_millis(50)), ..Default::default() };
359    let call = spawn_call(&bridge, &writer, "slow", opts, vec![]);
360    let id = recv(&mut rx).await["id"].as_u64().unwrap();
361    assert_eq!(call.await.unwrap().unwrap_err().code, codes::TIMEOUT);
362    assert_eq!(recv(&mut rx).await, json!({"type": "cancel", "id": id, "reason": "timeout"}));
363  }
364
365  fn client_opts(window: &str, client_id: &str) -> CallOptions {
366    CallOptions { client_id: Some(client_id.into()), window: Some(window.into()), ..Default::default() }
367  }
368
369  #[tokio::test]
370  async fn client_cancel() {
371    let (bridge, writer, mut rx) = setup();
372    let call = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
373    rx.recv().await.unwrap();
374    assert!(!bridge.cancel(Some(&writer), "other", "c1"));
375    assert!(bridge.cancel(Some(&writer), "main", "c1"));
376    assert!(!bridge.cancel(Some(&writer), "main", "c1"));
377    assert_eq!(call.await.unwrap().unwrap_err().code, codes::CANCELLED);
378    assert_eq!(recv(&mut rx).await["reason"], "client");
379  }
380
381  #[tokio::test]
382  async fn duplicate_pending_client_id_is_rejected() {
383    let (bridge, writer, mut rx) = setup();
384    let first = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
385    let id = recv(&mut rx).await["id"].as_u64().unwrap();
386    let dup = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
387    assert_eq!(dup.await.unwrap().unwrap_err().code, codes::INVALID_ARGS);
388    assert!(rx.try_recv().is_err(), "duplicate call must not reach python");
389    bridge.handle(response(id, Payload::None), &NullSink);
390    first.await.unwrap().unwrap();
391    let again = spawn_call(&bridge, &writer, "slow", client_opts("main", "c1"), vec![]);
392    rx.recv().await.unwrap();
393    assert!(bridge.cancel(Some(&writer), "main", "c1"));
394    assert_eq!(again.await.unwrap().unwrap_err().code, codes::CANCELLED);
395  }
396
397  #[tokio::test]
398  async fn same_client_id_in_different_windows_is_independent() {
399    let (bridge, writer, mut rx) = setup();
400    let a = spawn_call(&bridge, &writer, "slow", client_opts("a", "c1"), vec![]);
401    let id_a = recv(&mut rx).await["id"].as_u64().unwrap();
402    let b = spawn_call(&bridge, &writer, "slow", client_opts("b", "c1"), vec![]);
403    let id_b = recv(&mut rx).await["id"].as_u64().unwrap();
404    assert!(bridge.cancel(Some(&writer), "b", "c1"));
405    assert_eq!(b.await.unwrap().unwrap_err().code, codes::CANCELLED);
406    assert_eq!(recv(&mut rx).await["id"], id_b);
407    assert!(bridge.cancel(Some(&writer), "a", "c1"));
408    assert_eq!(a.await.unwrap().unwrap_err().code, codes::CANCELLED);
409    assert_eq!(recv(&mut rx).await["id"], id_a);
410  }
411
412  #[test]
413  fn finish_keeps_newer_client_mapping() {
414    let bridge = Bridge::new();
415    let key = ("main".to_string(), "c1".to_string());
416    let (tx, _rx) = oneshot::channel();
417    bridge.pending.lock().unwrap().insert(1, Pending { tx, channels: vec![], client: Some(key.clone()) });
418    bridge.clients.lock().unwrap().insert(key.clone(), 2);
419    assert!(bridge.finish(1).is_some());
420    assert_eq!(bridge.clients.lock().unwrap().get(&key), Some(&2));
421  }
422
423  #[tokio::test]
424  async fn per_call_timeout_is_capped() {
425    let (bridge, writer, mut rx) = setup();
426    let opts = CallOptions { timeout: Some(Duration::from_secs(60)), ..Default::default() };
427    let call = {
428      let (bridge, writer) = (bridge.clone(), writer.clone());
429      tokio::spawn(async move {
430        bridge.call(&writer, "slow", Payload::None, vec![], opts, Duration::from_millis(10)).await
431      })
432    };
433    rx.recv().await.unwrap();
434    let err = tokio::time::timeout(Duration::from_secs(2), call).await.expect("cap not applied").unwrap().unwrap_err();
435    assert_eq!(err.code, codes::TIMEOUT);
436    assert!(err.message.contains("100ms"), "{}", err.message);
437  }
438
439  #[tokio::test]
440  async fn channel_data_is_routed_until_response() {
441    let (bridge, writer, mut rx) = setup();
442    let received = Arc::new(Mutex::new(Vec::new()));
443    let sink_store = received.clone();
444    let sink: ChannelSink = Arc::new(move |p| sink_store.lock().unwrap().push(p));
445    let call = spawn_call(&bridge, &writer, "count", CallOptions::default(), vec![("onItem".into(), sink)]);
446    let req = recv(&mut rx).await;
447    let id = req["id"].as_u64().unwrap();
448    let channel = req["channels"]["onItem"].as_u64().unwrap();
449    let data = |n: u64| {
450      Frame::new(Message::ChannelData { channel, payload: Inline { value: Some(json!(n)), raw: false } })
451    };
452    bridge.handle(data(0), &NullSink);
453    bridge.handle(data(1), &NullSink);
454    bridge.handle(response(id, Payload::None), &NullSink);
455    call.await.unwrap().unwrap();
456    bridge.handle(data(2), &NullSink);
457    assert_eq!(*received.lock().unwrap(), vec![Payload::Json(json!(0)), Payload::Json(json!(1))]);
458  }
459
460  #[tokio::test]
461  async fn fail_all_rejects_pending_calls() {
462    let (bridge, writer, mut rx) = setup();
463    let call = spawn_call(&bridge, &writer, "x", CallOptions::default(), vec![]);
464    rx.recv().await.unwrap();
465    bridge.fail_all(codes::RUNTIME_EXITED, "gone");
466    assert_eq!(call.await.unwrap().unwrap_err().code, codes::RUNTIME_EXITED);
467  }
468}