From 4fdc036368adcd71b118c9acf6d4934fccced23b Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 10 Sep 2026 07:53:25 +0000 Subject: [PATCH] macOS: Dispose COM platform object on native app termination (#22200) --- native/Avalonia.Native/src/OSX/app.mm | 56 ++++++++++++++++++- .../ClassicDesktopStyleApplicationLifetime.cs | 2 + .../ShutdownRequestedEventArgs.cs | 5 ++ .../AvaloniaNativeApplicationPlatform.cs | 33 ++++++++--- src/Avalonia.Native/AvaloniaNativePlatform.cs | 15 ++++- src/Avalonia.Native/avn.idl | 10 +++- 6 files changed, 110 insertions(+), 11 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 092bde9c07..459bf5f8f2 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -9,6 +9,34 @@ NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; static NSMenu* s_dockMenu = nil; +static bool IsOSShutdown() +{ + auto evt = [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]; + if ([evt eventClass] != kCoreEventClass || [evt eventID] != kAEQuitApplication) + return false; + + auto reason = [evt paramDescriptorForKeyword:kAEQuitReason]; + if (reason == nil) + reason = [evt attributeDescriptorForKeyword:kAEQuitReason]; + + auto reasonCode = [reason enumCodeValue]; + if (reasonCode == 0) + reasonCode = [reason typeCodeValue]; + + switch (reasonCode) + { + case kAELogOut: + case kAEReallyLogOut: + case kAEShowRestartDialog: + case kAERestart: + case kAEShowShutdownDialog: + case kAEShutDown: + return true; + default: + return false; + } +} + @implementation AvnAppDelegate ComPtr _events; @@ -85,7 +113,33 @@ ComPtr _events; - (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { - return _events->TryShutdown() ? NSTerminateNow : NSTerminateCancel; + switch (_events->TryShutdown(IsOSShutdown())) + { + case ShutdownReplyCancel: + return NSTerminateCancel; + + // The managed dispatcher loop is exiting: let it handle the termination instead. + case ShutdownReplyDeferToManagedLoop: + return NSTerminateCancel; + + case ShutdownReplyTerminateNow: + return NSTerminateNow; + + // Shouldn't happen + default: + return NSTerminateNow; + } +} + +- (void)applicationWillTerminate:(NSNotification *)notification +{ + if (!_events) + return; + + // The process is about to exit() so this is the last point where managed code can still safely be called. + // Keep the application events object alive for the duration of the call, it's about to be released by the managed side. + ComPtr events(_events); + events->OnTerminating(); } - (NSMenu *)applicationDockMenu:(NSApplication *)sender diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs index 741a6380bc..025075bdb0 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs @@ -227,6 +227,8 @@ namespace Avalonia.Controls.ApplicationLifetimes if (!shutdownCancelled) { + e.WillExitMainLoop = _cts is not null; + _cts?.Cancel(); _cts = null; Dispatcher.UIThread.InvokeShutdown(); diff --git a/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs index 4dd62936df..1047e46a45 100644 --- a/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs +++ b/src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs @@ -8,5 +8,10 @@ namespace Avalonia.Controls.ApplicationLifetimes /// Is the operating system shutting down /// internal bool IsOSShutdown { get; init; } + + /// + /// Indicates that the accepted shutdown will exit the main loop. + /// + internal bool WillExitMainLoop { get; set; } } } diff --git a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs index b05aa0576e..bed212a303 100644 --- a/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Native.Interop; @@ -10,7 +9,8 @@ using Avalonia.Platform.Storage.FileIO; namespace Avalonia.Native { - internal class AvaloniaNativeApplicationPlatform : NativeCallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl + internal class AvaloniaNativeApplicationPlatform(AvaloniaNativePlatform platform) + : NativeCallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl { public event EventHandler? ShutdownRequested; @@ -105,12 +105,31 @@ namespace Avalonia.Native } } - public int TryShutdown() + void IAvnApplicationEvents.OnTerminating() { - if (ShutdownRequested is null) return 1; - var e = new ShutdownRequestedEventArgs(); - ShutdownRequested(this, e); - return (!e.Cancel).AsComBool(); + // The OS is terminating us directly: AppDomain.ProcessExit won't run, dispose now. + platform.Dispose(); + } + + public AvnShutdownReply TryShutdown(int isOSShutdown) + { + if (ShutdownRequested is not { } shutdownRequested) + return AvnShutdownReply.ShutdownReplyTerminateNow; + + var isOSShutdownBool = isOSShutdown.FromComBool(); + var e = new ShutdownRequestedEventArgs { IsOSShutdown = isOSShutdownBool }; + shutdownRequested.Invoke(this, e); + + if (e.Cancel) + return AvnShutdownReply.ShutdownReplyCancel; + + // If we know the main loop is going to exit (e.g. via a ClassicDesktopApplicationLifetime), + // tell the native side it doesn't have to exit, allowing the managed side to complete its shutdown. + if (e.WillExitMainLoop && !isOSShutdownBool) + return AvnShutdownReply.ShutdownReplyDeferToManagedLoop; + + return AvnShutdownReply.ShutdownReplyTerminateNow; + } } } diff --git a/src/Avalonia.Native/AvaloniaNativePlatform.cs b/src/Avalonia.Native/AvaloniaNativePlatform.cs index 825eb254be..87c5cb9b70 100644 --- a/src/Avalonia.Native/AvaloniaNativePlatform.cs +++ b/src/Avalonia.Native/AvaloniaNativePlatform.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.InteropServices; +using System.Threading; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Input; @@ -14,11 +15,12 @@ using MicroCom.Runtime; namespace Avalonia.Native { - class AvaloniaNativePlatform : IWindowingPlatform + internal sealed class AvaloniaNativePlatform : IWindowingPlatform, IDisposable { private readonly IAvaloniaNativeFactory _factory; private AvaloniaNativePlatformOptions? _options; private IPlatformGraphics? _platformGraphics; + private int _isDisposed; [DllImport("libAvaloniaNative")] static extern IntPtr CreateAvaloniaNative(); @@ -94,7 +96,7 @@ namespace Avalonia.Native { _options = options; - var applicationPlatform = new AvaloniaNativeApplicationPlatform(); + var applicationPlatform = new AvaloniaNativeApplicationPlatform(this); var macOpts = AvaloniaLocator.Current.GetService() ?? new MacOSPlatformOptions(); @@ -191,6 +193,15 @@ namespace Avalonia.Native private void OnProcessExit(object? sender, EventArgs e) { + // Self-dispose on exit, ensuring COM objects are properly released before the CLR shuts down. + Dispose(); + } + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _isDisposed, 1, 0) != 0) + return; + AppDomain.CurrentDomain.ProcessExit -= OnProcessExit; _factory.Dispose(); } diff --git a/src/Avalonia.Native/avn.idl b/src/Avalonia.Native/avn.idl index ee46a46f8c..9a05794273 100644 --- a/src/Avalonia.Native/avn.idl +++ b/src/Avalonia.Native/avn.idl @@ -1237,17 +1237,25 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown void ReleaseChild(); } +enum AvnShutdownReply +{ + ShutdownReplyCancel, + ShutdownReplyTerminateNow, + ShutdownReplyDeferToManagedLoop +} + [uuid(6575b5af-f27a-4609-866c-f1f014c20f79)] interface IAvnApplicationEvents : IUnknown { void FilesOpened (IAvnStringArray* args); void UrlsOpened (IAvnStringArray* urls); - bool TryShutdown(); + AvnShutdownReply TryShutdown(bool isOSShutdown); void OnReopen (); void OnHide (); void OnUnhide (); void OnActivate(); void OnDeactivate(); + void OnTerminating(); } [uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)]