feat(windows): add CLI flag to test a clickable update notification (#3526)

Looks a little odd in the Windows Server VM cause of the minimal desktop
environment, but it does open the browser, same as the "Sign In" button

![image](https://github.com/firezone/firezone/assets/13400041/772b755d-8291-44c4-9cb9-d0dca5c98f8e)

---------

Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com>
This commit is contained in:
Reactor Scram
2024-02-02 11:07:35 -06:00
committed by GitHub
parent b7294328e1
commit 1e596ce5d9
6 changed files with 99 additions and 35 deletions

28
rust/Cargo.lock generated
View File

@@ -2167,6 +2167,7 @@ dependencies = [
"tauri-build",
"tauri-runtime",
"tauri-utils",
"tauri-winrt-notification",
"thiserror",
"tokio",
"tracing",
@@ -3657,19 +3658,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mac-notification-sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51fca4d74ff9dbaac16a01b924bc3693fa2bba0862c2c633abc73f9a8ea21f64"
dependencies = [
"cc",
"dirs-next",
"objc-foundation",
"objc_id",
"time",
]
[[package]]
name = "mach2"
version = "0.4.2"
@@ -4053,19 +4041,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "notify-rust"
version = "4.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "827c5edfa80235ded4ab3fe8e9dc619b4f866ef16fe9b1c6b8a7f8692c0f2226"
dependencies = [
"log",
"mac-notification-sys",
"serde",
"tauri-winrt-notification",
"zbus",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@@ -6539,7 +6514,6 @@ dependencies = [
"heck 0.4.1",
"http 0.2.11",
"ignore",
"notify-rust",
"objc",
"once_cell",
"open",

View File

@@ -13,6 +13,10 @@ use std::path::PathBuf;
/// This should be identical to the `tauri.bundle.identifier` over in `tauri.conf.json`,
/// but sometimes I need to use this before Tauri has booted up, or in a place where
/// getting the Tauri app handle would be awkward.
///
/// Luckily this is also the AppUserModelId that Windows uses to label notifications,
/// so if your dev system has Firezone installed by MSI, the notifications will look right.
/// <https://learn.microsoft.com/en-us/windows/configuration/find-the-application-user-model-id-of-an-installed-app>
pub const BUNDLE_ID: &str = "dev.firezone.client";
/// Returns e.g. `C:/Users/User/AppData/Local/dev.firezone.client

View File

@@ -52,9 +52,10 @@ native-dialog = "0.7.0"
[target.'cfg(windows)'.dependencies]
# Tauri works fine on Linux, but it requires a lot of build-time deps like glib and gdk, so I've blocked it out for now.
tauri = { version = "1.5", features = [ "dialog", "notification", "shell-open-api", "system-tray" ] }
tauri = { version = "1.5", features = [ "dialog", "shell-open-api", "system-tray" ] }
tauri-runtime = "0.14.2"
tauri-utils = "1.5.1"
tauri-winrt-notification = "0.1.3"
winreg = "0.52.0"
wintun = "0.4.0"

View File

@@ -124,15 +124,23 @@ fn run_gui(cli: Cli) -> Result<()> {
Ok(result?)
}
/// The debug / test flags like `crash_on_purpose` and `test_update_notification`
/// don't propagate when we use `RunAs` to elevate ourselves. So those must be run
/// from an admin terminal, or with "Run as administrator" in the right-click menu.
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Cmd>,
/// If true, purposely crash the program to test the crash handler
#[arg(long, hide = true)]
crash_on_purpose: bool,
/// If true, slow down I/O operations to test how the GUI handles slow I/O
#[arg(long, hide = true)]
inject_faults: bool,
/// If true, show a fake update notification that opens the Firezone release page when clicked
#[arg(long, hide = true)]
test_update_notification: bool,
}
#[derive(clap::Subcommand)]

View File

@@ -14,7 +14,7 @@ use connlib_shared::{messages::ResourceId, windows::BUNDLE_ID};
use secrecy::{ExposeSecret, SecretString};
use std::{net::IpAddr, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
use system_tray_menu::Event as TrayMenuEvent;
use tauri::{api::notification::Notification, Manager, SystemTray, SystemTrayEvent};
use tauri::{Manager, SystemTray, SystemTrayEvent};
use tokio::sync::{mpsc, oneshot, Notify};
use ControllerRequest as Req;
@@ -53,12 +53,16 @@ impl Managed {
#[derive(Debug, thiserror::Error)]
pub(crate) enum Error {
#[error(r#"Couldn't show clickable notification titled "{0}""#)]
ClickableNotification(String),
#[error("Deep-link module error: {0}")]
DeepLink(#[from] deep_link::Error),
#[error("Can't show log filter error dialog: {0}")]
LogFilterErrorDialog(native_dialog::Error),
#[error("Logging module error: {0}")]
Logging(#[from] logging::Error),
#[error(r#"Couldn't show notification titled "{0}""#)]
Notification(String),
#[error(transparent)]
Tauri(#[from] tauri::Error),
#[error("tokio::runtime::Runtime::new failed: {0}")]
@@ -117,6 +121,9 @@ pub(crate) fn run(cli: client::Cli) -> Result<(), Error> {
let rt = tokio::runtime::Runtime::new().map_err(Error::TokioRuntimeNew)?;
let _guard = rt.enter();
let (ctlr_tx, ctlr_rx) = mpsc::channel(5);
let notify_controller = Arc::new(Notify::new());
if cli.crash_on_purpose {
tokio::spawn(async {
let delay = 10;
@@ -129,8 +136,17 @@ pub(crate) fn run(cli: client::Cli) -> Result<(), Error> {
});
}
let (ctlr_tx, ctlr_rx) = mpsc::channel(5);
let notify_controller = Arc::new(Notify::new());
if cli.test_update_notification {
// TODO: Clicking doesn't work if the notification times out and hides first.
// See docs for `show_clickable_notification`.
show_clickable_notification(
"Firezone update",
"Click here to open the release page.",
ctlr_tx.clone(),
Req::NotificationClicked,
)?;
}
// Make sure we're single-instance
// We register our deep links to call the `open-deep-link` subcommand,
@@ -278,6 +294,7 @@ pub(crate) enum ControllerRequest {
DisconnectedTokenExpired,
ExportLogs { path: PathBuf, stem: PathBuf },
GetAdvancedSettings(oneshot::Sender<AdvancedSettings>),
NotificationClicked,
SchemeRequest(url::Url),
SystemTrayMenu(TrayMenuEvent),
TunnelReady,
@@ -597,6 +614,14 @@ async fn run_controller(
Req::GetAdvancedSettings(tx) => {
tx.send(controller.advanced_settings.clone()).ok();
}
Req::NotificationClicked => {
tracing::info!("NotificationClicked in run_controller!");
tauri::api::shell::open(
&app.shell_scope(),
"https://example.com/notification_clicked",
None,
)?;
}
Req::SchemeRequest(url) => if let Err(e) = controller.handle_deep_link(&url).await {
tracing::error!("couldn't handle deep link: {e:#?}");
}
@@ -644,10 +669,57 @@ async fn run_controller(
///
/// May say "Windows Powershell" and have the wrong icon in dev mode
/// See <https://github.com/tauri-apps/tauri/issues/3700>
fn show_notification(title: &str, body: &str) -> Result<()> {
Notification::new(BUNDLE_ID)
fn show_notification(title: &str, body: &str) -> Result<(), Error> {
tauri_winrt_notification::Toast::new(BUNDLE_ID)
.title(title)
.body(body)
.show()?;
.text1(body)
.show()
.map_err(|_| Error::Notification(title.to_string()))?;
Ok(())
}
/// Show a notification that signals `Controller` when clicked
///
/// May say "Windows Powershell" and have the wrong icon in dev mode
/// See <https://github.com/tauri-apps/tauri/issues/3700>
///
/// Known issue: If the notification times out and goes into the notification center
/// (the little thing that pops up when you click the bell icon), then we may not get the
/// click signal.
///
/// I've seen this reported by people using Powershell, C#, etc., so I think it might
/// be a Windows bug?
/// - <https://superuser.com/questions/1488763/windows-10-notifications-not-activating-the-associated-app-when-clicking-on-it>
/// - <https://stackoverflow.com/questions/65835196/windows-toast-notification-com-not-working>
/// - <https://answers.microsoft.com/en-us/windows/forum/all/notifications-not-activating-the-associated-app/7a3b31b0-3a20-4426-9c88-c6e3f2ac62c6>
///
/// Firefox doesn't have this problem. Maybe they're using a different API.
fn show_clickable_notification(
title: &str,
body: &str,
tx: CtlrTx,
req: ControllerRequest,
) -> Result<(), Error> {
// For some reason `on_activated` is FnMut
let mut req = Some(req);
tauri_winrt_notification::Toast::new(BUNDLE_ID)
.title(title)
.text1(body)
.scenario(tauri_winrt_notification::Scenario::Reminder)
.on_activated(move || {
if let Some(req) = req.take() {
if let Err(error) = tx.blocking_send(req) {
tracing::error!(
?error,
"User clicked on notification, but we couldn't tell `Controller`"
);
}
}
Ok(())
})
.show()
.map_err(|_| Error::ClickableNotification(title.to_string()))?;
Ok(())
}

View File

@@ -1,3 +1,8 @@
//! Code for the Windows notification area
//!
//! "Notification Area" is Microsoft's official name instead of "System tray":
//! <https://learn.microsoft.com/en-us/windows/win32/shell/notification-area?redirectedfrom=MSDN#notifications-and-the-notification-area>
use connlib_client_shared::ResourceDescription;
use std::str::FromStr;
use tauri::{CustomMenuItem, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu};