Skip to content

Commit 96ae141

Browse files
wesbillmanCarl
andauthored
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>
1 parent 38bf642 commit 96ae141

1 file changed

Lines changed: 50 additions & 4 deletions

File tree

desktop/src-tauri/src/macos_notifications.rs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
99
use std::{
1010
collections::VecDeque,
11+
path::Path,
1112
ptr::NonNull,
1213
sync::{mpsc, Mutex, OnceLock},
1314
time::Duration,
@@ -128,7 +129,7 @@ pub(crate) fn init(app: &AppHandle) -> tauri::Result<()> {
128129
// objc2 cannot turn that exception into a Rust error, so do not call
129130
// into the framework at all in this environment.
130131
eprintln!(
131-
"buzz-desktop: macOS notifications disabled because the process has no bundle identifier"
132+
"buzz-desktop: macOS notifications disabled because the process is not running from an app bundle"
132133
);
133134
return Ok(());
134135
}
@@ -293,7 +294,30 @@ pub(crate) fn take_pending_activations() -> Result<Vec<serde_json::Value>, Strin
293294
}
294295

295296
fn is_bundled_application() -> bool {
296-
NSBundle::mainBundle().bundleIdentifier().is_some()
297+
let bundle = NSBundle::mainBundle();
298+
bundle.bundleIdentifier().is_some()
299+
&& bundle.executablePath().is_some_and(|executable_path| {
300+
is_application_bundle_layout(
301+
Path::new(&bundle.bundlePath().to_string()),
302+
Path::new(&executable_path.to_string()),
303+
)
304+
})
305+
}
306+
307+
fn is_application_bundle_layout(bundle_path: &Path, executable_path: &Path) -> bool {
308+
let Some(macos_path) = executable_path.parent() else {
309+
return false;
310+
};
311+
let Some(contents_path) = macos_path.parent() else {
312+
return false;
313+
};
314+
315+
bundle_path
316+
.extension()
317+
.is_some_and(|extension| extension == "app")
318+
&& macos_path.file_name() == Some("MacOS".as_ref())
319+
&& contents_path.file_name() == Some("Contents".as_ref())
320+
&& contents_path.parent() == Some(bundle_path)
297321
}
298322

299323
fn target_from_response(response: &UNNotificationResponse) -> Option<serde_json::Value> {
@@ -311,10 +335,12 @@ fn parse_target(serialized: &str) -> Option<serde_json::Value> {
311335
#[cfg(test)]
312336
mod tests {
313337
use super::{
314-
is_bundled_application, parse_target, permission_state, queue_activation,
315-
take_pending_activations, NotificationPermissionState, MAX_PENDING_ACTIVATIONS,
338+
is_application_bundle_layout, is_bundled_application, parse_target, permission_state,
339+
queue_activation, take_pending_activations, NotificationPermissionState,
340+
MAX_PENDING_ACTIVATIONS,
316341
};
317342
use objc2_user_notifications::UNAuthorizationStatus;
343+
use std::path::Path;
318344

319345
#[test]
320346
fn activation_queue_is_bounded_and_drained() {
@@ -336,6 +362,26 @@ mod tests {
336362
assert!(!is_bundled_application());
337363
}
338364

365+
#[test]
366+
fn requires_the_executable_to_use_the_app_bundle_layout() {
367+
assert!(is_application_bundle_layout(
368+
Path::new("/Applications/Buzz.app"),
369+
Path::new("/Applications/Buzz.app/Contents/MacOS/buzz-desktop"),
370+
));
371+
assert!(!is_application_bundle_layout(
372+
Path::new("/tmp/Fake.app"),
373+
Path::new("/tmp/Fake.app/buzz-desktop"),
374+
));
375+
assert!(!is_application_bundle_layout(
376+
Path::new("/Users/developer/buzz/desktop/src-tauri/target/debug"),
377+
Path::new("/Users/developer/buzz/desktop/src-tauri/target/debug/buzz-desktop"),
378+
));
379+
assert!(!is_application_bundle_layout(
380+
Path::new("/Applications/Buzz.app"),
381+
Path::new("/Applications/Other.app/Contents/MacOS/buzz-desktop"),
382+
));
383+
}
384+
339385
#[test]
340386
fn maps_native_authorization_states_to_frontend_contract() {
341387
assert_eq!(

0 commit comments

Comments
 (0)