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;
+ }
+}