diff --git a/.github/workflows/auto-bump-pr.yml b/.github/workflows/auto-bump-pr.yml index 1bcc78e..525b8f7 100644 --- a/.github/workflows/auto-bump-pr.yml +++ b/.github/workflows/auto-bump-pr.yml @@ -15,17 +15,18 @@ jobs: - name: Check out the repository uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - - name: Bump version in tauri.conf.json + - name: Bump version in tauri.conf.json and tauri.android.conf.json id: bump_version run: | - CURRENT_VERSION=$(jq -r '.package.version' src-tauri/tauri.conf.json) + CURRENT_VERSION=$(jq -r '.version' src-tauri/tauri.conf.json) IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" MAJOR=${VERSION_PARTS[0]} MINOR=${VERSION_PARTS[1]} PATCH=${VERSION_PARTS[2]} NEW_PATCH=$((PATCH + 1)) NEW_VERSION="$MAJOR.$MINOR.$NEW_PATCH" - jq --indent 4 ".package.version = \"$NEW_VERSION\"" src-tauri/tauri.conf.json > src-tauri/tauri.conf.json.tmp && mv src-tauri/tauri.conf.json.tmp src-tauri/tauri.conf.json + jq --indent 4 ".version = \"$NEW_VERSION\"" src-tauri/tauri.conf.json > src-tauri/tauri.conf.json.tmp && mv src-tauri/tauri.conf.json.tmp src-tauri/tauri.conf.json + jq --indent 4 ".version = \"$NEW_VERSION\"" src-tauri/tauri.android.conf.json > src-tauri/tauri.android.conf.json.tmp && mv src-tauri/tauri.android.conf.json.tmp src-tauri/tauri.android.conf.json echo "new_version=$NEW_VERSION" >> "$GITHUB_ENV" - name: Create Pull Request diff --git a/.github/workflows/refresh-main-tool-caches.yml b/.github/workflows/refresh-main-tool-caches.yml index ac0847f..ddbcfad 100644 --- a/.github/workflows/refresh-main-tool-caches.yml +++ b/.github/workflows/refresh-main-tool-caches.yml @@ -36,5 +36,5 @@ jobs: - name: cached install tauri-cli uses: taiki-e/cache-cargo-install-action@caa6f48d18d42462f9c30df89e2b4f71a42b7c2c # v2.0.1 with: - tool: tauri-cli + tool: tauri-cli@2.0.0 locked: true diff --git a/.github/workflows/tauri-pr-build.yml b/.github/workflows/tauri-pr-build.yml index 4df8ec7..98cf3ff 100644 --- a/.github/workflows/tauri-pr-build.yml +++ b/.github/workflows/tauri-pr-build.yml @@ -108,20 +108,14 @@ jobs: - name: Cached install tauri-cli uses: taiki-e/cache-cargo-install-action@caa6f48d18d42462f9c30df89e2b4f71a42b7c2c # v2.0.1 with: - tool: tauri-cli@1.6.2 + tool: tauri-cli@2.0.0 locked: true - name: Build using tauri action uses: tauri-apps/tauri-action@8c3e0753aa015d00d03631d6d4f64ad59489251d # v0.5.15 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} - TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} with: tauriScript: cargo --locked auditable tauri - - - name: Upload build artifacts - uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0 - with: - name: "bundles-${{matrix.platform}}${{matrix.args}}" - path: target/release/bundle diff --git a/.github/workflows/tauri-publish.yml b/.github/workflows/tauri-publish.yml index 35b9483..8333994 100644 --- a/.github/workflows/tauri-publish.yml +++ b/.github/workflows/tauri-publish.yml @@ -104,15 +104,15 @@ jobs: - name: Cached install tauri-cli uses: taiki-e/cache-cargo-install-action@caa6f48d18d42462f9c30df89e2b4f71a42b7c2c # v2.0.1 with: - tool: tauri-cli@1.6.2 + tool: tauri-cli@2.0.0 locked: true - name: Build using tauri action uses: tauri-apps/tauri-action@8c3e0753aa015d00d03631d6d4f64ad59489251d # v0.5.15 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} - TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} with: tagName: mdns-browser-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version releaseName: "mDNS-Browser Release v__VERSION__" diff --git a/Cargo.toml b/Cargo.toml index 68c66de..faf2689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,9 +24,9 @@ log = "0.4.22" serde = { version = "1.0.210", features = ["derive"] } serde-wasm-bindgen = "0.6.5" strsim = "0.11.1" -tauri-sys = { git = "https://github.com/JonasKruckenberg/tauri-sys", rev = "3e93a42", features = [ +tauri-sys = { git = "https://github.com/JonasKruckenberg/tauri-sys", branch = "v2", features = [ + "core", "event", - "tauri", ] } thaw = { version = "0.3.4", features = ["csr"] } thaw_utils = { version = "0.0.6", features = ["csr"] } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ce93a1c..3436fe8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -6,27 +6,22 @@ authors = ["hrzlgnm"] edition = "2021" [build-dependencies] -tauri-build = { version = "1.5.5", features = [] } +tauri-build = { version = "2.0.0", features = [] } + +[target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] +tauri-plugin-updater = "2.0.0" [dependencies] -clap = { version = "4.5.17", features = ["derive"] } +clap = { version = "4.5.19", features = ["derive"] } log = "0.4.22" mdns-sd = { version = "0.11.4", features = ["async", "log"] } open = "5" serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.128" -tauri = { version = "1.8.0", features = [ - "dialog-ask", - "http-api", - "http-request", - "process-exit", - "process-relaunch", - "shell-open", - "updater", - "window-all", -] } -tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } +tauri = { version = "2.0.0", features = [] } +tauri-plugin-log = "2.0.0" +tauri-plugin-shell = "2.0.0" -[features] -# This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! -custom-protocol = ["tauri/custom-protocol"] +[lib] +name = "mdns_browser_lib" +crate-type = ["staticlib", "cdylib", "lib"] diff --git a/src-tauri/capabilities/migrated.json b/src-tauri/capabilities/migrated.json new file mode 100644 index 0000000..e00db31 --- /dev/null +++ b/src-tauri/capabilities/migrated.json @@ -0,0 +1,17 @@ +{ + "identifier": "migrated", + "description": "permissions that were migrated from v1", + "local": true, + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "updater:default" + ], + "platforms": [ + "macOS", + "windows", + "linux" + ] +} diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json new file mode 100644 index 0000000..ef8b633 --- /dev/null +++ b/src-tauri/capabilities/mobile.json @@ -0,0 +1,14 @@ +{ + "identifier": "mobile", + "description": "permissions for mobile", + "local": true, + "windows": [ + "main" + ], + "permissions": [ + "core:default" + ], + "platforms": [ + "android" + ] +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..46a96d0 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,431 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use clap::builder::TypedValueParser as _; +use clap::Parser; +use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo}; +use serde::Serialize; +use std::{ + collections::HashMap, + net::IpAddr, + sync::{Arc, Mutex}, + time::{Duration, SystemTime}, +}; + +use tauri::Emitter; +#[cfg(desktop)] +use tauri::Manager; +use tauri::{State, Window}; +use tauri_plugin_log::{Target, TargetKind}; + +type SharedServiceDaemon = Arc>; + +struct ManagedState { + daemon: SharedServiceDaemon, + running_browsers: Arc>>, +} + +impl ManagedState { + fn new() -> Self { + Self { + daemon: get_shared_daemon(), + running_browsers: Arc::new(Mutex::new(Vec::new())), + } + } +} + +fn get_shared_daemon() -> SharedServiceDaemon { + let daemon = ServiceDaemon::new().expect("Failed to create daemon"); + Arc::new(Mutex::new(daemon)) +} + +#[derive(Serialize, Clone, Debug)] +struct TxtRecord { + key: String, + val: Option, +} + +#[derive(Serialize, Clone, Debug)] +pub struct ResolvedService { + instance_name: String, + hostname: String, + port: u16, + pub addresses: Vec, + subtype: Option, + txt: Vec, + updated_at_ms: u64, +} + +fn timestamp_millis() -> u64 { + let now = SystemTime::now(); + let since_epoch = now.duration_since(SystemTime::UNIX_EPOCH).unwrap(); + + since_epoch.as_secs() * 1000 + u64::from(since_epoch.subsec_millis()) +} + +fn string_with_control_characters_escaped(input: String) -> String { + input + .chars() + .map(|ch| { + if ch.is_control() { + format!(r"\u{:04x}", ch as u32) + } else { + ch.to_string() + } + }) + .collect() +} + +fn bytes_option_to_string_option_with_escaping(maybe_bytes: Option<&[u8]>) -> Option { + maybe_bytes.map(|bytes| match String::from_utf8(bytes.to_vec()) { + Ok(utf8_string) => string_with_control_characters_escaped(utf8_string), + Err(_) => byte_array_hexlified(bytes), + }) +} + +fn byte_array_hexlified(byte_array: &[u8]) -> String { + byte_array + .iter() + .map(|byte| format!("{:02x}", byte)) + .collect::>() + .join("") +} + +impl From<&ServiceInfo> for ResolvedService { + fn from(info: &ServiceInfo) -> ResolvedService { + let mut sorted_addresses: Vec = info.get_addresses().clone().drain().collect(); + sorted_addresses.sort(); + let mut sorted_txt: Vec = info + .get_properties() + .iter() + .map(|r| TxtRecord { + key: r.key().into(), + val: bytes_option_to_string_option_with_escaping(r.val()), + }) + .collect(); + sorted_txt.sort_by(|a, b| a.key.partial_cmp(&b.key).unwrap()); + ResolvedService { + instance_name: info.get_fullname().into(), + hostname: info.get_hostname().into(), + port: info.get_port(), + addresses: sorted_addresses, + subtype: info.get_subtype().clone(), + txt: sorted_txt, + updated_at_ms: timestamp_millis(), + } + } +} + +#[derive(Serialize, Clone, Debug)] +pub struct MetricsEvent { + metrics: HashMap, +} + +#[derive(Serialize, Clone, Debug)] +pub struct ServiceResolvedEvent { + service: ResolvedService, +} + +#[derive(Serialize, Clone, Debug)] +pub struct SearchStartedEvent { + service_type: String, +} + +type SearchStoppedEvent = SearchStartedEvent; + +#[derive(Serialize, Clone, Debug)] +pub struct ServiceRemovedEvent { + instance_name: String, + at_ms: u64, +} + +type ServiceFoundEvent = ServiceRemovedEvent; +type ServiceTypeFoundEvent = SearchStartedEvent; +type ServiceTypeRemovedEvent = SearchStartedEvent; + +const META_SERVICE: &str = "_services._dns-sd._udp.local."; + +#[tauri::command] +fn browse_types(window: Window, state: State) { + if let Ok(mdns) = state.daemon.lock() { + let mdns_for_thread = mdns.clone(); + std::thread::spawn(move || { + let receiver = mdns_for_thread + .browse(META_SERVICE) + .expect("Failed to browse"); + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceFound(_service_type, full_name) => { + log::debug!("Service type found: {}", full_name); + window + .emit( + "service-type-found", + &ServiceTypeFoundEvent { + service_type: full_name, + }, + ) + .expect("To emit"); + } + ServiceEvent::ServiceRemoved(_service_type, full_name) => { + log::debug!("Service type removed: {}", full_name); + window + .emit( + "service-type-removed", + &ServiceTypeRemovedEvent { + service_type: full_name, + }, + ) + .expect("To emit"); + } + ServiceEvent::SearchStopped(service_type) => { + if service_type == META_SERVICE { + break; + } + } + _ => (), + } + } + log::debug!("Browse type thread ending."); + }); + } +} +#[tauri::command] +fn stop_browse(service_type: String, state: State) { + if service_type.is_empty() { + return; + } + if let Ok(mdns) = state.daemon.lock() { + if let Ok(mut running_browsers) = state.running_browsers.lock() { + if running_browsers.contains(&service_type) { + mdns.stop_browse(service_type.as_str()) + .expect("To stop browsing"); + running_browsers.retain(|s| s != &service_type); + } + } + } +} + +#[tauri::command] +fn browse(service_type: String, window: Window, state: State) { + if service_type.is_empty() { + return; + } + if let Ok(mdns) = state.daemon.lock() { + if let Ok(mut running_browsers) = state.running_browsers.lock() { + if !running_browsers.contains(&service_type) { + running_browsers.push(service_type.clone()); + let receiver = mdns.browse(service_type.as_str()).expect("To browse"); + std::thread::spawn(move || { + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceFound(_service_type, instance_name) => { + window + .emit( + "service-found", + &ServiceFoundEvent { + instance_name, + at_ms: timestamp_millis(), + }, + ) + .expect("To emit"); + } + ServiceEvent::SearchStarted(service_type) => { + window + .emit("search-started", &SearchStartedEvent { service_type }) + .expect("to emit"); + } + ServiceEvent::ServiceResolved(info) => { + window + .emit( + "service-resolved", + &ServiceResolvedEvent { + service: ResolvedService::from(&info), + }, + ) + .expect("To emit"); + } + ServiceEvent::ServiceRemoved(_service_type, instance_name) => { + window + .emit( + "service-removed", + &ServiceRemovedEvent { + instance_name, + at_ms: timestamp_millis(), + }, + ) + .expect("To emit"); + } + ServiceEvent::SearchStopped(service_type) => { + window + .emit("search-stopped", &SearchStoppedEvent { service_type }) + .expect("To emit"); + break; + } + } + } + log::debug!("Browse thread for {} ending.", &service_type); + }); + } + } + } +} + +const METRIC_SEND_INTERVAL: Duration = Duration::from_secs(10); + +#[tauri::command] +fn send_metrics(window: Window, state: State) { + if let Ok(mdns) = state.daemon.lock() { + let mdns_for_thread = mdns.clone(); + std::thread::spawn(move || loop { + if let Ok(metrics_receiver) = mdns_for_thread.get_metrics() { + if let Ok(metrics) = metrics_receiver.recv() { + window + .emit("metrics", &MetricsEvent { metrics }) + .expect("To emit"); + } + } + std::thread::sleep(METRIC_SEND_INTERVAL); + }); + } +} + +#[tauri::command] +fn open(url: String) { + let _ = open::that(url.clone()).map_err(|e| log::error!("Failed to open {}: {}", url, e)); +} + +#[cfg(desktop)] +#[tauri::command] +fn version(window: Window) -> String { + window + .app_handle() + .config() + .version + .clone() + .unwrap_or(String::from("Unknown")) +} + +#[cfg(target_os = "linux")] +fn x11_workaround() { + let session_type_key = "XDG_SESSION_TYPE"; + match std::env::var(session_type_key) { + Ok(val) => { + if val == "x11" { + println!( + "Setting WEBKIT_DISABLE_COMPOSITING_MODE=1 to workaround rendering issues with x11 session" + ); + std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1") + } + } + Err(_e) => {} + } +} + +#[derive(Parser, Debug)] +struct Args { + #[arg( + short = 'l', + long, + default_value_t = foreign_crate::LevelFilter::Info, + value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]) + .map(|s| s.parse::().unwrap_or(foreign_crate::LevelFilter::Info)), + )] + log_level: foreign_crate::LevelFilter, +} + +mod foreign_crate { + #[derive(Copy, Clone, PartialEq, Eq, Debug)] + pub(crate) enum LevelFilter { + Trace, + Debug, + Info, + Warn, + Error, + } + + impl std::fmt::Display for LevelFilter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Trace => "trace", + Self::Debug => "debug", + Self::Info => "info", + Self::Warn => "warn", + Self::Error => "error", + }; + s.fmt(f) + } + } + impl std::str::FromStr for LevelFilter { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "trace" => Ok(Self::Trace), + "debug" => Ok(Self::Debug), + "info" => Ok(Self::Info), + "warn" => Ok(Self::Warn), + "error" => Ok(Self::Error), + _ => Err(format!("Unknown log level: {s}")), + } + } + } + impl From for log::LevelFilter { + fn from(val: LevelFilter) -> Self { + match val { + LevelFilter::Trace => log::LevelFilter::Trace, + LevelFilter::Debug => log::LevelFilter::Debug, + LevelFilter::Info => log::LevelFilter::Info, + LevelFilter::Warn => log::LevelFilter::Warn, + LevelFilter::Error => log::LevelFilter::Error, + } + } + } +} + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + #[cfg(target_os = "linux")] + x11_workaround(); + let args = Args::parse(); + tauri::Builder::default() + .manage(ManagedState::new()) + .plugin( + tauri_plugin_log::Builder::default() + .targets([ + Target::new(TargetKind::Stdout), + Target::new(TargetKind::Webview), + ]) + .level(args.log_level) + .build(), + ) + .setup(|app| { + #[cfg(desktop)] + { + app.handle() + .plugin(tauri_plugin_updater::Builder::new().build())?; + let splashscreen_window = app.get_webview_window("splashscreen").unwrap(); + let main_window = app.get_webview_window("main").unwrap(); + tauri::async_runtime::spawn(async move { + std::thread::sleep(std::time::Duration::from_secs(3)); + splashscreen_window.close().unwrap(); + main_window.show().unwrap(); + }); + } + #[cfg(not(desktop))] + { + let _dummy = app.handle(); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + browse, + browse_types, + open, + send_metrics, + stop_browse, + #[cfg(desktop)] + version, + ]) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 9edecfa..28a65d0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,416 +1,5 @@ -// Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use clap::builder::TypedValueParser as _; -use clap::Parser; -use mdns_sd::{ServiceDaemon, ServiceEvent, ServiceInfo}; -use serde::Serialize; -use std::{ - collections::HashMap, - net::IpAddr, - sync::{Arc, Mutex}, - time::{Duration, SystemTime}, -}; -use tauri::{Manager, State, Window}; -use tauri_plugin_log::LogTarget; - -type SharedServiceDaemon = Arc>; - -struct ManagedState { - daemon: SharedServiceDaemon, - running_browsers: Arc>>, -} - -impl ManagedState { - fn new() -> Self { - Self { - daemon: get_shared_daemon(), - running_browsers: Arc::new(Mutex::new(Vec::new())), - } - } -} - -fn get_shared_daemon() -> SharedServiceDaemon { - let daemon = ServiceDaemon::new().expect("Failed to create daemon"); - Arc::new(Mutex::new(daemon)) -} - -#[derive(Serialize, Clone, Debug)] -struct TxtRecord { - key: String, - val: Option, -} - -#[derive(Serialize, Clone, Debug)] -pub struct ResolvedService { - instance_name: String, - hostname: String, - port: u16, - pub addresses: Vec, - subtype: Option, - txt: Vec, - updated_at_ms: u64, -} - -fn timestamp_millis() -> u64 { - let now = SystemTime::now(); - let since_epoch = now.duration_since(SystemTime::UNIX_EPOCH).unwrap(); - - since_epoch.as_secs() * 1000 + u64::from(since_epoch.subsec_millis()) -} - -fn string_with_control_characters_escaped(input: String) -> String { - input - .chars() - .map(|ch| { - if ch.is_control() { - format!(r"\u{:04x}", ch as u32) - } else { - ch.to_string() - } - }) - .collect() -} - -fn bytes_option_to_string_option_with_escaping(maybe_bytes: Option<&[u8]>) -> Option { - maybe_bytes.map(|bytes| match String::from_utf8(bytes.to_vec()) { - Ok(utf8_string) => string_with_control_characters_escaped(utf8_string), - Err(_) => byte_array_hexlified(bytes), - }) -} - -fn byte_array_hexlified(byte_array: &[u8]) -> String { - byte_array - .iter() - .map(|byte| format!("{:02x}", byte)) - .collect::>() - .join("") -} - -impl From<&ServiceInfo> for ResolvedService { - fn from(info: &ServiceInfo) -> ResolvedService { - let mut sorted_addresses: Vec = info.get_addresses().clone().drain().collect(); - sorted_addresses.sort(); - let mut sorted_txt: Vec = info - .get_properties() - .iter() - .map(|r| TxtRecord { - key: r.key().into(), - val: bytes_option_to_string_option_with_escaping(r.val()), - }) - .collect(); - sorted_txt.sort_by(|a, b| a.key.partial_cmp(&b.key).unwrap()); - ResolvedService { - instance_name: info.get_fullname().into(), - hostname: info.get_hostname().into(), - port: info.get_port(), - addresses: sorted_addresses, - subtype: info.get_subtype().clone(), - txt: sorted_txt, - updated_at_ms: timestamp_millis(), - } - } -} - -#[derive(Serialize, Clone, Debug)] -pub struct MetricsEvent { - metrics: HashMap, -} - -#[derive(Serialize, Clone, Debug)] -pub struct ServiceResolvedEvent { - service: ResolvedService, -} - -#[derive(Serialize, Clone, Debug)] -pub struct SearchStartedEvent { - service_type: String, -} - -type SearchStoppedEvent = SearchStartedEvent; - -#[derive(Serialize, Clone, Debug)] -pub struct ServiceRemovedEvent { - instance_name: String, - at_ms: u64, -} - -type ServiceFoundEvent = ServiceRemovedEvent; -type ServiceTypeFoundEvent = SearchStartedEvent; -type ServiceTypeRemovedEvent = SearchStartedEvent; - -const META_SERVICE: &str = "_services._dns-sd._udp.local."; - -#[tauri::command] -fn browse_types(window: Window, state: State) { - if let Ok(mdns) = state.daemon.lock() { - let mdns_for_thread = mdns.clone(); - std::thread::spawn(move || { - let receiver = mdns_for_thread - .browse(META_SERVICE) - .expect("Failed to browse"); - while let Ok(event) = receiver.recv() { - match event { - ServiceEvent::ServiceFound(_service_type, full_name) => { - log::debug!("Service type found: {}", full_name); - window - .emit( - "service-type-found", - &ServiceTypeFoundEvent { - service_type: full_name, - }, - ) - .expect("To emit"); - } - ServiceEvent::ServiceRemoved(_service_type, full_name) => { - log::debug!("Service type removed: {}", full_name); - window - .emit( - "service-type-removed", - &ServiceTypeRemovedEvent { - service_type: full_name, - }, - ) - .expect("To emit"); - } - ServiceEvent::SearchStopped(service_type) => { - if service_type == META_SERVICE { - break; - } - } - _ => (), - } - } - log::debug!("Browse type thread ending."); - }); - } -} - -#[tauri::command] -fn stop_browse(service_type: String, state: State) { - if service_type.is_empty() { - return; - } - if let Ok(mdns) = state.daemon.lock() { - if let Ok(mut running_browsers) = state.running_browsers.lock() { - if running_browsers.contains(&service_type) { - mdns.stop_browse(service_type.as_str()) - .expect("To stop browsing"); - running_browsers.retain(|s| s != &service_type); - } - } - } -} - -#[tauri::command] -fn browse(service_type: String, window: Window, state: State) { - if service_type.is_empty() { - return; - } - if let Ok(mdns) = state.daemon.lock() { - if let Ok(mut running_browsers) = state.running_browsers.lock() { - if !running_browsers.contains(&service_type) { - running_browsers.push(service_type.clone()); - let receiver = mdns.browse(service_type.as_str()).expect("To browse"); - std::thread::spawn(move || { - while let Ok(event) = receiver.recv() { - match event { - ServiceEvent::ServiceFound(_service_type, instance_name) => { - window - .emit( - "service-found", - &ServiceFoundEvent { - instance_name, - at_ms: timestamp_millis(), - }, - ) - .expect("To emit"); - } - ServiceEvent::SearchStarted(service_type) => { - window - .emit("search-started", &SearchStartedEvent { service_type }) - .expect("to emit"); - } - ServiceEvent::ServiceResolved(info) => { - window - .emit( - "service-resolved", - &ServiceResolvedEvent { - service: ResolvedService::from(&info), - }, - ) - .expect("To emit"); - } - ServiceEvent::ServiceRemoved(_service_type, instance_name) => { - window - .emit( - "service-removed", - &ServiceRemovedEvent { - instance_name, - at_ms: timestamp_millis(), - }, - ) - .expect("To emit"); - } - ServiceEvent::SearchStopped(service_type) => { - window - .emit("search-stopped", &SearchStoppedEvent { service_type }) - .expect("To emit"); - break; - } - } - } - log::debug!("Browse thread for {} ending.", &service_type); - }); - } - } - } -} - -const METRIC_SEND_INTERVAL: Duration = Duration::from_secs(10); - -#[tauri::command] -fn send_metrics(window: Window, state: State) { - if let Ok(mdns) = state.daemon.lock() { - let mdns_for_thread = mdns.clone(); - std::thread::spawn(move || loop { - if let Ok(metrics_receiver) = mdns_for_thread.get_metrics() { - if let Ok(metrics) = metrics_receiver.recv() { - window - .emit("metrics", &MetricsEvent { metrics }) - .expect("To emit"); - } - } - std::thread::sleep(METRIC_SEND_INTERVAL); - }); - } -} - -#[tauri::command] -fn open(url: String) { - let _ = open::that(url.clone()).map_err(|e| log::error!("Failed to open {}: {}", url, e)); -} - -#[tauri::command] -fn version(window: Window) -> String { - window - .app_handle() - .config() - .package - .version - .clone() - .unwrap_or(String::from("Unknown")) -} - -#[cfg(target_os = "linux")] -fn platform_setup() { - let session_type_key = "XDG_SESSION_TYPE"; - match std::env::var(session_type_key) { - Ok(val) => { - if val == "x11" { - println!( - "Setting WEBKIT_DISABLE_COMPOSITING_MODE=1 to workaround rendering issues with x11 session" - ); - std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1") - } - } - Err(_e) => {} - } -} - -#[cfg(not(target_os = "linux"))] -fn platform_setup() {} - -#[derive(Parser, Debug)] -struct Args { - #[arg( - short = 'l', - long, - default_value_t = foreign_crate::LevelFilter::Info, - value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]) - .map(|s| s.parse::().unwrap_or(foreign_crate::LevelFilter::Info)), - )] - log_level: foreign_crate::LevelFilter, -} - -mod foreign_crate { - #[derive(Copy, Clone, PartialEq, Eq, Debug)] - pub(crate) enum LevelFilter { - Trace, - Debug, - Info, - Warn, - Error, - } - - impl std::fmt::Display for LevelFilter { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - Self::Trace => "trace", - Self::Debug => "debug", - Self::Info => "info", - Self::Warn => "warn", - Self::Error => "error", - }; - s.fmt(f) - } - } - impl std::str::FromStr for LevelFilter { - type Err = String; - - fn from_str(s: &str) -> Result { - match s { - "trace" => Ok(Self::Trace), - "debug" => Ok(Self::Debug), - "info" => Ok(Self::Info), - "warn" => Ok(Self::Warn), - "error" => Ok(Self::Error), - _ => Err(format!("Unknown log level: {s}")), - } - } - } - impl From for log::LevelFilter { - fn from(val: LevelFilter) -> Self { - match val { - LevelFilter::Trace => log::LevelFilter::Trace, - LevelFilter::Debug => log::LevelFilter::Debug, - LevelFilter::Info => log::LevelFilter::Info, - LevelFilter::Warn => log::LevelFilter::Warn, - LevelFilter::Error => log::LevelFilter::Error, - } - } - } -} - fn main() { - platform_setup(); - let args = Args::parse(); - tauri::Builder::default() - .manage(ManagedState::new()) - .plugin( - tauri_plugin_log::Builder::default() - .targets([LogTarget::LogDir, LogTarget::Stdout, LogTarget::Webview]) - .level(args.log_level) - .build(), - ) - .setup(|app| { - let splashscreen_window = app.get_window("splashscreen").unwrap(); - let main_window = app.get_window("main").unwrap(); - tauri::async_runtime::spawn(async move { - std::thread::sleep(std::time::Duration::from_secs(3)); - splashscreen_window.close().unwrap(); - main_window.show().unwrap(); - }); - Ok(()) - }) - .invoke_handler(tauri::generate_handler![ - browse, - browse_types, - open, - send_metrics, - stop_browse, - version - ]) - .run(tauri::generate_context!()) - .expect("error while running tauri application"); + mdns_browser_lib::run(); } diff --git a/src-tauri/tauri.android.conf.json b/src-tauri/tauri.android.conf.json new file mode 100644 index 0000000..621848e --- /dev/null +++ b/src-tauri/tauri.android.conf.json @@ -0,0 +1,31 @@ +{ + "build": { + "beforeDevCommand": "trunk serve", + "beforeBuildCommand": "trunk build", + "frontendDist": "../dist", + "devUrl": "http://localhost:1420" + }, + "bundle": { + "active": false + }, + "productName": "mDNS Browser", + "version": "0.8.0", + "identifier": "io.github.hrzlgnm.mdns-browser", + "plugins": {}, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "title": "mDNS-Browser", + "url": "index.html", + "width": 1600, + "height": 900, + "label": "main", + "visible": true + } + ], + "security": { + "csp": null + } + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0d19b12..790eebb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,41 +2,44 @@ "build": { "beforeDevCommand": "trunk serve", "beforeBuildCommand": "trunk build", - "devPath": "http://localhost:1420", - "distDir": "../dist", - "withGlobalTauri": true + "frontendDist": "../dist", + "devUrl": "http://localhost:1420" }, - "package": { - "productName": "mdns-browser", - "version": "0.7.18" + "bundle": { + "active": true, + "targets": [ + "deb", + "appimage", + "nsis", + "dmg", + "app" + ], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "createUpdaterArtifacts": "v1Compatible" }, - "tauri": { - "allowlist": { - "all": false, - "shell": { - "all": false, - "open": true - }, - "dialog": { - "all": false, - "ask": true - }, - "http": { - "all": false, - "request": true, - "scope": [ - "https://mdns-browser-updates.knulp.duckdns.org/*", - "https://mdns-browser-updates.knulp.v6.rocks/*" - ] - }, - "window": { - "all": true + "productName": "mnds-browser", + "version": "0.8.0", + "identifier": "io.github.hrzlgnm.mdns-browser", + "plugins": { + "updater": { + "windows": { + "installMode": "passive" }, - "process": { - "exit": true, - "relaunch": true - } - }, + "endpoints": [ + "https://mdns-browser-updates.knulp.duckdns.org/updates.json", + "https://mdns-browser-updates.knulp.v6.rocks/updates.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwQzYxQzI2MkI3NUI1RDgKUldUWXRYVXJKaHpHSUEvVmhlVGtNZW5HNXRNZ2xEREF3UkNtbXAxTW0zR0JJUVcveEhMZHFNMjgK" + } + }, + "app": { + "withGlobalTauri": true, "windows": [ { "title": "mDNS-Browser", @@ -62,37 +65,6 @@ ], "security": { "csp": null - }, - "updater": { - "active": true, - "endpoints": [ - "https://mdns-browser-updates.knulp.duckdns.org/updates.json", - "https://mdns-browser-updates.knulp.v6.rocks/updates.json" - ], - "dialog": true, - "windows": { - "installMode": "passive" - }, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDIwQzYxQzI2MkI3NUI1RDgKUldUWXRYVXJKaHpHSUEvVmhlVGtNZW5HNXRNZ2xEREF3UkNtbXAxTW0zR0JJUVcveEhMZHFNMjgK" - }, - "bundle": { - "active": true, - "targets": [ - "deb", - "appimage", - "nsis", - "dmg", - "app", - "updater" - ], - "identifier": "net.hrzlgnm.mdns-browser", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] } } } diff --git a/src/app.rs b/src/app.rs index 787b031..64051cd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,8 +12,8 @@ use leptos_meta::provide_meta_context; use leptos_meta::Style; use serde::{Deserialize, Serialize}; use strsim::jaro_winkler; +use tauri_sys::core::invoke; use tauri_sys::event::listen; -use tauri_sys::tauri::invoke; use thaw::{ AutoComplete, AutoCompleteOption, AutoCompleteSuffix, Button, ButtonSize, Card, CardFooter, CardHeaderExtra, Collapse, CollapseItem, GlobalStyle, Grid, GridItem, Icon, Layout, Modal, @@ -70,7 +70,7 @@ pub struct MetricsEventRes { } async fn invoke_unit(cmd: &str) { - let _: () = invoke(cmd, &()).await.unwrap(); + let _ = invoke::<()>(cmd, &()).await; } async fn listen_on_metrics_event(event_writer: WriteSignal>) { @@ -187,25 +187,23 @@ struct BrowseArgs<'a> { } async fn browse(service_type: String) { - let _: () = invoke( + let _ = invoke::<()>( "browse", &BrowseArgs { serviceType: &service_type, }, ) - .await - .unwrap(); + .await; } async fn stop_browse(service_type: String) { - let _: () = invoke( + let _ = invoke::<()>( "stop_browse", &BrowseArgs { serviceType: &service_type, }, ) - .await - .unwrap(); + .await; } /// Component to render a string vector into a table @@ -671,11 +669,11 @@ struct OpenArgs<'a> { } async fn open(url: &str) { - let _: () = invoke("open", &OpenArgs { url }).await.unwrap(); + let _ = invoke::<()>("open", &OpenArgs { url }).await; } async fn get_version(writer: WriteSignal) { - let ver: String = invoke("version", &()).await.unwrap(); + let ver = invoke::("version", &()).await; log::debug!("Got version {}", ver); writer.update(|v| *v = ver); }