mod.rs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997
  1. // Copyright 2019-2024 Tauri Programme within The Commons Conservancy
  2. // SPDX-License-Identifier: Apache-2.0
  3. // SPDX-License-Identifier: MIT
  4. use std::{
  5. borrow::Cow,
  6. collections::HashMap,
  7. fmt,
  8. net::Ipv4Addr,
  9. sync::{Arc, Mutex, MutexGuard},
  10. };
  11. use serde::Serialize;
  12. use url::Url;
  13. use tauri_macros::default_runtime;
  14. use tauri_utils::{
  15. assets::{AssetKey, CspHash},
  16. config::{Csp, CspDirectiveSources},
  17. html::{SCRIPT_NONCE_TOKEN, STYLE_NONCE_TOKEN},
  18. };
  19. use crate::{
  20. app::{AppHandle, GlobalWebviewEventListener, GlobalWindowEventListener, OnPageLoad},
  21. event::{assert_event_name_is_valid, Event, EventId, EventTarget, Listeners},
  22. ipc::{Invoke, InvokeHandler, RuntimeAuthority},
  23. plugin::PluginStore,
  24. utils::{config::Config, PackageInfo},
  25. Assets, Context, Pattern, Runtime, StateManager, Window,
  26. };
  27. use crate::{event::EmitArgs, resources::ResourceTable, Webview};
  28. #[cfg(desktop)]
  29. mod menu;
  30. #[cfg(all(desktop, feature = "tray-icon"))]
  31. mod tray;
  32. pub mod webview;
  33. pub mod window;
  34. #[derive(Default)]
  35. /// Spaced and quoted Content-Security-Policy hash values.
  36. struct CspHashStrings {
  37. script: Vec<String>,
  38. style: Vec<String>,
  39. }
  40. /// Sets the CSP value to the asset HTML if needed (on Linux).
  41. /// Returns the CSP string for access on the response header (on Windows and macOS).
  42. #[allow(clippy::borrowed_box)]
  43. pub(crate) fn set_csp<R: Runtime>(
  44. asset: &mut String,
  45. assets: &impl std::borrow::Borrow<dyn Assets<R>>,
  46. asset_path: &AssetKey,
  47. manager: &AppManager<R>,
  48. csp: Csp,
  49. ) -> HashMap<String, CspDirectiveSources> {
  50. let mut csp = csp.into();
  51. let hash_strings =
  52. assets
  53. .borrow()
  54. .csp_hashes(asset_path)
  55. .fold(CspHashStrings::default(), |mut acc, hash| {
  56. match hash {
  57. CspHash::Script(hash) => {
  58. acc.script.push(hash.into());
  59. }
  60. CspHash::Style(hash) => {
  61. acc.style.push(hash.into());
  62. }
  63. _csp_hash => {
  64. log::debug!("Unknown CspHash variant encountered: {:?}", _csp_hash);
  65. }
  66. }
  67. acc
  68. });
  69. let dangerous_disable_asset_csp_modification = &manager
  70. .config()
  71. .app
  72. .security
  73. .dangerous_disable_asset_csp_modification;
  74. if dangerous_disable_asset_csp_modification.can_modify("script-src") {
  75. replace_csp_nonce(
  76. asset,
  77. SCRIPT_NONCE_TOKEN,
  78. &mut csp,
  79. "script-src",
  80. hash_strings.script,
  81. );
  82. }
  83. if dangerous_disable_asset_csp_modification.can_modify("style-src") {
  84. replace_csp_nonce(
  85. asset,
  86. STYLE_NONCE_TOKEN,
  87. &mut csp,
  88. "style-src",
  89. hash_strings.style,
  90. );
  91. }
  92. csp
  93. }
  94. // inspired by <https://github.com/rust-lang/rust/blob/1be5c8f90912c446ecbdc405cbc4a89f9acd20fd/library/alloc/src/str.rs#L260-L297>
  95. fn replace_with_callback<F: FnMut() -> String>(
  96. original: &str,
  97. pattern: &str,
  98. mut replacement: F,
  99. ) -> String {
  100. let mut result = String::new();
  101. let mut last_end = 0;
  102. for (start, part) in original.match_indices(pattern) {
  103. result.push_str(unsafe { original.get_unchecked(last_end..start) });
  104. result.push_str(&replacement());
  105. last_end = start + part.len();
  106. }
  107. result.push_str(unsafe { original.get_unchecked(last_end..original.len()) });
  108. result
  109. }
  110. fn replace_csp_nonce(
  111. asset: &mut String,
  112. token: &str,
  113. csp: &mut HashMap<String, CspDirectiveSources>,
  114. directive: &str,
  115. hashes: Vec<String>,
  116. ) {
  117. let mut nonces = Vec::new();
  118. *asset = replace_with_callback(asset, token, || {
  119. #[cfg(target_pointer_width = "64")]
  120. let mut raw = [0u8; 8];
  121. #[cfg(target_pointer_width = "32")]
  122. let mut raw = [0u8; 4];
  123. #[cfg(target_pointer_width = "16")]
  124. let mut raw = [0u8; 2];
  125. getrandom::getrandom(&mut raw).expect("failed to get random bytes");
  126. let nonce = usize::from_ne_bytes(raw);
  127. nonces.push(nonce);
  128. nonce.to_string()
  129. });
  130. if !(nonces.is_empty() && hashes.is_empty()) {
  131. let nonce_sources = nonces
  132. .into_iter()
  133. .map(|n| format!("'nonce-{n}'"))
  134. .collect::<Vec<String>>();
  135. let sources = csp.entry(directive.into()).or_default();
  136. let self_source = "'self'".to_string();
  137. if !sources.contains(&self_source) {
  138. sources.push(self_source);
  139. }
  140. sources.extend(nonce_sources);
  141. sources.extend(hashes);
  142. }
  143. }
  144. /// A resolved asset.
  145. #[non_exhaustive]
  146. pub struct Asset {
  147. /// The asset bytes.
  148. pub bytes: Vec<u8>,
  149. /// The asset's mime type.
  150. pub mime_type: String,
  151. /// The `Content-Security-Policy` header value.
  152. pub csp_header: Option<String>,
  153. }
  154. impl Asset {
  155. /// The asset bytes.
  156. pub fn bytes(&self) -> &[u8] {
  157. &self.bytes
  158. }
  159. /// The asset's mime type.
  160. pub fn mime_type(&self) -> &str {
  161. &self.mime_type
  162. }
  163. /// The `Content-Security-Policy` header value.
  164. pub fn csp_header(&self) -> Option<&str> {
  165. self.csp_header.as_deref()
  166. }
  167. }
  168. #[default_runtime(crate::Wry, wry)]
  169. pub struct AppManager<R: Runtime> {
  170. pub runtime_authority: Mutex<RuntimeAuthority>,
  171. pub window: window::WindowManager<R>,
  172. pub webview: webview::WebviewManager<R>,
  173. #[cfg(all(desktop, feature = "tray-icon"))]
  174. pub tray: tray::TrayManager<R>,
  175. #[cfg(desktop)]
  176. pub menu: menu::MenuManager<R>,
  177. pub(crate) plugins: Mutex<PluginStore<R>>,
  178. pub listeners: Listeners,
  179. pub state: Arc<StateManager>,
  180. pub config: Config,
  181. #[cfg(dev)]
  182. pub config_parent: Option<std::path::PathBuf>,
  183. pub assets: Box<dyn Assets<R>>,
  184. pub app_icon: Option<Vec<u8>>,
  185. pub package_info: PackageInfo,
  186. /// Application pattern.
  187. pub pattern: Arc<Pattern>,
  188. /// Global API scripts collected from plugins.
  189. pub plugin_global_api_scripts: Arc<Option<&'static [&'static str]>>,
  190. /// Application Resources Table
  191. pub(crate) resources_table: Arc<Mutex<ResourceTable>>,
  192. /// Runtime-generated invoke key.
  193. pub(crate) invoke_key: String,
  194. }
  195. impl<R: Runtime> fmt::Debug for AppManager<R> {
  196. fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
  197. let mut d = f.debug_struct("AppManager");
  198. d.field("window", &self.window)
  199. .field("plugins", &self.plugins)
  200. .field("state", &self.state)
  201. .field("config", &self.config)
  202. .field("app_icon", &self.app_icon)
  203. .field("package_info", &self.package_info)
  204. .field("pattern", &self.pattern);
  205. #[cfg(all(desktop, feature = "tray-icon"))]
  206. {
  207. d.field("tray", &self.tray);
  208. }
  209. d.finish()
  210. }
  211. }
  212. impl<R: Runtime> AppManager<R> {
  213. #[allow(clippy::too_many_arguments, clippy::type_complexity)]
  214. pub(crate) fn with_handlers(
  215. #[allow(unused_mut)] mut context: Context<R>,
  216. plugins: PluginStore<R>,
  217. invoke_handler: Box<InvokeHandler<R>>,
  218. on_page_load: Option<Arc<OnPageLoad<R>>>,
  219. uri_scheme_protocols: HashMap<String, Arc<webview::UriSchemeProtocol<R>>>,
  220. state: StateManager,
  221. window_event_listeners: Vec<GlobalWindowEventListener<R>>,
  222. webiew_event_listeners: Vec<GlobalWebviewEventListener<R>>,
  223. #[cfg(desktop)] window_menu_event_listeners: HashMap<
  224. String,
  225. crate::app::GlobalMenuEventListener<Window<R>>,
  226. >,
  227. invoke_initialization_script: String,
  228. invoke_key: String,
  229. ) -> Self {
  230. // generate a random isolation key at runtime
  231. #[cfg(feature = "isolation")]
  232. if let Pattern::Isolation { ref mut key, .. } = &mut context.pattern {
  233. *key = uuid::Uuid::new_v4().to_string();
  234. }
  235. Self {
  236. runtime_authority: Mutex::new(context.runtime_authority),
  237. window: window::WindowManager {
  238. windows: Mutex::default(),
  239. default_icon: context.default_window_icon,
  240. event_listeners: Arc::new(window_event_listeners),
  241. },
  242. webview: webview::WebviewManager {
  243. webviews: Mutex::default(),
  244. invoke_handler,
  245. on_page_load,
  246. uri_scheme_protocols: Mutex::new(uri_scheme_protocols),
  247. event_listeners: Arc::new(webiew_event_listeners),
  248. invoke_initialization_script,
  249. invoke_key: invoke_key.clone(),
  250. },
  251. #[cfg(all(desktop, feature = "tray-icon"))]
  252. tray: tray::TrayManager {
  253. icon: context.tray_icon,
  254. icons: Default::default(),
  255. global_event_listeners: Default::default(),
  256. event_listeners: Default::default(),
  257. },
  258. #[cfg(desktop)]
  259. menu: menu::MenuManager {
  260. menus: Default::default(),
  261. menu: Default::default(),
  262. global_event_listeners: Default::default(),
  263. event_listeners: Mutex::new(window_menu_event_listeners),
  264. },
  265. plugins: Mutex::new(plugins),
  266. listeners: Listeners::default(),
  267. state: Arc::new(state),
  268. config: context.config,
  269. #[cfg(dev)]
  270. config_parent: context.config_parent,
  271. assets: context.assets,
  272. app_icon: context.app_icon,
  273. package_info: context.package_info,
  274. pattern: Arc::new(context.pattern),
  275. plugin_global_api_scripts: Arc::new(context.plugin_global_api_scripts),
  276. resources_table: Arc::default(),
  277. invoke_key,
  278. }
  279. }
  280. /// State managed by the application.
  281. pub(crate) fn state(&self) -> Arc<StateManager> {
  282. self.state.clone()
  283. }
  284. /// Get the base path to serve data from.
  285. ///
  286. /// * In dev mode, this will be based on the `devUrl` configuration value.
  287. /// * Otherwise, this will be based on the `frontendDist` configuration value.
  288. #[cfg(not(dev))]
  289. fn base_path(&self) -> Option<&Url> {
  290. use crate::utils::config::FrontendDist;
  291. match self.config.build.frontend_dist.as_ref() {
  292. Some(FrontendDist::Url(url)) => Some(url),
  293. _ => None,
  294. }
  295. }
  296. #[cfg(dev)]
  297. fn base_path(&self) -> Option<&Url> {
  298. self.config.build.dev_url.as_ref()
  299. }
  300. pub(crate) fn proxy_dev_server_url(&self) -> Option<Url> {
  301. let proxy = cfg!(mobile)
  302. && cfg!(dev)
  303. && self.config.build.dev_url.as_ref().map_or(false, |url| {
  304. url.host().map_or(false, |host| match host {
  305. url::Host::Domain(d) => d != "localhost",
  306. url::Host::Ipv4(ip) => ip != Ipv4Addr::LOCALHOST,
  307. _ => false,
  308. })
  309. });
  310. proxy.then(|| self.get_url().into_owned())
  311. }
  312. pub(crate) fn protocol_url(&self) -> Cow<'_, Url> {
  313. if cfg!(windows) || cfg!(target_os = "android") {
  314. Cow::Owned(Url::parse("http://tauri.localhost").unwrap())
  315. } else {
  316. Cow::Owned(Url::parse("tauri://localhost").unwrap())
  317. }
  318. }
  319. /// Get the base URL to use for webview requests.
  320. ///
  321. /// In dev mode, this will be based on the `devUrl` configuration value.
  322. pub(crate) fn get_url(&self) -> Cow<'_, Url> {
  323. match self.base_path() {
  324. Some(url) => Cow::Borrowed(url),
  325. _ => self.protocol_url(),
  326. }
  327. }
  328. fn csp(&self) -> Option<Csp> {
  329. if !crate::is_dev() {
  330. self.config.app.security.csp.clone()
  331. } else {
  332. self
  333. .config
  334. .app
  335. .security
  336. .dev_csp
  337. .clone()
  338. .or_else(|| self.config.app.security.csp.clone())
  339. }
  340. }
  341. pub fn get_asset(&self, mut path: String) -> Result<Asset, Box<dyn std::error::Error>> {
  342. let assets = &self.assets;
  343. if path.ends_with('/') {
  344. path.pop();
  345. }
  346. path = percent_encoding::percent_decode(path.as_bytes())
  347. .decode_utf8_lossy()
  348. .to_string();
  349. let path = if path.is_empty() {
  350. // if the url is `tauri://localhost`, we should load `index.html`
  351. "index.html".to_string()
  352. } else {
  353. // skip leading `/`
  354. path.chars().skip(1).collect::<String>()
  355. };
  356. let mut asset_path = AssetKey::from(path.as_str());
  357. let asset_response = assets
  358. .get(&path.as_str().into())
  359. .or_else(|| {
  360. log::debug!("Asset `{path}` not found; fallback to {path}.html");
  361. let fallback = format!("{}.html", path.as_str()).into();
  362. let asset = assets.get(&fallback);
  363. asset_path = fallback;
  364. asset
  365. })
  366. .or_else(|| {
  367. log::debug!(
  368. "Asset `{}` not found; fallback to {}/index.html",
  369. path,
  370. path
  371. );
  372. let fallback = format!("{}/index.html", path.as_str()).into();
  373. let asset = assets.get(&fallback);
  374. asset_path = fallback;
  375. asset
  376. })
  377. .or_else(|| {
  378. log::debug!("Asset `{}` not found; fallback to index.html", path);
  379. let fallback = AssetKey::from("index.html");
  380. let asset = assets.get(&fallback);
  381. asset_path = fallback;
  382. asset
  383. })
  384. .ok_or_else(|| crate::Error::AssetNotFound(path.clone()))
  385. .map(Cow::into_owned);
  386. let mut csp_header = None;
  387. let is_html = asset_path.as_ref().ends_with(".html");
  388. match asset_response {
  389. Ok(asset) => {
  390. let final_data = if is_html {
  391. let mut asset = String::from_utf8_lossy(&asset).into_owned();
  392. if let Some(csp) = self.csp() {
  393. #[allow(unused_mut)]
  394. let mut csp_map = set_csp(&mut asset, &self.assets, &asset_path, self, csp);
  395. #[cfg(feature = "isolation")]
  396. if let Pattern::Isolation { schema, .. } = &*self.pattern {
  397. let default_src = csp_map
  398. .entry("default-src".into())
  399. .or_insert_with(Default::default);
  400. default_src.push(crate::pattern::format_real_schema(schema));
  401. }
  402. csp_header.replace(Csp::DirectiveMap(csp_map).to_string());
  403. }
  404. asset.as_bytes().to_vec()
  405. } else {
  406. asset
  407. };
  408. let mime_type = tauri_utils::mime_type::MimeType::parse(&final_data, &path);
  409. Ok(Asset {
  410. bytes: final_data.to_vec(),
  411. mime_type,
  412. csp_header,
  413. })
  414. }
  415. Err(e) => {
  416. log::error!("{:?}", e);
  417. Err(Box::new(e))
  418. }
  419. }
  420. }
  421. pub(crate) fn listeners(&self) -> &Listeners {
  422. &self.listeners
  423. }
  424. pub fn run_invoke_handler(&self, invoke: Invoke<R>) -> bool {
  425. (self.webview.invoke_handler)(invoke)
  426. }
  427. pub fn extend_api(&self, plugin: &str, invoke: Invoke<R>) -> bool {
  428. self
  429. .plugins
  430. .lock()
  431. .expect("poisoned plugin store")
  432. .extend_api(plugin, invoke)
  433. }
  434. pub fn initialize_plugins(&self, app: &AppHandle<R>) -> crate::Result<()> {
  435. self
  436. .plugins
  437. .lock()
  438. .expect("poisoned plugin store")
  439. .initialize_all(app, &self.config.plugins)
  440. }
  441. pub fn config(&self) -> &Config {
  442. &self.config
  443. }
  444. #[cfg(dev)]
  445. pub fn config_parent(&self) -> Option<&std::path::PathBuf> {
  446. self.config_parent.as_ref()
  447. }
  448. pub fn package_info(&self) -> &PackageInfo {
  449. &self.package_info
  450. }
  451. pub fn listen<F: Fn(Event) + Send + 'static>(
  452. &self,
  453. event: String,
  454. target: EventTarget,
  455. handler: F,
  456. ) -> EventId {
  457. assert_event_name_is_valid(&event);
  458. self.listeners().listen(event, target, handler)
  459. }
  460. pub fn once<F: FnOnce(Event) + Send + 'static>(
  461. &self,
  462. event: String,
  463. target: EventTarget,
  464. handler: F,
  465. ) -> EventId {
  466. assert_event_name_is_valid(&event);
  467. self.listeners().once(event, target, handler)
  468. }
  469. pub fn unlisten(&self, id: EventId) {
  470. self.listeners().unlisten(id)
  471. }
  472. #[cfg_attr(
  473. feature = "tracing",
  474. tracing::instrument("app::emit", skip(self, payload))
  475. )]
  476. pub fn emit<S: Serialize + Clone>(&self, event: &str, payload: S) -> crate::Result<()> {
  477. assert_event_name_is_valid(event);
  478. #[cfg(feature = "tracing")]
  479. let _span = tracing::debug_span!("emit::run").entered();
  480. let emit_args = EmitArgs::new(event, payload)?;
  481. let listeners = self.listeners();
  482. listeners.emit_js(self.webview.webviews_lock().values(), event, &emit_args)?;
  483. listeners.emit(emit_args)?;
  484. Ok(())
  485. }
  486. #[cfg_attr(
  487. feature = "tracing",
  488. tracing::instrument("app::emit::filter", skip(self, payload, filter))
  489. )]
  490. pub fn emit_filter<S, F>(&self, event: &str, payload: S, filter: F) -> crate::Result<()>
  491. where
  492. S: Serialize + Clone,
  493. F: Fn(&EventTarget) -> bool,
  494. {
  495. assert_event_name_is_valid(event);
  496. #[cfg(feature = "tracing")]
  497. let _span = tracing::debug_span!("emit::run").entered();
  498. let emit_args = EmitArgs::new(event, payload)?;
  499. let listeners = self.listeners();
  500. listeners.emit_js_filter(
  501. self.webview.webviews_lock().values(),
  502. event,
  503. &emit_args,
  504. Some(&filter),
  505. )?;
  506. listeners.emit_filter(emit_args, Some(filter))?;
  507. Ok(())
  508. }
  509. #[cfg_attr(
  510. feature = "tracing",
  511. tracing::instrument("app::emit::to", skip(self, target, payload), fields(target))
  512. )]
  513. pub fn emit_to<I, S>(&self, target: I, event: &str, payload: S) -> crate::Result<()>
  514. where
  515. I: Into<EventTarget>,
  516. S: Serialize + Clone,
  517. {
  518. let target = target.into();
  519. #[cfg(feature = "tracing")]
  520. tracing::Span::current().record("target", format!("{target:?}"));
  521. match target {
  522. // if targeting all, emit to all using emit without filter
  523. EventTarget::Any => self.emit(event, payload),
  524. // if targeting any label, emit using emit_filter and filter labels
  525. EventTarget::AnyLabel {
  526. label: target_label,
  527. } => self.emit_filter(event, payload, |t| match t {
  528. EventTarget::Window { label }
  529. | EventTarget::Webview { label }
  530. | EventTarget::WebviewWindow { label } => label == &target_label,
  531. _ => false,
  532. }),
  533. // otherwise match same target
  534. _ => self.emit_filter(event, payload, |t| t == &target),
  535. }
  536. }
  537. pub fn get_window(&self, label: &str) -> Option<Window<R>> {
  538. self.window.windows_lock().get(label).cloned()
  539. }
  540. pub fn get_focused_window(&self) -> Option<Window<R>> {
  541. self
  542. .window
  543. .windows_lock()
  544. .iter()
  545. .find(|w| w.1.is_focused().unwrap_or(false))
  546. .map(|w| w.1.clone())
  547. }
  548. pub(crate) fn on_window_close(&self, label: &str) {
  549. let window = self.window.windows_lock().remove(label);
  550. if let Some(window) = window {
  551. for webview in window.webviews() {
  552. self.webview.webviews_lock().remove(webview.label());
  553. }
  554. }
  555. }
  556. #[cfg(desktop)]
  557. pub(crate) fn on_webview_close(&self, label: &str) {
  558. self.webview.webviews_lock().remove(label);
  559. if let Ok(webview_labels_array) = serde_json::to_string(&self.webview.labels()) {
  560. let _ = self.webview.eval_script_all(format!(
  561. r#"(function () {{ const metadata = window.__TAURI_INTERNALS__.metadata; if (metadata != null) {{ metadata.webviews = {webview_labels_array}.map(function (label) {{ return {{ label: label }} }}) }} }})()"#,
  562. ));
  563. }
  564. }
  565. pub fn windows(&self) -> HashMap<String, Window<R>> {
  566. self.window.windows_lock().clone()
  567. }
  568. pub fn get_webview(&self, label: &str) -> Option<Webview<R>> {
  569. self.webview.webviews_lock().get(label).cloned()
  570. }
  571. pub fn webviews(&self) -> HashMap<String, Webview<R>> {
  572. self.webview.webviews_lock().clone()
  573. }
  574. pub(crate) fn resources_table(&self) -> MutexGuard<'_, ResourceTable> {
  575. self
  576. .resources_table
  577. .lock()
  578. .expect("poisoned window manager")
  579. }
  580. pub(crate) fn invoke_key(&self) -> &str {
  581. &self.invoke_key
  582. }
  583. }
  584. #[cfg(desktop)]
  585. impl<R: Runtime> AppManager<R> {
  586. pub fn remove_menu_from_stash_by_id(&self, id: Option<&crate::menu::MenuId>) {
  587. if let Some(id) = id {
  588. let is_used_by_a_window = self
  589. .window
  590. .windows_lock()
  591. .values()
  592. .any(|w| w.is_menu_in_use(id));
  593. if !(self.menu.is_menu_in_use(id) || is_used_by_a_window) {
  594. self.menu.menus_stash_lock().remove(id);
  595. }
  596. }
  597. }
  598. }
  599. #[cfg(test)]
  600. mod tests {
  601. use super::replace_with_callback;
  602. #[test]
  603. fn string_replace_with_callback() {
  604. let mut tauri_index = 0;
  605. #[allow(clippy::single_element_loop)]
  606. for (src, pattern, replacement, result) in [(
  607. "tauri is awesome, tauri is amazing",
  608. "tauri",
  609. || {
  610. tauri_index += 1;
  611. tauri_index.to_string()
  612. },
  613. "1 is awesome, 2 is amazing",
  614. )] {
  615. assert_eq!(replace_with_callback(src, pattern, replacement), result);
  616. }
  617. }
  618. }
  619. #[cfg(test)]
  620. mod test {
  621. use std::{
  622. sync::mpsc::{channel, Receiver, Sender},
  623. time::Duration,
  624. };
  625. use crate::{
  626. event::EventTarget,
  627. generate_context,
  628. plugin::PluginStore,
  629. test::{mock_app, MockRuntime},
  630. webview::WebviewBuilder,
  631. window::WindowBuilder,
  632. App, Emitter, Listener, Manager, StateManager, Webview, WebviewWindow, WebviewWindowBuilder,
  633. Window, Wry,
  634. };
  635. use super::AppManager;
  636. const APP_LISTEN_ID: &str = "App::listen";
  637. const APP_LISTEN_ANY_ID: &str = "App::listen_any";
  638. const WINDOW_LISTEN_ID: &str = "Window::listen";
  639. const WINDOW_LISTEN_ANY_ID: &str = "Window::listen_any";
  640. const WEBVIEW_LISTEN_ID: &str = "Webview::listen";
  641. const WEBVIEW_LISTEN_ANY_ID: &str = "Webview::listen_any";
  642. const WEBVIEW_WINDOW_LISTEN_ID: &str = "WebviewWindow::listen";
  643. const WEBVIEW_WINDOW_LISTEN_ANY_ID: &str = "WebviewWindow::listen_any";
  644. const TEST_EVENT_NAME: &str = "event";
  645. #[test]
  646. fn check_get_url() {
  647. let context = generate_context!("test/fixture/src-tauri/tauri.conf.json", crate, test = true);
  648. let manager: AppManager<Wry> = AppManager::with_handlers(
  649. context,
  650. PluginStore::default(),
  651. Box::new(|_| false),
  652. None,
  653. Default::default(),
  654. StateManager::new(),
  655. Default::default(),
  656. Default::default(),
  657. Default::default(),
  658. "".into(),
  659. crate::generate_invoke_key().unwrap(),
  660. );
  661. #[cfg(custom_protocol)]
  662. {
  663. assert_eq!(
  664. manager.get_url().to_string(),
  665. if cfg!(windows) || cfg!(target_os = "android") {
  666. "http://tauri.localhost/"
  667. } else {
  668. "tauri://localhost"
  669. }
  670. );
  671. }
  672. #[cfg(dev)]
  673. assert_eq!(manager.get_url().to_string(), "http://localhost:4000/");
  674. }
  675. struct EventSetup {
  676. app: App<MockRuntime>,
  677. window: Window<MockRuntime>,
  678. webview: Webview<MockRuntime>,
  679. webview_window: WebviewWindow<MockRuntime>,
  680. tx: Sender<(&'static str, String)>,
  681. rx: Receiver<(&'static str, String)>,
  682. }
  683. fn setup_events(setup_any: bool) -> EventSetup {
  684. let app = mock_app();
  685. let window = WindowBuilder::new(&app, "main-window").build().unwrap();
  686. let webview = window
  687. .add_child(
  688. WebviewBuilder::new("main-webview", Default::default()),
  689. crate::LogicalPosition::new(0, 0),
  690. window.inner_size().unwrap(),
  691. )
  692. .unwrap();
  693. let webview_window = WebviewWindowBuilder::new(&app, "main-webview-window", Default::default())
  694. .build()
  695. .unwrap();
  696. let (tx, rx) = channel();
  697. macro_rules! setup_listener {
  698. ($type:ident, $id:ident, $any_id:ident) => {
  699. let tx_ = tx.clone();
  700. $type.listen(TEST_EVENT_NAME, move |evt| {
  701. tx_
  702. .send(($id, serde_json::from_str::<String>(evt.payload()).unwrap()))
  703. .unwrap();
  704. });
  705. if setup_any {
  706. let tx_ = tx.clone();
  707. $type.listen_any(TEST_EVENT_NAME, move |evt| {
  708. tx_
  709. .send((
  710. $any_id,
  711. serde_json::from_str::<String>(evt.payload()).unwrap(),
  712. ))
  713. .unwrap();
  714. });
  715. }
  716. };
  717. }
  718. setup_listener!(app, APP_LISTEN_ID, APP_LISTEN_ANY_ID);
  719. setup_listener!(window, WINDOW_LISTEN_ID, WINDOW_LISTEN_ANY_ID);
  720. setup_listener!(webview, WEBVIEW_LISTEN_ID, WEBVIEW_LISTEN_ANY_ID);
  721. setup_listener!(
  722. webview_window,
  723. WEBVIEW_WINDOW_LISTEN_ID,
  724. WEBVIEW_WINDOW_LISTEN_ANY_ID
  725. );
  726. EventSetup {
  727. app,
  728. window,
  729. webview,
  730. webview_window,
  731. tx,
  732. rx,
  733. }
  734. }
  735. fn assert_events(kind: &str, received: &[&str], expected: &[&str]) {
  736. for e in expected {
  737. assert!(received.contains(e), "{e} did not receive `{kind}` event");
  738. }
  739. assert_eq!(
  740. received.len(),
  741. expected.len(),
  742. "received {received:?} `{kind}` events but expected {expected:?}"
  743. );
  744. }
  745. #[test]
  746. fn emit() {
  747. let EventSetup {
  748. app,
  749. window,
  750. webview,
  751. webview_window,
  752. tx: _,
  753. rx,
  754. } = setup_events(true);
  755. run_emit_test("emit (app)", app, &rx);
  756. run_emit_test("emit (window)", window, &rx);
  757. run_emit_test("emit (webview)", webview, &rx);
  758. run_emit_test("emit (webview_window)", webview_window, &rx);
  759. }
  760. fn run_emit_test<M: Manager<MockRuntime> + Emitter<MockRuntime>>(
  761. kind: &str,
  762. m: M,
  763. rx: &Receiver<(&str, String)>,
  764. ) {
  765. let mut received = Vec::new();
  766. let payload = "global-payload";
  767. m.emit(TEST_EVENT_NAME, payload).unwrap();
  768. while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
  769. assert_eq!(p, payload);
  770. received.push(source);
  771. }
  772. assert_events(
  773. kind,
  774. &received,
  775. &[
  776. APP_LISTEN_ID,
  777. APP_LISTEN_ANY_ID,
  778. WINDOW_LISTEN_ID,
  779. WINDOW_LISTEN_ANY_ID,
  780. WEBVIEW_LISTEN_ID,
  781. WEBVIEW_LISTEN_ANY_ID,
  782. WEBVIEW_WINDOW_LISTEN_ID,
  783. WEBVIEW_WINDOW_LISTEN_ANY_ID,
  784. ],
  785. );
  786. }
  787. #[test]
  788. fn emit_to() {
  789. let EventSetup {
  790. app,
  791. window,
  792. webview,
  793. webview_window,
  794. tx,
  795. rx,
  796. } = setup_events(false);
  797. run_emit_to_test(
  798. "emit_to (App)",
  799. &app,
  800. &window,
  801. &webview,
  802. &webview_window,
  803. tx.clone(),
  804. &rx,
  805. );
  806. run_emit_to_test(
  807. "emit_to (window)",
  808. &window,
  809. &window,
  810. &webview,
  811. &webview_window,
  812. tx.clone(),
  813. &rx,
  814. );
  815. run_emit_to_test(
  816. "emit_to (webview)",
  817. &webview,
  818. &window,
  819. &webview,
  820. &webview_window,
  821. tx.clone(),
  822. &rx,
  823. );
  824. run_emit_to_test(
  825. "emit_to (webview_window)",
  826. &webview_window,
  827. &window,
  828. &webview,
  829. &webview_window,
  830. tx.clone(),
  831. &rx,
  832. );
  833. }
  834. fn run_emit_to_test<M: Manager<MockRuntime> + Emitter<MockRuntime>>(
  835. kind: &str,
  836. m: &M,
  837. window: &Window<MockRuntime>,
  838. webview: &Webview<MockRuntime>,
  839. webview_window: &WebviewWindow<MockRuntime>,
  840. tx: Sender<(&'static str, String)>,
  841. rx: &Receiver<(&'static str, String)>,
  842. ) {
  843. let mut received = Vec::new();
  844. let payload = "global-payload";
  845. macro_rules! test_target {
  846. ($target:expr, $id:ident) => {
  847. m.emit_to($target, TEST_EVENT_NAME, payload).unwrap();
  848. while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
  849. assert_eq!(p, payload);
  850. received.push(source);
  851. }
  852. assert_events(kind, &received, &[$id]);
  853. received.clear();
  854. };
  855. }
  856. test_target!(EventTarget::App, APP_LISTEN_ID);
  857. test_target!(window.label(), WINDOW_LISTEN_ID);
  858. test_target!(webview.label(), WEBVIEW_LISTEN_ID);
  859. test_target!(webview_window.label(), WEBVIEW_WINDOW_LISTEN_ID);
  860. let other_webview_listen_id = "OtherWebview::listen";
  861. let other_webview = WebviewWindowBuilder::new(
  862. window,
  863. kind.replace(['(', ')', ' '], ""),
  864. Default::default(),
  865. )
  866. .build()
  867. .unwrap();
  868. other_webview.listen(TEST_EVENT_NAME, move |evt| {
  869. tx.send((
  870. other_webview_listen_id,
  871. serde_json::from_str::<String>(evt.payload()).unwrap(),
  872. ))
  873. .unwrap();
  874. });
  875. m.emit_to(other_webview.label(), TEST_EVENT_NAME, payload)
  876. .unwrap();
  877. while let Ok((source, p)) = rx.recv_timeout(Duration::from_secs(1)) {
  878. assert_eq!(p, payload);
  879. received.push(source);
  880. }
  881. assert_events("emit_to", &received, &[other_webview_listen_id]);
  882. }
  883. }