Skip to content

macOS: dragging a file out of an open NSOpenPanel intermittently terminates the event loop; any [NSApp run] return is treated as exit #1312

Description

@fmarmori

macOS: dragging a file out of an open NSOpenPanel intermittently terminates the event loop; any [NSApp run] return is treated as exit

Environment

  • tao 0.35.3 (via Tauri 2.11.5, tauri-plugin-dialog 2.7.2 → rfd 0.16.0)
  • macOS 26.5.2 (25F84), Apple Silicon (Mac17,9)

Summary

With a file-open dialog shown as a sheet (NSOpenPanel via rfd's beginSheetModalForWindow:completionHandler:), dragging a file out of the panel's file list and dropping it outside the app intermittently causes the app's event loop to terminate as if the app had quit: [NSApp run] returns, tao emits LoopDestroyed, and the process exits with code 0. No exit was requested by the application, and the panel's completion handler never fired.

It is a race; it does not reproduce every time, but reliably enough that end users hit it in production.

Steps to reproduce

  1. Tauri v2 app; open a file picker from the frontend (@tauri-apps/plugin-dialog open({ multiple: true }), which runs FileDialogBuilder::blocking_pick_files → rfd AsyncFileDialog → sheet on the main window).
  2. While the panel is open, drag a file from the panel's file list and drop it outside the app (e.g. on the Desktop).
  3. Intermittently: the app exits.

Evidence the exit is spurious

From a crash report captured at the moment of exit (our app aborts later in exit-time static destructors, which is unrelated to tao; the tao bug is the exit itself):

  • Main thread is in exit() after main returned; the loop ended and tao ran process::exit with code 0 (control flow was never ExitWithCode).
  • The dialog was still pending: one thread parked in FileDialogBuilder::blocking_pick_filesstd::sync::mpsc::Receiver::recv (the plugin's rendezvous channel; completion handler never ran), and the rfd ModalFuture thread still blocked on the future.
  • The app's RunEvent::ExitRequested was never observed; no app.exit() / request_exit call exists on this path.

Trimmed thread states:

Thread 0 (main):  start → exit → __cxa_finalize_ranges → (C++ static destructors)
Thread N:  tauri_plugin_dialog FileDialogBuilder::blocking_pick_files
           → std::sync::mpmc::...::recv  (completion never delivered)
Thread M:  rfd ModalFuture / tokio block_on (pick_files future, still pending)

Analysis

src/platform_impl/macos/event_loop.rs (run_return) treats any return of [NSApp run] as an intentional loop end:

AppState::set_callback(weak_cb, Rc::clone(&self.window_target));
let () = msg_send![&app, run];
...
AppState::exit()   // → emits LoopDestroyed → run() → process::exit(exit_code)

There is no check that tao itself requested the stop (AppState::clearedmsg_send![&app, stop: nil]). Per Apple's docs, -[NSApplication stop:] merely sets a flag that is consumed by whichever run-loop invocation next finishes dispatching an actual NSEvent; a modal panel plus a drag session run nested event loops (the modern NSOpenPanel is additionally hosted out-of-process via ViewBridge). When the drag/panel teardown interleaves badly, a stop intended for (or generated inside) the nested machinery is consumed by the outer [NSApp run], which returns; tao then tears everything down.

This failure class has history in winit that tao no longer carries:

  • winit #1591 ("dialogs immediately close with run_return") → winit PR #1581 added explicit dialog awareness: it refused to call stop: while a dialog window was open or closing (dialog_is_closing + INTERRUPT_EVENT_LOOP_EXIT).
  • winit PR #2292 removed that guard as superseded by the observer in-callback guard; tao ported the removal in Fix native file dialogs freezing the event loop, closes #437 #440 (tao 0.12.0). The observer guard prevents a different problem (re-entrant callbacks / freezes, winit #2027) but does nothing about a stray stop reaching the outer loop.
  • Related analysis of the runModal-vs-[NSApp run] interplay: winit #2752 / rfd feat: add minimizable and maximizable options #116.

Suggested direction

Track whether tao requested the stop (set a flag in the should_exit path immediately before msg_send![&app, stop: nil]). If [NSApp run] returns without that flag set, re-enter run instead of tearing down; alternatively, restore dialog awareness along the lines of winit PR #1581. Happy to test a patch against our reproduction.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions