zng_view_api/
view_process.rs

1use std::{env, mem, time::Duration};
2
3#[cfg(not(target_arch = "wasm32"))]
4use std::time::Instant;
5
6#[cfg(target_arch = "wasm32")]
7use web_time::Instant;
8
9use parking_lot::Mutex;
10use zng_txt::Txt;
11
12use crate::{VIEW_MODE, VIEW_SERVER, VIEW_VERSION};
13
14/// Configuration for starting a view-process.
15#[derive(Clone, Debug)]
16#[non_exhaustive]
17pub struct ViewConfig {
18    /// The [`VERSION`] of the API crate in the app-process.
19    ///
20    /// [`VERSION`]: crate::VERSION
21    pub version: Txt,
22
23    /// Name of the initial channel used in [`connect_view_process`] to setup the connections to the
24    /// client app-process.
25    ///
26    /// [`connect_view_process`]: crate::ipc::connect_view_process
27    pub server_name: Txt,
28
29    /// If the server should consider all window requests, headless window requests.
30    pub headless: bool,
31}
32impl ViewConfig {
33    /// New config.
34    pub fn new(version: impl Into<Txt>, server_name: impl Into<Txt>, headless: bool) -> Self {
35        Self {
36            version: version.into(),
37            server_name: server_name.into(),
38            headless,
39        }
40    }
41
42    /// Reads config from environment variables set by the [`Controller`] in a view-process instance.
43    ///
44    /// View API implementers should call this to get the config when it suspects that is running as a view-process.
45    /// Returns `Some(_)` if the process was initialized as a view-process.
46    ///
47    /// [`Controller`]: crate::Controller
48    pub fn from_env() -> Option<Self> {
49        if let Ok(version) = env::var(VIEW_VERSION)
50            && let Ok(server_name) = env::var(VIEW_SERVER)
51        {
52            let headless = env::var(VIEW_MODE).map(|m| m == "headless").unwrap_or(false);
53            Some(ViewConfig {
54                version: Txt::from_str(&version),
55                server_name: Txt::from_str(&server_name),
56                headless,
57            })
58        } else {
59            None
60        }
61    }
62
63    /// Returns `true` if the current process is awaiting for the config to start the
64    /// view process in the same process.
65    pub(crate) fn is_awaiting_same_process() -> bool {
66        matches!(*same_process().lock(), SameProcess::Awaiting)
67    }
68
69    /// Sets and unblocks the same-process config if there is a request.
70    ///
71    /// # Panics
72    ///
73    /// If there is no pending `wait_same_process`.
74    pub(crate) fn set_same_process(cfg: ViewConfig) {
75        if Self::is_awaiting_same_process() {
76            *same_process().lock() = SameProcess::Ready(cfg);
77        } else {
78            unreachable!("use `waiting_same_process` to check, then call `set_same_process` only once")
79        }
80    }
81
82    /// Wait for config from same-process.
83    ///
84    /// View API implementers should call this to sign that view-process config should be send to the same process
85    /// and then start the "app-process" code path in a different thread. This function returns when the app code path sends
86    /// the "view-process" configuration.
87    pub fn wait_same_process() -> Self {
88        let _s = tracing::trace_span!("ViewConfig::wait_same_process").entered();
89
90        if !matches!(*same_process().lock(), SameProcess::Not) {
91            panic!("`wait_same_process` can only be called once");
92        }
93
94        *same_process().lock() = SameProcess::Awaiting;
95
96        let time = Instant::now();
97        let timeout = Duration::from_secs(5);
98        let sleep = Duration::from_millis(10);
99
100        while Self::is_awaiting_same_process() {
101            std::thread::sleep(sleep);
102            if time.elapsed() >= timeout {
103                panic!("timeout, `wait_same_process` waited for `{timeout:?}`");
104            }
105        }
106
107        match mem::replace(&mut *same_process().lock(), SameProcess::Done) {
108            SameProcess::Ready(cfg) => cfg,
109            _ => unreachable!(),
110        }
111    }
112
113    /// Assert that the [`VERSION`] is the same in the app-process and view-process.
114    ///
115    /// This method must be called in the view-process implementation, it fails if the versions don't match, panics if
116    /// `is_same_process` or writes to *stderr* and exits with code .
117    ///
118    /// [`VERSION`]: crate::VERSION
119    pub fn assert_version(&self, is_same_process: bool) {
120        if self.version != crate::VERSION {
121            let msg = format!(
122                "view API version is not equal, app-process: {}, view-process: {}",
123                self.version,
124                crate::VERSION
125            );
126            if is_same_process {
127                panic!("{}", msg)
128            } else {
129                eprintln!("{msg}");
130                zng_env::exit(i32::from_le_bytes(*b"vapi"));
131            }
132        }
133    }
134
135    /// Returns `true` if a view-process exited because of [`assert_version`].
136    ///
137    /// [`assert_version`]: Self::assert_version
138    pub fn is_version_err(exit_code: Option<i32>, stderr: Option<&str>) -> bool {
139        exit_code.map(|e| e == i32::from_le_bytes(*b"vapi")).unwrap_or(false)
140            || stderr.map(|s| s.contains("view API version is not equal")).unwrap_or(false)
141    }
142}
143
144enum SameProcess {
145    Not,
146    Awaiting,
147    Ready(ViewConfig),
148    Done,
149}
150
151// because some view libs are dynamically loaded this variable needs to be patchable.
152//
153// This follows the same idea as the "hot-reload" patches, just manually implemented.
154static mut SAME_PROCESS: &Mutex<SameProcess> = &SAME_PROCESS_COLD;
155static SAME_PROCESS_COLD: Mutex<SameProcess> = Mutex::new(SameProcess::Not);
156
157fn same_process() -> &'static Mutex<SameProcess> {
158    // SAFETY: this is safe because SAME_PROCESS is only mutated on dynamic lib init, before any other code.
159    unsafe { *std::ptr::addr_of!(SAME_PROCESS) }
160}
161
162/// Dynamic view-process implementations must patch the static variables used by
163/// the view-api. This patch also propagates the tracing and log contexts.
164pub struct StaticPatch {
165    same_process: *const Mutex<SameProcess>,
166    tracing: tracing_shared::SharedLogger,
167}
168impl StaticPatch {
169    /// Called in the main executable.
170    pub fn capture() -> Self {
171        Self {
172            same_process: same_process(),
173            tracing: tracing_shared::SharedLogger::new(),
174        }
175    }
176
177    /// Called in the dynamic library.
178    ///
179    /// # Safety
180    ///
181    /// Only safe if it is the first view-process code to run in the dynamic library.
182    pub unsafe fn install(&self) {
183        // SAFETY: safety handled by the caller
184        unsafe {
185            *std::ptr::addr_of_mut!(SAME_PROCESS) = &*self.same_process;
186        }
187        self.tracing.install();
188    }
189}