diff --git a/Documentation/build.md b/Documentation/build.md index 9f5436e68e..ddd38be887 100644 --- a/Documentation/build.md +++ b/Documentation/build.md @@ -1,6 +1,6 @@ # Windows -Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on Windows. +Avalonia requires at least Visual Studio 2022 and dotnet 6 SDK 6.0.100 to build on all platforms. ### Clone the Avalonia repository @@ -16,7 +16,7 @@ Go to https://dotnet.microsoft.com/download/visual-studio-sdks and install the l ### Open in Visual Studio -Open the `Avalonia.sln` solution in Visual Studio 2019 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application. +Open the `Avalonia.sln` solution in Visual Studio 2022 or newer. The free Visual Studio Community edition works fine. Build and run the `Samples\ControlCatalog.Desktop` or `ControlCatalog.NetCore` project to see the sample application. ### Troubleshooting @@ -43,27 +43,6 @@ Go to https://www.microsoft.com/net/core and follow the instructions for your OS The build process needs [Xcode](https://developer.apple.com/xcode/) to build the native library. Following the install instructions at the [Xcode](https://developer.apple.com/xcode/) website to properly install. -Linux operating systems ship with their own respective package managers however we will use [Homebrew](https://brew.sh/) to manage packages on macOS. To install follow the instructions [here](https://docs.brew.sh/Installation). - -### Install CastXML (pre Nov 2020) - -Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processing during the build process. The easiest way to install this is via the operating system's package managers, such as below. - -On macOS: -``` -brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb -``` - -On Debian based Linux (Debian, Ubuntu, Mint, etc): -``` -sudo apt install castxml -``` - -On Red Hat based Linux (Fedora, CentOS, RHEL, etc) using `yum` (`dnf` takes same arguments though): -``` -sudo yum install castxml -``` - ### Clone the Avalonia repository diff --git a/build/SourceLink.props b/build/SourceLink.props index 0f77c2a613..1e007e01eb 100644 --- a/build/SourceLink.props +++ b/build/SourceLink.props @@ -3,7 +3,7 @@ true false true - full + embedded $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 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..6ffcad60bf --- /dev/null +++ b/src/Avalonia.X11/SMLib.cs @@ -0,0 +1,133 @@ +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..a18f91d301 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(this)); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); @@ -230,7 +231,19 @@ namespace Avalonia /// on their input devices by using sequences of characters or mouse operations that are natively available on their input devices. /// public bool? EnableIme { get; set; } - + + /// + /// Determines whether to enable support for the + /// X Session Management Protocol. + /// + /// + /// X Session Management Protocol is a standard implemented on most + /// Linux systems that uses Xorg. This enables apps to control how they + /// can control and/or cancel the pending shutdown requested by the user. + /// + public bool EnableSessionManagement { get; set; } = + Environment.GetEnvironmentVariable("AVALONIA_X11_USE_SESSION_MANAGEMENT") != "0"; + public IList GlProfiles { get; set; } = new List { new GlVersion(GlProfileType.OpenGL, 4, 0), diff --git a/src/Avalonia.X11/X11PlatformLifetimeEvents.cs b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs new file mode 100644 index 0000000000..06e1d8879f --- /dev/null +++ b/src/Avalonia.X11/X11PlatformLifetimeEvents.cs @@ -0,0 +1,259 @@ +#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; +using Avalonia.Threading; + +namespace Avalonia.X11 +{ + internal unsafe class X11PlatformLifetimeEvents : IDisposable, IPlatformLifetimeEventsImpl + { + private readonly AvaloniaX11Platform _platform; + 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; + + internal X11PlatformLifetimeEvents(AvaloniaX11Platform platform) + { + _platform = platform; + + 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) + { + Dispatcher.UIThread.Post(() => ActualInteractHandler(smcConn)); + } + + private void ActualInteractHandler(IntPtr smcConn) + { + var e = new ShutdownRequestedEventArgs(); + + if (_platform.Options?.EnableSessionManagement ?? false) + { + ShutdownRequested?.Invoke(this, e); + } + + SMLib.SmcInteractDone(smcConn, e.Cancel); + + if (e.Cancel) + { + 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; + } +}