Files
portmaster/desktop/tauri/src-tauri/src/portmaster/mod.rs

295 lines
9.6 KiB
Rust

/// This module contains a custom tauri plugin that handles all communication
/// with the angular app loaded from the portmaster api.
///
/// Using a custom-plugin for this has the advantage that all code that has
/// access to a tauri::Window or a tauri::AppHandle can get access to the
/// portmaster plugin using the Runtime/Manager extension by just calling
/// window.portmaster() or app_handle.portmaster().
///
/// Any portmaster related features (like changing a portmaster setting) should
/// live in this module.
///
/// Code that handles windows should NOT live here but should rather be placed
/// in the crate root.
// The commands module contains tauri commands that are available to Javascript
// using the invoke() and our custom invokeAsync() command.
mod commands;
// The websocket module spawns an async function on tauri's runtime that manages
// a persistent connection to the Portmaster websocket API and updates the tauri Portmaster
// Plugin instance.
mod websocket;
// The notification module manages system notifications from portmaster.
mod notifications;
use crate::portapi::{
client::PortAPI, message::Payload, models::config::BooleanValue, types::Request,
};
use std::{
collections::HashMap,
sync::atomic::{AtomicBool, Ordering},
};
use log::debug;
use serde;
use std::sync::Mutex;
use tauri::{
plugin::{Builder, TauriPlugin},
AppHandle, Manager, Runtime,
};
pub trait Handler {
fn on_connect(&mut self, cli: PortAPI) -> ();
fn on_disconnect(&mut self);
}
pub struct PortmasterPlugin<R: Runtime> {
#[allow(dead_code)]
app: AppHandle<R>,
// state allows the angular application to store arbitrary values in the
// tauri application memory using the get_state and set_state
// tauri::commands.
state: Mutex<HashMap<String, String>>,
// an atomic boolean that indicates if we're currently connected to
// portmaster or not.
is_reachable: AtomicBool,
// holds the portapi client if any.
api: Mutex<Option<PortAPI>>,
// a vector of handlers that should be invoked on connect and disconnect of
// the portmaster API.
handlers: Mutex<Vec<Box<dyn Handler + Send>>>,
// whether or not we should handle notifications here.
handle_notifications: AtomicBool,
// whether or not we should handle prompts.
handle_prompts: AtomicBool,
// whether or not the angular application should call window.show after it
// finished bootstrapping.
should_show_after_bootstrap: AtomicBool,
}
impl<R: Runtime> PortmasterPlugin<R> {
/// Returns a state stored in the portmaster plugin.
pub fn get_state(&self, key: String) -> Option<String> {
let map = self.state.lock();
if let Ok(map) = map {
match map.get(&key) {
Some(value) => Some(value.clone()),
None => None,
}
} else {
None
}
}
/// Adds a new state to the portmaster plugin.
pub fn set_state(&self, key: String, value: String) {
let map = self.state.lock();
if let Ok(mut map) = map {
map.insert(key, value);
}
}
/// Reports wheter or not we're currently connected to the Portmaster API.
pub fn is_reachable(&self) -> bool {
self.is_reachable.load(Ordering::Relaxed)
}
/// Registers a new connection handler that is called on connect
/// and disconnect of the Portmaster websocket API.
pub fn register_handler(&self, mut handler: impl Handler + Send + 'static) {
// register_handler can only be invoked after the plugin setup
// completed. in this case, the websocket thread is already spawned and
// we might already be connected or know that the connection failed.
// Call the respective handler method immediately now.
if let Some(api) = self.get_api() {
handler.on_connect(api);
} else {
handler.on_disconnect();
}
if let Ok(mut handlers) = self.handlers.lock() {
handlers.push(Box::new(handler));
}
}
/// Returns the current portapi client.
pub fn get_api(&self) -> Option<PortAPI> {
if let Ok(mut api) = self.api.lock() {
match &mut *api {
Some(api) => Some(api.clone()),
None => None,
}
} else {
None
}
}
/// Feature functions (enable/disable certain features).
/// Configures whether or not our tauri app should show system
/// notifications. This excludes connection prompts. Use
/// with_connection_prompts to enable handling of connection prompts.
pub fn with_notification_support(&self, enable: bool) {
self.handle_notifications.store(enable, Ordering::Relaxed);
// kick of the notification handler if we are connected.
if enable {
self.start_notification_handler();
}
}
/// Configures whether or not our angular application should show connection
/// prompts via tauri.
pub fn with_connection_prompts(&self, enable: bool) {
self.handle_prompts.store(enable, Ordering::Relaxed);
}
/// Whether or not the angular application should call window.show after it
/// finished bootstrapping.
pub fn set_show_after_bootstrap(&self, show: bool) {
self.should_show_after_bootstrap
.store(show, Ordering::Relaxed);
}
/// Returns whether or not the angular application should call window.show
/// after it finished bootstrapping.
pub fn get_show_after_bootstrap(&self) -> bool {
self.should_show_after_bootstrap.load(Ordering::Relaxed)
}
/// Tells the angular applicatoin to show the window by emitting an event.
/// It calls set_show_after_bootstrap(true) automatically so the application
/// also shows after bootstrapping.
pub fn show_window(&self) {
debug!("[tauri] showing main window");
// set show_after_bootstrap to true so the app will even show if it
// misses the event below because it's still bootstrapping.
self.set_show_after_bootstrap(true);
// ignore the error here, there's nothing we could do about it anyways.
let _ = self.app.emit("portmaster:show", "");
}
/// Enables or disables the SPN.
pub fn set_spn_enabled(&self, enabled: bool) {
if let Some(api) = self.get_api() {
let body: Result<Payload, serde_json::Error> = BooleanValue {
value: Some(enabled),
}
.try_into();
if let Ok(payload) = body {
tauri::async_runtime::spawn(async move {
_ = api
.request(Request::Update("config:spn/enable".to_string(), payload))
.await;
});
}
}
}
//// Internal functions
fn start_notification_handler(&self) {
if let Some(api) = self.get_api() {
let cli = api.clone();
tauri::async_runtime::spawn(async move {
notifications::notification_handler(cli).await;
});
}
}
/// Internal method to call all on_connect handlers
fn on_connect(&self, api: PortAPI) {
self.is_reachable.store(true, Ordering::Relaxed);
// store the new api client.
let mut guard = self.api.lock().unwrap();
*guard = Some(api.clone());
drop(guard);
// fire-off the notification handler.
if self.handle_notifications.load(Ordering::Relaxed) {
self.start_notification_handler();
}
if let Ok(mut handlers) = self.handlers.lock() {
for handler in handlers.iter_mut() {
handler.on_connect(api.clone());
}
}
}
/// Internal method to call all on_disconnect handlers
fn on_disconnect(&self) {
self.is_reachable.store(false, Ordering::Relaxed);
// clear the current api client reference.
let mut guard = self.api.lock().unwrap();
*guard = None;
drop(guard);
if let Ok(mut handlers) = self.handlers.lock() {
for handler in handlers.iter_mut() {
handler.on_disconnect();
}
}
}
}
pub trait PortmasterExt<R: Runtime> {
fn portmaster(&self) -> &PortmasterPlugin<R>;
}
#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub struct Config {}
impl<R: Runtime, T: Manager<R>> PortmasterExt<R> for T {
fn portmaster(&self) -> &PortmasterPlugin<R> {
self.state::<PortmasterPlugin<R>>().inner()
}
}
pub fn init<R: Runtime>() -> TauriPlugin<R, Option<Config>> {
Builder::<R, Option<Config>>::new("portmaster")
.invoke_handler(tauri::generate_handler![
commands::get_app_info,
commands::get_service_manager_status,
commands::start_service,
commands::get_state,
commands::set_state,
commands::should_show,
commands::should_handle_prompts
])
.setup(|app, _api| {
let plugin = PortmasterPlugin {
app: app.clone(),
state: Mutex::new(HashMap::new()),
is_reachable: AtomicBool::new(false),
handlers: Mutex::new(Vec::new()),
api: Mutex::new(None),
handle_notifications: AtomicBool::new(false),
handle_prompts: AtomicBool::new(false),
should_show_after_bootstrap: AtomicBool::new(true),
};
app.manage(plugin);
// fire of the websocket handler
websocket::start_websocket_thread(app.clone());
Ok(())
})
.build()
}