mirror of
https://github.com/block/buzz.git
synced 2026-11-08 02:11:36 +00:00
fix(desktop): skip native notifications outside app bundles (#5004)
## Summary - require the macOS process to be running from an actual `.app` bundle before initializing `UNUserNotificationCenter` - keep the existing bundle-identifier requirement - cover packaged, case-insensitive `.app`, raw `target/debug`, and extensionless paths ## Why PR #4799 guarded native notification initialization with `NSBundle.mainBundle.bundleIdentifier != nil`. Tauri embeds a bundle identifier in raw development executables, so `tauri dev` passed that guard and `UNUserNotificationCenter.current()` raised an uncaught `NSInternalInconsistencyException` because LaunchServices had no bundle proxy. ## Validation - focused macOS notification tests: 6 passed - direct raw debug executable no longer raises the notification-center exception - pre-commit formatting hook passed - pre-push package checks passed on pushed commit `f29a6664d2a863e7b8aa527f6149fd00b183e4de` The first push attempt hit an unrelated timing-test failure in `relay_admission::tests::concurrent_429_extends_the_window_for_parked_waiters`; its focused rerun passed, and the complete pre-push package suite passed on the next push. Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
path::Path,
|
||||
ptr::NonNull,
|
||||
sync::{mpsc, Mutex, OnceLock},
|
||||
time::Duration,
|
||||
@@ -128,7 +129,7 @@ pub(crate) fn init(app: &AppHandle) -> tauri::Result<()> {
|
||||
// objc2 cannot turn that exception into a Rust error, so do not call
|
||||
// into the framework at all in this environment.
|
||||
eprintln!(
|
||||
"buzz-desktop: macOS notifications disabled because the process has no bundle identifier"
|
||||
"buzz-desktop: macOS notifications disabled because the process is not running from an app bundle"
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
@@ -293,7 +294,30 @@ pub(crate) fn take_pending_activations() -> Result<Vec<serde_json::Value>, Strin
|
||||
}
|
||||
|
||||
fn is_bundled_application() -> bool {
|
||||
NSBundle::mainBundle().bundleIdentifier().is_some()
|
||||
let bundle = NSBundle::mainBundle();
|
||||
bundle.bundleIdentifier().is_some()
|
||||
&& bundle.executablePath().is_some_and(|executable_path| {
|
||||
is_application_bundle_layout(
|
||||
Path::new(&bundle.bundlePath().to_string()),
|
||||
Path::new(&executable_path.to_string()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_application_bundle_layout(bundle_path: &Path, executable_path: &Path) -> bool {
|
||||
let Some(macos_path) = executable_path.parent() else {
|
||||
return false;
|
||||
};
|
||||
let Some(contents_path) = macos_path.parent() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
bundle_path
|
||||
.extension()
|
||||
.is_some_and(|extension| extension == "app")
|
||||
&& macos_path.file_name() == Some("MacOS".as_ref())
|
||||
&& contents_path.file_name() == Some("Contents".as_ref())
|
||||
&& contents_path.parent() == Some(bundle_path)
|
||||
}
|
||||
|
||||
fn target_from_response(response: &UNNotificationResponse) -> Option<serde_json::Value> {
|
||||
@@ -311,10 +335,12 @@ fn parse_target(serialized: &str) -> Option<serde_json::Value> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
is_bundled_application, parse_target, permission_state, queue_activation,
|
||||
take_pending_activations, NotificationPermissionState, MAX_PENDING_ACTIVATIONS,
|
||||
is_application_bundle_layout, is_bundled_application, parse_target, permission_state,
|
||||
queue_activation, take_pending_activations, NotificationPermissionState,
|
||||
MAX_PENDING_ACTIVATIONS,
|
||||
};
|
||||
use objc2_user_notifications::UNAuthorizationStatus;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn activation_queue_is_bounded_and_drained() {
|
||||
@@ -336,6 +362,26 @@ mod tests {
|
||||
assert!(!is_bundled_application());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requires_the_executable_to_use_the_app_bundle_layout() {
|
||||
assert!(is_application_bundle_layout(
|
||||
Path::new("/Applications/Buzz.app"),
|
||||
Path::new("/Applications/Buzz.app/Contents/MacOS/buzz-desktop"),
|
||||
));
|
||||
assert!(!is_application_bundle_layout(
|
||||
Path::new("/tmp/Fake.app"),
|
||||
Path::new("/tmp/Fake.app/buzz-desktop"),
|
||||
));
|
||||
assert!(!is_application_bundle_layout(
|
||||
Path::new("/Users/developer/buzz/desktop/src-tauri/target/debug"),
|
||||
Path::new("/Users/developer/buzz/desktop/src-tauri/target/debug/buzz-desktop"),
|
||||
));
|
||||
assert!(!is_application_bundle_layout(
|
||||
Path::new("/Applications/Buzz.app"),
|
||||
Path::new("/Applications/Other.app/Contents/MacOS/buzz-desktop"),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maps_native_authorization_states_to_frontend_contract() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user