From 4b2aa35684632ed2afd7dec4ad848df5704868e4 Mon Sep 17 00:00:00 2001 From: chip Date: Sun, 1 Aug 2021 19:54:10 -0700 Subject: [PATCH] Add back WebDriver support (#2324) --- .changes/config.json | 5 + .changes/tauri-driver.md | 5 + .changes/webdriver.md | 5 + core/tauri-runtime-wry/src/lib.rs | 49 ++- tooling/webdriver/.gitignore | 4 + tooling/webdriver/.license_template | 3 + tooling/webdriver/Cargo.lock | 535 +++++++++++++++++++++++++++ tooling/webdriver/Cargo.toml | 28 ++ tooling/webdriver/LICENSE.spdx | 1 + tooling/webdriver/LICENSE_APACHE-2.0 | 1 + tooling/webdriver/LICENSE_MIT | 1 + tooling/webdriver/README.md | 42 +++ tooling/webdriver/src/cli.rs | 58 +++ tooling/webdriver/src/main.rs | 23 ++ tooling/webdriver/src/server.rs | 196 ++++++++++ tooling/webdriver/src/webdriver.rs | 51 +++ 16 files changed, 1000 insertions(+), 7 deletions(-) create mode 100644 .changes/tauri-driver.md create mode 100644 .changes/webdriver.md create mode 100644 tooling/webdriver/.gitignore create mode 100644 tooling/webdriver/.license_template create mode 100644 tooling/webdriver/Cargo.lock create mode 100644 tooling/webdriver/Cargo.toml create mode 120000 tooling/webdriver/LICENSE.spdx create mode 120000 tooling/webdriver/LICENSE_APACHE-2.0 create mode 120000 tooling/webdriver/LICENSE_MIT create mode 100644 tooling/webdriver/README.md create mode 100644 tooling/webdriver/src/cli.rs create mode 100644 tooling/webdriver/src/main.rs create mode 100644 tooling/webdriver/src/server.rs create mode 100644 tooling/webdriver/src/webdriver.rs diff --git a/.changes/config.json b/.changes/config.json index 334ca0992..96fc2fc71 100644 --- a/.changes/config.json +++ b/.changes/config.json @@ -274,6 +274,11 @@ "create-tauri-app": { "path": "./tooling/create-tauri-app", "manager": "javascript" + }, + "tauri-driver": { + "path": "./tooling/webdriver", + "manager": "rust", + "postversion": "node ../../.scripts/sync-prerelease.js ${ pkg.pkg } ${ release.type }" } } } diff --git a/.changes/tauri-driver.md b/.changes/tauri-driver.md new file mode 100644 index 000000000..70d3b4623 --- /dev/null +++ b/.changes/tauri-driver.md @@ -0,0 +1,5 @@ +--- +"tauri-driver": minor +--- + +Initial release including Linux and Windows support. diff --git a/.changes/webdriver.md b/.changes/webdriver.md new file mode 100644 index 000000000..426b54e3c --- /dev/null +++ b/.changes/webdriver.md @@ -0,0 +1,5 @@ +--- +"tauri-runtime-wry": patch +--- + +Add webdriver support to Tauri. diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 9964a0fa2..a75ed0110 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -64,9 +64,13 @@ pub use wry::application::window::{Window, WindowBuilder as WryWindowBuilder}; use wry::webview::WebviewExtWindows; use std::{ - collections::HashMap, + collections::{ + hash_map::Entry::{Occupied, Vacant}, + HashMap, + }, convert::TryFrom, fs::read, + path::PathBuf, sync::{ atomic::{AtomicBool, Ordering}, mpsc::{channel, Sender}, @@ -83,6 +87,7 @@ use menu::*; mod mime_type; use mime_type::MimeType; +type WebContextStore = Mutex, WebContext>>; type WindowEventHandler = Box; type WindowEventListenersMap = Arc>>; type WindowEventListeners = Arc>>; @@ -752,7 +757,9 @@ pub(crate) enum Message { #[cfg(feature = "system-tray")] Tray(TrayMessage), CreateWebview( - Box) -> Result + Send>, + Box< + dyn FnOnce(&EventLoopWindowTarget, &WebContextStore) -> Result + Send, + >, Sender, ), CreateWindow( @@ -956,7 +963,9 @@ impl Dispatch for WryDispatcher { .context .proxy .send_event(Message::CreateWebview( - Box::new(move |event_loop| create_webview(event_loop, context, pending)), + Box::new(move |event_loop, web_context| { + create_webview(event_loop, web_context, context, pending) + }), tx, )) .map_err(|_| Error::FailedToSendMessage)?; @@ -1243,6 +1252,7 @@ pub struct Wry { is_event_loop_running: Arc, event_loop: EventLoop, windows: Arc>>, + web_context: WebContextStore, window_event_listeners: WindowEventListeners, #[cfg(feature = "menu")] menu_event_listeners: MenuEventListeners, @@ -1288,7 +1298,9 @@ impl RuntimeHandle for WryHandle { .dispatcher_context .proxy .send_event(Message::CreateWebview( - Box::new(move |event_loop| create_webview(event_loop, dispatcher_context, pending)), + Box::new(move |event_loop, web_context| { + create_webview(event_loop, web_context, dispatcher_context, pending) + }), tx, )) .map_err(|_| Error::FailedToSendMessage)?; @@ -1348,6 +1360,7 @@ impl Runtime for Wry { is_event_loop_running, event_loop, windows: Default::default(), + web_context: Default::default(), window_event_listeners: Default::default(), #[cfg(feature = "menu")] menu_event_listeners: Default::default(), @@ -1382,6 +1395,7 @@ impl Runtime for Wry { let proxy = self.event_loop.create_proxy(); let webview = create_webview( &self.event_loop, + &self.web_context, DispatcherContext { main_thread_id: self.main_thread_id, is_event_loop_running: self.is_event_loop_running.clone(), @@ -1497,6 +1511,7 @@ impl Runtime for Wry { fn run_iteration(&mut self, callback: F) -> RunIteration { use wry::application::platform::run_return::EventLoopExtRunReturn; let windows = self.windows.clone(); + let web_context = &self.web_context; let window_event_listeners = self.window_event_listeners.clone(); #[cfg(feature = "menu")] let menu_event_listeners = self.menu_event_listeners.clone(); @@ -1531,6 +1546,7 @@ impl Runtime for Wry { #[cfg(feature = "system-tray")] tray_context: &tray_context, }, + web_context, ); }); self.is_event_loop_running.store(false, Ordering::Relaxed); @@ -1541,6 +1557,7 @@ impl Runtime for Wry { fn run(self, callback: F) { self.is_event_loop_running.store(true, Ordering::Relaxed); let windows = self.windows.clone(); + let web_context = self.web_context; let window_event_listeners = self.window_event_listeners.clone(); #[cfg(feature = "menu")] let menu_event_listeners = self.menu_event_listeners.clone(); @@ -1567,6 +1584,7 @@ impl Runtime for Wry { #[cfg(feature = "system-tray")] tray_context: &tray_context, }, + &web_context, ); }) } @@ -1590,6 +1608,7 @@ fn handle_event_loop( event_loop: &EventLoopWindowTarget, control_flow: &mut ControlFlow, context: EventLoopIterationContext<'_>, + web_context: &WebContextStore, ) -> RunIteration { let EventLoopIterationContext { callback, @@ -1892,7 +1911,7 @@ fn handle_event_loop( } } } - Message::CreateWebview(handler, sender) => match handler(event_loop) { + Message::CreateWebview(handler, sender) => match handler(event_loop, web_context) { Ok(webview) => { let window_id = webview.inner.window().id(); windows.insert(window_id, webview); @@ -1936,6 +1955,7 @@ fn handle_event_loop( sender.send(Err(Error::CreateWindow)).unwrap(); } } + #[cfg(feature = "system-tray")] Message::Tray(tray_message) => match tray_message { TrayMessage::UpdateItem(menu_id, update) => { @@ -2068,6 +2088,7 @@ fn center_window(window: &Window) -> Result<()> { fn create_webview( event_loop: &EventLoopWindowTarget, + web_context: &WebContextStore, context: DispatcherContext, pending: PendingWindow, ) -> Result { @@ -2134,13 +2155,27 @@ fn create_webview( .map_err(|_| wry::Error::InitScriptError) }); } - let mut context = WebContext::new(webview_attributes.data_directory); - webview_builder = webview_builder.with_web_context(&mut context); + for script in webview_attributes.initialization_scripts { webview_builder = webview_builder.with_initialization_script(&script); } + let mut web_context = web_context.lock().expect("poisoned WebContext store"); + let is_first_context = web_context.is_empty(); + let web_context = match web_context.entry(webview_attributes.data_directory) { + Occupied(occupied) => occupied.into_mut(), + Vacant(vacant) => { + let mut web_context = WebContext::new(vacant.key().clone()); + web_context.set_allows_automation(match std::env::var("TAURI_AUTOMATION").as_deref() { + Ok("true") => is_first_context, + _ => false, + }); + vacant.insert(web_context) + } + }; + let webview = webview_builder + .with_web_context(web_context) .build() .map_err(|e| Error::CreateWebview(Box::new(e)))?; diff --git a/tooling/webdriver/.gitignore b/tooling/webdriver/.gitignore new file mode 100644 index 000000000..5e4155847 --- /dev/null +++ b/tooling/webdriver/.gitignore @@ -0,0 +1,4 @@ +/target +.DS_Store +*.rs.bk +*~ diff --git a/tooling/webdriver/.license_template b/tooling/webdriver/.license_template new file mode 100644 index 000000000..ef4038304 --- /dev/null +++ b/tooling/webdriver/.license_template @@ -0,0 +1,3 @@ +// Copyright {20\d{2}(-20\d{2})?} Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT diff --git a/tooling/webdriver/Cargo.lock b/tooling/webdriver/Cargo.lock new file mode 100644 index 000000000..641a9a944 --- /dev/null +++ b/tooling/webdriver/Cargo.lock @@ -0,0 +1,535 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "anyhow" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bytes" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" + +[[package]] +name = "futures-executor" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" + +[[package]] +name = "futures-macro" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +dependencies = [ + "autocfg", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" + +[[package]] +name = "futures-task" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" + +[[package]] +name = "futures-util" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +dependencies = [ + "autocfg", + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "proc-macro-hack", + "proc-macro-nested", + "slab", +] + +[[package]] +name = "http" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" + +[[package]] +name = "httpdate" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" + +[[package]] +name = "hyper" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3f71a7eea53a3f8257a7b4795373ff886397178cd634430ea94e12d7fe4fe34" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" + +[[package]] +name = "mio" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", +] + +[[package]] +name = "pico-args" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d" + +[[package]] +name = "pin-project" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro-nested" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" + +[[package]] +name = "proc-macro2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "signal-hook-tokio" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8" +dependencies = [ + "futures-core", + "libc", + "signal-hook", + "tokio", +] + +[[package]] +name = "slab" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" + +[[package]] +name = "socket2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "syn" +version = "1.0.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tauri-driver" +version = "0.0.0" +dependencies = [ + "anyhow", + "futures", + "hyper", + "pico-args", + "serde", + "serde_json", + "signal-hook", + "signal-hook-tokio", + "tokio", + "which", +] + +[[package]] +name = "tokio" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975" +dependencies = [ + "autocfg", + "libc", + "mio", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "which" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe" +dependencies = [ + "either", + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/tooling/webdriver/Cargo.toml b/tooling/webdriver/Cargo.toml new file mode 100644 index 000000000..30f7b220c --- /dev/null +++ b/tooling/webdriver/Cargo.toml @@ -0,0 +1,28 @@ +workspace = { } + +[package] +name = "tauri-driver" +version = "0.0.0" +authors = ["Tauri Programme within The Commons Conservancy"] +categories = ["gui", "web-programming"] +license = "Apache-2.0 OR MIT" +homepage = "https://tauri.studio" +repository = "https://github.com/tauri-apps/tauri" +description = "Webdriver server for Tauri applications" +readme = "README.md" +exclude = [".license_template", "/target"] +edition = "2018" + +[dependencies] +anyhow = "1" +hyper = { version = "0.14", features = [ "client", "http1", "runtime", "server", "stream", "tcp" ] } +futures = "0.3" +pico-args = "0.4" +serde = { version = "1", features = [ "derive" ] } +serde_json = "1" +tokio = { version = "1", features = [ "macros" ] } # other required features enabled by hyper +which = "4" + +[target."cfg(unix)".dependencies] +signal-hook = "0.3" +signal-hook-tokio = { version = "0.3", features = [ "futures-v0_3" ] } diff --git a/tooling/webdriver/LICENSE.spdx b/tooling/webdriver/LICENSE.spdx new file mode 120000 index 000000000..12947c5c4 --- /dev/null +++ b/tooling/webdriver/LICENSE.spdx @@ -0,0 +1 @@ +../../LICENSE.spdx \ No newline at end of file diff --git a/tooling/webdriver/LICENSE_APACHE-2.0 b/tooling/webdriver/LICENSE_APACHE-2.0 new file mode 120000 index 000000000..c7abb8c4f --- /dev/null +++ b/tooling/webdriver/LICENSE_APACHE-2.0 @@ -0,0 +1 @@ +../../LICENSE_APACHE-2.0 \ No newline at end of file diff --git a/tooling/webdriver/LICENSE_MIT b/tooling/webdriver/LICENSE_MIT new file mode 120000 index 000000000..0304881f4 --- /dev/null +++ b/tooling/webdriver/LICENSE_MIT @@ -0,0 +1 @@ +../../LICENSE_MIT \ No newline at end of file diff --git a/tooling/webdriver/README.md b/tooling/webdriver/README.md new file mode 100644 index 000000000..18e05f3bf --- /dev/null +++ b/tooling/webdriver/README.md @@ -0,0 +1,42 @@ +# `tauri-driver` _(pre-alpha)_ + +Cross-platform WebDriver server for Tauri applications. + +This is a [WebDriver Intermediary Node] that wraps the native WebDriver server +for platforms that [Tauri] supports. Your WebDriver client will connect to the +running `tauri-driver` server, and `tauri-driver` will handle starting the +native WebDriver server for you behind the scenes. It requires two separate +ports to be used since two distinct [WebDriver Remote Ends] run. + +You can configure the ports used with arguments when starting the binary: +* `--port` (default: `4444`) +* `--native-port` (default: `4445`) + +Supported platforms: +* **[In Progress]** Linux w/ `WebKitWebDriver` +* **[In Progress]** Windows w/ [Microsoft Edge Driver] +* **[Todo]** macOS w/ [Appium Mac2 Driver] (probably) + +_note: the (probably) items haven't been proof-of-concept'd yet, and if it is +not possible to use the listed native webdriver, then a custom implementation +will be used that wraps around [wry]._ + + +## Trying it out + +**Until this branch is merged into Tauri `dev`, this code works for pure [wry] +applications only.** + +Currently, this uses a branch on [wry] `feat/webdriver`. The support for +automated actions goes all the way down to wry with no real layer for just +Tauri yet. For Windows, the [wry] branch only supports the `win32` backend +and not `winrt`, unless you are okay with the webview not being closable by +the webdriver. + + +[WebDriver Intermediary Node]: https://www.w3.org/TR/webdriver/#dfn-intermediary-nodes +[WebDriver Remote Ends]: https://www.w3.org/TR/webdriver/#dfn-remote-ends +[Microsoft Edge Driver]: https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ +[Appium Mac2 Driver]: https://github.com/appium/appium-mac2-driver +[wry]: https://github.com/tauri-apps/wry +[Tauri]: https://github.com/tauri-apps/tauri diff --git a/tooling/webdriver/src/cli.rs b/tooling/webdriver/src/cli.rs new file mode 100644 index 000000000..5f2e77abb --- /dev/null +++ b/tooling/webdriver/src/cli.rs @@ -0,0 +1,58 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::path::PathBuf; + +const HELP: &str = "\ +USAGE: tauri-driver [FLAGS] [OPTIONS] + +FLAGS: + -h, --help Prints help information + +OPTIONS: + --port NUMBER Sets the tauri-driver intermediary port + --native-port NUMBER Sets the port of the underlying WebDriver + --native-driver PATH Sets the path to the native WebDriver binary +"; + +#[derive(Debug, Clone)] +pub struct Args { + pub port: u16, + pub native_port: u16, + pub native_driver: Option, +} + +impl From for Args { + fn from(mut args: pico_args::Arguments) -> Self { + // if the user wanted help, we don't care about parsing the rest of the args + if args.contains(["-h", "--help"]) { + println!("{}", HELP); + std::process::exit(0); + } + + let native_driver = match args.opt_value_from_str("--native-driver") { + Ok(native_driver) => native_driver, + Err(e) => { + eprintln!("Error while parsing option --native-driver: {}", e); + std::process::exit(1); + } + }; + + let parsed = Args { + port: args.value_from_str("--port").unwrap_or(4444), + native_port: args.value_from_str("--native-port").unwrap_or(4445), + native_driver, + }; + + // be strict about accepting args, error for anything extraneous + let rest = args.finish(); + if !rest.is_empty() { + eprintln!("Error: unused arguments left: {:?}", rest); + eprintln!("{}", HELP); + std::process::exit(1); + } + + parsed + } +} diff --git a/tooling/webdriver/src/main.rs b/tooling/webdriver/src/main.rs new file mode 100644 index 000000000..d1d5a777a --- /dev/null +++ b/tooling/webdriver/src/main.rs @@ -0,0 +1,23 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +mod cli; +mod server; +mod webdriver; + +fn main() { + let args = pico_args::Arguments::from_env().into(); + + // start the native webdriver on the port specified in args + let mut driver = webdriver::native(&args); + let driver = driver + .spawn() + .expect("error while running native webdriver"); + + // start our webdriver intermediary node + if let Err(e) = server::run(args, driver) { + eprintln!("error while running server: {}", e); + std::process::exit(1); + } +} diff --git a/tooling/webdriver/src/server.rs b/tooling/webdriver/src/server.rs new file mode 100644 index 000000000..15c44c399 --- /dev/null +++ b/tooling/webdriver/src/server.rs @@ -0,0 +1,196 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::cli::Args; +use anyhow::Error; +use futures::TryFutureExt; +use hyper::header::CONTENT_LENGTH; +use hyper::http::uri::Authority; +use hyper::service::{make_service_fn, service_fn}; +use hyper::{Body, Client, Method, Request, Response, Server}; +use serde::Deserialize; +use serde_json::{json, Map, Value}; +use std::convert::Infallible; +use std::path::PathBuf; +use std::process::Child; + +type HttpClient = Client; + +const TAURI_OPTIONS: &str = "tauri:options"; + +#[derive(Debug, Deserialize)] +struct TauriOptions { + application: PathBuf, +} + +impl TauriOptions { + #[cfg(target_os = "linux")] + fn into_native_object(self) -> Map { + let mut map = Map::new(); + map.insert( + "webkitgtk:browserOptions".into(), + json!({"binary": self.application}), + ); + map + } + + #[cfg(target_os = "windows")] + fn into_native_object(self) -> Map { + let mut map = Map::new(); + map.insert("ms:edgeChromium".into(), json!(true)); + map.insert("browserName".into(), json!("webview2")); + map.insert("ms:edgeOptions".into(), json!({"binary": self.application})); + map + } +} + +async fn handle( + client: HttpClient, + mut req: Request, + args: Args, +) -> Result, Error> { + // manipulate a new session to convert options to the native driver format + if let (&Method::POST, "/session") = (req.method(), req.uri().path()) { + let (mut parts, body) = req.into_parts(); + + // get the body from the future stream and parse it as json + let body = hyper::body::to_bytes(body).await?; + let json: Value = serde_json::from_slice(&body)?; + + // manipulate the json to convert from tauri option to native driver options + let json = map_capabilities(json); + + // serialize json and update the content-length header to be accurate + let bytes = serde_json::to_vec(&json)?; + parts.headers.insert(CONTENT_LENGTH, bytes.len().into()); + + req = Request::from_parts(parts, bytes.into()); + } + + client + .request(forward_to_native_driver(req, args)?) + .err_into() + .await +} + +/// Transform the request to a request for the native webdriver server. +fn forward_to_native_driver(mut req: Request, args: Args) -> Result, Error> { + let host: Authority = { + let headers = req.headers_mut(); + headers.remove("host").expect("hyper request has host") + } + .to_str()? + .parse()?; + + let path = req + .uri() + .path_and_query() + .expect("hyper request has uri") + .clone(); + + let uri = format!( + "http://{}:{}{}", + host.host(), + args.native_port, + path.as_str() + ); + + let (mut parts, body) = req.into_parts(); + parts.uri = uri.parse()?; + Ok(Request::from_parts(parts, body)) +} + +/// only happy path for now, no errors +fn map_capabilities(mut json: Value) -> Value { + let mut native = None; + if let Some(capabilities) = json.get_mut("capabilities") { + if let Some(always_match) = capabilities.get_mut("alwaysMatch") { + if let Some(always_match) = always_match.as_object_mut() { + if let Some(tauri_options) = always_match.remove(TAURI_OPTIONS) { + if let Ok(options) = serde_json::from_value::(tauri_options) { + native = Some(options.into_native_object()); + } + } + + if let Some(native) = native.clone() { + always_match.extend(native); + } + } + } + } + + if let Some(native) = native { + if let Some(desired) = json.get_mut("desiredCapabilities") { + if let Some(desired) = desired.as_object_mut() { + desired.remove(TAURI_OPTIONS); + desired.extend(native); + } + } + } + + json +} + +#[tokio::main(flavor = "current_thread")] +pub async fn run(args: Args, mut _driver: Child) -> Result<(), Error> { + #[cfg(unix)] + let (signals_handle, signals_task) = { + use futures::StreamExt; + use signal_hook::consts::signal::*; + + let signals = signal_hook_tokio::Signals::new(&[SIGTERM, SIGINT, SIGQUIT])?; + let signals_handle = signals.handle(); + let signals_task = tokio::spawn(async move { + let mut signals = signals.fuse(); + while let Some(signal) = signals.next().await { + match signal { + SIGTERM | SIGINT | SIGQUIT => { + _driver + .kill() + .expect("unable to kill native webdriver server"); + std::process::exit(0); + } + _ => unreachable!(), + } + } + }); + (signals_handle, signals_task) + }; + + let address = std::net::SocketAddr::from(([127, 0, 0, 1], args.port)); + + // the client we use to proxy requests to the native webdriver + let client = Client::builder() + .http1_preserve_header_case(true) + .http1_title_case_headers(true) + .retry_canceled_requests(false) + .build_http(); + + // pass a copy of the client to the http request handler + let service = make_service_fn(move |_| { + let client = client.clone(); + let args = args.clone(); + async move { + Ok::<_, Infallible>(service_fn(move |request| { + handle(client.clone(), request, args.clone()) + })) + } + }); + + // set up a http1 server that uses the service we just created + Server::bind(&address) + .http1_title_case_headers(true) + .http1_preserve_header_case(true) + .http1_only(true) + .serve(service) + .await?; + + #[cfg(unix)] + { + signals_handle.close(); + signals_task.await?; + } + + Ok(()) +} diff --git a/tooling/webdriver/src/webdriver.rs b/tooling/webdriver/src/webdriver.rs new file mode 100644 index 000000000..177b17d18 --- /dev/null +++ b/tooling/webdriver/src/webdriver.rs @@ -0,0 +1,51 @@ +// Copyright 2019-2021 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::cli::Args; +use std::{env::current_dir, process::Command}; + +// the name of the binary to find in $PATH +#[cfg(target_os = "linux")] +const DRIVER_BINARY: &str = "WebKitWebDriver"; + +#[cfg(target_os = "windows")] +const DRIVER_BINARY: &str = "msedgedriver.exe"; + +/// Find the native driver binary in the PATH, or exits the process with an error. +pub fn native(args: &Args) -> Command { + let native_binary = match args.native_driver.as_deref() { + Some(custom) => { + if custom.exists() { + custom.to_owned() + } else { + eprintln!( + "can not find the supplied binary path {}. This is currently required.", + custom.display() + ); + match current_dir() { + Ok(cwd) => eprintln!("current working directory: {}", cwd.display()), + Err(error) => eprintln!("can not find current working directory: {}", error), + } + std::process::exit(1); + } + } + None => match which::which(DRIVER_BINARY) { + Ok(binary) => binary, + Err(error) => { + eprintln!( + "can not find binary {} in the PATH. This is currently required.\ + You can also pass a custom path with --native-driver", + DRIVER_BINARY + ); + eprintln!("{:?}", error); + std::process::exit(1); + } + }, + }; + + let mut cmd = Command::new(native_binary); + cmd.env("TAURI_AUTOMATION", "true"); + cmd.arg(format!("--port={}", args.native_port)); + cmd +}