Skip to main content

tauri_runtime_cef/
devtools.rs

1// Copyright 2019-2024 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use std::sync::atomic::{AtomicI32, Ordering};
6
7/// First identifier of the range reserved for the runtime's own requests.
8///
9/// Callers allocate below this bound, starting at 1, and the runtime allocates
10/// from it up to `i32::MAX`. Each counter is bounded by its own range and fails
11/// closed there, so neither can ever reach the other one, no matter how many
12/// identifiers are allocated. CDP identifiers are signed 32-bit integers, so
13/// both ranges stay within what the DevTools agent accepts.
14const RESERVED_RANGE_START: i32 = 1_000_000_000;
15
16static NEXT_MESSAGE_ID: AtomicI32 = AtomicI32::new(1);
17static NEXT_RESERVED_MESSAGE_ID: AtomicI32 = AtomicI32::new(RESERVED_RANGE_START);
18
19/// The process-local native DevTools request identifier space is exhausted.
20#[derive(Clone, Copy, Debug)]
21pub struct DevToolsMessageIdExhausted;
22
23impl std::fmt::Display for DevToolsMessageIdExhausted {
24  fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25    formatter.write_str("native CEF DevTools message identifiers are exhausted")
26  }
27}
28
29impl std::error::Error for DevToolsMessageIdExhausted {}
30
31/// Allocates one native DevTools request ID for a caller of this crate.
32///
33/// Observers see the whole browser, so every BrowserHost message a caller sends
34/// must use this allocator to avoid consuming another caller's result. The
35/// runtime allocates its own requests from a reserved range this function never
36/// returns, so a caller cannot answer an internal request by picking its number.
37///
38/// IDs are positive and never reused, including after cancellation or browser
39/// teardown. Numeric correlation does not authorize a browser or document.
40pub fn allocate_devtools_message_id() -> Result<i32, DevToolsMessageIdExhausted> {
41  allocate_from(&NEXT_MESSAGE_ID, RESERVED_RANGE_START - 1)
42}
43
44/// Allocates one native DevTools request ID for the runtime itself.
45///
46/// The identifiers come from the range reserved above every caller-visible one,
47/// so internal correlation (`pending_initial_loads` and the script evaluation
48/// callbacks) only ever matches requests the runtime sent, even when a caller
49/// ignores [`allocate_devtools_message_id`] and hardcodes an `id`.
50pub(crate) fn allocate_runtime_devtools_message_id() -> Result<i32, DevToolsMessageIdExhausted> {
51  allocate_from(&NEXT_RESERVED_MESSAGE_ID, i32::MAX)
52}
53
54/// Allocates the next identifier of `counter`, which hands out values up to
55/// `last`. Each range fails closed at its own boundary instead of wrapping or
56/// growing into the other one.
57fn allocate_from(counter: &AtomicI32, last: i32) -> Result<i32, DevToolsMessageIdExhausted> {
58  counter
59    .try_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
60      if value > last {
61        return None;
62      }
63      value.checked_add(1)
64    })
65    .map_err(|_| DevToolsMessageIdExhausted)
66}
67
68#[cfg(test)]
69mod tests {
70  use super::*;
71  use std::collections::HashSet;
72
73  #[test]
74  fn concurrent_native_producers_never_share_a_response_id() {
75    let threads = (0..4)
76      .map(|_| {
77        std::thread::spawn(|| {
78          (0..1_000)
79            .map(|_| allocate_devtools_message_id().unwrap())
80            .collect::<Vec<_>>()
81        })
82      })
83      .collect::<Vec<_>>();
84    let ids = threads
85      .into_iter()
86      .flat_map(|thread| thread.join().unwrap())
87      .collect::<Vec<_>>();
88    assert!(ids.iter().all(|id| *id > 0));
89    assert_eq!(ids.iter().collect::<HashSet<_>>().len(), 4_000);
90  }
91
92  #[test]
93  fn callers_and_the_runtime_never_share_a_response_id() {
94    let caller_ids = (0..1_000)
95      .map(|_| allocate_devtools_message_id().unwrap())
96      .collect::<HashSet<_>>();
97    let runtime_ids = (0..1_000)
98      .map(|_| allocate_runtime_devtools_message_id().unwrap())
99      .collect::<HashSet<_>>();
100    assert!(
101      caller_ids
102        .iter()
103        .all(|id| *id > 0 && *id < RESERVED_RANGE_START)
104    );
105    assert!(runtime_ids.iter().all(|id| *id >= RESERVED_RANGE_START));
106    assert!(caller_ids.is_disjoint(&runtime_ids));
107  }
108
109  #[test]
110  fn caller_exhaustion_cannot_reach_the_reserved_range() {
111    let last = RESERVED_RANGE_START - 1;
112    let counter = AtomicI32::new(last);
113    assert_eq!(allocate_from(&counter, last).unwrap(), last);
114    assert!(allocate_from(&counter, last).is_err());
115    assert!(allocate_from(&counter, last).is_err());
116    assert_eq!(counter.load(Ordering::Relaxed), RESERVED_RANGE_START);
117  }
118
119  #[test]
120  fn exhaustion_cannot_reuse_a_late_response_id() {
121    let counter = AtomicI32::new(i32::MAX - 1);
122    assert_eq!(allocate_from(&counter, i32::MAX).unwrap(), i32::MAX - 1);
123    assert!(allocate_from(&counter, i32::MAX).is_err());
124    assert!(allocate_from(&counter, i32::MAX).is_err());
125    assert_eq!(counter.load(Ordering::Relaxed), i32::MAX);
126  }
127}