diff --git a/Cargo.lock b/Cargo.lock index 64fd52c730..0df20916de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4123,6 +4123,7 @@ dependencies = [ "tauri-plugin-listener2", "tauri-plugin-local-stt", "tauri-plugin-misc", + "tauri-plugin-network", "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-os", @@ -15457,6 +15458,22 @@ dependencies = [ "vergen-gix", ] +[[package]] +name = "tauri-plugin-network" +version = "0.1.0" +dependencies = [ + "ractor", + "ractor-supervisor", + "reqwest 0.12.24", + "serde", + "specta", + "specta-typescript", + "tauri", + "tauri-plugin", + "tauri-specta", + "tracing", +] + [[package]] name = "tauri-plugin-notification" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9469c0f339..40447fa9b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ tauri-plugin-listener2 = { path = "plugins/listener2" } tauri-plugin-local-llm = { path = "plugins/local-llm" } tauri-plugin-local-stt = { path = "plugins/local-stt" } tauri-plugin-misc = { path = "plugins/misc" } +tauri-plugin-network = { path = "plugins/network" } tauri-plugin-notification = { path = "plugins/notification" } tauri-plugin-permissions = { path = "plugins/permissions" } tauri-plugin-sfx = { path = "plugins/sfx" } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index f8911d2ff6..fe7d88521c 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ tauri-plugin-listener = { workspace = true } tauri-plugin-listener2 = { workspace = true } tauri-plugin-local-stt = { workspace = true } tauri-plugin-misc = { workspace = true } +tauri-plugin-network = { workspace = true } tauri-plugin-notification = { workspace = true } tauri-plugin-opener = { workspace = true } tauri-plugin-os = { workspace = true } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 79377f9b4b..d82c0e0ec4 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -90,6 +90,13 @@ pub async fn main() { .map(|ctx| ctx.supervisor.get_cell()), }, )) + .plugin(tauri_plugin_network::init( + tauri_plugin_network::InitOptions { + parent_supervisor: root_supervisor_ctx + .as_ref() + .map(|ctx| ctx.supervisor.get_cell()), + }, + )) .plugin(tauri_plugin_autostart::init( tauri_plugin_autostart::MacosLauncher::LaunchAgent, Some(vec!["--background"]), diff --git a/plugins/network/Cargo.toml b/plugins/network/Cargo.toml new file mode 100644 index 0000000000..100f9a064b --- /dev/null +++ b/plugins/network/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tauri-plugin-network" +version = "0.1.0" +authors = ["You"] +edition = "2021" +exclude = ["/js", "/node_modules"] +links = "tauri-plugin-network" +description = "" + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +specta-typescript = { workspace = true } + +[dependencies] +tauri = { workspace = true, features = ["specta"] } +tauri-specta = { workspace = true, features = ["derive", "typescript"] } + +serde = { workspace = true } +specta = { workspace = true } + +ractor = { workspace = true } +ractor-supervisor = { workspace = true } + +reqwest = { workspace = true } +tracing = { workspace = true } diff --git a/plugins/network/build.rs b/plugins/network/build.rs new file mode 100644 index 0000000000..a220cd4b25 --- /dev/null +++ b/plugins/network/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &[]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/plugins/network/src/actor.rs b/plugins/network/src/actor.rs new file mode 100644 index 0000000000..f4d375d726 --- /dev/null +++ b/plugins/network/src/actor.rs @@ -0,0 +1,97 @@ +use ractor::concurrency::Duration; +use ractor::{Actor, ActorProcessingErr, ActorRef}; +use tauri_specta::Event; + +use crate::event::NetworkStatusEvent; + +const CHECK_INTERVAL: Duration = Duration::from_secs(2); +const CHECK_URL: &str = "https://www.google.com/generate_204"; +const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); + +pub const NETWORK_ACTOR_NAME: &str = "network_actor"; + +pub enum NetworkMsg { + Check, +} + +pub struct NetworkArgs { + pub app: tauri::AppHandle, +} + +pub struct NetworkState { + app: tauri::AppHandle, + is_online: bool, +} + +pub struct NetworkActor; + +impl NetworkActor { + pub fn name() -> ractor::ActorName { + NETWORK_ACTOR_NAME.into() + } +} + +#[ractor::async_trait] +impl Actor for NetworkActor { + type Msg = NetworkMsg; + type State = NetworkState; + type Arguments = NetworkArgs; + + async fn pre_start( + &self, + myself: ActorRef, + args: Self::Arguments, + ) -> Result { + schedule_check(myself); + + Ok(NetworkState { + app: args.app, + is_online: true, + }) + } + + async fn handle( + &self, + myself: ActorRef, + message: Self::Msg, + state: &mut Self::State, + ) -> Result<(), ActorProcessingErr> { + match message { + NetworkMsg::Check => { + let is_online = check_network().await; + + if is_online != state.is_online { + state.is_online = is_online; + + let event = NetworkStatusEvent { is_online }; + if let Err(e) = event.emit(&state.app) { + tracing::error!(?e, "failed_to_emit_network_status_event"); + } + } + + schedule_check(myself); + } + } + Ok(()) + } +} + +fn schedule_check(actor: ActorRef) { + ractor::time::send_after(CHECK_INTERVAL, actor.get_cell(), || NetworkMsg::Check); +} + +async fn check_network() -> bool { + let client = reqwest::Client::builder() + .timeout(REQUEST_TIMEOUT.into()) + .build(); + + let client = match client { + Ok(c) => c, + Err(_) => return false, + }; + + match client.head(CHECK_URL).send().await { + Ok(response) => response.status().is_success() || response.status().as_u16() == 204, + Err(_) => false, + } +} diff --git a/plugins/network/src/event.rs b/plugins/network/src/event.rs new file mode 100644 index 0000000000..e5ff2b8515 --- /dev/null +++ b/plugins/network/src/event.rs @@ -0,0 +1,4 @@ +#[derive(serde::Serialize, Clone, specta::Type, tauri_specta::Event)] +pub struct NetworkStatusEvent { + pub is_online: bool, +} diff --git a/plugins/network/src/lib.rs b/plugins/network/src/lib.rs new file mode 100644 index 0000000000..4ba6d26564 --- /dev/null +++ b/plugins/network/src/lib.rs @@ -0,0 +1,76 @@ +use ractor::{Actor, ActorCell}; +use tauri::Manager; + +mod actor; +pub mod event; + +pub use actor::*; +pub use event::*; + +const PLUGIN_NAME: &str = "network"; + +#[derive(Default)] +pub struct InitOptions { + pub parent_supervisor: Option, +} + +fn make_specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .events(tauri_specta::collect_events![NetworkStatusEvent]) + .error_handling(tauri_specta::ErrorHandlingMode::Result) +} + +pub fn init(options: InitOptions) -> tauri::plugin::TauriPlugin { + let specta_builder = make_specta_builder(); + + tauri::plugin::Builder::new(PLUGIN_NAME) + .invoke_handler(specta_builder.invoke_handler()) + .setup(move |app, _api| { + specta_builder.mount_events(app); + + let app_handle = app.app_handle().clone(); + let parent = options.parent_supervisor.clone(); + + tauri::async_runtime::spawn(async move { + match Actor::spawn( + Some(NetworkActor::name()), + NetworkActor, + NetworkArgs { app: app_handle }, + ) + .await + { + Ok((actor_ref, _)) => { + if let Some(parent_cell) = parent { + actor_ref.get_cell().link(parent_cell); + } + tracing::info!("network_actor_spawned"); + } + Err(e) => { + tracing::error!(?e, "failed_to_spawn_network_actor"); + } + } + }); + + Ok(()) + }) + .build() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn export_types() { + make_specta_builder::() + .export( + specta_typescript::Typescript::default() + .header("// @ts-nocheck\n\n") + .formatter(specta_typescript::formatter::prettier) + .bigint(specta_typescript::BigIntExportBehavior::Number), + "./js/bindings.gen.ts", + ) + .unwrap() + } +}