diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 1f553d116e..9be5c7ad52 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/samples/ControlCatalog.Desktop/Program.cs @@ -142,6 +142,7 @@ namespace ControlCatalog.Desktop public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure() .UsePlatformDetect() + .UseWaylandWithFallback() .With(new X11PlatformOptions { EnableMultiTouch = true, diff --git a/src/Avalonia.Wayland/AvaloniaWaylandPlatformExtensions.cs b/src/Avalonia.Wayland/AvaloniaWaylandPlatformExtensions.cs index b0152b8422..6edafe3292 100644 --- a/src/Avalonia.Wayland/AvaloniaWaylandPlatformExtensions.cs +++ b/src/Avalonia.Wayland/AvaloniaWaylandPlatformExtensions.cs @@ -1,3 +1,5 @@ +using System; +using Avalonia.Logging; using Avalonia.Wayland; // UseXxx are deliberately in global Avalonia namespace @@ -25,4 +27,39 @@ public static class AvaloniaWaylandPlatformExtensions return builder; } + /// + /// Configures the application to use the Wayland windowing backend when a usable Wayland + /// compositor is available, falling back to the previously configured windowing backend + /// otherwise. Call it after UseX11 or UsePlatformDetect, e. g. + /// .UsePlatformDetect().UseWaylandWithFallback(). Does nothing on non-Linux platforms. + /// + /// The application builder. + /// The same for chaining. + /// + /// No windowing backend was configured prior to this call (on Linux). + /// + public static AppBuilder UseWaylandWithFallback(this AppBuilder builder) + { + if (!OperatingSystem.IsLinux()) + return builder; + + var fallback = builder.WindowingSubsystemInitializer + ?? throw new InvalidOperationException( + "A fallback windowing backend must be configured before calling UseWaylandWithFallback, " + + "e.g. via UseX11 or UsePlatformDetect."); + + return builder + .UseStandardRuntimePlatformSubsystem() + .UseWindowingSubsystem(() => + { + var error = WaylandPlatform.TryInitialize( + AvaloniaLocator.Current.GetService() ?? new WaylandPlatformOptions()); + if (error != null) + { + Logger.TryGet(LogEventLevel.Warning, LogArea.Platform)?.Log(null, + "Unable to initialize the Wayland backend, falling back: {Error}", error.SourceException); + fallback(); + } + }); + } } \ No newline at end of file diff --git a/src/Avalonia.Wayland/Server/Interop/WakeupFd.cs b/src/Avalonia.Wayland/Server/Interop/WakeupFd.cs index 67afd24aa3..a65425401b 100644 --- a/src/Avalonia.Wayland/Server/Interop/WakeupFd.cs +++ b/src/Avalonia.Wayland/Server/Interop/WakeupFd.cs @@ -28,7 +28,7 @@ unsafe class WakeupFd : System.IDisposable { lock (_lock) { - if(!_signaled) + if(_disposed || !_signaled) return; var readNow = read(_read, s_readBuf, 1); Debug.Assert(readNow <= 1); @@ -40,7 +40,7 @@ unsafe class WakeupFd : System.IDisposable { lock (_lock) { - if(_signaled) + if(_disposed || _signaled) return; byte b = 0; write(_write, &b, 1); diff --git a/src/Avalonia.Wayland/Server/WaylandWorker.cs b/src/Avalonia.Wayland/Server/WaylandWorker.cs index c6712efa34..7bfbd5e505 100644 --- a/src/Avalonia.Wayland/Server/WaylandWorker.cs +++ b/src/Avalonia.Wayland/Server/WaylandWorker.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using Avalonia.Logging; @@ -113,12 +114,26 @@ partial class WaylandWorker return tcs.Task; } - void Run(WaylandPlatformOptions options, WaylandConnection connection) + void Run(WaylandPlatformOptions options, WaylandConnection connection, + TaskCompletionSource? initTcs) { var reconnectsDisabled = options.EnableReconnects == false || options.DisplayFd.HasValue; while (true) { - RunConnection(connection, options); + try + { + RunConnection(connection, options, initTcs); + } + catch (Exception e) when (initTcs?.Task.IsCompleted == false) + { + // Globals init failed on the initial connection: report to the waiting caller + // and exit without entering the reconnect loop + initTcs.TrySetResult(ExceptionDispatchInfo.Capture(e)); + connection.Dispose(); + _wakeupFd.Dispose(); + return; + } + initTcs = null; foreach (var obj in _persistentObjects.ToList()) obj.OnDisconnected(); Compositor.Server.ResetAllGpuResources(); @@ -160,11 +175,15 @@ partial class WaylandWorker } } - void RunConnection(WaylandConnection connection, WaylandPlatformOptions options) + void RunConnection(WaylandConnection connection, WaylandPlatformOptions options, + TaskCompletionSource? initTcs = null) { _thread = Thread.CurrentThread; _connection = connection; _globals = new WaylandGlobals(_connection, this, options, _outputsSink); + // Globals are fully constructed (required globals bound, sanity checks passed), + // the display is deemed usable + initTcs?.TrySetResult(null); foreach (var obj in _persistentObjects) ConnectPersistentObject(obj); @@ -229,8 +248,11 @@ partial class WaylandWorker } } - public static WaylandConnection? Probe(WaylandPlatformOptions options) + public static WaylandConnection? Probe(WaylandPlatformOptions options) => Probe(options, out _); + + public static WaylandConnection? Probe(WaylandPlatformOptions options, out ExceptionDispatchInfo? error) { + error = null; try { var conn = options.DisplayFd is { } fd @@ -243,30 +265,38 @@ partial class WaylandWorker catch(Exception e) { Logger.TryGet(LogEventLevel.Error, "Wayland")?.Log(null, "Failed to connect to Wayland display: " + e); + error = ExceptionDispatchInfo.Capture(e); return null; } } + /// + /// Starts the worker on a probed connection. When is provided, it completes + /// with null once the initial construction succeeds, or with the + /// captured failure, in which case the worker thread exits without reconnecting. + /// public void Start(WaylandPlatformOptions options, WaylandConnection probedConnection, - WaylandOutputsSinkProxy? outputsSink = null) + WaylandOutputsSinkProxy? outputsSink = null, + TaskCompletionSource? initTcs = null) { _outputsSink = outputsSink; - StartCore(options, probedConnection, null); + StartCore(options, probedConnection, null, initTcs); } - + public void Start(WaylandPlatformOptions options, WlDisplay foreignDisplay, WaylandOutputsSinkProxy? outputsSink = null) { _outputsSink = outputsSink; - StartCore(options, null, foreignDisplay); + StartCore(options, null, foreignDisplay, null); } - - void StartCore(WaylandPlatformOptions options, WaylandConnection? probedConnection, WlDisplay? foreignDisplay) + + void StartCore(WaylandPlatformOptions options, WaylandConnection? probedConnection, WlDisplay? foreignDisplay, + TaskCompletionSource? initTcs) { new Thread(() => { if(probedConnection!=null) - Run(options, probedConnection); + Run(options, probedConnection, initTcs); else if (foreignDisplay != null) { RunConnection(new WaylandConnection(foreignDisplay), options); diff --git a/src/Avalonia.Wayland/WaylandPlatform.cs b/src/Avalonia.Wayland/WaylandPlatform.cs index 01e608ef92..95e0f22a84 100644 --- a/src/Avalonia.Wayland/WaylandPlatform.cs +++ b/src/Avalonia.Wayland/WaylandPlatform.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; using Avalonia.Controls.Platform; using Avalonia.FreeDesktop; using Avalonia.Input; @@ -16,32 +19,55 @@ namespace Avalonia.Wayland; class WaylandPlatform { - public static void Initialize(WaylandPlatformOptions options) + public static void Initialize(WaylandPlatformOptions options) => TryInitialize(options)?.Throw(); + + internal static ExceptionDispatchInfo? TryInitialize(WaylandPlatformOptions options) { - var connection = WaylandWorker.Probe(options); + // In some cases we aren't allowed to open multiple connections (e. g. WAYLAND_SOCKET env + // is used), so we can't do separate usability checks and bail before doing any initialization. + // Instead we do full startup sequence with WaylandWorker on compositor thread and wait for it + // to get to WaylandGlobals, which is the point where we can make an informed decision + // about wl_display being usable. + + var connection = WaylandWorker.Probe(options, out var connectError); if (connection == null) - throw new AvaloniaWaylandException("Unable to connect to Wayland display"); - // TODO: wait for globals to be ready on the wayland thread and perform sanity checks too. - // This is needed for UsePlatformDetect() in Avalonia.Desktop: if we detect a compositor we - // can't support we should fail here so the caller can fall back to X11. Later we'll also want - // feature-detection flags (e.g. xdg-toplevel-drag-v1 for docks) so apps can request features - // that force an X11 fallback when the compositor lacks them. + return connectError?.SourceException is AvaloniaWaylandException + ? connectError + : ExceptionDispatchInfo.Capture( + new AvaloniaWaylandException("Unable to connect to Wayland display", + connectError?.SourceException)); - var inputDispatchQueue = new AutomaticRawEventGrouperDispatchQueue(); - IDispatcherImpl dispatcherImpl = options.UseGLibMainLoop - ? new WaylandGlibDispatcher(options.ExternalGLibMainLoopExceptionLogger) - : new ManagedDispatcherImpl(null); - Dispatcher.InitializeUIThreadDispatcher(dispatcherImpl); - - var worker = new WaylandWorker(inputDispatchQueue); + WaylandWorker worker; + SnapshotScreensImpl screens; + TaskCompletionSource initTcs; + try + { + // NOTE: This technically mutates the global state since it touches Dispatcher, + // which designates the current thread as the UI one in static ctor, but it's generally safe to do + var inputDispatchQueue = new AutomaticRawEventGrouperDispatchQueue(); - var screens = new SnapshotScreensImpl(); - var screensProxy = new WaylandOutputsSinkProxy(screens, WaylandMarshallers.UIThread); + worker = new WaylandWorker(inputDispatchQueue); - worker.Start(options, connection, screensProxy); + screens = new SnapshotScreensImpl(); + var screensProxy = new WaylandOutputsSinkProxy(screens, WaylandMarshallers.UIThread); + initTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + worker.Start(options, connection, screensProxy, initTcs); + } + catch (Exception e) + { + connection.Dispose(); + return ExceptionDispatchInfo.Capture(e); + } + if (initTcs.Task.GetAwaiter().GetResult() is { } initError) + return initError; + IDispatcherImpl dispatcherImpl = options.UseGLibMainLoop + ? new WaylandGlibDispatcher(options.ExternalGLibMainLoopExceptionLogger) + : new ManagedDispatcherImpl(null); + Dispatcher.InitializeUIThreadDispatcher(dispatcherImpl); + var clipboardImpl = new WaylandClipboardImpl(worker); var clipboard = new Input.Platform.Clipboard(clipboardImpl); @@ -61,5 +87,7 @@ class WaylandPlatform .Bind().ToConstant(new LinuxMountedVolumeInfoProvider()) .Bind().ToConstant(new X11IconLoader()) .Bind().ToConstant(screens); + + return null; } } \ No newline at end of file