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;
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<IAvnApplicationEvents> _events;
@ -85,7 +113,33 @@ ComPtr<IAvnApplicationEvents> _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<IAvnApplicationEvents> events(_events);
events->OnTerminating();
}
- (NSMenu *)applicationDockMenu:(NSApplication *)sender

2
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();

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

@ -8,5 +8,10 @@ namespace Avalonia.Controls.ApplicationLifetimes
/// Is the operating system shutting down
/// </summary>
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.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<ShutdownRequestedEventArgs>? 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;
}
}
}

15
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<MacOSPlatformOptions>() ?? 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();
}

10
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)]

Loading…
Cancel
Save