From f62d943de68cd47f51f219a8096fd2b0d456a327 Mon Sep 17 00:00:00 2001 From: Karem Date: Sun, 14 Jun 2026 15:22:46 +0300 Subject: [PATCH] feat(notify): file-based diagnostic logging for the Windows toast issue (#3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.3.2 registered the AUMID (the reporter confirmed Get-StartApps + the HKCU key) but toasts still don't render on Windows 11, and there's no signal as to why: a GUI-subsystem build has no console, so the v0.3.2 eprintln! went nowhere (the reporter's stdout/stderr redirect captured nothing). Worse, the notification plugin's show() dispatches the toast on a detached task and discards its result, so the plugin can never report a failure upward. Add a tiny dlog module that writes a per-launch (truncated) session log to %LOCALAPPDATA%\whatRust\logs\whatrust.log (and the XDG data dir on Linux), and record the notification control flow: - aumid::register — the real, un-swallowed HRESULTs of the registry write and SetCurrentProcessExplicitAppUserModelID; - commands::notify — whether the command is invoked at all and whether a gate (locked / notifications-disabled) suppressed it; - notify::show — that dispatch was reached. No message titles/bodies are logged (no PII). This disambiguates the remaining hypotheses in one build: if "commands::notify invoked" never appears when a message arrives, the page used a path our window.Notification shim doesn't intercept (e.g. the service-worker showNotification); if it appears but no toast shows, the failure is in the Windows toast layer. Pairs with a standalone PowerShell toast test (posted on #3) that checks whether the AUMID can render a banner independent of the app. No speculative shortcut/COM code until the logs tell us where it breaks. Ships as v0.3.3. Co-Authored-By: Claude Opus 4.8 (1M context) --- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/aumid.rs | 16 +++++++++--- src-tauri/src/commands.rs | 8 ++++++ src-tauri/src/dlog.rs | 54 +++++++++++++++++++++++++++++++++++++++ src-tauri/src/lib.rs | 6 +++++ src-tauri/src/notify.rs | 15 ++++++----- src-tauri/tauri.conf.json | 2 +- 8 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 src-tauri/src/dlog.rs diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 851efd7..8992b5a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4695,7 +4695,7 @@ dependencies = [ [[package]] name = "whatrust" -version = "0.3.2" +version = "0.3.3" dependencies = [ "argon2", "block2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 98e4722..9e11598 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "whatrust" -version = "0.3.2" +version = "0.3.3" edition = "2021" rust-version = "1.82" diff --git a/src-tauri/src/aumid.rs b/src-tauri/src/aumid.rs index 2746e78..72b42f8 100644 --- a/src-tauri/src/aumid.rs +++ b/src-tauri/src/aumid.rs @@ -52,14 +52,22 @@ mod win { pub fn register(aumid: &str, display_name: &str) { // Step 1 — the registry entry is what makes the Action Center render // the toast for an unpackaged desktop app. - if let Err(e) = write_registry(aumid, display_name) { - eprintln!("[whatrust] AUMID registry registration failed: {e:?}"); + match write_registry(aumid, display_name) { + Ok(()) => crate::dlog::log(&format!( + "aumid: HKCU\\Software\\Classes\\AppUserModelId\\{aumid} registered" + )), + Err(e) => crate::dlog::log(&format!("aumid: registry registration FAILED: {e:?}")), } // Step 2 — pin this process to the AUMID (taskbar grouping + toast // attribution). Harmless if it fails; we just log. let id = wide(aumid); - if let Err(e) = unsafe { SetCurrentProcessExplicitAppUserModelID(PCWSTR(id.as_ptr())) } { - eprintln!("[whatrust] SetCurrentProcessExplicitAppUserModelID failed: {e:?}"); + match unsafe { SetCurrentProcessExplicitAppUserModelID(PCWSTR(id.as_ptr())) } { + Ok(()) => crate::dlog::log(&format!( + "aumid: SetCurrentProcessExplicitAppUserModelID({aumid}) ok" + )), + Err(e) => crate::dlog::log(&format!( + "aumid: SetCurrentProcessExplicitAppUserModelID FAILED: {e:?}" + )), } } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 40b7956..4564626 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -22,13 +22,21 @@ fn is_remote_label(label: &str) -> bool { #[tauri::command] pub fn notify(window: tauri::Window, app: tauri::AppHandle, title: String, body: String) { + // issue #3 diagnostics: confirm the command is actually reached from the + // injected bridge. If this line never appears in the log when a message + // arrives, the page never called our Notification shim (e.g. it used the + // service-worker showNotification path), not the OS toast layer. No message + // content is logged (PII) — only that an event occurred. + crate::dlog::log("commands::notify invoked"); // While locked, suppress notifications entirely so message previews don't leak // to the OS notification center / lock screen. The tray unread badge still updates // via set_unread (a count only, no content). if !crate::lock::is_unlocked(&app) { + crate::dlog::log("commands::notify suppressed: app is locked"); return; } if !crate::settings::load(&app).notifications { + crate::dlog::log("commands::notify suppressed: notifications disabled in settings"); return; } // Prefix the account name when more than one account exists, so notifications diff --git a/src-tauri/src/dlog.rs b/src-tauri/src/dlog.rs new file mode 100644 index 0000000..9183a75 --- /dev/null +++ b/src-tauri/src/dlog.rs @@ -0,0 +1,54 @@ +//! Minimal diagnostic log to a file under the app-data dir, so failures are +//! visible even on a Windows GUI-subsystem build (which has no attached +//! console — `eprintln!` goes nowhere there). Added for issue #3 (Windows +//! toast notifications not appearing). +//! +//! - Windows: `%LOCALAPPDATA%\whatRust\logs\whatrust.log` +//! - Linux: `$XDG_DATA_HOME/whatRust/logs/whatrust.log` (or `~/.local/share/...`) +//! +//! Truncated once per launch via [`init`], appended thereafter, so it stays +//! bounded to a single session. Best-effort: never panics. It records control +//! flow and error codes only — never message titles or bodies (no PII). + +use std::io::Write; +use std::path::PathBuf; +use std::sync::Mutex; + +/// Serializes writes across the per-account notification threads. +static LOCK: Mutex<()> = Mutex::new(()); + +fn log_path() -> Option { + #[cfg(windows)] + let base: Option = std::env::var_os("LOCALAPPDATA").map(PathBuf::from); + #[cfg(not(windows))] + let base: Option = std::env::var_os("XDG_DATA_HOME") + .map(PathBuf::from) + .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/share"))); + + let dir = base?.join("whatRust").join("logs"); + std::fs::create_dir_all(&dir).ok()?; + Some(dir.join("whatrust.log")) +} + +/// Start a fresh log for this launch (truncate). Best-effort. +pub fn init() { + if let Some(p) = log_path() { + let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let _ = std::fs::File::create(&p); + } + log("=== session start ==="); +} + +/// Append one line. Best-effort: silently does nothing if the path or write +/// fails, and never panics (mutex poison is recovered). +pub fn log(msg: &str) { + let Some(p) = log_path() else { return }; + let ts = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let _g = LOCK.lock().unwrap_or_else(|e| e.into_inner()); + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&p) { + let _ = writeln!(f, "[{ts}] {msg}"); + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index fcb2bb8..95f8f98 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -9,6 +9,7 @@ mod tray; mod commands; mod notify; mod aumid; +mod dlog; use tauri::Manager; @@ -94,6 +95,11 @@ pub fn run() { .setup(|app| { let handle = app.handle(); + // Start a fresh diagnostic log for this launch (issue #3): the only + // way to see notification failures on a Windows GUI build with no + // console. See dlog.rs. + dlog::init(); + // Windows: register our AppUserModelID so WinRT toast notifications // actually render for the installed app (no-op elsewhere). Must run // before any account window can fire a notification. See aumid.rs. diff --git a/src-tauri/src/notify.rs b/src-tauri/src/notify.rs index dd9ee91..703646d 100644 --- a/src-tauri/src/notify.rs +++ b/src-tauri/src/notify.rs @@ -2,10 +2,13 @@ use tauri::AppHandle; use tauri_plugin_notification::NotificationExt; pub fn show(app: &AppHandle, title: &str, body: &str) { - // Don't silently swallow failures: on Windows a toast can fail (e.g. an - // unregistered AppUserModelID — see aumid.rs) and the only signal is this - // Result. Logging it makes such failures diagnosable from the console. - if let Err(e) = app.notification().builder().title(title).body(body).show() { - eprintln!("[whatrust] failed to show notification: {e:?}"); - } + // NOTE: tauri-plugin-notification's `show()` dispatches the real toast on a + // detached async task and discards its result, so the value returned here is + // effectively always Ok — it does NOT reflect whether the OS actually + // rendered the toast. We still log that this point was reached (issue #3 + // diagnostics): if "notify::show dispatched" appears in the log but no toast + // shows, the failure is downstream in the Windows toast layer, not in our + // command/IPC path. No message content is logged (PII). + let r = app.notification().builder().title(title).body(body).show(); + crate::dlog::log(&format!("notify::show dispatched (plugin returned {r:?})")); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index b308f2c..a06ae30 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "whatRust", - "version": "0.3.2", + "version": "0.3.3", "identifier": "com.karem.whatrust", "build": { "frontendDist": "../settings-ui"