diff --git a/src/Avalonia.X11/ICELib.cs b/src/Avalonia.X11/ICELib.cs new file mode 100644 index 0000000000..8ef21dd000 --- /dev/null +++ b/src/Avalonia.X11/ICELib.cs @@ -0,0 +1,60 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.X11 +{ + internal static class ICELib + { + private const string LibIce = "libICE.so.6"; + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern int IceAddConnectionWatch( + IntPtr watchProc, + IntPtr clientData + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern void IceRemoveConnectionWatch( + IntPtr watchProc, + IntPtr clientData + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern IceProcessMessagesStatus IceProcessMessages( + IntPtr iceConn, + out IntPtr replyWait, + out bool replyReadyRet + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr IceSetErrorHandler( + IntPtr handler + ); + + [DllImport(LibIce, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr IceSetIOErrorHandler( + IntPtr handler + ); + + public enum IceProcessMessagesStatus + { + IceProcessMessagesIoError = 1 + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void IceErrorHandler( + IntPtr iceConn, + bool swap, + int offendingMinorOpcode, + ulong offendingSequence, + int errorClass, + int severity, + IntPtr values + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void IceIOErrorHandler( + IntPtr iceConn + ); + } +} diff --git a/src/Avalonia.X11/SMLib.cs b/src/Avalonia.X11/SMLib.cs new file mode 100644 index 0000000000..e4114823dc --- /dev/null +++ b/src/Avalonia.X11/SMLib.cs @@ -0,0 +1,134 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.X11 +{ + internal static unsafe class SMLib + { + private const string LibSm = "libSM.so.6"; + + [DllImport(LibSm, CharSet = CharSet.Ansi)] + public static extern IntPtr SmcOpenConnection( + [MarshalAs(UnmanagedType.LPWStr)] string networkId, + IntPtr content, + int xsmpMajorRev, + int xsmpMinorRev, + ulong mask, + ref SmcCallbacks callbacks, + [MarshalAs(UnmanagedType.LPWStr)] [Out] + out string previousId, + [MarshalAs(UnmanagedType.LPWStr)] [Out] + out string clientIdRet, + int errorLength, + [Out] char[] errorStringRet); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern int SmcCloseConnection( + IntPtr smcConn, + int count, + string[] reasonMsgs + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern void SmcSaveYourselfDone( + IntPtr smcConn, + bool success + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern int SmcInteractRequest( + IntPtr smcConn, + SmDialogValue dialogType, + IntPtr interactProc, + IntPtr clientData + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern void SmcInteractDone( + IntPtr smcConn, + bool success + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr SmcGetIceConnection( + IntPtr smcConn + ); + + [DllImport(LibSm, CallingConvention = CallingConvention.StdCall)] + public static extern IntPtr SmcSetErrorHandler( + IntPtr handler + ); + + public enum SmDialogValue + { + SmDialogError = 0 + } + + [StructLayout(LayoutKind.Sequential)] + public struct SmcCallbacks + { + public IntPtr SaveYourself; + private readonly IntPtr Unused0; + public IntPtr Die; + private readonly IntPtr Unused1; + public IntPtr SaveComplete; + private readonly IntPtr Unused2; + public IntPtr ShutdownCancelled; + private readonly IntPtr Unused3; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void IceWatchProc( + IntPtr iceConn, + IntPtr clientData, + bool opening, + IntPtr* watchData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcDieProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcInteractProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcSaveCompleteProc( + IntPtr smcConn, + IntPtr clientData + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcSaveYourselfProc( + IntPtr smcConn, + IntPtr clientData, + int saveType, + bool shutdown, + int interactStyle, + bool fast + ); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcShutdownCancelledProc( + IntPtr smcConn, + IntPtr clientData + ); + + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void SmcErrorHandler( + IntPtr smcConn, + bool swap, + int offendingMinorOpcode, + ulong offendingSequence, + int errorClass, + int severity, + IntPtr values + ); + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 8ff3b4f5e0..baa097a422 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -80,7 +80,8 @@ namespace Avalonia.X11 .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) .Bind().ToConstant(new GtkSystemDialog()) - .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()); + .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) + .Bind().ToConstant(new X11PlatformLifetimeEvents()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs new file mode 100644 index 0000000000..4e20f68451 --- /dev/null +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -0,0 +1,246 @@ +#nullable enable +using System; +using System.Collections.Concurrent; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Logging; +using Avalonia.Platform; + +namespace Avalonia.X11 +{ + public unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl + { + private const ulong SmcSaveYourselfProcMask = 1L; + private const ulong SmcDieProcMask = 2L; + private const ulong SmcSaveCompleteProcMask = 4L; + private const ulong SmcShutdownCancelledProcMask = 8L; + + private static readonly ConcurrentDictionary s_nativeToManagedMapper = new ConcurrentDictionary(); + + private static readonly SMLib.SmcSaveYourselfProc s_saveYourselfProcDelegate = SmcSaveYourselfHandler; + private static readonly SMLib.SmcDieProc s_dieDelegate = SmcDieHandler; + + private static readonly SMLib.SmcShutdownCancelledProc + s_shutdownCancelledDelegate = SmcShutdownCancelledHandler; + + private static readonly SMLib.SmcSaveCompleteProc s_saveCompleteDelegate = SmcSaveCompleteHandler; + private static readonly SMLib.SmcInteractProc s_smcInteractDelegate = StaticInteractHandler; + private static readonly SMLib.SmcErrorHandler s_smcErrorHandlerDelegate = StaticErrorHandler; + private static readonly ICELib.IceErrorHandler s_iceErrorHandlerDelegate = StaticErrorHandler; + private static readonly ICELib.IceIOErrorHandler s_iceIoErrorHandlerDelegate = StaticIceIOErrorHandler; + private static readonly SMLib.IceWatchProc s_iceWatchProcDelegate = IceWatchHandler; + + private static SMLib.SmcCallbacks s_callbacks = new SMLib.SmcCallbacks() + { + ShutdownCancelled = Marshal.GetFunctionPointerForDelegate(s_shutdownCancelledDelegate), + Die = Marshal.GetFunctionPointerForDelegate(s_dieDelegate), + SaveYourself = Marshal.GetFunctionPointerForDelegate(s_saveYourselfProcDelegate), + SaveComplete = Marshal.GetFunctionPointerForDelegate(s_saveCompleteDelegate) + }; + + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private readonly IntPtr _currentIceConn; + private readonly IntPtr _currentSmcConn; + + private bool _saveYourselfPhase; + + public X11PlatformLifetimeEvents() + { + if (ICELib.IceAddConnectionWatch( + Marshal.GetFunctionPointerForDelegate(s_iceWatchProcDelegate), + IntPtr.Zero) == 0) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib was unable to add an ICE connection watcher."); + return; + } + + var errorBuf = new char[255]; + + var smcConn = SMLib.SmcOpenConnection(null!, + IntPtr.Zero, 1, 0, + SmcSaveYourselfProcMask | + SmcSaveCompleteProcMask | + SmcShutdownCancelledProcMask | + SmcDieProcMask, + ref s_callbacks, + out _, + out _, + errorBuf.Length, + errorBuf); + + if (smcConn == IntPtr.Zero) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + $"SMLib/ICELib reported a new error: {new string(errorBuf)}"); + return; + } + + if (!s_nativeToManagedMapper.TryAdd(smcConn, this)) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib was unable to add this instance to the native to managed map."); + return; + } + + _ = SMLib.SmcSetErrorHandler(Marshal.GetFunctionPointerForDelegate(s_smcErrorHandlerDelegate)); + _ = ICELib.IceSetErrorHandler(Marshal.GetFunctionPointerForDelegate(s_iceErrorHandlerDelegate)); + _ = ICELib.IceSetIOErrorHandler(Marshal.GetFunctionPointerForDelegate(s_iceIoErrorHandlerDelegate)); + + _currentSmcConn = smcConn; + _currentIceConn = SMLib.SmcGetIceConnection(smcConn); + + Task.Run(() => + { + var token = _cancellationTokenSource.Token; + while (!token.IsCancellationRequested) HandleRequests(); + }, _cancellationTokenSource.Token); + } + + public void Dispose() + { + if (_currentSmcConn == IntPtr.Zero) return; + + s_nativeToManagedMapper.TryRemove(_currentSmcConn, out _); + + _ = SMLib.SmcCloseConnection(_currentSmcConn, 1, + new[] { $"{nameof(X11PlatformLifetimeEvents)} was disposed in managed code." }); + } + + private static void SmcSaveCompleteHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.SaveCompleteHandler(); + } + + private static X11PlatformLifetimeEvents? GetInstance(IntPtr smcConn) + { + return s_nativeToManagedMapper.TryGetValue(smcConn, out var instance) ? instance : null; + } + + private static void SmcShutdownCancelledHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.ShutdownCancelledHandler(); + } + + private static void SmcDieHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.DieHandler(); + } + + private static void SmcSaveYourselfHandler(IntPtr smcConn, IntPtr clientData, int saveType, + bool shutdown, int interactStyle, bool fast) + { + GetInstance(smcConn)?.SaveYourselfHandler(smcConn, clientData, shutdown, fast); + } + + private static void StaticInteractHandler(IntPtr smcConn, IntPtr clientData) + { + GetInstance(smcConn)?.InteractHandler(smcConn); + } + + private static void StaticIceIOErrorHandler(IntPtr iceConn) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(null, + "ICELib reported an unknown IO Error."); + } + + private static void StaticErrorHandler(IntPtr smcConn, bool swap, int offendingMinorOpcode, + ulong offendingSequence, int errorClass, int severity, IntPtr values) + { + GetInstance(smcConn) + ?.ErrorHandler(swap, offendingMinorOpcode, offendingSequence, errorClass, severity, values); + } + + // ReSharper disable UnusedParameter.Local + private void ErrorHandler(bool swap, int offendingMinorOpcode, ulong offendingSequence, int errorClass, + int severity, IntPtr values) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib reported an error:" + + $" severity {severity:X}" + + $" mOpcode {offendingMinorOpcode:X}" + + $" mSeq {offendingSequence:X}" + + $" errClass {errorClass:X}."); + } + + private void HandleRequests() + { + if (ICELib.IceProcessMessages(_currentIceConn, out _, out _) == + ICELib.IceProcessMessagesStatus.IceProcessMessagesIoError) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.X11Platform)?.Log(this, + "SMLib lost its underlying ICE connection."); + Dispose(); + } + } + + private void SaveCompleteHandler() + { + _saveYourselfPhase = false; + } + + private void ShutdownCancelledHandler() + { + if (_saveYourselfPhase) + SMLib.SmcSaveYourselfDone(_currentSmcConn, true); + _saveYourselfPhase = false; + } + + private void DieHandler() + { + Dispose(); + } + + private void SaveYourselfHandler(IntPtr smcConn, IntPtr clientData, bool shutdown, bool fast) + { + if (_saveYourselfPhase) + SMLib.SmcSaveYourselfDone(smcConn, true); + _saveYourselfPhase = true; + + if (shutdown && !fast) + { + var _ = SMLib.SmcInteractRequest(smcConn, SMLib.SmDialogValue.SmDialogError, + Marshal.GetFunctionPointerForDelegate(s_smcInteractDelegate), + clientData); + } + else + { + SMLib.SmcSaveYourselfDone(smcConn, true); + _saveYourselfPhase = false; + } + } + + private void InteractHandler(IntPtr smcConn) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalse + if (ShutdownRequested is null) + return; + + var e = new ShutdownRequestedEventArgs(); + + ShutdownRequested(this, e); + + var shutdownCancelled = e.Cancel; + + SMLib.SmcInteractDone(smcConn, shutdownCancelled); + + if (shutdownCancelled) + return; + + _saveYourselfPhase = false; + + SMLib.SmcSaveYourselfDone(smcConn, true); + } + + private static void IceWatchHandler(IntPtr iceConn, IntPtr clientData, bool opening, IntPtr* watchData) + { + if (!opening) return; + ICELib.IceRemoveConnectionWatch(Marshal.GetFunctionPointerForDelegate(s_iceWatchProcDelegate), + IntPtr.Zero); + } + + public event EventHandler ShutdownRequested = null!; + } +}