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