Browse Source

macOS: Dispose COM platform object on native app termination (#22200)

pull/21771/merge
Julien Lebosquain 3 weeks ago
committed by GitHub
parent
commit
4fdc036368
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 56
      native/Avalonia.Native/src/OSX/app.mm
  2. 2
      src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs
  3. 5
      src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs
  4. 33
      src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs
  5. 15
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  6. 10
      src/Avalonia.Native/avn.idl

56
native/Avalonia.Native/src/OSX/app.mm

@ -9,6 +9,34 @@
NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
static NSMenu* s_dockMenu = nil; 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 @implementation AvnAppDelegate
ComPtr<IAvnApplicationEvents> _events; ComPtr<IAvnApplicationEvents> _events;
@ -85,7 +113,33 @@ ComPtr<IAvnApplicationEvents> _events;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender - (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<IAvnApplicationEvents> events(_events);
events->OnTerminating();
} }
- (NSMenu *)applicationDockMenu:(NSApplication *)sender - (NSMenu *)applicationDockMenu:(NSApplication *)sender

2
src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs

@ -227,6 +227,8 @@ namespace Avalonia.Controls.ApplicationLifetimes
if (!shutdownCancelled) if (!shutdownCancelled)
{ {
e.WillExitMainLoop = _cts is not null;
_cts?.Cancel(); _cts?.Cancel();
_cts = null; _cts = null;
Dispatcher.UIThread.InvokeShutdown(); Dispatcher.UIThread.InvokeShutdown();

5
src/Avalonia.Controls/ApplicationLifetimes/ShutdownRequestedEventArgs.cs

@ -8,5 +8,10 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// Is the operating system shutting down /// Is the operating system shutting down
/// </summary> /// </summary>
internal bool IsOSShutdown { get; init; } internal bool IsOSShutdown { get; init; }
/// <summary>
/// Indicates that the accepted shutdown will exit the main loop.
/// </summary>
internal bool WillExitMainLoop { get; set; }
} }
} }

33
src/Avalonia.Native/AvaloniaNativeApplicationPlatform.cs

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
@ -10,7 +9,8 @@ using Avalonia.Platform.Storage.FileIO;
namespace Avalonia.Native namespace Avalonia.Native
{ {
internal class AvaloniaNativeApplicationPlatform : NativeCallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl internal class AvaloniaNativeApplicationPlatform(AvaloniaNativePlatform platform)
: NativeCallbackBase, IAvnApplicationEvents, IPlatformLifetimeEventsImpl
{ {
public event EventHandler<ShutdownRequestedEventArgs>? ShutdownRequested; public event EventHandler<ShutdownRequestedEventArgs>? ShutdownRequested;
@ -105,12 +105,31 @@ namespace Avalonia.Native
} }
} }
public int TryShutdown() void IAvnApplicationEvents.OnTerminating()
{ {
if (ShutdownRequested is null) return 1; // The OS is terminating us directly: AppDomain.ProcessExit won't run, dispose now.
var e = new ShutdownRequestedEventArgs(); platform.Dispose();
ShutdownRequested(this, e); }
return (!e.Cancel).AsComBool();
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;
} }
} }
} }

15
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Input;
@ -14,11 +15,12 @@ using MicroCom.Runtime;
namespace Avalonia.Native namespace Avalonia.Native
{ {
class AvaloniaNativePlatform : IWindowingPlatform internal sealed class AvaloniaNativePlatform : IWindowingPlatform, IDisposable
{ {
private readonly IAvaloniaNativeFactory _factory; private readonly IAvaloniaNativeFactory _factory;
private AvaloniaNativePlatformOptions? _options; private AvaloniaNativePlatformOptions? _options;
private IPlatformGraphics? _platformGraphics; private IPlatformGraphics? _platformGraphics;
private int _isDisposed;
[DllImport("libAvaloniaNative")] [DllImport("libAvaloniaNative")]
static extern IntPtr CreateAvaloniaNative(); static extern IntPtr CreateAvaloniaNative();
@ -94,7 +96,7 @@ namespace Avalonia.Native
{ {
_options = options; _options = options;
var applicationPlatform = new AvaloniaNativeApplicationPlatform(); var applicationPlatform = new AvaloniaNativeApplicationPlatform(this);
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions(); var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>() ?? new MacOSPlatformOptions();
@ -191,6 +193,15 @@ namespace Avalonia.Native
private void OnProcessExit(object? sender, EventArgs e) 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; AppDomain.CurrentDomain.ProcessExit -= OnProcessExit;
_factory.Dispose(); _factory.Dispose();
} }

10
src/Avalonia.Native/avn.idl

@ -1237,17 +1237,25 @@ interface IAvnNativeControlHostTopLevelAttachment : IUnknown
void ReleaseChild(); void ReleaseChild();
} }
enum AvnShutdownReply
{
ShutdownReplyCancel,
ShutdownReplyTerminateNow,
ShutdownReplyDeferToManagedLoop
}
[uuid(6575b5af-f27a-4609-866c-f1f014c20f79)] [uuid(6575b5af-f27a-4609-866c-f1f014c20f79)]
interface IAvnApplicationEvents : IUnknown interface IAvnApplicationEvents : IUnknown
{ {
void FilesOpened (IAvnStringArray* args); void FilesOpened (IAvnStringArray* args);
void UrlsOpened (IAvnStringArray* urls); void UrlsOpened (IAvnStringArray* urls);
bool TryShutdown(); AvnShutdownReply TryShutdown(bool isOSShutdown);
void OnReopen (); void OnReopen ();
void OnHide (); void OnHide ();
void OnUnhide (); void OnUnhide ();
void OnActivate(); void OnActivate();
void OnDeactivate(); void OnDeactivate();
void OnTerminating();
} }
[uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)] [uuid(b4284791-055b-4313-8c2e-50f0a8c72ce9)]

Loading…
Cancel
Save