utils.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. use crate::{
  2. api::{
  3. assets::Assets,
  4. config::WindowUrl,
  5. rpc::{format_callback, format_callback_result},
  6. },
  7. app::{Icon, InvokeResponse},
  8. ApplicationExt, WebviewBuilderExt,
  9. };
  10. use super::{
  11. webview::{CustomProtocol, WebviewBuilderExtPrivate, WebviewRpcHandler},
  12. App, Context, PageLoadPayload, RpcRequest, Webview, WebviewManager,
  13. };
  14. use serde::Deserialize;
  15. use serde_json::Value as JsonValue;
  16. use std::{borrow::Cow, sync::Arc};
  17. #[derive(Debug, Deserialize)]
  18. struct Message {
  19. #[serde(rename = "__tauriModule")]
  20. tauri_module: Option<String>,
  21. callback: String,
  22. error: String,
  23. #[serde(rename = "mainThread", default)]
  24. main_thread: bool,
  25. #[serde(flatten)]
  26. inner: JsonValue,
  27. }
  28. // setup content for dev-server
  29. #[cfg(dev)]
  30. pub(super) fn get_url(context: &Context) -> String {
  31. let config = &context.config;
  32. if config.build.dev_path.starts_with("http") {
  33. config.build.dev_path.clone()
  34. } else {
  35. let path = "index.html";
  36. format!(
  37. "data:text/html;base64,{}",
  38. base64::encode(
  39. context
  40. .assets
  41. .get(&path)
  42. .ok_or_else(|| crate::Error::AssetNotFound(path.to_string()))
  43. .map(Cow::into_owned)
  44. .expect("Unable to find `index.html` under your devPath folder")
  45. )
  46. )
  47. }
  48. }
  49. #[cfg(custom_protocol)]
  50. pub(super) fn get_url(_: &Context) -> String {
  51. // Custom protocol doesn't require any setup, so just return URL
  52. "tauri://index.html".into()
  53. }
  54. // spawn an updater process.
  55. #[cfg(feature = "updater")]
  56. #[allow(dead_code)]
  57. pub(super) fn spawn_updater() {
  58. std::thread::spawn(|| {
  59. tauri_api::command::spawn_relative_command(
  60. "updater".to_string(),
  61. Vec::new(),
  62. std::process::Stdio::inherit(),
  63. )
  64. .expect("Unable to spawn relative command");
  65. });
  66. }
  67. pub(super) fn initialization_script(
  68. plugin_initialization_script: &str,
  69. tauri_script: &str,
  70. ) -> String {
  71. format!(
  72. r#"
  73. {tauri_initialization_script}
  74. {event_initialization_script}
  75. if (window.rpc) {{
  76. window.__TAURI__.invoke("__initialized", {{ url: window.location.href }})
  77. }} else {{
  78. window.addEventListener('DOMContentLoaded', function () {{
  79. window.__TAURI__.invoke("__initialized", {{ url: window.location.href }})
  80. }})
  81. }}
  82. {plugin_initialization_script}
  83. "#,
  84. tauri_initialization_script = tauri_script,
  85. event_initialization_script = event_initialization_script(),
  86. plugin_initialization_script = plugin_initialization_script
  87. )
  88. }
  89. fn event_initialization_script() -> String {
  90. return format!(
  91. "
  92. window['{queue}'] = [];
  93. window['{fn}'] = function (payload, salt, ignoreQueue) {{
  94. const listeners = (window['{listeners}'] && window['{listeners}'][payload.type]) || []
  95. if (!ignoreQueue && listeners.length === 0) {{
  96. window['{queue}'].push({{
  97. payload: payload,
  98. salt: salt
  99. }})
  100. }}
  101. if (listeners.length > 0) {{
  102. window.__TAURI__.invoke('tauri', {{
  103. __tauriModule: 'Internal',
  104. message: {{
  105. cmd: 'validateSalt',
  106. salt: salt
  107. }}
  108. }}).then(function (flag) {{
  109. if (flag) {{
  110. for (let i = listeners.length - 1; i >= 0; i--) {{
  111. const listener = listeners[i]
  112. if (listener.once)
  113. listeners.splice(i, 1)
  114. listener.handler(payload)
  115. }}
  116. }}
  117. }})
  118. }}
  119. }}
  120. ",
  121. fn = crate::event::emit_function_name(),
  122. queue = crate::event::event_queue_object_name(),
  123. listeners = crate::event::event_listeners_object_name()
  124. );
  125. }
  126. pub(super) type BuiltWebview<A> = (
  127. <A as ApplicationExt>::WebviewBuilder,
  128. Option<WebviewRpcHandler<<A as ApplicationExt>::Dispatcher>>,
  129. Option<CustomProtocol>,
  130. );
  131. // build the webview.
  132. pub(super) fn build_webview<A: ApplicationExt + 'static>(
  133. application: Arc<App<A>>,
  134. webview: Webview<A>,
  135. webview_manager: &WebviewManager<A>,
  136. content_url: &str,
  137. window_labels: &[String],
  138. plugin_initialization_script: &str,
  139. context: &Context,
  140. ) -> crate::Result<BuiltWebview<A>> {
  141. let webview_url = match &webview.url {
  142. WindowUrl::App => content_url.to_string(),
  143. WindowUrl::Custom(url) => url.to_string(),
  144. };
  145. let is_local = match webview.url {
  146. WindowUrl::App => true,
  147. WindowUrl::Custom(url) => &url[0..8] == "tauri://",
  148. };
  149. let (webview_builder, rpc_handler, custom_protocol) = if is_local {
  150. let mut webview_builder = webview.builder.url(webview_url)
  151. .initialization_script(&initialization_script(plugin_initialization_script, &context.tauri_script))
  152. .initialization_script(&format!(
  153. r#"
  154. window.__TAURI__.__windows = {window_labels_array}.map(function (label) {{ return {{ label: label }} }});
  155. window.__TAURI__.__currentWindow = {{ label: "{current_window_label}" }}
  156. "#,
  157. window_labels_array =
  158. serde_json::to_string(&window_labels).unwrap(),
  159. current_window_label = webview.label,
  160. ));
  161. if !webview_builder.has_icon() {
  162. if let Some(default_window_icon) = &context.default_window_icon {
  163. webview_builder = webview_builder.icon(Icon::Raw(default_window_icon.to_vec()))?;
  164. }
  165. }
  166. let webview_manager_ = webview_manager.clone();
  167. let rpc_handler: Box<dyn Fn(<A as ApplicationExt>::Dispatcher, RpcRequest) + Send> =
  168. Box::new(move |_, request: RpcRequest| {
  169. let command = request.command.clone();
  170. let arg = request
  171. .params
  172. .unwrap()
  173. .as_array_mut()
  174. .unwrap()
  175. .first_mut()
  176. .unwrap_or(&mut JsonValue::Null)
  177. .take();
  178. let webview_manager = webview_manager_.clone();
  179. match serde_json::from_value::<Message>(arg) {
  180. Ok(message) => {
  181. let application = application.clone();
  182. let callback = message.callback.to_string();
  183. let error = message.error.to_string();
  184. if message.main_thread {
  185. crate::async_runtime::block_on(async move {
  186. execute_promise(
  187. &webview_manager,
  188. on_message(
  189. application,
  190. webview_manager.clone(),
  191. command.clone(),
  192. message,
  193. ),
  194. callback,
  195. error,
  196. )
  197. .await;
  198. });
  199. } else {
  200. crate::async_runtime::spawn(async move {
  201. execute_promise(
  202. &webview_manager,
  203. on_message(application, webview_manager.clone(), command, message),
  204. callback,
  205. error,
  206. )
  207. .await;
  208. });
  209. }
  210. }
  211. Err(e) => {
  212. if let Ok(dispatcher) = webview_manager.current_webview() {
  213. let error: crate::Error = e.into();
  214. let _ = dispatcher.eval(&format!(
  215. r#"console.error({})"#,
  216. JsonValue::String(error.to_string())
  217. ));
  218. }
  219. }
  220. }
  221. });
  222. let assets = context.assets;
  223. let custom_protocol = CustomProtocol {
  224. name: "tauri".into(),
  225. handler: Box::new(move |path| {
  226. let mut path = path.to_string().replace("tauri://", "");
  227. if path.ends_with('/') {
  228. path.pop();
  229. }
  230. let path =
  231. if let Some((first, components)) = path.split('/').collect::<Vec<&str>>().split_first() {
  232. match components.len() {
  233. 0 => first.to_string(),
  234. _ => components.join("/"),
  235. }
  236. } else {
  237. path
  238. };
  239. let asset_response = assets
  240. .get(&path)
  241. .ok_or(crate::Error::AssetNotFound(path))
  242. .map(Cow::into_owned);
  243. match asset_response {
  244. Ok(asset) => Ok(asset),
  245. Err(e) => {
  246. #[cfg(debug_assertions)]
  247. eprintln!("{:?}", e); // TODO log::error!
  248. Err(e)
  249. }
  250. }
  251. }),
  252. };
  253. (webview_builder, Some(rpc_handler), Some(custom_protocol))
  254. } else {
  255. (webview.builder.url(webview_url), None, None)
  256. };
  257. Ok((webview_builder, rpc_handler, custom_protocol))
  258. }
  259. /// Asynchronously executes the given task
  260. /// and evaluates its Result to the JS promise described by the `success_callback` and `error_callback` function names.
  261. ///
  262. /// If the Result `is_ok()`, the callback will be the `success_callback` function name and the argument will be the Ok value.
  263. /// If the Result `is_err()`, the callback will be the `error_callback` function name and the argument will be the Err value.
  264. async fn execute_promise<
  265. A: ApplicationExt + 'static,
  266. F: std::future::Future<Output = crate::Result<InvokeResponse>> + Send + 'static,
  267. >(
  268. webview_manager: &crate::WebviewManager<A>,
  269. task: F,
  270. success_callback: String,
  271. error_callback: String,
  272. ) {
  273. let callback_string = match format_callback_result(
  274. task
  275. .await
  276. .and_then(|response| response.json)
  277. .map_err(|err| err.to_string()),
  278. success_callback,
  279. error_callback.clone(),
  280. ) {
  281. Ok(callback_string) => callback_string,
  282. Err(e) => format_callback(error_callback, e.to_string()),
  283. };
  284. if let Ok(dispatcher) = webview_manager.current_webview() {
  285. let _ = dispatcher.eval(callback_string.as_str());
  286. }
  287. }
  288. async fn on_message<A: ApplicationExt + 'static>(
  289. application: Arc<App<A>>,
  290. webview_manager: WebviewManager<A>,
  291. command: String,
  292. message: Message,
  293. ) -> crate::Result<InvokeResponse> {
  294. if &command == "__initialized" {
  295. let payload: PageLoadPayload = serde_json::from_value(message.inner)?;
  296. application
  297. .run_on_page_load(&webview_manager, payload.clone())
  298. .await;
  299. crate::plugin::on_page_load(A::plugin_store(), &webview_manager, payload).await;
  300. Ok(().into())
  301. } else if let Some(module) = &message.tauri_module {
  302. crate::endpoints::handle(
  303. &webview_manager,
  304. module.to_string(),
  305. message.inner,
  306. &application.context,
  307. )
  308. .await
  309. } else {
  310. let mut response = match application
  311. .run_invoke_handler(&webview_manager, command.clone(), &message.inner)
  312. .await
  313. {
  314. Ok(value) => {
  315. if let Some(value) = value {
  316. Ok(value)
  317. } else {
  318. Err(crate::Error::UnknownApi(None))
  319. }
  320. }
  321. Err(e) => Err(e),
  322. };
  323. if let Err(crate::Error::UnknownApi(_)) = response {
  324. match crate::plugin::extend_api(A::plugin_store(), &webview_manager, command, &message.inner)
  325. .await
  326. {
  327. Ok(value) => {
  328. // If value is None, that means that no plugin matched the command
  329. // and the UnknownApi error should be sent to the webview
  330. // Otherwise, send the result of plugin handler
  331. if value.is_some() {
  332. response = Ok(value.into());
  333. }
  334. }
  335. Err(e) => {
  336. // A plugin handler was found but it failed
  337. response = Err(e);
  338. }
  339. }
  340. }
  341. response
  342. }
  343. }
  344. #[cfg(test)]
  345. mod test {
  346. use crate::{generate_context, Context};
  347. #[test]
  348. fn check_get_url() {
  349. let context = generate_context!("test/fixture/src-tauri/tauri.conf.json");
  350. let context = Context::new(context);
  351. let res = super::get_url(&context);
  352. #[cfg(custom_protocol)]
  353. assert!(res == "tauri://index.html");
  354. #[cfg(dev)]
  355. {
  356. let config = &context.config;
  357. assert_eq!(res, config.build.dev_path);
  358. }
  359. }
  360. }