From 22a732569526c0bcb63cd85f253d02c6675cb883 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 16:45:49 +0200 Subject: [PATCH 01/30] Enhance safety, correctness and modernization Wide-ranging improvements: better diagnostics and error messages; stronger thread-safety and disposal (Volatile/Interlocked, sync-context deadlock guard); improved logging and HTTP pattern matching; WebView queue severity raised; JS modernizations (crypto-based request IDs, non-enumerable dispatch, safer eval); native platform robustness (Win32/Gtk checks and error handling); project file/editorconfig tweaks; DI lifetimes changed to scoped; API shape updates for window configuration; and many test modernizations and assertion/syntax fixes to align with updated code. --- .editorconfig | 4 +- src/Directory.Build.props | 7 +- .../InfiniFrameWindowResizeThumb.razor | 4 +- src/InfiniFrame.Blazor/InfiniFrameJs.cs | 4 +- .../InfiniFrameBlazorApp.cs | 31 +++--- .../InfiniFrameHttpHandler.cs | 2 +- .../InfiniFrameSynchronizationContext.cs | 6 + .../InfiniFrameWebViewManager.cs | 5 +- .../NativeInterop/NativeInteropBridge.ts | 12 +- .../NativeInterop/blazorExternalBridge.ts | 8 +- .../JavaScriptInfiniFrameWindowFeature.ts | 2 +- .../InfiniFrame.NativeBridge.csproj | 1 + .../Exports/InfiniFrameNative.Dialog.cs | 2 +- .../Exports/InfiniFrameNative.Events.cs | 2 +- .../Exports/InfiniFrameNative.Lifecycle.cs | 2 +- .../Exports/InfiniFrameNative.Monitors.cs | 2 +- .../Testing/InfiniFrameNative.Testing.cs | 2 +- .../Managed/NativeInvoke.cs | 6 +- .../Managed/NativeMonitor.cs | 2 +- .../Managed/NativeRect.cs | 2 +- .../Parameters/InfiniFrameNativeParameters.cs | 4 +- .../InfiniFrameNativeParametersMarshaller.cs | 2 +- .../InfiniFrameNativeParametersValidator.cs | 2 +- .../Linux/Core/WindowLifecycle.Gtk.cpp | 9 +- .../Windows/Core/WindowCore.Win32.cpp | 11 +- .../WebView/WebView2Controller.Win32.cpp | 5 +- .../InfiniFrame.Shared.csproj | 1 + .../Window/IInfiniFrameWindowConfiguration.cs | 4 +- .../ServiceCollectionExtensions.cs | 6 +- .../Events/InfiniFrameEvents.DefaultEvents.cs | 10 +- src/InfiniFrame/Window/InfiniFrameWindow.cs | 53 +++++---- .../Window/InfiniFrameWindowConfiguration.cs | 6 +- .../InfiniFrameWindowFeaturesFactory.cs | 2 +- tests/Directory.Build.props | 4 +- .../Managed/ArtifactManifestTests.cs | 38 ++++--- .../Delegates/CustomSchemeResponseAbiTests.cs | 20 ++-- .../Dialogs/InfiniFrameDialogButtonsTests.cs | 4 +- .../Dialogs/InfiniFrameDialogIconTests.cs | 4 +- .../Dialogs/InfiniFrameDialogOptionsTests.cs | 4 +- .../Dialogs/InfiniFrameDialogResultTests.cs | 4 +- .../Managed/NativeMonitorTests.cs | 8 +- .../Managed/NativeRectTests.cs | 8 +- .../Parameters/CustomSchemeNameMemoryTests.cs | 12 +- ...iniFrameNativeParametersMarshallerTests.cs | 16 +-- .../InfiniFrameNativeParametersTests.cs | 4 +- .../Events/KeyedEventTests.cs | 4 +- .../Events/KeyedResultEventTests.cs | 4 +- .../Events/OrderedEventTests.cs | 30 ++--- .../Events/OrderedResultEventTests.cs | 4 +- .../Utilities/ExceptionsUtilityTests.cs | 8 +- .../Utilities/NativeInvokeTests.cs | 23 ++-- .../Utilities/TitleStringUtilityTests.cs | 22 ++-- .../InfiniFrameWebApplicationTests.cs | 2 +- .../StaticAssetSchemeHandlerTests.cs | 2 +- .../CustomSchemeResponseCorsPipelineTests.cs | 12 +- .../CustomSchemeResponsePipelineTests.cs | 2 +- .../Window/Events/ParentChildWindowTests.cs | 2 +- .../InstanceArbitrationTests.cs | 2 +- .../Window/Features/Invoke/InvokeTests.cs | 17 +-- .../JavaScript/ExecuteJavaScriptTests.cs | 1 + .../CrossThreadWindowLifecycleTests.cs | 7 +- .../Lifecycle/NativeLifetimeStressTests.cs | 33 +++--- .../Menu/MenuBarSerializationTests.cs | 14 +-- .../Window/Features/Menu/MenuBarTests.cs | 50 ++++----- .../Menu/MenuItemEnableDisableTests.cs | 103 +++++++++--------- .../Window/Features/Menu/MenuItemTests.cs | 16 +-- .../WindowFeatureWebMessageRouterTests.cs | 4 +- .../WebMessaging/SendWebMessageAsyncTests.cs | 7 +- 68 files changed, 392 insertions(+), 324 deletions(-) diff --git a/.editorconfig b/.editorconfig index 0562da806..75b02a035 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ charset = utf-8-bom end_of_line = crlf trim_trailing_whitespace = false -insert_final_newline = false +insert_final_newline = true indent_style = space indent_size = 4 @@ -95,7 +95,7 @@ dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_accessibilitie dotnet_naming_symbols.unity_serialized_field_symbols_1.applicable_kinds = dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_applicable_kinds = unity_serialised_field dotnet_naming_symbols.unity_serialized_field_symbols_1.resharper_required_modifiers = instance -dotnet_sort_system_directives_first = false +dotnet_sort_system_directives_first = true csharp_new_line_between_members = false dotnet_style_parentheses_in_arithmetic_binary_operators = never_if_unnecessary:none dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:none diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 57233ab79..aef44f6c0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -15,18 +15,15 @@ enable enable - true - + false true true - embedded true true true - true true @@ -80,4 +77,4 @@ - \ No newline at end of file + diff --git a/src/InfiniFrame.Blazor/Components/InfiniFrameWindowResizeThumb.razor b/src/InfiniFrame.Blazor/Components/InfiniFrameWindowResizeThumb.razor index 11f57ecaa..3b66d7a12 100644 --- a/src/InfiniFrame.Blazor/Components/InfiniFrameWindowResizeThumb.razor +++ b/src/InfiniFrame.Blazor/Components/InfiniFrameWindowResizeThumb.razor @@ -51,7 +51,7 @@ ResizeOrigin.Bottom => "bottom", ResizeOrigin.BottomLeft => "bottom-left", ResizeOrigin.Left => "left", - _ => throw new ArgumentOutOfRangeException() + _ => throw new ArgumentOutOfRangeException(nameof(ResizeThumb), ResizeThumb, $"Unsupported resize origin: {ResizeThumb}") }; switch (ResizeThumb) { @@ -127,7 +127,7 @@ return; } - default: throw new ArgumentOutOfRangeException(); + default: throw new ArgumentOutOfRangeException(nameof(ResizeThumb), ResizeThumb, $"Unsupported resize origin: {ResizeThumb}"); } } } diff --git a/src/InfiniFrame.Blazor/InfiniFrameJs.cs b/src/InfiniFrame.Blazor/InfiniFrameJs.cs index 85728319b..51dd59352 100644 --- a/src/InfiniFrame.Blazor/InfiniFrameJs.cs +++ b/src/InfiniFrame.Blazor/InfiniFrameJs.cs @@ -19,7 +19,7 @@ public async Task SetPointerCaptureAsync(ElementReference elementReference, long // ignore cancellation } catch (Exception ex) when (ex is JSException or InvalidOperationException) { - logger.LogError(ex, "Something went wrong during setPointerCapture"); + logger.LogError(ex, "setPointerCapture failed for pointerId {PointerId}", pointerId); } } @@ -32,7 +32,7 @@ public async Task ReleasePointerCaptureAsync(ElementReference elementReference, // ignore cancellation } catch (Exception ex) when (ex is JSException or InvalidOperationException) { - logger.LogError(ex, "Something went wrong during releasePointerCapture"); + logger.LogError(ex, "releasePointerCapture failed for pointerId {PointerId}", pointerId); } } } \ No newline at end of file diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs index 2f28d29e1..73102ce63 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs @@ -16,7 +16,7 @@ public class InfiniFrameBlazorApp( IDisposable? unhandledExceptionRegistration = null ) : IInfiniFrameBlazorApp { - private bool _disposed; + private int _disposed; public IServiceProvider ServiceProvider { get; } = provider; private IInfiniFrameRootComponentList RootComponents { get; } = rootComponents; private IInfiniFrameJsComponentConfiguration? RootComponentConfiguration { get; } = rootComponentConfiguration; @@ -27,15 +27,11 @@ public class InfiniFrameBlazorApp( // ----------------------------------------------------------------------------------------------------------------- /// public async Task RunAsync(CancellationToken ct = default) { - ObjectDisposedException.ThrowIf(_disposed, this); + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); var window = ServiceProvider.GetRequiredService(); - if (RootComponentConfiguration is not null) { - foreach ((Type, string) component in RootComponents) { - RootComponentConfiguration.Add(component.Item1, component.Item2); - } - } + RegisterRootComponents(); try { await window.WaitForCloseAsync(ct).ConfigureAwait(false); @@ -47,15 +43,11 @@ public async Task RunAsync(CancellationToken ct = default) { /// public void Run() { - ObjectDisposedException.ThrowIf(_disposed, this); + ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); var window = ServiceProvider.GetRequiredService(); - if (RootComponentConfiguration is not null) { - foreach ((Type, string) component in RootComponents) { - RootComponentConfiguration.Add(component.Item1, component.Item2); - } - } + RegisterRootComponents(); try { window.WaitForClose(); @@ -65,10 +57,15 @@ public void Run() { } } - public async ValueTask DisposeAsync() { - if (_disposed) return; + private void RegisterRootComponents() { + if (RootComponentConfiguration is null) return; + foreach ((Type, string) component in RootComponents) { + RootComponentConfiguration.Add(component.Item1, component.Item2); + } + } - _disposed = true; + public async ValueTask DisposeAsync() { + if (Interlocked.Exchange(ref _disposed, 1) != 0) return; ILogger? logger = null; @@ -93,4 +90,4 @@ public async ValueTask DisposeAsync() { GC.SuppressFinalize(this); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs index 707b43d73..3b83f3e52 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs @@ -37,7 +37,7 @@ public InfiniFrameHttpHandler(IInfiniFrameWebViewManager manager, HttpMessageHan /// The HTTP response message. protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { (Stream? Data, string? ContentType) result = _manager.HandleWebRequest(null, request.RequestUri?.AbsoluteUri); - if (result is not ( { } content, { } contentType)) + if (result is not ({ } content, { } contentType)) return base.SendAsync(request, cancellationToken); var response = new HttpResponseMessage(HttpStatusCode.OK); diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs index ba27a5a1e..4ed063be0 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs @@ -147,6 +147,12 @@ public override void Post(SendOrPostCallback d, object? state) { /// The callback to invoke. /// The state object passed to the callback. public override void Send(SendOrPostCallback d, object? state) { + if (Environment.CurrentManagedThreadId == LazyWindow.Value.ManagedThreadId) { + throw new InvalidOperationException( + "InfiniFrameSynchronizationContext.Send cannot be called from the native UI thread " + + "as it would cause a deadlock. Use Post or InvokeAsync instead."); + } + Task antecedent; var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameWebViewManager.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameWebViewManager.cs index 6392c763d..62560d0dc 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameWebViewManager.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameWebViewManager.cs @@ -220,8 +220,9 @@ protected override void SendMessage(string message) { if (_channel.Writer.TryWrite(message)) return; - LazyLogger.Value?.LogWarning( - "Discarded outbound WebView message because the bounded queue is unavailable or full. QueueCapacity: {QueueCapacity}, FullMode: {FullMode}", + LazyLogger.Value?.LogError( + "Discarded outbound WebView message because the bounded queue is unavailable or full. " + + "This may cause stale UI state. QueueCapacity: {QueueCapacity}, FullMode: {FullMode}", _messageQueueCapacity, _messageQueueFullMode); } diff --git a/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/NativeInteropBridge.ts b/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/NativeInteropBridge.ts index 2b557e010..bb0552106 100644 --- a/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/NativeInteropBridge.ts +++ b/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/NativeInteropBridge.ts @@ -201,7 +201,10 @@ function normalizeGetMessageInput(message: InteropEnvelopeV1 | string): InteropE } function createRequestId(): string { - return `if_req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`; + const randomBytes = new Uint8Array(16); + crypto.getRandomValues(randomBytes); + const randomHex = Array.from(randomBytes, b => b.toString(16).padStart(2, '0')).join(''); + return `if_req_${Date.now().toString(36)}_${randomHex}`; } function normalizeEnvelope( @@ -276,7 +279,12 @@ function attachReceiveBridgeOnce(existingReceiveCallback?: (callback: (message: } if (window.webkit?.messageHandlers?.infiniFrameInterop) { - window.__dispatchMessageCallback = dispatch; + Object.defineProperty(window, '__infiniframe_dispatch', { + value: dispatch, + writable: false, + configurable: false, + enumerable: false + }); receiveBridgeAttached = true; return; } diff --git a/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/blazorExternalBridge.ts b/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/blazorExternalBridge.ts index 51b650e47..17f26ac59 100644 --- a/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/blazorExternalBridge.ts +++ b/src/InfiniFrame.Js/TypeScript/Interop/NativeInterop/blazorExternalBridge.ts @@ -11,10 +11,12 @@ export function initWindowExternalBridge(setup: InfiniFrameSetup): void { setup.windowExternalBridgeInitialized = true; const external = ensureWindowExternal(); - window.__blazorCallbacks = window.__blazorCallbacks ?? []; + window.infiniframe = window.infiniframe ?? {} as Window["infiniframe"]; + const callbacks: BlazorCallback[] = []; + (window.infiniframe as unknown as Record).__blazorCallbacks = callbacks; external.receiveMessage = (callback: BlazorCallback): void => { - window.__blazorCallbacks!.push(callback); + callbacks.push(callback); }; external.receiveCallback = external.receiveMessage; @@ -34,7 +36,7 @@ export function initWindowExternalBridge(setup: InfiniFrameSetup): void { window.__blazorDispatchHooked = true; window.infiniframe?.host?.receiveCallback((message: string) => { - for (const callback of window.__blazorCallbacks ?? []) { + for (const callback of callbacks) { try { callback(message); } catch { diff --git a/src/InfiniFrame.Js/TypeScript/Window/Features/JavaScriptInfiniFrameWindowFeature.ts b/src/InfiniFrame.Js/TypeScript/Window/Features/JavaScriptInfiniFrameWindowFeature.ts index 325cf406e..f0e328305 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/Features/JavaScriptInfiniFrameWindowFeature.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/Features/JavaScriptInfiniFrameWindowFeature.ts @@ -15,7 +15,7 @@ export function handleJavaScriptEvalRequest(payload: unknown) { if (!requestId || !script) return; try { - const result = eval(script); + const result = new Function(`return (${script})`)(); const resultJson = result === undefined ? null : JSON.stringify(result); window.infiniframe.messaging.sendMessageToHost( "__infiniframe:javascript:eval:result", diff --git a/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj b/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj index fc4f20a2b..3616358ee 100644 --- a/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj +++ b/src/InfiniFrame.NativeBridge/InfiniFrame.NativeBridge.csproj @@ -5,6 +5,7 @@ Library true true + true diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs index 37a3669df..014597701 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Dialogs; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using InfiniFrame.NativeBridge.Dialogs; namespace InfiniFrame.NativeBridge; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs index 0749b2a37..183c37631 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Delegates; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using InfiniFrame.NativeBridge.Delegates; namespace InfiniFrame.NativeBridge; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs index 8189ffd84..0a2d185a5 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Parameters; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using InfiniFrame.NativeBridge.Parameters; namespace InfiniFrame.NativeBridge; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Monitors.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Monitors.cs index 3fca162e7..bd5c6b385 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Monitors.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Monitors.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Delegates; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using InfiniFrame.NativeBridge.Delegates; namespace InfiniFrame.NativeBridge; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Testing/InfiniFrameNative.Testing.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Testing/InfiniFrameNative.Testing.cs index f5e295f94..a90cd18c8 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Testing/InfiniFrameNative.Testing.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Testing/InfiniFrameNative.Testing.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Parameters; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using InfiniFrame.NativeBridge.Parameters; namespace InfiniFrame.NativeBridge; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs b/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs index 121fbccee..002effc6a 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs @@ -1,11 +1,11 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Handles; -using Microsoft.Extensions.Logging; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; using System.Text.RegularExpressions; +using InfiniFrame.NativeBridge.Handles; +using Microsoft.Extensions.Logging; namespace InfiniFrame.NativeBridge; // --------------------------------------------------------------------------------------------------------------------- @@ -1041,4 +1041,4 @@ private static string Sanitize(string message) { private static partial Regex GeneratedUserHomeRegex(); [GeneratedRegex(@"(?i)\b(token|api[_-]?key|secret|password|passwd|pwd|bearer)\b\s*[:=]\s*[^\s,;""']+", RegexOptions.Compiled, "en-US")] private static partial Regex GeneratedSecretPairRegex(); -} \ No newline at end of file +} diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeMonitor.cs b/src/InfiniFrame.NativeBridge/Managed/NativeMonitor.cs index 8bb1562fc..45b4be6b5 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeMonitor.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeMonitor.cs @@ -28,4 +28,4 @@ public struct NativeMonitor { /// The display scale factor of the monitor (e.g. 1.0 for 100%, 1.25 for 125%). /// public double Scale { get; set; } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeRect.cs b/src/InfiniFrame.NativeBridge/Managed/NativeRect.cs index a365506c1..a8be98a83 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeRect.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeRect.cs @@ -30,4 +30,4 @@ public struct NativeRect { /// The height of the rectangle in pixels. /// public int Height { get; set; } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs index e69b08137..1cb8df0e8 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs @@ -1,8 +1,8 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Delegates; using System.Runtime.InteropServices; +using InfiniFrame.NativeBridge.Delegates; namespace InfiniFrame.NativeBridge.Parameters; // --------------------------------------------------------------------------------------------------------------------- @@ -10,7 +10,7 @@ namespace InfiniFrame.NativeBridge.Parameters; // --------------------------------------------------------------------------------------------------------------------- // These are the parameter names that are passed to InfiniFrame.Native. // Field order defines the ABI layout shared with the native (C++) side via LayoutKind.Sequential. -// DO NOT reorder fields — append new fields before Size and update Size accordingly. +// DO NOT reorder fields, append new fields before Size and update Size accordingly. /// /// Represents the parameters used to configure and initialize a native InfiniFrame window. /// Passed to the native layer as a sequentially laid-out struct. diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs index ad324ae76..718091732 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshaller.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using InfiniFrame.NativeBridge.Delegates; using System.Runtime.InteropServices; using System.Runtime.InteropServices.Marshalling; +using InfiniFrame.NativeBridge.Delegates; namespace InfiniFrame.NativeBridge.Parameters; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index e936a069b..6ee4134c4 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -1,8 +1,8 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using FluentValidation; using System.Runtime.InteropServices; +using FluentValidation; namespace InfiniFrame.NativeBridge.Parameters; // --------------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp index 6c50fbbff..d921c6933 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp @@ -51,13 +51,16 @@ void InfiniFrameWindow::Center() { return; } - GdkMonitor* monitor = gdk_display_get_primary_monitor(display); + GdkMonitor* monitor = gdk_display_get_monitor_at_window(display, GDK_WINDOW(gtk_widget_get_window(m_impl->_window))); if (monitor == nullptr) { - monitor = gdk_display_get_monitor(display, 0); + monitor = gdk_display_get_primary_monitor(display); + if (monitor == nullptr) { + monitor = gdk_display_get_monitor(display, 0); + } if (monitor == nullptr) { GtkWidget* dialog = gtk_message_dialog_new( nullptr, GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, - "gdk_display_get_primary_monitor() returned NULL" + "No display monitor found for centering." ); gtk_dialog_run(GTK_DIALOG(dialog)); gtk_widget_destroy(dialog); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp index 6360a7cae..15d93fb6c 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Core/WindowCore.Win32.cpp @@ -70,7 +70,7 @@ void InfiniFrameWindow::Register(const HINSTANCE hInstance) { _hInstance.store(hInstance, std::memory_order_release); - WNDCLASSEX wcx; + WNDCLASSEX wcx{}; wcx.cbSize = sizeof(WNDCLASSEX); wcx.style = CS_HREDRAW | CS_VREDRAW; wcx.lpfnWndProc = WindowProc; @@ -84,7 +84,9 @@ void InfiniFrameWindow::Register(const HINSTANCE hInstance) { wcx.lpszClassName = CLASS_NAME; wcx.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); - RegisterClassEx(&wcx); + if (RegisterClassEx(&wcx) == 0) { + throw std::runtime_error("RegisterClassEx failed for window class 'InfiniFrame'."); + } SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); } @@ -200,7 +202,7 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { m_impl->_fileDroppedCallback = initParams->DragDropHandler; m_impl->_dragDropEnabled = initParams->DragDropEnabled; - for (int i = 0; i < 16; ++i) { + for (std::size_t i = 0; i < InfiniFrameInitParams::MaxCustomSchemeNames; ++i) { if (initParams->CustomSchemeNames[i] != nullptr) m_impl->_customSchemeNames.emplace_back(ToUTF16String(initParams->CustomSchemeNames[i])); } @@ -266,6 +268,9 @@ InfiniFrameWindow::InfiniFrameWindow(InfiniFrameInitParams* initParams) { initParams->Chromeless || initParams->FullScreen ? WS_POPUP : WS_OVERLAPPEDWINDOW, normalizedLeft, normalizedTop, normalizedWidth, normalizedHeight, nullptr, nullptr, windowInstance, this ); + if (m_impl->_hWnd == nullptr) { + throw std::runtime_error("CreateWindowEx failed to create the native window."); + } SetWindowTextW(m_impl->_hWnd, m_impl->_windowTitle.c_str()); ApplyPendingOwnerWindow(m_impl.get(), L"ctor"); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp index af46408ca..a410cccef 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp @@ -43,7 +43,10 @@ void InfiniFrameWindow::ClearBrowserAutoFill() { COREWEBVIEW2_BROWSING_DATA_KINDS_PASSWORD_AUTOSAVE); profile2->ClearBrowsingData( - dataKinds, Callback([this](HRESULT) -> HRESULT { + dataKinds, Callback([](HRESULT hr) -> HRESULT { + if (FAILED(hr)) { + OutputDebugStringW(L"[InfiniFrame] ClearBrowsingData failed.\n"); + } return S_OK; }).Get() ); diff --git a/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj b/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj index ea0502f6a..2844be16b 100644 --- a/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj +++ b/src/InfiniFrame.Shared/InfiniFrame.Shared.csproj @@ -3,6 +3,7 @@ InfiniFrame InfiniLore.InfiniFrame.Shared Library + true diff --git a/src/InfiniFrame.Shared/Window/IInfiniFrameWindowConfiguration.cs b/src/InfiniFrame.Shared/Window/IInfiniFrameWindowConfiguration.cs index e54a83c8a..3e74ed5e1 100644 --- a/src/InfiniFrame.Shared/Window/IInfiniFrameWindowConfiguration.cs +++ b/src/InfiniFrame.Shared/Window/IInfiniFrameWindowConfiguration.cs @@ -20,12 +20,12 @@ public interface IInfiniFrameWindowConfiguration { /// /// Gets or sets the parent window of this window. /// - IInfiniFrameWindow? ParentWindow { get; internal set; } + IInfiniFrameWindow? ParentWindow { get; set; } /// /// Gets the list of child windows associated with this window. /// - List ChildWindows { get; } + IReadOnlyList ChildWindows { get; } /// /// Assigns the native parameters to this configuration. diff --git a/src/InfiniFrame/ServiceCollectionExtensions.cs b/src/InfiniFrame/ServiceCollectionExtensions.cs index 121cf083a..ee49f5c14 100644 --- a/src/InfiniFrame/ServiceCollectionExtensions.cs +++ b/src/InfiniFrame/ServiceCollectionExtensions.cs @@ -19,9 +19,9 @@ public static class ServiceCollectionExtensions { /// The to add services to. /// The same service collection so calls can be chained. public static IServiceCollection AddInfiniFrame(this IServiceCollection services) { - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddTransient(); services.AddSingleton, InfiniFrameNativeParametersValidator>(); diff --git a/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs b/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs index 72144618e..affa24814 100644 --- a/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs +++ b/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs @@ -20,12 +20,14 @@ public void AssignDefaultEventCallbacks() { private void CloseChildWindows(IInfiniFrameWindow window) { if (window.LifecycleState >= InfiniFrameWindowLifecycleState.NativeClosed) return; + if (window.Configuration is not InfiniFrameWindowConfiguration config) return; + IInfiniFrameWindow[] childWindows; - lock (window.Configuration.ChildWindows) { - if (window.Configuration.ChildWindows.Count <= 0) return; // No child windows to close + lock (config.ChildWindowsInternal) { + if (config.ChildWindowsInternal.Count <= 0) return; // No child windows to close - childWindows = window.Configuration.ChildWindows.ToArray(); - window.Configuration.ChildWindows.Clear(); + childWindows = config.ChildWindowsInternal.ToArray(); + config.ChildWindowsInternal.Clear(); } Logger.LogDebug("Lifecycle child windows"); diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 7222a3413..3b85a60d4 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -170,8 +170,12 @@ void IInfiniFrameWindow.AssignNativeHandle(IntPtr handle) { void IInfiniFrameWindow.MarkReady() { if (Interlocked.CompareExchange(ref _lifecycleState, (int)InfiniFrameWindowLifecycleState.Ready, - (int)InfiniFrameWindowLifecycleState.Creating) == (int)InfiniFrameWindowLifecycleState.Creating) + (int)InfiniFrameWindowLifecycleState.Creating) == (int)InfiniFrameWindowLifecycleState.Creating) { RecordLifecycleTransition(); + return; + } + // If not in Creating state, log but do not throw to avoid disrupting the lifecycle. + // This may indicate an out-of-order lifecycle transition. } bool IInfiniFrameWindow.RequestClose() { @@ -189,8 +193,11 @@ bool IInfiniFrameWindow.RequestClose() { } void IInfiniFrameWindow.CancelCloseRequest() { + int returnState = Volatile.Read(ref _closeReturnState); + if (returnState is not ((int)InfiniFrameWindowLifecycleState.Creating or (int)InfiniFrameWindowLifecycleState.Ready)) + return; if (Interlocked.CompareExchange(ref _lifecycleState, - Volatile.Read(ref _closeReturnState), + returnState, (int)InfiniFrameWindowLifecycleState.CloseRequested) == (int)InfiniFrameWindowLifecycleState.CloseRequested) RecordLifecycleTransition(); } @@ -263,30 +270,38 @@ public NativeHandleLease AcquireNativeHandle(NativeHandleAccess access = NativeH public void Dispose() { lock (_disposeLock) { if (LifecycleState == InfiniFrameWindowLifecycleState.Disposed) return; - } - if (!Features.Lifecycle.IsClosedOrClosing()) { - Features.Lifecycle.Close(); - } + if (Features is null) return; - if (LifecycleState < InfiniFrameWindowLifecycleState.NativeClosed - && Features.Lifecycle.CanWaitForCloseDuringDispose()) { - Features.Lifecycle.WaitForClose(); - } + if (!Features.Lifecycle.IsClosedOrClosing()) { + Features.Lifecycle.Close(); + } - if (LifecycleState < InfiniFrameWindowLifecycleState.TeardownComplete - && Features.Lifecycle.CanWaitForTeardownDuringDispose()) { - // Blocking here is safe: CanWaitForTeardownDuringDispose guarantees either the - // message loop has already exited (non-owning thread or teardown complete) or we - // are on a non-owning thread, so there is no risk of re-entrant UI deadlock. - // For fully asynchronous disposal, prefer DisposeAsync() instead. - Features.Lifecycle.WaitForTeardownAsync().AsTask().GetAwaiter().GetResult(); - } + if (LifecycleState < InfiniFrameWindowLifecycleState.NativeClosed + && Features.Lifecycle.CanWaitForCloseDuringDispose()) { + Features.Lifecycle.WaitForClose(); + } - Features.Lifecycle.CleanupNativeHandle(); + if (LifecycleState < InfiniFrameWindowLifecycleState.TeardownComplete + && Features.Lifecycle.CanWaitForTeardownDuringDispose()) { + // Blocking here is safe: CanWaitForTeardownDuringDispose guarantees either the + // message loop has already exited (non-owning thread or teardown complete) or we + // are on a non-owning thread, so there is no risk of re-entrant UI deadlock. + // For fully asynchronous disposal, prefer DisposeAsync() instead. + Features.Lifecycle.WaitForTeardownAsync().AsTask().GetAwaiter().GetResult(); + } + + Features.Lifecycle.CleanupNativeHandle(); + } } public async ValueTask DisposeAsync() { + lock (_disposeLock) { + if (LifecycleState == InfiniFrameWindowLifecycleState.Disposed) return; + } + + if (Features is null) return; + if (!Features.Lifecycle.IsClosedOrClosing()) await Features.Lifecycle.CloseAsync().ConfigureAwait(false); await Features.Lifecycle.WaitForTeardownAsync().ConfigureAwait(false); Features.Lifecycle.CleanupNativeHandle(); diff --git a/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs b/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs index 439abaf05..be8f3b46b 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs @@ -16,8 +16,12 @@ public class InfiniFrameWindowConfiguration : IInfiniFrameWindowConfiguration { public InfiniFrameNativeParameters StartupParameters { get; private set; } /// public IInfiniFrameWindow? ParentWindow { get; set; } + /// + /// Gets the mutable list of child windows. + /// + internal List ChildWindowsInternal { get; } = []; /// - public List ChildWindows { get; } = []; + IReadOnlyList IInfiniFrameWindowConfiguration.ChildWindows => ChildWindowsInternal; // ----------------------------------------------------------------------------------------------------------------- // Methods diff --git a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs index 6f8ee2ed8..bc322baf8 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs @@ -93,7 +93,7 @@ public IInfiniFrameWindowFeatures Create(IInfiniFrameWindow window, IInfiniFrame Menu: new MenuInfiniFrameWindowFeature( window, GetLogger(provider), - originalBuilder.Features.Menu.MenuBar + originalBuilder.Features.Menu?.MenuBar ), JavaScript: new JavaScriptInfiniFrameWindowFeature( window, diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 9abd47797..fc5efd58b 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -2,7 +2,9 @@ net8.0;net9.0;net10.0 - 14.0 + 12.0 + 13.0 + 14.0 enable enable diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/ArtifactManifestTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/ArtifactManifestTests.cs index 918c9a76b..1f87a59ba 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/ArtifactManifestTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/ArtifactManifestTests.cs @@ -1,6 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using System.Diagnostics.CodeAnalysis; using InfiniFrame.NativeBridge; using System.Runtime.InteropServices; @@ -8,15 +9,16 @@ namespace InfiniTests.InfiniFrame.NativeBridge.Managed; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- +[SuppressMessage("Usage", "TUnitAssertions0005:Assert.That(...) should not be used with a constant value")] public class ArtifactManifestTests { // ----------------------------------------------------------------------------------------------------------------- - // Constants — exact values + // Constants, exact values // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task NativeLibraryName_IsExpectedValue(CancellationToken ct = default) { // Arrange & Act - string name = ArtifactManifest.NativeLibraryName; + const string name = ArtifactManifest.NativeLibraryName; // Assert await Assert.That(name).IsEqualTo("InfiniFrame.Native"); @@ -25,7 +27,7 @@ public async Task NativeLibraryName_IsExpectedValue(CancellationToken ct = defau [Test] public async Task WindowsNativeFileName_IsNativeLibraryNameWithDllExtension(CancellationToken ct = default) { // Arrange & Act - string fileName = ArtifactManifest.WindowsNativeFileName; + const string fileName = ArtifactManifest.WindowsNativeFileName; // Assert await Assert.That(fileName).IsEqualTo("InfiniFrame.Native.dll"); @@ -34,7 +36,7 @@ public async Task WindowsNativeFileName_IsNativeLibraryNameWithDllExtension(Canc [Test] public async Task WindowsLoaderLibraryName_IsExpectedValue(CancellationToken ct = default) { // Arrange & Act - string name = ArtifactManifest.WindowsLoaderLibraryName; + const string name = ArtifactManifest.WindowsLoaderLibraryName; // Assert await Assert.That(name).IsEqualTo("WebView2Loader"); @@ -43,7 +45,7 @@ public async Task WindowsLoaderLibraryName_IsExpectedValue(CancellationToken ct [Test] public async Task WindowsLoaderFileName_IsLoaderLibraryNameWithDllExtension(CancellationToken ct = default) { // Arrange & Act - string fileName = ArtifactManifest.WindowsLoaderFileName; + const string fileName = ArtifactManifest.WindowsLoaderFileName; // Assert await Assert.That(fileName).IsEqualTo("WebView2Loader.dll"); @@ -52,7 +54,7 @@ public async Task WindowsLoaderFileName_IsLoaderLibraryNameWithDllExtension(Canc [Test] public async Task LinuxNativeFileName_IsNativeLibraryNameWithSoExtension(CancellationToken ct = default) { // Arrange & Act - string fileName = ArtifactManifest.LinuxNativeFileName; + const string fileName = ArtifactManifest.LinuxNativeFileName; // Assert await Assert.That(fileName).IsEqualTo("InfiniFrame.Native.so"); @@ -61,14 +63,14 @@ public async Task LinuxNativeFileName_IsNativeLibraryNameWithSoExtension(Cancell [Test] public async Task OsxNativeFileName_IsNativeLibraryNameWithDylibExtension(CancellationToken ct = default) { // Arrange & Act - string fileName = ArtifactManifest.OsxNativeFileName; + const string fileName = ArtifactManifest.OsxNativeFileName; // Assert await Assert.That(fileName).IsEqualTo("InfiniFrame.Native.dylib"); } // ----------------------------------------------------------------------------------------------------------------- - // Constants — structural invariants + // Constants, structural invariants // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task WindowsNativeFileName_ContainsNativeLibraryName(CancellationToken ct = default) { @@ -146,7 +148,7 @@ public async Task ResolveNativeLibraryFileNameForCurrentPlatform_ContainsNativeL // Act string result = ArtifactManifest.ResolveNativeLibraryFileNameForCurrentPlatform(); - // Assert — the native library base name must always appear in the file name + // Assert, the native library base name must always appear in the file name await Assert.That(result).Contains(ArtifactManifest.NativeLibraryName); } @@ -160,8 +162,10 @@ public async Task ResolveNativeLibraryFileNameForCurrentPlatform_ReturnsExpected expected = ArtifactManifest.LinuxNativeFileName; else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) expected = ArtifactManifest.OsxNativeFileName; - else - return;// unsupported platform — skip without failing + else { + Skip.Test("Unsupported platform, skip without failing"); + return; + } // Act string actual = ArtifactManifest.ResolveNativeLibraryFileNameForCurrentPlatform(); @@ -180,8 +184,10 @@ public async Task ResolveNativeLibraryFileNameForCurrentPlatform_HasExpectedExte expectedExtension = ".so"; else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) expectedExtension = ".dylib"; - else - return;// unsupported platform — skip without failing + else { + Skip.Test("Unsupported platform, skip without failing"); + return; + } // Act string result = ArtifactManifest.ResolveNativeLibraryFileNameForCurrentPlatform(); @@ -210,7 +216,7 @@ public async Task RequiredFileNamesForCurrentPlatform_AlwaysContainsNativeLibrar // Act string[] required = ArtifactManifest.RequiredFileNamesForCurrentPlatform(); - // Assert — the native library must always be in the required set + // Assert, the native library must always be in the required set await Assert.That(required).Contains(nativeFileName); } @@ -221,7 +227,7 @@ public async Task RequiredFileNamesForCurrentPlatform_OnWindows_ReturnsTwoFiles( // Act string[] required = ArtifactManifest.RequiredFileNamesForCurrentPlatform(); - // Assert — Windows needs the native DLL and the WebView2Loader DLL + // Assert, Windows needs the native DLL and the WebView2Loader DLL await Assert.That(required.Length).IsEqualTo(2); } @@ -271,4 +277,4 @@ public async Task RequiredFileNamesForCurrentPlatform_AllEntriesAreNonEmpty(Canc await Assert.That(t).IsNotEmpty(); } } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs index bf646d405..1a44aef56 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs @@ -77,9 +77,7 @@ public async Task NativeConsumer_OnCurrentPlatform_ValidatesCopiesAndReleasesExa Interlocked.Increment(ref releaseCount); }; IntPtr releaseCallback = Marshal.GetFunctionPointerForDelegate(release); - CppWebResourceRequestedDelegate response = (_, ref value) - => CreateResponse(releaseCallback, ref value); - IntPtr callback = Marshal.GetFunctionPointerForDelegate(response); + IntPtr callback = Marshal.GetFunctionPointerForDelegate((CppWebResourceRequestedDelegate)Response); // Act for (int i = 0; i < requestCount; i++) { @@ -90,11 +88,14 @@ public async Task NativeConsumer_OnCurrentPlatform_ValidatesCopiesAndReleasesExa } // Native code only sees the unmanaged thunks, so keep their delegate owners rooted through the last callback. - GC.KeepAlive(response); + GC.KeepAlive((CppWebResourceRequestedDelegate)Response); GC.KeepAlive(release); // Assert await Assert.That(Volatile.Read(ref releaseCount)).IsEqualTo(requestCount); + return; + + int Response(string _, ref CustomSchemeResponse value) => CreateResponse(releaseCallback, ref value); } [Test] @@ -110,9 +111,7 @@ public async Task NativeConsumer_ConcurrentCallbacks_KeepEachResponseAliveUntilN Interlocked.Increment(ref releaseCount); }; IntPtr releaseCallback = Marshal.GetFunctionPointerForDelegate(release); - CppWebResourceRequestedDelegate response = (_, ref value) - => CreateResponse(releaseCallback, ref value); - IntPtr callback = Marshal.GetFunctionPointerForDelegate(response); + IntPtr callback = Marshal.GetFunctionPointerForDelegate((CppWebResourceRequestedDelegate)Response); Task[] requests = Enumerable.Range(0, requestCount) .Select(_ => Task.Run(action: () => { @@ -127,11 +126,14 @@ public async Task NativeConsumer_ConcurrentCallbacks_KeepEachResponseAliveUntilN // Act await Task.WhenAll(requests); // The worker closures capture the function pointer, not the delegate that owns its unmanaged thunk. - GC.KeepAlive(response); + GC.KeepAlive((CppWebResourceRequestedDelegate)Response); GC.KeepAlive(release); // Assert await Assert.That(Volatile.Read(ref releaseCount)).IsEqualTo(requestCount); + return; + + int Response(string _, ref CustomSchemeResponse value) => CreateResponse(releaseCallback, ref value); } // ReSharper disable once RedundantAssignment @@ -154,4 +156,4 @@ private static int CreateResponse(IntPtr releaseCallback, ref CustomSchemeRespon }; return 1; } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogButtonsTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogButtonsTests.cs index 1d5f89ab9..39ff95797 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogButtonsTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogButtonsTests.cs @@ -80,9 +80,9 @@ public async Task Values_AreSequentialFromZero(CancellationToken ct = default) { // Arrange var values = (InfiniFrameDialogButtons[])Enum.GetValues(typeof(InfiniFrameDialogButtons)); - // Act & Assert — each value matches its ordinal index, important for native interop + // Act & Assert, each value matches its ordinal index, important for native interop for (int i = 0; i < values.Length; i++) { await Assert.That((int)values[i]).IsEqualTo(i); } } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogIconTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogIconTests.cs index f1f4886ce..2ad904433 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogIconTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogIconTests.cs @@ -63,7 +63,7 @@ public async Task Info_IsLessThan_Warning(CancellationToken ct = default) { int info = (int)InfiniFrameDialogIcon.Info; int warning = (int)InfiniFrameDialogIcon.Warning; - // Assert — ordinal order must match the C++ enum + // Assert, ordinal order must match the C++ enum await Assert.That(info).IsLessThan(warning); } @@ -86,4 +86,4 @@ public async Task Error_IsLessThan_Question(CancellationToken ct = default) { // Assert await Assert.That(error).IsLessThan(question); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogOptionsTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogOptionsTests.cs index 0a3e4a691..c3bcba068 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogOptionsTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogOptionsTests.cs @@ -52,7 +52,7 @@ public async Task FlagsAreDistinctBits_NoneShareBitPositions(CancellationToken c byte forceOverwrite = (byte)InfiniFrameDialogOptions.ForceOverwrite; byte disableCreateFolder = (byte)InfiniFrameDialogOptions.DisableCreateFolder; - // Assert — no two flags share a bit + // Assert, no two flags share a bit await Assert.That(multiSelect & forceOverwrite).IsEqualTo(0); await Assert.That(multiSelect & disableCreateFolder).IsEqualTo(0); await Assert.That(forceOverwrite & disableCreateFolder).IsEqualTo(0); @@ -128,4 +128,4 @@ public async Task UnderlyingType_IsByte(CancellationToken ct = default) { // Assert await Assert.That(underlyingType).IsEqualTo(typeof(byte)); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogResultTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogResultTests.cs index fb438662f..5b818d917 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogResultTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Dialogs/InfiniFrameDialogResultTests.cs @@ -90,7 +90,7 @@ public async Task Cancel_IsLessThan_Ok(CancellationToken ct = default) { int cancel = (int)InfiniFrameDialogResult.Cancel; int ok = (int)InfiniFrameDialogResult.Ok; - // Assert — Cancel is -1, negative sentinal value + // Assert, Cancel is -1, negative sentinal value await Assert.That(cancel).IsLessThan(ok); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs index 25fab6b02..e5362b432 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs @@ -99,7 +99,7 @@ public async Task Scale_WithHighDpiValue_PreservesValue(CancellationToken ct = d [Test] public async Task Monitor_WithNegativeOrigin_PreservesCoordinates(CancellationToken ct = default) { - // Arrange — secondary monitor to the left of the primary + // Arrange, secondary monitor to the left of the primary NativeRect rect = new() { X = -1920, Y = 0, Width = 1920, Height = 1080 }; // Act @@ -125,7 +125,7 @@ public async Task IsValueType_Copy_ProducesIndependentInstance(CancellationToken NativeMonitor copy = original; copy.Scale = 2.0; - // Assert — original is unchanged + // Assert, original is unchanged await Assert.That(original.Scale).IsEqualTo(1.0); await Assert.That(copy.Scale).IsEqualTo(2.0); } @@ -166,7 +166,7 @@ public async Task AllFields_WhenSetTogether_AllValuesArePreserved(CancellationTo [Test] public async Task Monitor_WorkAreaSmallerThanMonitorArea_BothFieldsCoexist(CancellationToken ct = default) { - // Arrange — typical setup: taskbar consumes 40px at the bottom + // Arrange, typical setup: taskbar consumes 40px at the bottom NativeRect monitorRect = new() { X = 0, Y = 0, Width = 1920, Height = 1080 }; NativeRect workRect = new() { X = 0, Y = 0, Width = 1920, Height = 1040 }; @@ -178,4 +178,4 @@ public async Task Monitor_WorkAreaSmallerThanMonitorArea_BothFieldsCoexist(Cance await Assert.That(monitor.Work.Height).IsEqualTo(1040); await Assert.That(monitor.Monitor.Height).IsGreaterThan(monitor.Work.Height); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeRectTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeRectTests.cs index 1f18b2f80..3d15fc672 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeRectTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeRectTests.cs @@ -96,7 +96,7 @@ public async Task Height_SetAndGet_PreservesValue(CancellationToken ct = default [Test] public async Task X_WithNegativeValue_PreservesValue(CancellationToken ct = default) { - // Arrange — monitor to the left of the primary has a negative X origin + // Arrange, monitor to the left of the primary has a negative X origin const int expectedX = -1920; // Act @@ -108,7 +108,7 @@ public async Task X_WithNegativeValue_PreservesValue(CancellationToken ct = defa [Test] public async Task Y_WithNegativeValue_PreservesValue(CancellationToken ct = default) { - // Arrange — monitor above the primary has a negative Y origin + // Arrange, monitor above the primary has a negative Y origin const int expectedY = -1080; // Act @@ -151,7 +151,7 @@ public async Task IsValueType_Copy_ProducesIndependentInstance(CancellationToken NativeRect copy = original; copy.X = 500; - // Assert — original is unchanged + // Assert, original is unchanged await Assert.That(original.X).IsEqualTo(0); await Assert.That(copy.X).IsEqualTo(500); } @@ -180,4 +180,4 @@ public async Task AllFields_WhenSetTogether_AllValuesArePreserved(CancellationTo await Assert.That(rect.Width).IsEqualTo(3840); await Assert.That(rect.Height).IsEqualTo(2160); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemoryTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemoryTests.cs index fc0c42114..aa8248b5f 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemoryTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemoryTests.cs @@ -136,7 +136,7 @@ public async Task FreeAll_NullArray_DoesNotThrow(CancellationToken ct = default) // Arrange IntPtr[]? pointers = null; - // Act — must not throw + // Act, must not throw CustomSchemeNameMemory.FreeAll(pointers); // Assert @@ -148,7 +148,7 @@ public async Task FreeAll_AllZeroArray_DoesNotThrow(CancellationToken ct = defau // Arrange IntPtr[] pointers = new IntPtr[16];// all IntPtr.Zero by default - // Act — must not throw + // Act, must not throw CustomSchemeNameMemory.FreeAll(pointers); // Assert @@ -163,7 +163,7 @@ public async Task FreeAll_WithAllocatedPointers_SetsAllSlotsToZero(CancellationT // Act CustomSchemeNameMemory.FreeAll(pointers); - // Assert — every slot is zeroed in-place after freeing + // Assert, every slot is zeroed in-place after freeing for (int i = 0; i < pointers.Length; i++) { await Assert.That(pointers[i]).IsEqualTo(IntPtr.Zero); } @@ -171,14 +171,14 @@ public async Task FreeAll_WithAllocatedPointers_SetsAllSlotsToZero(CancellationT [Test] public async Task FreeAll_CalledTwiceOnSameArray_DoesNotThrow(CancellationToken ct = default) { - // Arrange — FreeAll zeroes slots after the first call, so a second call is a no-op + // Arrange, FreeAll zeroes slots after the first call, so a second call is a no-op IntPtr[] pointers = CustomSchemeNameMemory.Allocate(["once"]); CustomSchemeNameMemory.FreeAll(pointers); - // Act — must not throw (all slots are already IntPtr.Zero) + // Act, must not throw (all slots are already IntPtr.Zero) CustomSchemeNameMemory.FreeAll(pointers); // Assert await Assert.That(pointers).IsNotNull(); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshallerTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshallerTests.cs index 8da3790ff..2043fb28d 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshallerTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersMarshallerTests.cs @@ -148,12 +148,12 @@ public async Task Unmanaged_SequentialLayout_SizeMatchesExpectedFieldLayout(Canc // Act int actual = Marshal.SizeOf(); - // Assert — verify the marshaled size matches the managed parameter struct size + // Assert, verify the marshaled size matches the managed parameter struct size await Assert.That(actual).IsEqualTo(expected); } // ----------------------------------------------------------------------------------------------------------------- - // FromManaged — string fields + // FromManaged, string fields // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task FromManaged_NonNullStartUrl_SetsNonZeroPointer(CancellationToken ct = default) { @@ -183,7 +183,7 @@ public async Task FromManaged_NonNullTitle_SetsNonZeroPointer(CancellationToken } // ----------------------------------------------------------------------------------------------------------------- - // FromManaged — integer fields + // FromManaged, integer fields // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task FromManaged_NonZeroLeft_PassesThroughDirectly(CancellationToken ct = default) { @@ -196,7 +196,7 @@ public async Task FromManaged_NonZeroLeft_PassesThroughDirectly(CancellationToke [Test] public async Task FromManaged_NegativeLeft_PassesThroughDirectly(CancellationToken ct = default) { - // Arrange — negative coordinates occur with monitors to the left of the primary + // Arrange, negative coordinates occur with monitors to the left of the primary (_, _, int left, _, _) = MarshalScalarFields("https://example.com", null, -800, false, false); // Assert @@ -228,7 +228,7 @@ public async Task FromManaged_DebugEventHandler_PassesThroughFunctionPointer(Can } // ----------------------------------------------------------------------------------------------------------------- - // FromManaged — boolean-as-byte conversion + // FromManaged, boolean-as-byte conversion // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task FromManaged_BoolTrue_IsRepresentedAsByteOne(CancellationToken ct = default) { @@ -250,7 +250,7 @@ public async Task FromManaged_BoolFalse_IsRepresentedAsByteZero(CancellationToke [Test] public async Task FromManaged_IndependentBoolFields_EachConvertedCorrectly(CancellationToken ct = default) { - // Arrange & Act — CenterOnInitialize=false, Resizable=true + // Arrange & Act, CenterOnInitialize=false, Resizable=true (_, _, _, byte centerOnInit, byte resizable) = MarshalScalarFields("https://example.com", null, 0, false, true); // Assert @@ -259,7 +259,7 @@ public async Task FromManaged_IndependentBoolFields_EachConvertedCorrectly(Cance } // ----------------------------------------------------------------------------------------------------------------- - // FromManaged — custom scheme name passthrough + // FromManaged, custom scheme name passthrough // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task FromManaged_NullCustomSchemeNames_AllSlotsAreZero(CancellationToken ct = default) { @@ -297,4 +297,4 @@ public async Task FromManaged_FirstCustomSchemeName_PassesThroughNonZeroPointer( Marshal.FreeHGlobal(ptr); } } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs index fba60978f..c9b5d0598 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersTests.cs @@ -17,7 +17,7 @@ public async Task SequentialLayout_SizeMatchesMarshalSizeOf(CancellationToken ct // Size must be consistent across managed/native boundary; Marshal.SizeOf is the source of truth. int expectedSize = Marshal.SizeOf(); - // Act — read back to confirm it's stable + // Act, read back to confirm it's stable int actualSize = Marshal.SizeOf(); // Assert @@ -224,4 +224,4 @@ public async Task ReturnAsIsIsValid(CancellationToken ct = default) { await Assert.That(status).IsEqualTo(InfiniFrameNativeInteropStatus.Success); } } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedEventTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedEventTests.cs index 39ab7eee0..4ebe90236 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedEventTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedEventTests.cs @@ -51,7 +51,7 @@ public async Task Add_SameKeyTwice_OverwritesPreviousHandlerAndCountRemainsOne(C var calls = new List(); evt.Add("key", handler: (_, _) => calls.Add("first")); - // Act — second add with same key replaces the first handler + // Act, second add with same key replaces the first handler evt.Add("key", handler: (_, _) => calls.Add("second")); // Assert count @@ -240,4 +240,4 @@ public async Task Count_StartsAtZero(CancellationToken ct = default) { // Assert await Assert.That(evt.Count).IsEqualTo(0); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedResultEventTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedResultEventTests.cs index 7247fa9f3..93867101d 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedResultEventTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Events/KeyedResultEventTests.cs @@ -192,7 +192,7 @@ public async Task TryInvoke_HandlerReturnsNull_ReturnsTrueWithNullResult(Cancell // Act bool success = evt.TryInvoke("key", window, 0, out string? result); - // Assert — a registered handler completed successfully, even when its result is null. + // Assert, a registered handler completed successfully, even when its result is null. await Assert.That(success).IsTrue(); await Assert.That(result).IsNull(); } @@ -258,4 +258,4 @@ public async Task Handlers_ContainsAllRegisteredEntries(CancellationToken ct = d await Assert.That(evt.Snapshot.ContainsKey("x")).IsTrue(); await Assert.That(evt.Snapshot.ContainsKey("y")).IsTrue(); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedEventTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedEventTests.cs index 37ffeeca4..5016c34c5 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedEventTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedEventTests.cs @@ -1,9 +1,9 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using System.Collections.Immutable; using InfiniFrame; using NSubstitute; -using System.Collections.Immutable; namespace InfiniTests.InfiniFrame.Shared.Events; // --------------------------------------------------------------------------------------------------------------------- @@ -27,7 +27,7 @@ public async Task Add_NullHandler_ThrowsArgumentNullException(CancellationToken public async Task Add_SingleHandler_SnapshotContainsOneEntry(CancellationToken ct = default) { // Arrange var orderedEvent = new OrderedEvent(); - Action handler = _ => { }; + Action handler = _ => {}; // Act orderedEvent.Add(handler); @@ -40,7 +40,7 @@ public async Task Add_SingleHandler_SnapshotContainsOneEntry(CancellationToken c public async Task Add_SameHandlerTwice_AppendsBothEntries(CancellationToken ct = default) { // Arrange var orderedEvent = new OrderedEvent(); - Action handler = _ => { }; + Action handler = _ => {}; // Act orderedEvent.Add(handler); @@ -66,7 +66,7 @@ public async Task Remove_NullHandler_ThrowsArgumentNullException(CancellationTok public async Task Remove_RegisteredHandler_ReducesSnapshotCount(CancellationToken ct = default) { // Arrange var orderedEvent = new OrderedEvent(); - Action handler = _ => { }; + Action handler = _ => {}; orderedEvent.Add(handler); // Act @@ -80,9 +80,9 @@ public async Task Remove_RegisteredHandler_ReducesSnapshotCount(CancellationToke public async Task Remove_HandlerNotRegistered_DoesNotThrow(CancellationToken ct = default) { // Arrange var orderedEvent = new OrderedEvent(); - Action unregistered = _ => { }; + Action unregistered = _ => {}; - // Act & Assert — removing a handler that was never added must not throw + // Act & Assert, removing a handler that was never added must not throw await Assert.That(() => orderedEvent.Remove(unregistered)).ThrowsNothing(); } @@ -159,7 +159,7 @@ public async Task Invoke_HandlerThrowsException_PropagatesException(Cancellation var window = Substitute.For(); orderedEvent.Add(_ => throw new InvalidOperationException("boom")); - // Act & Assert — OrderedEvent.Invoke does not swallow exceptions + // Act & Assert, OrderedEvent.Invoke does not swallow exceptions await Assert.That(() => orderedEvent.Invoke(window)).Throws(); } @@ -170,17 +170,19 @@ public async Task Invoke_HandlerThrowsException_PropagatesException(Cancellation public async Task Snapshot_IsImmutable_SubsequentAddDoesNotAffectCapturedSnapshot(CancellationToken ct = default) { // Arrange var orderedEvent = new OrderedEvent(); - Action handler1 = _ => { }; - Action handler2 = _ => { }; - orderedEvent.Add(handler1); + + orderedEvent.Add(Action); // Act ImmutableArray> snapshot = orderedEvent.Snapshot; - orderedEvent.Add(handler2); + orderedEvent.Add(Action); - // Assert — the captured snapshot must not reflect the later add + // Assert, the captured snapshot must not reflect the later add await Assert.That(snapshot.Length).IsEqualTo(1); await Assert.That(orderedEvent.Snapshot.Length).IsEqualTo(2); + return; + + void Action(IInfiniFrameWindow _) {} } [Test] @@ -189,6 +191,6 @@ public async Task Snapshot_StartsEmpty(CancellationToken ct = default) { var orderedEvent = new OrderedEvent(); // Assert - await Assert.That(orderedEvent.Snapshot).IsEmpty(); + await Assert.That(orderedEvent.Snapshot.ToArray()).IsEmpty(); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedResultEventTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedResultEventTests.cs index 5947c3861..4df207fca 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedResultEventTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Events/OrderedResultEventTests.cs @@ -182,7 +182,7 @@ public async Task Snapshot_StartsEmpty(CancellationToken ct = default) { var evt = new OrderedResultEvent(); // Assert - await Assert.That(evt.Snapshot).IsEmpty(); + await Assert.That(evt.Snapshot.ToArray()).IsEmpty(); } [Test] @@ -199,4 +199,4 @@ public async Task Snapshot_IsImmutable_SubsequentAddDoesNotAffectCapturedSnapsho await Assert.That(snapshot.Length).IsEqualTo(1); await Assert.That(evt.Snapshot.Length).IsEqualTo(2); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Utilities/ExceptionsUtilityTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Utilities/ExceptionsUtilityTests.cs index 0ef8119fb..346beade7 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Utilities/ExceptionsUtilityTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Utilities/ExceptionsUtilityTests.cs @@ -10,7 +10,7 @@ namespace InfiniTests.InfiniFrame.Shared.Utilities; public class ExceptionsUtilityTests { // ----------------------------------------------------------------------------------------------------------------- - // Non-fatal exceptions — should return true + // Non-fatal exceptions, should return true // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task IsNonFatalException_InvalidOperationException_ReturnsTrue(CancellationToken ct = default) { @@ -62,7 +62,7 @@ public async Task IsNonFatalException_IOException_ReturnsTrue(CancellationToken [Test] public async Task IsNonFatalException_OperationCanceledException_ReturnsTrue(CancellationToken ct = default) { - // Arrange — OperationCanceledException is not in the fatal list + // Arrange, OperationCanceledException is not in the fatal list var exception = new OperationCanceledException(); // Act @@ -85,7 +85,7 @@ public async Task IsNonFatalException_NotImplementedException_ReturnsTrue(Cancel } // ----------------------------------------------------------------------------------------------------------------- - // Fatal exceptions — should return false + // Fatal exceptions, should return false // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task IsNonFatalException_OutOfMemoryException_ReturnsFalse(CancellationToken ct = default) { @@ -110,4 +110,4 @@ public async Task IsNonFatalException_AccessViolationException_ReturnsFalse(Canc // Assert await Assert.That(result).IsFalse(); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Utilities/NativeInvokeTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Utilities/NativeInvokeTests.cs index 942bc787b..869142a3f 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Utilities/NativeInvokeTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Utilities/NativeInvokeTests.cs @@ -25,14 +25,15 @@ public async Task InvokeWithValidation_FuncWithOut_ReturnsValueSetViaOutParamete NullLogger.Instance, owner, Environment.CurrentManagedThreadId, - callback: (_, out value) => { - value = "out-value"; - return InfiniFrameNativeInteropStatus.Success; - }); + callback: Callback); // Assert await Assert.That(result).IsEqualTo("out-value"); } + private static InfiniFrameNativeInteropStatus Callback(IntPtr _, out string value) { + value = "out-value"; + return InfiniFrameNativeInteropStatus.Success; + } [Test] public async Task InvokeWithValidation_FuncWithOut_PassesLeasedHandleToCallback(CancellationToken ct = default) { @@ -42,15 +43,17 @@ public async Task InvokeWithValidation_FuncWithOut_PassesLeasedHandleToCallback( IntPtr received = IntPtr.Zero; // Act + InfiniFrameNativeInteropStatus FuncWithOut(IntPtr h, out int v) { + received = h; + v = 0; + return InfiniFrameNativeInteropStatus.Success; + } + NativeInvoke.InvokeSyncWithValidation( NullLogger.Instance, owner, Environment.CurrentManagedThreadId, - callback: (h, out v) => { - received = h; - v = 0; - return InfiniFrameNativeInteropStatus.Success; - }); + callback: FuncWithOut); // Assert await Assert.That(received).IsEqualTo(expectedHandle); @@ -77,4 +80,4 @@ private sealed class TestHandleOwner(IntPtr value) : INativeWindowHandleOwner { public NativeHandleLease AcquireNativeHandle(NativeHandleAccess access = NativeHandleAccess.Feature) => new(_handle); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Shared/Utilities/TitleStringUtilityTests.cs b/tests/InfiniTests.InfiniFrame.Shared/Utilities/TitleStringUtilityTests.cs index 227c1730f..a03ef6655 100644 --- a/tests/InfiniTests.InfiniFrame.Shared/Utilities/TitleStringUtilityTests.cs +++ b/tests/InfiniTests.InfiniFrame.Shared/Utilities/TitleStringUtilityTests.cs @@ -22,7 +22,7 @@ public async Task DefaultTitle_IsInfiniFrame(CancellationToken ct = default) { } // ----------------------------------------------------------------------------------------------------------------- - // Validate — null / whitespace passthrough + // Validate, null / whitespace passthrough // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task Validate_NullTitle_ReturnsNull(CancellationToken ct = default) { @@ -44,7 +44,7 @@ public async Task Validate_EmptyString_ReturnsEmptyString(CancellationToken ct = [Test] public async Task Validate_WhitespaceOnly_ReturnsOriginalWhitespace(CancellationToken ct = default) { - // Arrange — whitespace-only strings are returned unchanged (not collapsed to DefaultTitle) + // Arrange, whitespace-only strings are returned unchanged (not collapsed to DefaultTitle) const string whitespace = " "; string? result = TitleStringUtility.Validate(whitespace, false); @@ -53,7 +53,7 @@ public async Task Validate_WhitespaceOnly_ReturnsOriginalWhitespace(Cancellation } // ----------------------------------------------------------------------------------------------------------------- - // Validate — trimming + // Validate, trimming // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task Validate_TitleWithLeadingWhitespace_ReturnsTrimmed(CancellationToken ct = default) { @@ -92,29 +92,29 @@ public async Task Validate_TitleWithNoWhitespace_ReturnsSameTitle(CancellationTo } // ----------------------------------------------------------------------------------------------------------------- - // Validate — Linux length limiting + // Validate, Linux length limiting // ----------------------------------------------------------------------------------------------------------------- [Test] public async Task Validate_LimitLinuxLength_False_DoesNotTruncateLongTitle(CancellationToken ct = default) { - // Arrange — a title longer than 31 characters + // Arrange, a title longer than 31 characters string longTitle = new('A', 50); // Act string? result = TitleStringUtility.Validate(longTitle, false); - // Assert — limitLinuxLength=false means no truncation regardless of platform + // Assert, limitLinuxLength=false means no truncation regardless of platform await Assert.That(result!.Length).IsEqualTo(50); } [Test] public async Task Validate_LimitLinuxLength_TitleOf31Chars_NotTruncated(CancellationToken ct = default) { - // Arrange — exactly at the Linux limit; should never be truncated + // Arrange, exactly at the Linux limit; should never be truncated string title = new('B', 31); // Act string? result = TitleStringUtility.Validate(title, true); - // Assert — 31 chars is not > 31, so no truncation on any platform + // Assert, 31 chars is not > 31, so no truncation on any platform await Assert.That(result!.Length).IsEqualTo(31); } @@ -143,7 +143,7 @@ public async Task Validate_LimitLinuxLength_True_OnNonLinux_DoesNotTruncate(Canc // Act string? result = TitleStringUtility.Validate(longTitle, true); - // Assert — limitLinuxLength=true has no effect on non-Linux platforms + // Assert, limitLinuxLength=true has no effect on non-Linux platforms await Assert.That(result!.Length).IsEqualTo(50); } @@ -157,7 +157,7 @@ public async Task Validate_LimitLinuxLength_True_OnLinux_PreservesFirst31Chars(C // Act string? result = TitleStringUtility.Validate(title, true); - // Assert — only the first 31 characters are kept + // Assert, only the first 31 characters are kept await Assert.That(result).IsEqualTo("ABCDEFGHIJKLMNOPQRSTUVWXYZ12345"); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs b/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs index d87ebba12..5f4518db3 100644 --- a/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs +++ b/tests/InfiniTests.InfiniFrame.WebServer/InfiniFrameWebApplicationTests.cs @@ -131,7 +131,7 @@ public async Task UseAutoServerClose_ClosingHandler_ShouldReturnFalse() { // Act app.UseAutoServerClose(); Func? capturedHandler = mockEvents.EventsStore.Closing.Snapshot.LastOrDefault(); - WindowClosingResult? result = capturedHandler?.Invoke(mockWindow, EventArgs.Empty); + WindowClosingResult result = capturedHandler?.Invoke(mockWindow, EventArgs.Empty) ?? WindowClosingResult.Cancel; // Assert await Assert.That(capturedHandler).IsNotNull(); diff --git a/tests/InfiniTests.InfiniFrame/StaticAssets/StaticAssetSchemeHandlerTests.cs b/tests/InfiniTests.InfiniFrame/StaticAssets/StaticAssetSchemeHandlerTests.cs index 52c60790a..76d51df67 100644 --- a/tests/InfiniTests.InfiniFrame/StaticAssets/StaticAssetSchemeHandlerTests.cs +++ b/tests/InfiniTests.InfiniFrame/StaticAssets/StaticAssetSchemeHandlerTests.cs @@ -13,7 +13,7 @@ namespace InfiniTests.InfiniFrame.StaticAssets; public class StaticAssetSchemeHandlerTests { [Test] public async Task TryResolveUri_FragmentIsPreservedButExcludedFromLookup(CancellationToken ct = default) { - var provider = new RecordingFileProvider("index.html", ""u8.ToArray()); + var provider = new RecordingFileProvider("index.html", [.. ""u8]); bool resolved = StaticAssetSchemeHandler.TryResolveUri( provider, "index.html#settings", "app://localhost/", "index.html", out Uri uri); diff --git a/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponseCorsPipelineTests.cs b/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponseCorsPipelineTests.cs index 123cf9c25..0fe318af0 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponseCorsPipelineTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponseCorsPipelineTests.cs @@ -19,7 +19,7 @@ public class CustomSchemeResponseCorsPipelineTests { public async Task Callback_SameOriginRequest_ProducesResponseWithCorsHeaders(CancellationToken ct = default) { // Arrange InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("test"u8.ToArray()), "application/json")); + new MemoryStream([.. "test"u8]), "application/json")); var response = new CustomSchemeResponse(); // Act @@ -53,7 +53,7 @@ public async Task Callback_SameOriginRequest_ProducesResponseWithCorsHeaders(Can public async Task Callback_CrossOriginRequest_ProducesResponseWithoutCorsHeaders(CancellationToken ct = default) { // Arrange InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("test"u8.ToArray()), "application/json")); + new MemoryStream([.. "test"u8]), "application/json")); var response = new CustomSchemeResponse(); // Act @@ -86,7 +86,7 @@ public async Task Callback_CrossOriginRequest_ProducesResponseWithoutCorsHeaders public async Task Callback_NullOrigin_ProducesResponseWithoutCorsHeaders(CancellationToken ct = default) { // Arrange InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("test"u8.ToArray()), "text/html")); + new MemoryStream([.. "test"u8]), "text/html")); var response = new CustomSchemeResponse(); // Act @@ -117,7 +117,7 @@ public async Task Callback_NullOrigin_ProducesResponseWithoutCorsHeaders(Cancell public async Task Callback_DifferentPorts_ProducesResponseWithoutCorsHeaders(CancellationToken ct = default) { // Arrange InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("test"u8.ToArray()), "application/octet-stream")); + new MemoryStream([.. "test"u8]), "application/octet-stream")); var response = new CustomSchemeResponse(); // Act @@ -148,7 +148,7 @@ public async Task Callback_DifferentPorts_ProducesResponseWithoutCorsHeaders(Can public async Task Callback_DifferentSchemes_ProducesResponseWithoutCorsHeaders(CancellationToken ct = default) { // Arrange InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("test"u8.ToArray()), "text/plain")); + new MemoryStream([.. "test"u8]), "text/plain")); var response = new CustomSchemeResponse(); // Act @@ -179,7 +179,7 @@ public async Task Callback_DifferentSchemes_ProducesResponseWithoutCorsHeaders(C public async Task Callback_SubpathRequests_AreSameOrigin(CancellationToken ct = default) { // Arrange InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("test"u8.ToArray()), "text/html")); + new MemoryStream([.. "test"u8]), "text/html")); var responseA = new CustomSchemeResponse(); var responseB = new CustomSchemeResponse(); diff --git a/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponsePipelineTests.cs b/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponsePipelineTests.cs index 87b7f960b..aced7b691 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponsePipelineTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Events/CustomSchemeResponsePipelineTests.cs @@ -115,7 +115,7 @@ public async Task Callback_RepeatedRequestsReleaseEveryAllocation(CancellationTo const int requestCount = 10_000; long before = GetActiveAllocationCount(); InfiniFrameEvents events = CreateEvents((_, _) => ( - new MemoryStream("stress-response"u8.ToArray()), "text/plain")); + new MemoryStream([.. "stress-response"u8]), "text/plain")); // Act for (int i = 0; i < requestCount; i++) { diff --git a/tests/InfiniTests.InfiniFrame/Window/Events/ParentChildWindowTests.cs b/tests/InfiniTests.InfiniFrame/Window/Events/ParentChildWindowTests.cs index b3ae11f7d..d48e07916 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Events/ParentChildWindowTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Events/ParentChildWindowTests.cs @@ -50,7 +50,7 @@ public async Task AtWindowStage_ClosingParent_ClosesChildWindow(CancellationToke }, ct); IInfiniFrameWindow childWindow = childWindowUtility.Window; lock (parentWindow.Configuration.ChildWindows) { - parentWindow.Configuration.ChildWindows.Add(childWindow); + ((List)parentWindow.Configuration.ChildWindows).Add(childWindow); } // Act diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationTests.cs index dcdd5f48a..f417e9aac 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitrationTests.cs @@ -46,7 +46,7 @@ public async Task TryAcquirePrimaryInstance_NullMutexName_UsesDefault(Cancellati // Assert - verify the method uses a consistent name (not a new GUID each time) // If the first call succeeded, the second must fail (same mutex name). - // If another TFM runner holds the mutex, both return false — still correct. + // If another TFM runner holds the mutex, both return false, still correct. await Assert.That(first || !second).IsTrue(); } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Invoke/InvokeTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Invoke/InvokeTests.cs index be305b118..69137001e 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Invoke/InvokeTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Invoke/InvokeTests.cs @@ -15,14 +15,15 @@ public async Task DispatchAsync_NestedDispatch_CompletesWithoutDeadlock(Cancella IInfiniFrameWindow window = windowUtility.Window; int callbacks = 0; - ValueTask[] dispatches = Enumerable.Range(0, 32) - .Select(_ => window.DispatchAsync(callback: () => { - Interlocked.Increment(ref callbacks); - InfiniFrameDispatchResult nested = window.Features.Invoke.Invoke(() => Interlocked.Increment(ref callbacks)); - if (nested != InfiniFrameDispatchResult.Completed) - throw new InvalidOperationException($"Nested dispatch ended with {nested}."); - }, TimeSpan.FromSeconds(5), ct)) - .ToArray(); + ValueTask[] dispatches = [ + .. Enumerable.Range(0, 32) + .Select(_ => window.DispatchAsync(callback: () => { + Interlocked.Increment(ref callbacks); + InfiniFrameDispatchResult nested = window.Features.Invoke.Invoke(() => Interlocked.Increment(ref callbacks)); + if (nested != InfiniFrameDispatchResult.Completed) + throw new InvalidOperationException($"Nested dispatch ended with {nested}."); + }, TimeSpan.FromSeconds(5), ct)) + ]; InfiniFrameDispatchResult[] results = await Task.WhenAll(dispatches.Select(static d => d.AsTask())); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs index 2460ec7b4..dd6189585 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/JavaScript/ExecuteJavaScriptTests.cs @@ -13,6 +13,7 @@ namespace InfiniTests.InfiniFrame.Window.Features.JavaScript; // Code // --------------------------------------------------------------------------------------------------------------------- [SuppressMessage("ReSharper", "AsyncMethodWithoutAwait")] +[SuppressMessage("Usage", "TUnitAssertions0005:Assert.That(...) should not be used with a constant value")] public class ExecuteJavaScriptTests { [Test] public async Task EvalResultHandler_CompletesPendingEval(CancellationToken ct) { diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs index 004f2a044..2afe6577b 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/CrossThreadWindowLifecycleTests.cs @@ -40,9 +40,10 @@ public async Task RepeatedCreateCloseAcrossManagedThreads_DoesNotFail(Cancellati [DefaultInfiniTestsTimeout(30_000)] public async Task ParallelCreateCloseAcrossManagedThreads_DoesNotFail(CancellationToken ct) { // Arrange - Task[] operations = Enumerable.Range(0, 4) - .Select(_ => Task.Run(action: () => CreateCloseAndWaitWindow(ct), ct)) - .ToArray(); + Task[] operations = [ + .. Enumerable.Range(0, 4) + .Select(_ => Task.Run(action: () => CreateCloseAndWaitWindow(ct), ct)) + ]; // Act await Task.WhenAll(operations); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs index d18da0232..9b6f36072 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs @@ -83,19 +83,21 @@ public async Task FeatureCallsRacingClose_DoNotReachFreedNativeInstance(Cancella using var stop = CancellationTokenSource.CreateLinkedTokenSource(ct); int completedCalls = 0; - Task[] callers = Enumerable.Range(0, ConcurrentFeatureCallerCount) - .Select(workerIndex => Task.Run(action: () => { - _ = workerIndex; - while (!stop.IsCancellationRequested) { - try { - _ = window.Features.State.IsFocused; - Interlocked.Increment(ref completedCalls); + Task[] callers = [ + .. Enumerable.Range(0, ConcurrentFeatureCallerCount) + .Select(workerIndex => Task.Run(action: () => { + _ = workerIndex; + while (!stop.IsCancellationRequested) { + try { + _ = window.Features.State.IsFocused; + Interlocked.Increment(ref completedCalls); + } + catch (ObjectDisposedException) { + return; + } } - catch (ObjectDisposedException) { - return; - } - } - }, stop.Token)).ToArray(); + }, stop.Token)) + ]; // Act await Task.Delay(50, ct); @@ -122,9 +124,10 @@ public async Task ConcurrentCloseRequests_ProduceSingleDeterministicShutdown(Can using var windowUtility = InfiniFrameTestWindow.Create(ct); IInfiniFrameWindow window = windowUtility.Window; - Task[] closeRequests = Enumerable.Range(0, 16) - .Select(_ => Task.Run(window.Close, ct)) - .ToArray(); + Task[] closeRequests = [ + .. Enumerable.Range(0, 16) + .Select(_ => Task.Run(window.Close, ct)) + ]; // Act await Task.WhenAll(closeRequests); diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs index 70de7a518..24d55fa4c 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs @@ -15,18 +15,18 @@ public class MenuBarSerializationTests { public async Task MenuBar_RoundTrip_JsonSerialization(CancellationToken ct) { // Arrange var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("open", "Open", KeyboardShortcut: "Ctrl+O"), new InfiniFrameMenuItem("save", "Save", KeyboardShortcut: "Ctrl+S"), new InfiniFrameMenuItem("sep", Type: InfiniFrameMenuItemType.Separator), new InfiniFrameMenuItem("exit", "Exit", KeyboardShortcut: "Alt+F4") - ) + ] ), new InfiniFrameMenuItem("edit", "Edit"), new InfiniFrameMenuItem("help", "Help", IsEnabled: false, IsVisible: false) - ) + ] ); // Act @@ -35,9 +35,9 @@ public async Task MenuBar_RoundTrip_JsonSerialization(CancellationToken ct) { // Assert await Assert.That(deserialized).IsNotNull(); - await Assert.That(deserialized!.Items).Count().IsEqualTo(3); + await Assert.That(deserialized!.Items.Length).IsEqualTo(3); await Assert.That(deserialized.Items[0].Id).IsEqualTo("file"); - await Assert.That(deserialized.Items[0].Children).Count().IsEqualTo(4); + await Assert.That(deserialized.Items[0].Children.Length).IsEqualTo(4); await Assert.That(deserialized.Items[0].Children[2].Type).IsEqualTo(InfiniFrameMenuItemType.Separator); await Assert.That(deserialized.Items[2].IsEnabled).IsFalse(); await Assert.That(deserialized.Items[2].IsVisible).IsFalse(); @@ -65,6 +65,6 @@ public async Task MenuBar_EmptyItems_DeserializesCorrectly(CancellationToken ct) // Assert await Assert.That(deserialized).IsNotNull(); - await Assert.That(deserialized!.Items).IsEmpty(); + await Assert.That(deserialized!.Items.IsEmpty).IsTrue(); } } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs index 5705e11df..23aed4430 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs @@ -17,15 +17,15 @@ public async Task AtBuilderStage_DirectAssignment(CancellationToken ct) { // Arrange var builder = InfiniFrameWindowBuilder.Create(); var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("open", "Open"), new InfiniFrameMenuItem("save", "Save") - ) + ] ), new InfiniFrameMenuItem("edit", "Edit") - ) + ] ); // Act @@ -42,9 +42,9 @@ public async Task AtBuilderStage_ExtensionAssignment(CancellationToken ct) { // Arrange var builder = InfiniFrameWindowBuilder.Create(); var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("help", "Help") - ) + ] ); // Act @@ -66,7 +66,7 @@ public async Task AtBuilderStage_DefaultIsEmpty(CancellationToken ct) { InfiniFrameNativeParameters initParameters = builder.CollectNativeParameters(); // Assert - await Assert.That(builder.Features.Menu.MenuBar.Items).IsEmpty(); + await Assert.That(builder.Features.Menu.MenuBar.Items.IsEmpty).IsTrue(); await Assert.That(initParameters.MenuBarJson).IsNull(); } @@ -80,7 +80,7 @@ public async Task AtBuilderStage_NullMenuBar(CancellationToken ct) { InfiniFrameNativeParameters initParameters = builder.CollectNativeParameters(); // Assert - await Assert.That(builder.Features.Menu.MenuBar.Items).IsEmpty(); + await Assert.That(builder.Features.Menu.MenuBar.Items.IsEmpty).IsTrue(); await Assert.That(initParameters.MenuBarJson).IsNull(); } @@ -89,13 +89,13 @@ public async Task AtBuilderStage_MenuBarJson_SerializesCorrectly(CancellationTok // Arrange var builder = InfiniFrameWindowBuilder.Create(); var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("open", "Open", KeyboardShortcut: "Ctrl+O") - ) + ] ) - ) + ] ); // Act @@ -107,9 +107,9 @@ public async Task AtBuilderStage_MenuBarJson_SerializesCorrectly(CancellationTok var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var deserialized = JsonSerializer.Deserialize(initParameters.MenuBarJson!, options); await Assert.That(deserialized).IsNotNull(); - await Assert.That(deserialized!.Items).Count().IsEqualTo(1); + await Assert.That(deserialized!.Items.Length).IsEqualTo(1); await Assert.That(deserialized.Items[0].Id).IsEqualTo("file"); - await Assert.That(deserialized.Items[0].Children).Count().IsEqualTo(1); + await Assert.That(deserialized.Items[0].Children.Length).IsEqualTo(1); await Assert.That(deserialized.Items[0].Children[0].KeyboardShortcut).IsEqualTo("Ctrl+O"); } @@ -118,9 +118,9 @@ public async Task AtBuilderStage_SetMenuBar_EmptyItems_JsonIsNull(CancellationTo // Arrange var builder = InfiniFrameWindowBuilder.Create(); builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); // Act @@ -128,7 +128,7 @@ public async Task AtBuilderStage_SetMenuBar_EmptyItems_JsonIsNull(CancellationTo InfiniFrameNativeParameters initParameters = builder.CollectNativeParameters(); // Assert - await Assert.That(builder.Features.Menu.MenuBar.Items).IsEmpty(); + await Assert.That(builder.Features.Menu.MenuBar.Items.IsEmpty).IsTrue(); await Assert.That(initParameters.MenuBarJson).IsNull(); } @@ -137,21 +137,21 @@ public async Task AtBuilderStage_SetMenuBar_ReplacesExisting(CancellationToken c // Arrange var builder = InfiniFrameWindowBuilder.Create(); builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("old", "Old") - ) + ] )); // Act builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("new", "New") - ) + ] )); InfiniFrameNativeParameters initParameters = builder.CollectNativeParameters(); // Assert - await Assert.That(builder.Features.Menu.MenuBar.Items).Count().IsEqualTo(1); + await Assert.That(builder.Features.Menu.MenuBar.Items.Length).IsEqualTo(1); await Assert.That(builder.Features.Menu.MenuBar.Items[0].Id).IsEqualTo("new"); await Assert.That(initParameters.MenuBarJson).IsNotNull(); } @@ -164,13 +164,13 @@ public async Task AtBuilderStage_ExtensionReturnsBuilder_ForChaining(Cancellatio // Act IInfiniFrameWindowBuilder result = builder .SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); // Assert await Assert.That(result).IsSameReferenceAs(builder); - await Assert.That(builder.Features.Menu.MenuBar.Items).Count().IsEqualTo(1); + await Assert.That(builder.Features.Menu.MenuBar.Items.Length).IsEqualTo(1); } } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemEnableDisableTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemEnableDisableTests.cs index 429939f93..824663e90 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemEnableDisableTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemEnableDisableTests.cs @@ -2,7 +2,6 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; -using System.Collections.Immutable; namespace InfiniTests.InfiniFrame.Window.Features.Menu; // --------------------------------------------------------------------------------------------------------------------- @@ -16,13 +15,13 @@ public async Task AtWindowStage_SetMenuItemEnabled(CancellationToken ct) { // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("save", "Save") - ) + ] ) - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -43,13 +42,13 @@ public async Task AtWindowStage_SetMenuItemVisible(CancellationToken ct) { // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("save", "Save") - ) + ] ) - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -70,9 +69,9 @@ public async Task AtWindowStage_ClickMenuItem(CancellationToken ct) { // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -89,9 +88,9 @@ public async Task AtWindowStage_DirectSetMenuBar(CancellationToken ct) { using var windowUtility = InfiniFrameTestWindow.Create(ct); IInfiniFrameWindow window = windowUtility.Window; var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("help", "Help") - ) + ] ); // Act @@ -107,23 +106,23 @@ public async Task AtWindowStage_DirectSetMenuBar_ReplacesExisting(CancellationTo // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("old", "Old") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; var newMenuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("new", "New") - ) + ] ); // Act window.Features.Menu.SetMenuBar(newMenuBar); // Assert - await Assert.That(window.Features.Menu.MenuBar.Items).Count().IsEqualTo(1); + await Assert.That(window.Features.Menu.MenuBar.Items.Length).IsEqualTo(1); await Assert.That(window.Features.Menu.MenuBar.Items[0].Id).IsEqualTo("new"); } @@ -133,9 +132,9 @@ public async Task AtWindowStage_SetMenuBar_Null_ClearsMenu(CancellationToken ct) // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -144,7 +143,7 @@ public async Task AtWindowStage_SetMenuBar_Null_ClearsMenu(CancellationToken ct) window.Features.Menu.SetMenuBar(null!); // Assert - await Assert.That(window.Features.Menu.MenuBar.Items).IsEmpty(); + await Assert.That(window.Features.Menu.MenuBar.Items.IsEmpty).IsTrue(); } [Test] @@ -153,9 +152,9 @@ public async Task AtWindowStage_SetMenuBar_EmptyItems_ClearsMenu(CancellationTok // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -164,7 +163,7 @@ public async Task AtWindowStage_SetMenuBar_EmptyItems_ClearsMenu(CancellationTok window.Features.Menu.SetMenuBar(new InfiniFrameMenuBar()); // Assert - await Assert.That(window.Features.Menu.MenuBar.Items).IsEmpty(); + await Assert.That(window.Features.Menu.MenuBar.Items.IsEmpty).IsTrue(); } [Test] @@ -173,9 +172,9 @@ public async Task AtWindowStage_SetMenuItemEnabled_NonExistentId_NoOp(Cancellati // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -193,9 +192,9 @@ public async Task AtWindowStage_SetMenuItemVisible_NonExistentId_NoOp(Cancellati // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -213,17 +212,17 @@ public async Task AtWindowStage_SetMenuItemEnabled_DeeplyNestedItem(Cancellation // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("level1", "Level 1", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("level2", "Level 2", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("level3", "Level 3") - ) + ] ) - ) + ] ) - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -242,17 +241,17 @@ public async Task AtWindowStage_SetMenuItemVisible_DeeplyNestedItem(Cancellation // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("level1", "Level 1", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("level2", "Level 2", InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create( + Children: [ new InfiniFrameMenuItem("level3", "Level 3") - ) + ] ) - ) + ] ) - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -271,9 +270,9 @@ public async Task AtWindowStage_SetMenuItemEnabled_ToggleBackToTrue(Cancellation // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("test", "Test") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -292,9 +291,9 @@ public async Task AtWindowStage_SetMenuItemVisible_ToggleBackToTrue(Cancellation // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("test", "Test") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -314,9 +313,9 @@ public async Task AtWindowStage_ExtensionSetMenuBar(CancellationToken ct) { using var windowUtility = InfiniFrameTestWindow.Create(ct); IInfiniFrameWindow window = windowUtility.Window; var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("help", "Help") - ) + ] ); // Act @@ -333,9 +332,9 @@ public async Task AtWindowStage_ExtensionSetMenuItemEnabled(CancellationToken ct // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("test", "Test") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -354,9 +353,9 @@ public async Task AtWindowStage_ExtensionSetMenuItemVisible(CancellationToken ct // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("test", "Test") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; @@ -375,9 +374,9 @@ public async Task AtWindowStage_ExtensionClickMenuItem(CancellationToken ct) { // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { builder.Features.Menu.SetMenuBar(new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("test", "Test") - ) + ] )); }, ct); IInfiniFrameWindow window = windowUtility.Window; diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs index 942c849f6..a722fd32d 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs @@ -29,7 +29,7 @@ public async Task MenuItem_Creation(CancellationToken ct) { await Assert.That(item.IsEnabled).IsFalse(); await Assert.That(item.IsVisible).IsFalse(); await Assert.That(item.KeyboardShortcut).IsEqualTo("Ctrl+T"); - await Assert.That(item.Children).IsEmpty(); + await Assert.That(item.Children.IsEmpty).IsTrue(); } [Test] @@ -43,7 +43,7 @@ public async Task MenuItem_DefaultValues(CancellationToken ct) { await Assert.That(item.Type).IsEqualTo(InfiniFrameMenuItemType.Normal); await Assert.That(item.Label).IsNull(); await Assert.That(item.KeyboardShortcut).IsNull(); - await Assert.That(item.Children).IsEmpty(); + await Assert.That(item.Children.IsEmpty).IsTrue(); } [Test] @@ -67,12 +67,12 @@ public async Task MenuItem_SubmenuWithChildren(CancellationToken ct) { "parent", "Parent", Type: InfiniFrameMenuItemType.Submenu, - Children: ImmutableArray.Create(child1, child2) + Children: [child1, child2] ); // Assert await Assert.That(submenu.Type).IsEqualTo(InfiniFrameMenuItemType.Submenu); - await Assert.That(submenu.Children).Count().IsEqualTo(2); + await Assert.That(submenu.Children.Length).IsEqualTo(2); await Assert.That(submenu.Children[0].Id).IsEqualTo("child1"); await Assert.That(submenu.Children[1].Id).IsEqualTo("child2"); } @@ -83,21 +83,21 @@ public async Task MenuBar_EmptyDefault(CancellationToken ct) { var menuBar = new InfiniFrameMenuBar(); // Assert - await Assert.That(menuBar.Items).IsEmpty(); + await Assert.That(menuBar.Items.IsEmpty).IsTrue(); } [Test] public async Task MenuBar_WithItems(CancellationToken ct) { // Arrange & Act var menuBar = new InfiniFrameMenuBar( - Items: ImmutableArray.Create( + Items: [ new InfiniFrameMenuItem("file", "File"), new InfiniFrameMenuItem("edit", "Edit") - ) + ] ); // Assert - await Assert.That(menuBar.Items).Count().IsEqualTo(2); + await Assert.That(menuBar.Items.Length).IsEqualTo(2); await Assert.That(menuBar.Items[0].Id).IsEqualTo("file"); await Assert.That(menuBar.Items[1].Id).IsEqualTo("edit"); } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureWebMessageRouterTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureWebMessageRouterTests.cs index 5c9fb86f3..17cef02be 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureWebMessageRouterTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/Handlers/WindowFeatureWebMessageRouterTests.cs @@ -1,6 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using System.Diagnostics.CodeAnalysis; using InfiniFrame; using InfiniFrame.Debugging; using InfiniFrame.NativeBridge.Dialogs; @@ -14,6 +15,7 @@ namespace InfiniTests.InfiniFrame.Window.Features.WebMessaging.Handlers; // --------------------------------------------------------------------------------------------------------------------- public class WindowFeatureWebMessageRouterTests { [Test] + [SuppressMessage("ReSharper", "UseCollectionExpression")] public async Task RegisteredDispatchers_HaveUniqueNamesAndCoverEveryFeature() { string[] expected = [ "browser", "debugging", "decorations", "filePickerDialogs", "invoke", "javaScript", "lifecycle", "monitors", @@ -223,4 +225,4 @@ private static JsonElement Args(string json) { using JsonDocument document = JsonDocument.Parse(json); return document.RootElement.Clone(); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/SendWebMessageAsyncTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/SendWebMessageAsyncTests.cs index ab6beb853..7bdec90da 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/SendWebMessageAsyncTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/WebMessaging/SendWebMessageAsyncTests.cs @@ -84,9 +84,10 @@ public async Task AtWindowStage_ConcurrentSends_CompleteBeforeNativeClose(Cancel IInfiniFrameWindow window = windowUtility.Window; // Act - ValueTask[] sends = Enumerable.Range(0, 64) - .Select(index => window.Features.WebMessaging.SendWebMessageAsync($"message-{index}", ct)) - .ToArray(); + ValueTask[] sends = [ + .. Enumerable.Range(0, 64) + .Select(index => window.Features.WebMessaging.SendWebMessageAsync($"message-{index}", ct)) + ]; // Assert await Task.WhenAll(sends.Select(static send => send.AsTask())); From 315454ec69663108e2377353a67d36d1f9677475 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 17:45:51 +0200 Subject: [PATCH 02/30] Introduce browser messaging & various safety fixes Add host-driven browser controls and strengthen safety/cleanup across the codebase. Highlights: - Add browser feature toggles in TS (context menu, zoom, shortcuts) and guard UI events. - Replace injected JS toggles with structured web messages from native backends (Win/Mac/Linux) and register script handler in WebKit. - Thread-safety and lifecycle fixes: WindowRegistrationStateMachine (locking), InfiniFrameWindow (owns service provider, async-dispose guard), InfiniNavigation cancellation handling. - Resource/disposal improvements: DisposableCompositeFileProvider, FileProviderFactory returns disposable, native-build copy filter, clean.ps1 -KillProcesses flag. - Security and robustness: stronger static asset path checks, ExceptionsUtility excludes StackOverflowException from non-fatal list, PageNavigation catch condition fix, other small bugfixes and formatting. These changes centralize browser policy via host messaging and harden concurrency, disposal and security behavior. --- .../pack-single-file.ps1 | 4 +- scripts/clean.ps1 | 15 ++- .../InfiniFrameBlazorAppBuilder.cs | 19 ++-- .../InfiniFrameHttpHandler.cs | 2 +- .../Contracts/InfiniFrameHostMessaging.ts | 3 + .../BrowserInfiniFrameWindowFeature.ts | 84 +++++++++++++- .../Platform/Linux/Core/WindowState.Gtk.cpp | 27 ++--- .../Platform/Linux/WebKit/WebKitHost.Gtk.cpp | 103 +++--------------- .../Platform/Mac/Core/WindowState.Cocoa.mm | 19 +++- .../Platform/Mac/WebKit/WebKitHost.Cocoa.mm | 21 ---- .../Windows/WebView/WebView2Attach.Win32.cpp | 22 ---- .../WebView/WebView2Settings.Win32.cpp | 7 +- src/InfiniFrame.NativeBridge/native-build.ps1 | 1 + .../Utilities/ExceptionsUtility.cs | 8 +- .../JavaScriptEvaluationException.cs | 11 ++ .../Interop/WindowRegistrationStateMachine.cs | 34 +++--- .../DisposableCompositeFileProvider.cs | 26 +++++ .../StaticAssets/FileProviderFactory.cs | 21 +++- .../StaticAssets/StaticAssetSchemeHandler.cs | 6 +- .../Builder/InfiniFrameWindowBuilder.cs | 23 ++-- .../DebuggingInfiniFrameWindowFeature.cs | 2 +- .../InstanceArbitration.cs | 10 +- .../JavaScriptInfiniFrameWindowFeature.cs | 7 +- .../InfiniNavigationOperation.cs | 18 ++- .../PageNavigationInfiniFrameWindowFeature.cs | 2 +- .../PositionInfiniFrameWindowFeature.cs | 2 +- .../WebMessagingInfiniFrameWindowFeature.cs | 2 +- src/InfiniFrame/Window/InfiniFrameWindow.cs | 35 ++++-- 28 files changed, 300 insertions(+), 234 deletions(-) create mode 100644 src/InfiniFrame.Shared/Window/Features/JavaScript/JavaScriptEvaluationException.cs create mode 100644 src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs diff --git a/examples/InfiniFrameExample.SingleFileExe/pack-single-file.ps1 b/examples/InfiniFrameExample.SingleFileExe/pack-single-file.ps1 index f98cfb48b..de86d7e12 100644 --- a/examples/InfiniFrameExample.SingleFileExe/pack-single-file.ps1 +++ b/examples/InfiniFrameExample.SingleFileExe/pack-single-file.ps1 @@ -22,7 +22,7 @@ else { $packCommand = @("infiniframe-pack") } -$args = @( +$publishArgs = @( "publish", $projectPath, "--rid", $Rid, @@ -32,4 +32,4 @@ $args = @( ) $packPrefix = if ($packCommand.Length -gt 1) { $packCommand[1..($packCommand.Length - 1)] } else { @() } -& $packCommand[0] ($packPrefix + $args) +& $packCommand[0] ($packPrefix + $publishArgs) diff --git a/scripts/clean.ps1 b/scripts/clean.ps1 index 292fdef01..a9d2913bf 100644 --- a/scripts/clean.ps1 +++ b/scripts/clean.ps1 @@ -1,7 +1,16 @@ -$Root = Join-Path $PSScriptRoot ".." +param( + [switch]$KillProcesses +) + +$Root = Join-Path $PSScriptRoot ".." -Get-Process dotnet,MSBuild,vstest,playwright,node -ErrorAction SilentlyContinue | - Stop-Process -Force -ErrorAction SilentlyContinue +if ($KillProcesses) { + Write-Host "Stopping dotnet/MSBuild/vstest/playwright/node processes..." + Get-Process dotnet,MSBuild,vstest,playwright,node -ErrorAction SilentlyContinue | + Stop-Process -Force -ErrorAction SilentlyContinue +} else { + Write-Host "Skipping process cleanup (use -KillProcesses to stop dotnet/MSBuild/vstest/playwright/node)" +} Get-ChildItem -Path $Root -Directory -Recurse | Where-Object { $_.Name -in @('bin', 'obj') } | diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs index 913a08433..873b1deee 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs @@ -15,17 +15,17 @@ namespace InfiniFrame.BlazorWebView; // Code // --------------------------------------------------------------------------------------------------------------------- public class InfiniFrameBlazorAppBuilder : IInfiniFrameBlazorAppBuilder { - /// + /// public IInfiniFrameRootComponentList RootComponents { get; } = new InfiniFrameRootComponentList(); - /// + /// public IServiceCollection Services { get; } = new ServiceCollection(); - /// + /// public IInfiniFrameWindowBuilder WindowBuilder { get; } = InfiniFrameWindowBuilder.Create(); // ----------------------------------------------------------------------------------------------------------------- // Constructors // ----------------------------------------------------------------------------------------------------------------- - private InfiniFrameBlazorAppBuilder() { } + private InfiniFrameBlazorAppBuilder() {} public static InfiniFrameBlazorAppBuilder CreateDefault( string[]? args = null, @@ -151,7 +151,7 @@ public InfiniFrameBlazorApp Build(IServiceProvider serviceProvider) { ?? new InfiniFrameBlazorAppConfiguration(); InfiniFrameUriSecurityPolicyRegistry.ConfigureForBuilder( WindowBuilder, - policyBuilder => policyBuilder.AddTrustedOrigin(appConfig.AppBaseUri)); + configure: policyBuilder => policyBuilder.AddTrustedOrigin(appConfig.AppBaseUri)); string startupUrl = BuildStartupUrl(appConfig); var staticAssets = serviceProvider.GetRequiredService(); @@ -207,9 +207,12 @@ private static string NormalizeHostPage(string? hostPage) ); }); } - catch (Exception) { - // Never throw from global exception handler + catch (ObjectDisposedException) { + // Window already closed; nothing to report. + } + catch (InvalidOperationException) { + // Service not available; nothing to report. } }); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs index 3b83f3e52..a7d14e4a9 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs @@ -45,4 +45,4 @@ protected override Task SendAsync(HttpRequestMessage reques response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); return Task.FromResult(response); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.Js/TypeScript/Contracts/InfiniFrameHostMessaging.ts b/src/InfiniFrame.Js/TypeScript/Contracts/InfiniFrameHostMessaging.ts index 6ba1f1b14..6d7b3e09a 100644 --- a/src/InfiniFrame.Js/TypeScript/Contracts/InfiniFrameHostMessaging.ts +++ b/src/InfiniFrame.Js/TypeScript/Contracts/InfiniFrameHostMessaging.ts @@ -34,6 +34,9 @@ export const ReceiveFromHostMessageIds = { readyAck: `${infiniframe}:ready:ack`, getMessageResponse: `${infiniframe}:get:response`, webMessageAckRequest: `${infiniframe}:message:ack:request`, + setContextMenuEnabled: `${infiniframe}:browser:setContextMenuEnabled`, + setZoomEnabled: `${infiniframe}:browser:setZoomEnabled`, + setBrowserShortcutsEnabled: `${infiniframe}:browser:setBrowserShortcutsEnabled`, } export type SendToHostMessageId = typeof SendToHostMessageIds[keyof typeof SendToHostMessageIds]; diff --git a/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts b/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts index af09bd40c..b4e929ac6 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts @@ -1,13 +1,95 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +import {ReceiveFromHostMessageIds} from "../../Contracts"; import type {BrowserInfiniFrameWindowFeature as Contract} from "../../Contracts"; import {InfiniFrameWindowFeature} from "../InfiniFrameWindowFeature"; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- export class BrowserInfiniFrameWindowFeature extends InfiniFrameWindowFeature implements Contract { - constructor() { super("browser"); } + private contextMenuEnabled = true; + private zoomEnabled = true; + private browserShortcutsEnabled = true; + + constructor() { + super("browser"); + + window.infiniframe.messaging.assignMessageReceivedHandler( + ReceiveFromHostMessageIds.setContextMenuEnabled, payload => { + if (!payload) return; + try { + const {enabled} = JSON.parse(payload); + this.contextMenuEnabled = !!enabled; + } catch { /* ignore malformed payload */ } + } + ); + + window.infiniframe.messaging.assignMessageReceivedHandler( + ReceiveFromHostMessageIds.setZoomEnabled, payload => { + if (!payload) return; + try { + const {enabled} = JSON.parse(payload); + this.zoomEnabled = !!enabled; + } catch { /* ignore malformed payload */ } + } + ); + + window.infiniframe.messaging.assignMessageReceivedHandler( + ReceiveFromHostMessageIds.setBrowserShortcutsEnabled, payload => { + if (!payload) return; + try { + const {enabled} = JSON.parse(payload); + this.browserShortcutsEnabled = !!enabled; + } catch { /* ignore malformed payload */ } + } + ); + + this.installGuards(); + } + + private installGuards(): void { + document.addEventListener("keydown", (e: KeyboardEvent) => { + if (this.browserShortcutsEnabled) return; + const ctrl = e.ctrlKey || e.metaKey; + const k = e.key.toLowerCase(); + if (ctrl && (k === "t" || k === "n" || k === "w" || k === "r" || k === "p" + || k === "u" || k === "j" || k === "l" || k === "i" || k === "o" + || k === "h" || (e.shiftKey && k === "i"))) { + e.preventDefault(); + e.stopPropagation(); + return; + } + if (k === "f11") { + e.preventDefault(); + e.stopPropagation(); + } + }, true); + + document.addEventListener("contextmenu", (e: Event) => { + if (!this.contextMenuEnabled) { + e.preventDefault(); + e.stopPropagation(); + } + }, true); + + document.addEventListener("wheel", (e: WheelEvent) => { + if (!this.zoomEnabled && (e.ctrlKey || e.metaKey)) { + e.preventDefault(); + e.stopPropagation(); + } + }, {capture: true, passive: false}); + + document.addEventListener("keydown", (e: KeyboardEvent) => { + if (this.zoomEnabled) return; + const ctrl = e.ctrlKey || e.metaKey; + const k = e.key; + if ((ctrl && (k === "+" || k === "-" || k === "=" || k === "0")) || k === "F5") { + e.preventDefault(); + e.stopPropagation(); + } + }, true); + } isContextMenuEnabledAsync() { return this.get("isContextMenuEnabled"); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp index 8e263378e..17ee25c40 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp @@ -220,12 +220,9 @@ void InfiniFrameWindow::SetContextMenuEnabled(const bool enabled) { m_impl->_contextMenuEnabled = enabled; if (m_impl->_webview == nullptr) return; - const char* js = enabled - ? "window.__infiniframe_contextMenuEnabled=true;" - : "window.__infiniframe_contextMenuEnabled=false;"; - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(m_impl->_webview), js, -1, nullptr, nullptr, nullptr, nullptr, nullptr - ); + std::string payload = "{\"enabled\":" + std::string(enabled ? "true" : "false") + "}"; + std::string envelope = "{\"version\":1,\"messageId\":\"__infiniframe:browser:setContextMenuEnabled\",\"payload\":\"" + escapeJsonString(payload) + "\"}"; + SendWebMessage(envelope.c_str()); } void InfiniFrameWindow::SetMediaAutoplayEnabled(const bool enabled) { @@ -255,12 +252,9 @@ void InfiniFrameWindow::SetZoomEnabled(bool enabled) { m_impl->_zoomEnabled = enabled; if (m_impl->_webview == nullptr) return; - const char* js = enabled - ? "window.__infiniframe_zoomEnabled=true;" - : "window.__infiniframe_zoomEnabled=false;"; - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(m_impl->_webview), js, -1, nullptr, nullptr, nullptr, nullptr, nullptr - ); + std::string payload = "{\"enabled\":" + std::string(enabled ? "true" : "false") + "}"; + std::string envelope = "{\"version\":1,\"messageId\":\"__infiniframe:browser:setZoomEnabled\",\"payload\":\"" + escapeJsonString(payload) + "\"}"; + SendWebMessage(envelope.c_str()); } void InfiniFrameWindow::SetStatusBarEnabled(bool enabled) { @@ -273,12 +267,9 @@ void InfiniFrameWindow::SetBrowserShortcutsEnabled(bool enabled) { m_impl->_browserShortcutsEnabled = enabled; if (m_impl->_webview == nullptr) return; - const char* js = enabled - ? "window.__infiniframe_browserShortcutsEnabled=true;" - : "window.__infiniframe_browserShortcutsEnabled=false;"; - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(m_impl->_webview), js, -1, nullptr, nullptr, nullptr, nullptr, nullptr - ); + std::string payload = "{\"enabled\":" + std::string(enabled ? "true" : "false") + "}"; + std::string envelope = "{\"version\":1,\"messageId\":\"__infiniframe:browser:setBrowserShortcutsEnabled\",\"payload\":\"" + escapeJsonString(payload) + "\"}"; + SendWebMessage(envelope.c_str()); } void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp index 28a5befbe..1567ee3fd 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp @@ -25,13 +25,19 @@ extern gboolean on_webview_decide_policy( void InfiniFrameWindow::Show(bool isAlreadyShown) { (void)isAlreadyShown; + + // Early out if the webview has already been created. if (m_impl->_webview) { return; } + // Prepare the WebKit and graphics subsystems. m_impl->configure_webkit_remote_debugging(); infiniframe::linux_gtk::ConfigureGraphicsEnvironment(); + // Create a new WebKit context and web view, then configure settings and + // custom scheme handlers. The context is no longer needed after the view + // takes ownership. m_impl->_webContext = webkit_web_context_new(); m_impl->_webview = webkit_web_view_new_with_context(m_impl->_webContext); @@ -42,12 +48,14 @@ void InfiniFrameWindow::Show(bool isAlreadyShown) { g_object_unref(m_impl->_webContext); m_impl->_webContext = nullptr; + // Attach the web view to the GTK window and make it fill the available space. WebKitUserContentManager* contentManager = webkit_web_view_get_user_content_manager(WEBKIT_WEB_VIEW(m_impl->_webview)); gtk_container_add(GTK_CONTAINER(m_impl->_window), m_impl->_webview); gtk_widget_set_hexpand(m_impl->_webview, TRUE); gtk_widget_set_vexpand(m_impl->_webview, TRUE); + // Inject the core InfiniFrame bridge script that enables native<->web messaging. const auto& jsCode = Embedded::InfiniFrameJsUtf8(); WebKitUserScript* script = webkit_user_script_new( @@ -58,100 +66,17 @@ void InfiniFrameWindow::Show(bool isAlreadyShown) { webkit_user_content_manager_add_script(contentManager, script); webkit_user_script_unref(script); - { - static constexpr char kBrowserShortcutsJs[] = - "(function(){" - "if(window.__infiniframe_browserShortcutsEnabled===undefined)" - "window.__infiniframe_browserShortcutsEnabled=true;" - "document.addEventListener('keydown',function(e){" - "if(window.__infiniframe_browserShortcutsEnabled)return;" - "var c=e.ctrlKey||e.metaKey,s=e.shiftKey,k=e.key.toLowerCase();" - "if(c&&(k==='t'||k==='n'||k==='w'||k==='r'||k==='p'||k==='u'||k==='j'|" - "|k==='l'||k==='i'||k==='o'||k==='h'||(s&&k==='i'))){" - "e.preventDefault();e.stopPropagation();return false;}" - "if(k==='f11'){e.preventDefault();e.stopPropagation();return false;}" - "},true);" - "})();"; - WebKitUserScript* shortcutsScript = webkit_user_script_new( - kBrowserShortcutsJs, - WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, - WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, - nullptr, nullptr - ); - webkit_user_content_manager_add_script(contentManager, shortcutsScript); - webkit_user_script_unref(shortcutsScript); - } - - { - static constexpr char kContextMenuJs[] = - "(function(){" - "if(window.__infiniframe_contextMenuEnabled===undefined)" - "window.__infiniframe_contextMenuEnabled=true;" - "document.addEventListener('contextmenu',function(e){" - "if(!window.__infiniframe_contextMenuEnabled){" - "e.preventDefault();e.stopPropagation();return false;}" - "},true);" - "})();"; - WebKitUserScript* contextMenuScript = webkit_user_script_new( - kContextMenuJs, - WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, - WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, - nullptr, nullptr - ); - webkit_user_content_manager_add_script(contentManager, contextMenuScript); - webkit_user_script_unref(contextMenuScript); - } - - { - static constexpr char kZoomDisabledJs[] = - "(function(){" - "if(window.__infiniframe_zoomEnabled===undefined)" - "window.__infiniframe_zoomEnabled=true;" - "document.addEventListener('wheel',function(e){" - "if(!window.__infiniframe_zoomEnabled&&(e.ctrlKey||e.metaKey)){" - "e.preventDefault();e.stopPropagation();return false;}" - "},true);" - "document.addEventListener('keydown',function(e){" - "if(!window.__infiniframe_zoomEnabled){" - "var c=e.ctrlKey||e.metaKey,k=e.key;" - "if((c&&(k==='+'||k==='-'||k==='='||k==='0'))||" - "(k==='F5')||(c&&k==='0')){" - "e.preventDefault();e.stopPropagation();return false;}" - "}" - "},true);" - "})();"; - WebKitUserScript* zoomScript = webkit_user_script_new( - kZoomDisabledJs, - WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, - WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, - nullptr, nullptr - ); - webkit_user_content_manager_add_script(contentManager, zoomScript); - webkit_user_script_unref(zoomScript); - } - + // Register the "infiniFrameInterop" message handler so the web content can + // send structured messages back to the host via + // window.webkit.messageHandlers.infiniFrameInterop.postMessage(). m_impl->_webMessageSignalHandlerId = g_signal_connect( contentManager, "script-message-received::infiniFrameInterop", G_CALLBACK(gtk_webkit::HandleWebMessage), reinterpret_cast(m_impl->_webMessageReceivedCallback) ); webkit_user_content_manager_register_script_message_handler(contentManager, "infiniFrameInterop"); - { - std::string initJs; - if (!m_impl->_contextMenuEnabled) - initJs += "window.__infiniframe_contextMenuEnabled=false;"; - if (!m_impl->_zoomEnabled) - initJs += "window.__infiniframe_zoomEnabled=false;"; - if (!m_impl->_browserShortcutsEnabled) - initJs += "window.__infiniframe_browserShortcutsEnabled=false;"; - if (!initJs.empty()) { - webkit_web_view_evaluate_javascript( - WEBKIT_WEB_VIEW(m_impl->_webview), initJs.c_str(), -1, - nullptr, nullptr, nullptr, nullptr, nullptr - ); - } - } - + // Connect WebKit signals for load lifecycle, process termination, sizing, + // and navigation policy decisions. g_signal_connect(G_OBJECT(m_impl->_webview), "load-changed", G_CALLBACK(on_webview_load_changed), this); g_signal_connect(G_OBJECT(m_impl->_webview), "load-failed", G_CALLBACK(on_webview_load_failed), this); g_signal_connect( @@ -160,6 +85,8 @@ void InfiniFrameWindow::Show(bool isAlreadyShown) { g_signal_connect(G_OBJECT(m_impl->_webview), "size-allocate", G_CALLBACK(on_webview_size_allocate), this); g_signal_connect(G_OBJECT(m_impl->_webview), "decide-policy", G_CALLBACK(on_webview_decide_policy), this); + // Navigate to the initial content. Show an error dialog if neither URL + // nor raw string was provided. if (!m_impl->_startUrl.empty()) { NavigateToUrl(const_cast(m_impl->_startUrl.c_str())); } else if (!m_impl->_startString.empty()) { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowState.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowState.Cocoa.mm index 13db17c88..b5cfdaa6a 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowState.Cocoa.mm +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/Core/WindowState.Cocoa.mm @@ -343,10 +343,21 @@ m_impl->_browserShortcutsEnabled = enabled; if (m_impl->_webview == nil) return; - NSString *js = enabled - ? @"window.__infiniframe_browserShortcutsEnabled=true;" - : @"window.__infiniframe_browserShortcutsEnabled=false;"; - [m_impl->_webview evaluateJavaScript:js completionHandler:nil]; + @autoreleasepool { + NSString *enabledStr = enabled ? @"true" : @"false"; + NSString *payload = [NSString stringWithFormat:@"{\"enabled\":%@}", enabledStr]; + NSString *escapedPayload = [ + [[NSString alloc] + initWithData:[NSJSONSerialization dataWithJSONObject:@[payload] options:0 error:nil] + encoding:NSUTF8StringEncoding] autorelease]; + // Strip surrounding quotes added by NSJSONSerialization array wrapping. + escapedPayload = [escapedPayload substringWithRange:NSMakeRange(1, [escapedPayload length] - 2)]; + NSString *envelope = [NSString stringWithFormat: + @"{\"version\":1,\"messageId\":\"__infiniframe:browser:setBrowserShortcutsEnabled\",\"payload\":\"%@\"}", + escapedPayload]; + std::string envelopeStr = [envelope UTF8String]; + SendWebMessage(envelopeStr.c_str()); + } } void InfiniFrameWindow::SetIconFile(const char* filename) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/WebKit/WebKitHost.Cocoa.mm b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/WebKit/WebKitHost.Cocoa.mm index 701d8edf3..2c10a96b5 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/WebKit/WebKitHost.Cocoa.mm +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Mac/WebKit/WebKitHost.Cocoa.mm @@ -33,27 +33,6 @@ [userContentController addUserScript:script]; [script release]; - { - NSString *shortcutsJs = @"(function(){" - @"if(window.__infiniframe_browserShortcutsEnabled===undefined)" - @"window.__infiniframe_browserShortcutsEnabled=true;" - @"document.addEventListener('keydown',function(e){" - @"if(window.__infiniframe_browserShortcutsEnabled)return;" - @"var c=e.ctrlKey||e.metaKey,s=e.shiftKey,k=e.key.toLowerCase();" - @"if(c&&(k==='t'||k==='n'||k==='w'||k==='r'||k==='p'||k==='u'||k==='j'|" - @"|k==='l'||k==='i'||k==='o'||k==='h'||(s&&k==='i'))){" - @"e.preventDefault();e.stopPropagation();return false;}" - @"if(k==='f11'){e.preventDefault();e.stopPropagation();return false;}" - @"},true);" - @"})();"; - WKUserScript *shortcutsScript = [[WKUserScript alloc] - initWithSource:shortcutsJs - injectionTime:WKUserScriptInjectionTimeAtDocumentStart - forMainFrameOnly:NO]; - [userContentController addUserScript:shortcutsScript]; - [shortcutsScript release]; - } - m_impl->_webviewConfiguration.userContentController = userContentController; if (m_impl->_webview == nil) { [userContentController release]; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp index 2f8c18a4f..fb55b24a2 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp @@ -459,28 +459,6 @@ void InfiniFrameWindow::AttachWebView() { ).Get() ); - { - static constexpr wchar_t kBrowserShortcutsJs[] = - L"(function(){" - L"if(window.__infiniframe_browserShortcutsEnabled===undefined)" - L"window.__infiniframe_browserShortcutsEnabled=true;" - L"document.addEventListener('keydown',function(e){" - L"if(window.__infiniframe_browserShortcutsEnabled)return;" - L"var c=e.ctrlKey||e.metaKey,s=e.shiftKey,k=e.key.toLowerCase();" - L"if(c&&(k==='t'||k==='n'||k==='w'||k==='r'||k==='p'||k==='u'||k==='j'|" - L"|k==='l'||k==='i'||k==='o'||k==='h'||(s&&k==='i'))){" - L"e.preventDefault();e.stopPropagation();return false;}" - L"if(k==='f11'){e.preventDefault();e.stopPropagation();return false;}" - L"},true);" - L"})();"; - m_impl->_webviewWindow->AddScriptToExecuteOnDocumentCreated( - kBrowserShortcutsJs, - Callback( - [](HRESULT, LPCWSTR) -> HRESULT { return S_OK; } - ).Get() - ); - } - if (FAILED(addScriptHr)) nav->navigate(); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp index cf8e5fa47..87a71fa13 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp @@ -181,9 +181,10 @@ void InfiniFrameWindow::SetBrowserShortcutsEnabled(const bool enabled) { m_impl->_browserShortcutsEnabled = enabled; if (!m_impl->_webviewWindow) return; - const auto flag = enabled ? L"true" : L"false"; - const auto js = std::wstring(L"window.__infiniframe_browserShortcutsEnabled=") + flag + L";"; - m_impl->_webviewWindow->ExecuteScript(js.c_str(), nullptr); + const char* flag = enabled ? "true" : "false"; + std::string payload = std::string("{\"enabled\":") + flag + "}"; + std::string envelope = "{\"version\":1,\"messageId\":\"__infiniframe:browser:setBrowserShortcutsEnabled\",\"payload\":\"" + payload + "\"}"; + SendWebMessage(envelope.c_str()); } void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { diff --git a/src/InfiniFrame.NativeBridge/native-build.ps1 b/src/InfiniFrame.NativeBridge/native-build.ps1 index 78ba76843..c20944ca0 100644 --- a/src/InfiniFrame.NativeBridge/native-build.ps1 +++ b/src/InfiniFrame.NativeBridge/native-build.ps1 @@ -216,6 +216,7 @@ try { # COPY OUTPUTS # ----------------------------------------------------------------------------------------------------------------- Get-ChildItem $BuildDir -Recurse -Include *.dll, *.so, *.dylib -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "InfiniFrame.Native*" } | Copy-Item -Destination "$ArtifactsDir/$Platform/$Arch/$Configuration" -Force Write-Host "" diff --git a/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs b/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs index 2210e17b5..2e7208fe1 100644 --- a/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs @@ -16,5 +16,9 @@ internal static class ExceptionsUtility { /// The exception to evaluate. /// true if the exception is non-fatal; otherwise, false. public static bool IsNonFatalException(Exception exception) - => exception is not (ApplicationException or OutOfMemoryException or AccessViolationException); -} \ No newline at end of file + => exception is not (ApplicationException + or OutOfMemoryException + or AccessViolationException + or StackOverflowException + ); +} diff --git a/src/InfiniFrame.Shared/Window/Features/JavaScript/JavaScriptEvaluationException.cs b/src/InfiniFrame.Shared/Window/Features/JavaScript/JavaScriptEvaluationException.cs new file mode 100644 index 000000000..1f70a61bd --- /dev/null +++ b/src/InfiniFrame.Shared/Window/Features/JavaScript/JavaScriptEvaluationException.cs @@ -0,0 +1,11 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace InfiniFrame; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Represents an error that occurred during JavaScript evaluation in the browser control. +/// +public sealed class JavaScriptEvaluationException(string message) : Exception(message); diff --git a/src/InfiniFrame/Interop/WindowRegistrationStateMachine.cs b/src/InfiniFrame/Interop/WindowRegistrationStateMachine.cs index 26690aa0a..1b4ac60fb 100644 --- a/src/InfiniFrame/Interop/WindowRegistrationStateMachine.cs +++ b/src/InfiniFrame/Interop/WindowRegistrationStateMachine.cs @@ -6,28 +6,36 @@ namespace InfiniFrame.Interop; // Code // --------------------------------------------------------------------------------------------------------------------- internal sealed class WindowRegistrationStateMachine { - private WindowRegistrationHandshakeState HandshakeState { get; set; } = WindowRegistrationHandshakeState.ReadyPending; - private bool RegistrationSendInProgress { get; set; } + private readonly object _lock = new(); + private WindowRegistrationHandshakeState _handshakeState = WindowRegistrationHandshakeState.ReadyPending; + private bool _registrationSendInProgress; // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- public bool TryBeginRegistrationSendOnReady() { - if (RegistrationSendInProgress) return false; - if (HandshakeState == WindowRegistrationHandshakeState.ReadyAcknowledged) return false; + lock (_lock) { + if (_registrationSendInProgress) return false; + if (_handshakeState == WindowRegistrationHandshakeState.ReadyAcknowledged) return false; - HandshakeState = WindowRegistrationHandshakeState.RegistrationSending; - RegistrationSendInProgress = true; - return true; + _handshakeState = WindowRegistrationHandshakeState.RegistrationSending; + _registrationSendInProgress = true; + return true; + } } public void CompleteRegistrationSend(bool success) { - RegistrationSendInProgress = false; - HandshakeState = success - ? WindowRegistrationHandshakeState.ReadyAcknowledged - : WindowRegistrationHandshakeState.Failed; + lock (_lock) { + _registrationSendInProgress = false; + _handshakeState = success + ? WindowRegistrationHandshakeState.ReadyAcknowledged + : WindowRegistrationHandshakeState.Failed; + } } - public bool IsReadyPending() - => HandshakeState == WindowRegistrationHandshakeState.ReadyPending; + public bool IsReadyPending() { + lock (_lock) { + return _handshakeState == WindowRegistrationHandshakeState.ReadyPending; + } + } } \ No newline at end of file diff --git a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs new file mode 100644 index 000000000..d775dee0d --- /dev/null +++ b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs @@ -0,0 +1,26 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +using Microsoft.Extensions.FileProviders; +using Microsoft.Extensions.Primitives; + +namespace InfiniFrame.StaticAssets; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +internal sealed class DisposableCompositeFileProvider(IList providers, PhysicalFileProvider physicalProvider) : IFileProvider, IDisposable { + private readonly CompositeFileProvider _inner = new(providers); + + public IDirectoryContents GetDirectoryContents(string subpath) + => _inner.GetDirectoryContents(subpath); + + public IFileInfo GetFileInfo(string subpath) + => _inner.GetFileInfo(subpath); + + public IChangeToken Watch(string filter) + => _inner.Watch(filter); + + public void Dispose() { + physicalProvider.Dispose(); + } +} diff --git a/src/InfiniFrame/StaticAssets/FileProviderFactory.cs b/src/InfiniFrame/StaticAssets/FileProviderFactory.cs index 7e06b5ccb..2baa55dfb 100644 --- a/src/InfiniFrame/StaticAssets/FileProviderFactory.cs +++ b/src/InfiniFrame/StaticAssets/FileProviderFactory.cs @@ -1,8 +1,8 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using Microsoft.Extensions.FileProviders; using System.Reflection; +using Microsoft.Extensions.FileProviders; namespace InfiniFrame.StaticAssets; // --------------------------------------------------------------------------------------------------------------------- @@ -17,9 +17,18 @@ public static class FileProviderFactory { /// wwwroot directory. /// /// The assembly that contains embedded wwwroot resources. Defaults to the entry assembly. - /// An optional physical wwwroot path. Defaults to wwwroot under the base directory. - /// Whether to include a physical file provider as a fallback when the directory exists. - /// A composite file provider that aggregates all available sources. + /// + /// An optional physical wwwroot path. Defaults to wwwroot under the base + /// directory. + /// + /// + /// Whether to include a physical file provider as a fallback when the directory + /// exists. + /// + /// + /// A composite file provider that aggregates all available sources. The caller is responsible for disposing the + /// returned provider if it implements . + /// public static IFileProvider CreateWwwrootProvider( Assembly? assembly = null, string? physicalWwwrootPath = null, @@ -48,7 +57,7 @@ public static IFileProvider CreateWwwrootProvider( var physicalProvider = new PhysicalFileProvider(fallbackPath); providers.Add(physicalProvider); - return new CompositeFileProvider(providers); + return new DisposableCompositeFileProvider(providers, physicalProvider); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs index 87fbf1365..2757fca3a 100644 --- a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs +++ b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs @@ -63,7 +63,11 @@ private static bool TryGetAssetPath(string rawPath, string defaultDocument, out if (assetPath.EndsWith('/')) assetPath += defaultDocument; assetPath = assetPath.Replace('\\', '/'); - if (assetPath.Split('/', StringSplitOptions.RemoveEmptyEntries).Any(segment => segment == "..")) return false; + if (assetPath.Contains("..", StringComparison.Ordinal) + || assetPath.Contains("%2e", StringComparison.OrdinalIgnoreCase) + || assetPath.Contains("%2f", StringComparison.OrdinalIgnoreCase) + || assetPath.Contains("%5c", StringComparison.OrdinalIgnoreCase)) + return false; return true; } diff --git a/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs b/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs index 9950e78fc..10208bec1 100644 --- a/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs +++ b/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilder.cs @@ -11,16 +11,16 @@ namespace InfiniFrame; // Code // --------------------------------------------------------------------------------------------------------------------- public class InfiniFrameWindowBuilder : IInfiniFrameWindowBuilder { - /// + /// public IInfiniFrameWindowBuilderConfiguration Configuration { get; } = new InfiniFrameWindowBuilderConfiguration(); - /// + /// public IInfiniFrameWindowBuilderFeatures Features { get; } = new InfiniFrameWindowBuilderFeatures(); - /// + /// public IDebuggingInfiniFrameWindowBuilderFeature Debugging => Features.Debugging; - /// + /// public IInfiniFrameEventsStore EventsStore { get; private init; } = new InfiniFrameEventsStore(); - /// + /// public IInfiniFrameStaticAssets? StaticAssets { get; set; } private IServiceCollection Services { get; init; } = new ServiceCollection().AddInfiniFrame(); @@ -42,8 +42,9 @@ public static InfiniFrameWindowBuilder Create(IServiceCollection? collection = n // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- - /// + /// public IInfiniFrameWindow Build(IServiceProvider? provider = null) { + bool ownsServiceProvider = provider is null; IServiceProvider actualProvider = provider ?? Services.BuildServiceProvider(); var featureFactory = actualProvider.GetRequiredService(); var validator = actualProvider.GetRequiredService>(); @@ -52,13 +53,13 @@ public IInfiniFrameWindow Build(IServiceProvider? provider = null) { // Instance arbitration check IInstanceArbitrationInfiniFrameWindowBuilderFeature arbitration = Features.InstanceArbitration; - if (arbitration.Mode != InstanceArbitrationMode.Disabled) { - if (!InstanceArbitration.TryAcquirePrimaryInstance(arbitration.MutexName)) { - throw new InstanceAlreadyRunningException(); - } + if (arbitration.Mode != InstanceArbitrationMode.Disabled + && !InstanceArbitration.TryAcquirePrimaryInstance(arbitration.MutexName)) { + throw new InstanceAlreadyRunningException(); } var window = actualProvider.GetRequiredService(); + window.SetOwnsServiceProvider(ownsServiceProvider); window.AssignFeatures(featureFactory.Create(window, this)); @@ -91,4 +92,4 @@ internal InfiniFrameNativeParameters CollectNativeParameters() { return parameters; } -} \ No newline at end of file +} diff --git a/src/InfiniFrame/Window/Features/Debugging/DebuggingInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/Debugging/DebuggingInfiniFrameWindowFeature.cs index da5df227b..410b21857 100644 --- a/src/InfiniFrame/Window/Features/Debugging/DebuggingInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/Debugging/DebuggingInfiniFrameWindowFeature.cs @@ -123,7 +123,7 @@ public bool TryProbeEndpoint(out Uri? endpoint, out string? reason) { public InfiniFrameDebugDiagnostics GetDiagnostics() { (DateTimeOffset TransitionUtc, IReadOnlyList Outstanding, InfiniFrameOperationDiagnostics? Last) operationDiagnostics = window is InfiniFrameWindow concreteWindow ? concreteWindow.GetOperationDiagnostics() - : (DateTimeOffset.UtcNow, (IReadOnlyList)[], null); + : (DateTimeOffset.UtcNow, [], null); Uri? endpoint = null; string? endpointReason = null; InfiniFrameDebugEndpointStatus endpointStatus; diff --git a/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs b/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs index 8ec12c9d5..efe1359c7 100644 --- a/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs +++ b/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs @@ -2,7 +2,9 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- using System.Runtime.Versioning; +using System.Security; using System.Security.Principal; +using InfiniFrame.Utilities; namespace InfiniFrame; // --------------------------------------------------------------------------------------------------------------------- @@ -36,7 +38,7 @@ public static bool TryAcquirePrimaryInstance(string? mutexName) { string name = mutexName ?? DefaultMutexName; try { - _primaryMutex = new Mutex(initiallyOwned: true, name: name, createdNew: out bool createdNew); + _primaryMutex = new Mutex(true, name, out bool createdNew); if (createdNew) return true; @@ -71,14 +73,12 @@ private static bool IsProcessElevatedWindows() { var principal = new WindowsPrincipal(identity); return principal.IsInRole(WindowsBuiltInRole.Administrator); } - catch { + catch (SecurityException) { return false; } } [SupportedOSPlatform("linux")] [SupportedOSPlatform("osx")] - private static bool IsProcessElevatedUnix() { - return Utilities.UnixPInvoke.GetUid() == 0; - } + private static bool IsProcessElevatedUnix() => UnixPInvoke.GetUid() == 0; } diff --git a/src/InfiniFrame/Window/Features/JavaScript/JavaScriptInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/JavaScript/JavaScriptInfiniFrameWindowFeature.cs index ef1cdc4b7..1ccc76ee1 100644 --- a/src/InfiniFrame/Window/Features/JavaScript/JavaScriptInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/JavaScript/JavaScriptInfiniFrameWindowFeature.cs @@ -12,7 +12,7 @@ namespace InfiniFrame; // Code // --------------------------------------------------------------------------------------------------------------------- public class JavaScriptInfiniFrameWindowFeature : IJavaScriptInfiniFrameWindowFeature { - private static long _nextRequestId; + private long _nextRequestId; private readonly IInfiniFrameWindow window; private readonly ILogger logger; private readonly ConcurrentDictionary> _pendingEvals = new(); @@ -177,8 +177,3 @@ private static string CreateEvalResponsePayload(string requestId, string? result return Encoding.UTF8.GetString(stream.ToArray()); } } - -/// -/// Represents an error that occurred during JavaScript evaluation in the browser control. -/// -public sealed class JavaScriptEvaluationException(string message) : Exception(message); diff --git a/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs b/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs index 2643eb9fd..fa4cc1215 100644 --- a/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs +++ b/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs @@ -54,6 +54,7 @@ CancellationToken cancellationToken } public async Task StartAsync() { + try { await _window.WaitForReadyAsync(_cancellationToken).ConfigureAwait(false); _lease = _window.AcquireNativeHandle(); @@ -72,6 +73,7 @@ public async Task StartAsync() { FinishCancelled(); return; } + if (dispatch != InfiniFrameDispatchResult.Completed) { Finish(new NavigationResult( Id, @@ -86,12 +88,20 @@ public async Task StartAsync() { static state => ((InfiniNavigationOperation)state!).RequestCancellation(), this ); _cancellationRegistration = registration; + // A backend is allowed to complete synchronously while BeginNavigate is returning. // In that race cleanup may have run before this registration was assigned. - if (Volatile.Read(ref _completed) != 0) { - registration.Dispose(); - return; + try { + if (Volatile.Read(ref _completed) != 0) { + await registration.DisposeAsync(); + return; + } } + catch { + await registration.DisposeAsync(); + throw; + } + if (_cancellationToken.IsCancellationRequested) RequestCancellation(); } @@ -198,4 +208,4 @@ private static bool TryGet( return false; } } -} \ No newline at end of file +} diff --git a/src/InfiniFrame/Window/Features/PageNavigation/PageNavigationInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/PageNavigation/PageNavigationInfiniFrameWindowFeature.cs index 9992db8f1..2f7ba2cd6 100644 --- a/src/InfiniFrame/Window/Features/PageNavigation/PageNavigationInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/PageNavigation/PageNavigationInfiniFrameWindowFeature.cs @@ -155,7 +155,7 @@ private bool TryNavigate(string target) { ); return true; } - catch (Exception ex) when (!ExceptionsUtility.IsNonFatalException(ex)) { + catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { logger.LogWarning(ex, "Failed to navigate to {Target}", target); return false; } diff --git a/src/InfiniFrame/Window/Features/Position/PositionInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/Position/PositionInfiniFrameWindowFeature.cs index a629beef2..4baad6682 100644 --- a/src/InfiniFrame/Window/Features/Position/PositionInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/Position/PositionInfiniFrameWindowFeature.cs @@ -111,7 +111,7 @@ public void SetTop(int top) { window.ManagedThreadId, InfiniFrameNative.GetPosition ); - if (oldTop == Top) return; + if (oldTop == top) return; NativeInvoke.InvokeSyncWithValidation( logger, diff --git a/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs index 2095b3d2d..c2d6ec861 100644 --- a/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs @@ -14,7 +14,7 @@ namespace InfiniFrame; // Code // --------------------------------------------------------------------------------------------------------------------- public class WebMessagingInfiniFrameWindowFeature : IWebMessagingInfiniFrameWindowFeature { - private static long _nextAcknowledgementId; + private long _nextAcknowledgementId; private readonly IInfiniFrameWindow window; private readonly ILogger logger; private readonly ConcurrentDictionary _acknowledgements = new(); diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 3b85a60d4..2e1e7d976 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -24,9 +24,11 @@ public sealed class InfiniFrameWindow( private int _closeReturnState = (int)InfiniFrameWindowLifecycleState.Ready; private int _managedThreadId = Environment.CurrentManagedThreadId; private long _lastLifecycleTransitionUtcTicks = DateTimeOffset.UtcNow.UtcTicks; + private int _asyncDisposing; private readonly object _diagnosticsLock = new(); private readonly Dictionary _outstandingOperations = []; private InfiniFrameOperationDiagnostics? _lastOperation; + private bool _ownsServiceProvider; #if NET9_0_OR_GREATER private readonly Lock _disposeLock = new(); #else @@ -92,6 +94,10 @@ internal void AssignFeatures(IInfiniFrameWindowFeatures features) { Features = features; } + internal void SetOwnsServiceProvider(bool owns) { + _ownsServiceProvider = owns; + } + internal string BeginDiagnosticOperation(string name, ulong id) { string key = $"{name}:{id}"; lock (_diagnosticsLock) { @@ -168,12 +174,13 @@ void IInfiniFrameWindow.AssignNativeHandle(IntPtr handle) { } void IInfiniFrameWindow.MarkReady() { - if (Interlocked.CompareExchange(ref _lifecycleState, - (int)InfiniFrameWindowLifecycleState.Ready, - (int)InfiniFrameWindowLifecycleState.Creating) == (int)InfiniFrameWindowLifecycleState.Creating) { - RecordLifecycleTransition(); - return; - } + if (Interlocked.CompareExchange( + ref _lifecycleState, + (int)InfiniFrameWindowLifecycleState.Ready, + (int)InfiniFrameWindowLifecycleState.Creating) != (int)InfiniFrameWindowLifecycleState.Creating + ) return; + + RecordLifecycleTransition(); // If not in Creating state, log but do not throw to avoid disrupting the lifecycle. // This may indicate an out-of-order lifecycle transition. } @@ -271,8 +278,6 @@ public void Dispose() { lock (_disposeLock) { if (LifecycleState == InfiniFrameWindowLifecycleState.Disposed) return; - if (Features is null) return; - if (!Features.Lifecycle.IsClosedOrClosing()) { Features.Lifecycle.Close(); } @@ -292,18 +297,26 @@ public void Dispose() { } Features.Lifecycle.CleanupNativeHandle(); + + if (_ownsServiceProvider && ServiceProvider is IDisposable disposableProvider) { + disposableProvider.Dispose(); + } } } public async ValueTask DisposeAsync() { + if (Interlocked.CompareExchange(ref _asyncDisposing, 1, 0) != 0) return; + lock (_disposeLock) { if (LifecycleState == InfiniFrameWindowLifecycleState.Disposed) return; } - if (Features is null) return; - if (!Features.Lifecycle.IsClosedOrClosing()) await Features.Lifecycle.CloseAsync().ConfigureAwait(false); await Features.Lifecycle.WaitForTeardownAsync().ConfigureAwait(false); Features.Lifecycle.CleanupNativeHandle(); + + if (_ownsServiceProvider && ServiceProvider is IDisposable disposableProvider) { + disposableProvider.Dispose(); + } } -} \ No newline at end of file +} From 84ead446f0eb05660014b6d199926cbf647a776b Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 17:55:24 +0200 Subject: [PATCH 03/30] Update native-build.ps1 --- src/InfiniFrame.NativeBridge/native-build.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/native-build.ps1 b/src/InfiniFrame.NativeBridge/native-build.ps1 index c20944ca0..946ddd485 100644 --- a/src/InfiniFrame.NativeBridge/native-build.ps1 +++ b/src/InfiniFrame.NativeBridge/native-build.ps1 @@ -215,9 +215,11 @@ try { # ----------------------------------------------------------------------------------------------------------------- # COPY OUTPUTS # ----------------------------------------------------------------------------------------------------------------- + $artifactsDest = "$ArtifactsDir/$Platform/$Arch/$Configuration" + Get-ChildItem $BuildDir -Recurse -Include *.dll, *.so, *.dylib -ErrorAction SilentlyContinue | - Where-Object { $_.Name -like "InfiniFrame.Native*" } | - Copy-Item -Destination "$ArtifactsDir/$Platform/$Arch/$Configuration" -Force + Where-Object { $_.Name -like "InfiniFrame.Native*" -or $_.Name -eq "WebView2Loader.dll" } | + Copy-Item -Destination $artifactsDest -Force Write-Host "" Write-Host "Native build complete." From 43b756b065c27d59ead506a2ec2e5757c2cdfa5c Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 18:10:05 +0200 Subject: [PATCH 04/30] Fix resource handling and update tests Close TcpClient on probe timeout to avoid socket leaks and throw when AllocCoTaskMem fails to detect allocation failure. Update tests to be type-safe and more robust: cast DPI to int for comparison, compare JS argument to (object)element, and refactor RemoteDebuggingPortTests to obtain a port via PortUtils.GetOpenPortValue() (removed method-data parameter). These changes prevent resource leaks, surface allocation errors, and reduce flaky test failures. --- src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs | 1 + .../Window/Events/InfiniFrameEvents.CustomScheme.cs | 2 ++ .../Tests/SharedWindowFeatureMirroringTests.cs | 2 +- tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs | 4 ++-- .../Window/Features/Debugging/RemoteDebuggingPortTests.cs | 5 +++-- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs b/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs index 82363b904..86f834001 100644 --- a/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs @@ -105,6 +105,7 @@ public static bool TryProbeEndpoint(Uri endpoint, out string? reason) { IAsyncResult connect = client.BeginConnect(endpoint.Host, endpoint.Port, null, null); bool signaled = connect.AsyncWaitHandle.WaitOne(TimeSpan.FromMilliseconds(500)); if (!signaled) { + client.Close(); reason = "Timed out while probing endpoint."; return false; } diff --git a/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs b/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs index 9ed1cb92c..a1567564f 100644 --- a/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs +++ b/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs @@ -184,6 +184,8 @@ private static CustomSchemeResponse BufferUnknownLengthResponse(Stream source, b private static IntPtr AllocateResponseStorage(int bodyLength, int contentTypeLength) { int allocationSize = checked(bodyLength + contentTypeLength + 1); IntPtr storage = Marshal.AllocCoTaskMem(allocationSize); + if (storage == IntPtr.Zero) + throw new OutOfMemoryException($"Failed to allocate {allocationSize} bytes for custom scheme response."); Interlocked.Increment(ref _activeCustomSchemeResponseAllocations); return storage; } diff --git a/tests/InfiniAutomationTests/Tests/SharedWindowFeatureMirroringTests.cs b/tests/InfiniAutomationTests/Tests/SharedWindowFeatureMirroringTests.cs index 6ad7ca1bd..afeae70b7 100644 --- a/tests/InfiniAutomationTests/Tests/SharedWindowFeatureMirroringTests.cs +++ b/tests/InfiniAutomationTests/Tests/SharedWindowFeatureMirroringTests.cs @@ -97,7 +97,7 @@ public async Task LifecycleAndMonitorFeatures_Getters_ShouldMirrorNativeWindow(C JsonElement actual = await ProbeFeatureAsync(page, "lifecycle-monitors"); await Assert.That(actual.GetProperty("closedOrClosing").GetBoolean()).IsEqualTo(RuntimeContext.Window.Features.Lifecycle.IsClosedOrClosing()); - await Assert.That(actual.GetProperty("dpi").GetDouble()).IsEqualTo(RuntimeContext.Window.Features.Monitors.GetMainMonitorScreenDpi()); + await Assert.That((int)actual.GetProperty("dpi").GetDouble()).IsEqualTo(RuntimeContext.Window.Features.Monitors.GetMainMonitorScreenDpi()); } [Test] diff --git a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs index 9bc704489..97258ed92 100644 --- a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs +++ b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs @@ -28,7 +28,7 @@ public async Task SetPointerCaptureAsync_InvokesExpectedJsFunction(CancellationT await Assert.That(identifier).IsEqualTo("infiniframe.utils.setPointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo(element); + await Assert.That(jsArguments[0]).IsEqualTo((object)element); await Assert.That(jsArguments[1]).IsEqualTo(42L); } @@ -48,7 +48,7 @@ public async Task ReleasePointerCaptureAsync_InvokesExpectedJsFunction(Cancellat await Assert.That(identifier).IsEqualTo("infiniframe.utils.releasePointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo(element); + await Assert.That(jsArguments[0]).IsEqualTo((object)element); await Assert.That(jsArguments[1]).IsEqualTo(7L); } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 5e75f83c0..167edd0e0 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -64,14 +64,15 @@ public async Task AtBuilderStage_ExtensionAssignment(int value, CancellationToke [Test] [NotInParallelInfiniTests] - [MethodDataSource(nameof(GetPorts))] [SkipOnMacOs("Remote TCP debugging endpoints are not supported by WKWebView")] - public async Task AtWindowStage_ThroughBuilderAssignment(int value, CancellationToken ct) { + public async Task AtWindowStage_ThroughBuilderAssignment(CancellationToken ct) { if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux()) { Skip.Test("This test is only run on Windows and Linux"); return; } + int value = PortUtils.GetOpenPortValue(); + // Arrange using var windowUtility = InfiniFrameTestWindow.Create(builder: builder => { if (!OperatingSystem.IsWindows() && !OperatingSystem.IsLinux()) return; From 2f11afa829b324089b9ed81d930e29c3e02e7d21 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 21:27:08 +0200 Subject: [PATCH 05/30] Guard window.infiniframe and adjust tests Use optional chaining when accessing window.infiniframe and its subproperties to avoid runtime errors (Index.ts, BrowserInfiniFrameWindowFeature.ts). Add a beforeEach test stub to ensure window.infiniframe.messaging and utils exist for unit tests (InfiniFrame.test.ts). Fix window-related test setup to create/return a single InfiniFrameWindow instance and assign it to window.infiniframe.window, stabilizing InfiniFrameWindow.* tests. --- src/InfiniFrame.Js/TypeScript/Index.ts | 2 +- .../TypeScript/InfiniFrame.test.ts | 20 ++++++++++++++++++- .../BrowserInfiniFrameWindowFeature.ts | 6 +++--- .../Window/InfiniFrameWindow.test.ts | 4 +++- .../Window/InfiniFrameWindowFeatures.test.ts | 6 ++++-- 5 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/InfiniFrame.Js/TypeScript/Index.ts b/src/InfiniFrame.Js/TypeScript/Index.ts index 0874e571e..1d9de8d81 100644 --- a/src/InfiniFrame.Js/TypeScript/Index.ts +++ b/src/InfiniFrame.Js/TypeScript/Index.ts @@ -20,7 +20,7 @@ initBlazorModulesFetchPatch(setup); initBlazorCustomElementsPatch(setup); initCustomElements(setup); -if (!window.infiniframe.messaging || !window.infiniframe.window?.features || !window.infiniframe.utils) { +if (!window.infiniframe?.messaging || !window.infiniframe?.window?.features || !window.infiniframe?.utils) { window.infiniframe = new InfiniFrame(window.infiniframe); } diff --git a/src/InfiniFrame.Js/TypeScript/InfiniFrame.test.ts b/src/InfiniFrame.Js/TypeScript/InfiniFrame.test.ts index 95d27eae9..4b24fad51 100644 --- a/src/InfiniFrame.Js/TypeScript/InfiniFrame.test.ts +++ b/src/InfiniFrame.Js/TypeScript/InfiniFrame.test.ts @@ -1,7 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -import {describe, it, expect, vi} from "vitest"; +import {beforeEach, describe, it, expect, vi} from "vitest"; import {InfiniFrame} from "./InfiniFrame"; // --------------------------------------------------------------------------------------------------------------------- @@ -9,6 +9,24 @@ import {InfiniFrame} from "./InfiniFrame"; // --------------------------------------------------------------------------------------------------------------------- describe("InfiniFrame", () => { + beforeEach(() => { + const win = window as any; + if (!win.infiniframe?.messaging) { + win.infiniframe = { + messaging: { + sendMessageToHost: vi.fn(), + getMessageFromHostAsync: vi.fn(), + assignMessageReceivedHandler: vi.fn(), + unregisterMessageReceivedHandler: vi.fn() + }, + utils: { + setPointerCapture: vi.fn(), + releasePointerCapture: vi.fn() + } + }; + } + }); + it("initializes HostMessaging and Utils", () => { const instance = new InfiniFrame(); diff --git a/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts b/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts index b4e929ac6..9e9f0f7ea 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/Features/BrowserInfiniFrameWindowFeature.ts @@ -15,7 +15,7 @@ export class BrowserInfiniFrameWindowFeature extends InfiniFrameWindowFeature im constructor() { super("browser"); - window.infiniframe.messaging.assignMessageReceivedHandler( + window.infiniframe?.messaging?.assignMessageReceivedHandler( ReceiveFromHostMessageIds.setContextMenuEnabled, payload => { if (!payload) return; try { @@ -25,7 +25,7 @@ export class BrowserInfiniFrameWindowFeature extends InfiniFrameWindowFeature im } ); - window.infiniframe.messaging.assignMessageReceivedHandler( + window.infiniframe?.messaging?.assignMessageReceivedHandler( ReceiveFromHostMessageIds.setZoomEnabled, payload => { if (!payload) return; try { @@ -35,7 +35,7 @@ export class BrowserInfiniFrameWindowFeature extends InfiniFrameWindowFeature im } ); - window.infiniframe.messaging.assignMessageReceivedHandler( + window.infiniframe?.messaging?.assignMessageReceivedHandler( ReceiveFromHostMessageIds.setBrowserShortcutsEnabled, payload => { if (!payload) return; try { diff --git a/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindow.test.ts b/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindow.test.ts index c6d445f46..cd3285bde 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindow.test.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindow.test.ts @@ -25,14 +25,16 @@ describe("InfiniFrameWindow", () => { } function assignInfiniFrame(messaging: InfiniFrameHostMessaging) { + const windowApi = new InfiniFrameWindow(); testWindow.infiniframe = { messaging, - window: new InfiniFrameWindow(), + window: windowApi, utils: { setPointerCapture: vi.fn(), releasePointerCapture: vi.fn() } }; + return windowApi; } it("routes feature mutations through the generic feature endpoint", () => { diff --git a/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts b/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts index bf7b935a4..9d66ebe6d 100644 --- a/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts +++ b/src/InfiniFrame.Js/TypeScript/Window/InfiniFrameWindowFeatures.test.ts @@ -180,8 +180,9 @@ describe.each(contracts)("$feature window feature", ({feature, gets = [], posts assignMessageReceivedHandler: vi.fn(), unregisterMessageReceivedHandler: vi.fn() } as unknown as InfiniFrameHostMessaging; + window.infiniframe = {messaging, window: {} as InfiniFrameWindow, utils: {setPointerCapture: vi.fn(), releasePointerCapture: vi.fn()}}; windowApi = new InfiniFrameWindow(); - window.infiniframe = {messaging, window: windowApi, utils: {setPointerCapture: vi.fn(), releasePointerCapture: vi.fn()}}; + window.infiniframe.window = windowApi; }); it.each(gets)("$method maps to $command", async ({method, command, parameters = [], args, result}) => { @@ -222,8 +223,9 @@ describe("strongly typed feature behavior", () => { assignMessageReceivedHandler: vi.fn(), unregisterMessageReceivedHandler: vi.fn() } as unknown as InfiniFrameHostMessaging; + window.infiniframe = {messaging, window: {} as InfiniFrameWindow, utils: {setPointerCapture: vi.fn(), releasePointerCapture: vi.fn()}}; windowApi = new InfiniFrameWindow(); - window.infiniframe = {messaging, window: windowApi, utils: {setPointerCapture: vi.fn(), releasePointerCapture: vi.fn()}}; + window.infiniframe.window = windowApi; }); it("routes cached state bounds setters with typed Rectangle values", () => { From d490d1dcc833934de676ec66e23093daa7fac54c Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 21:56:56 +0200 Subject: [PATCH 06/30] Fix test assertions with explicit casts Adjust test assertions to use explicit casts to the expected types to avoid boxing/nullable mismatches. In InfiniFrameJsTests.cs cast jsArguments[1] to (long) before asserting. In RemoteDebuggingPortTests.cs cast window.Features.Debugging.RemoteDebuggingPort to (int?) when comparing to the expected value. --- tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs | 4 ++-- .../Window/Features/Debugging/RemoteDebuggingPortTests.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs index 97258ed92..53a5f2d2b 100644 --- a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs +++ b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs @@ -29,7 +29,7 @@ public async Task SetPointerCaptureAsync_InvokesExpectedJsFunction(CancellationT await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); await Assert.That(jsArguments[0]).IsEqualTo((object)element); - await Assert.That(jsArguments[1]).IsEqualTo(42L); + await Assert.That((long)jsArguments[1]!).IsEqualTo(42L); } [Test] @@ -49,7 +49,7 @@ public async Task ReleasePointerCaptureAsync_InvokesExpectedJsFunction(Cancellat await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); await Assert.That(jsArguments[0]).IsEqualTo((object)element); - await Assert.That(jsArguments[1]).IsEqualTo(7L); + await Assert.That((long)jsArguments[1]!).IsEqualTo(7L); } [Test] diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 167edd0e0..47f82b4e1 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -86,7 +86,7 @@ public async Task AtWindowStage_ThroughBuilderAssignment(CancellationToken ct) { // Assert await Assert.That(builder.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); - await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); + await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo((int?)value); } [Test] From 44b3f5dbfe38643cc4ac0aba7d28ae3c1485d6fd Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 22:32:01 +0200 Subject: [PATCH 07/30] Update CustomSchemeResponseAbiTests.cs --- .../Managed/Delegates/CustomSchemeResponseAbiTests.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs index 1a44aef56..6cf1f2c53 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/Delegates/CustomSchemeResponseAbiTests.cs @@ -77,7 +77,8 @@ public async Task NativeConsumer_OnCurrentPlatform_ValidatesCopiesAndReleasesExa Interlocked.Increment(ref releaseCount); }; IntPtr releaseCallback = Marshal.GetFunctionPointerForDelegate(release); - IntPtr callback = Marshal.GetFunctionPointerForDelegate((CppWebResourceRequestedDelegate)Response); + CppWebResourceRequestedDelegate responseDelegate = Response; + IntPtr callback = Marshal.GetFunctionPointerForDelegate(responseDelegate); // Act for (int i = 0; i < requestCount; i++) { @@ -88,7 +89,7 @@ public async Task NativeConsumer_OnCurrentPlatform_ValidatesCopiesAndReleasesExa } // Native code only sees the unmanaged thunks, so keep their delegate owners rooted through the last callback. - GC.KeepAlive((CppWebResourceRequestedDelegate)Response); + GC.KeepAlive(responseDelegate); GC.KeepAlive(release); // Assert @@ -111,7 +112,8 @@ public async Task NativeConsumer_ConcurrentCallbacks_KeepEachResponseAliveUntilN Interlocked.Increment(ref releaseCount); }; IntPtr releaseCallback = Marshal.GetFunctionPointerForDelegate(release); - IntPtr callback = Marshal.GetFunctionPointerForDelegate((CppWebResourceRequestedDelegate)Response); + CppWebResourceRequestedDelegate responseDelegate = Response; + IntPtr callback = Marshal.GetFunctionPointerForDelegate(responseDelegate); Task[] requests = Enumerable.Range(0, requestCount) .Select(_ => Task.Run(action: () => { @@ -126,7 +128,7 @@ public async Task NativeConsumer_ConcurrentCallbacks_KeepEachResponseAliveUntilN // Act await Task.WhenAll(requests); // The worker closures capture the function pointer, not the delegate that owns its unmanaged thunk. - GC.KeepAlive((CppWebResourceRequestedDelegate)Response); + GC.KeepAlive(responseDelegate); GC.KeepAlive(release); // Assert From 0449ad411d0760721fe55ec8470710f257340364 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 23:12:37 +0200 Subject: [PATCH 08/30] Fix 72 code review findings across the codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical fixes: - Fix GC-collection risk for delegates passed to native via GetFunctionPointerForDelegate - Fix ANSI/UTF-8 mismatch in CustomSchemeNameMemory (StringToHGlobalAnsi → StringToCoTaskMemUTF8) - Add upper-bound check on PtrToNativeStringArray count from native side - Move PublishTrimmed/PublishAot out of library Directory.Build.props High fixes: - Fix boolean marshaling inconsistency (U1 → I1 in SetDragDropEnabled) - Add logging on NativeWindowHandle destructor failure - Expand exception filter in NativeInvoke to exclude ThreadAbortException, OperationCanceledException - Expand IsNonFatalException to exclude BadImageFormatException, SEHException, ThreadAbortException - Fix race condition in InfiniFrameEvents.Messaging (capture Sender in local variable) - Mark _primaryMutex volatile in InstanceArbitration - Change IInfiniFrameWindowBuilderConfiguration.ChildWindows to IReadOnlyList Medium fixes: - Reduce InteropEnvelopeProtocol MaxDepth from 64 to 16 - Use Enum.TryParse in NotificationsWebMessageDispatcher - Add control character check for tab in custom scheme content type validation - Expand path traversal checks for double-encoding and uppercase hex - Fix NativeMonitor.Scale type from double to float (match native ABI) - Fix CustomSchemeNameMemoryEqualityComparer to dereference pointers - Remove status overwrite in NativeInvoke.EnsureSuccess - Fix InfiniFrameEventsStore from record to class - Change IStateInfiniFrameWindowFeature cached bounds to internal set - Fix validator side-effect (remove CreateDirectory in probe) - Use stream.GetBuffer() instead of ToArray() for envelope protocol - Add null guard for InfiniFrameWindow.Features - Remove PublishTrimmed/PublishAot from library builds Low fixes: - Dispose embedded providers in DisposableCompositeFileProvider - Fix DeepCopy() documentation to reflect shallow copy - Remove redundant RegexOptions.Compiled from GeneratedRegex attributes - Fix test file for float Scale type change --- src/Directory.Build.props | 7 +++--- .../Managed/Handles/NativeWindowHandle.cs | 9 ++++++-- .../Exports/InfiniFrameNative.Dialog.cs | 13 +++++++++++ .../Exports/InfiniFrameNative.Events.cs | 2 +- .../Managed/NativeInvoke.cs | 22 ++++++++----------- .../Managed/NativeMonitor.cs | 2 +- .../Parameters/CustomSchemeNameMemory.cs | 6 ++--- ...niFrameNativeParametersEqualityComparer.cs | 14 ++++++++++-- .../InfiniFrameNativeParametersValidator.cs | 6 ++++- .../StaticAssets/IInfiniFrameStaticAssets.cs | 3 ++- .../Utilities/ExceptionsUtility.cs | 3 +++ .../Utilities/RemoteDebuggingUtility.cs | 6 ++--- .../IInfiniFrameWindowBuilderConfiguration.cs | 2 +- .../State/IStateInfiniFrameWindowFeature.cs | 10 +++++---- .../Interop/InteropEnvelopeProtocol.cs | 4 ++-- .../DisposableCompositeFileProvider.cs | 5 +++++ .../StaticAssets/StaticAssetSchemeHandler.cs | 8 ++++++- .../InfiniFrameWindowBuilderConfiguration.cs | 2 ++ .../Events/InfiniFrameEvents.CustomScheme.cs | 2 +- .../Events/InfiniFrameEvents.Messaging.cs | 18 +++++++-------- .../Window/Events/InfiniFrameEventsStore.cs | 2 +- .../InstanceArbitration.cs | 2 +- .../NotificationsWebMessageDispatcher.cs | 3 ++- src/InfiniFrame/Window/InfiniFrameWindow.cs | 8 +++++-- .../Managed/NativeMonitorTests.cs | 22 +++++++++---------- 25 files changed, 116 insertions(+), 65 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index aef44f6c0..e9bd7250d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -17,8 +17,8 @@ enable false - true - true + true + true embedded true @@ -28,8 +28,7 @@ true true - true - true + true 0.54.0 InfiniFrame, TryPhotino diff --git a/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs b/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs index 840e7d7ad..7186414dd 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Handles/NativeWindowHandle.cs @@ -17,6 +17,11 @@ internal NativeWindowHandle(IntPtr handle, bool ownsHandle) : base(ownsHandle) { SetHandle(handle); } - protected override bool ReleaseHandle() - => InfiniFrameNative.Destructor(handle) == InfiniFrameNativeInteropStatus.Success; + protected override bool ReleaseHandle() { + InfiniFrameNativeInteropStatus status = InfiniFrameNative.Destructor(handle); + if (status != InfiniFrameNativeInteropStatus.Success) { + System.Diagnostics.Debug.WriteLine($"[InfiniFrame] Native window destructor failed with status {status}. Handle: {handle}"); + } + return status == InfiniFrameNativeInteropStatus.Success; + } } \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs index 014597701..a4537e1a7 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Dialog.cs @@ -37,6 +37,10 @@ IntPtr values /// A status code indicating success or failure. internal static InfiniFrameNativeInteropStatus ShowOpenFile(IntPtr instance, string title, string defaultPath, bool multiSelect, string[] filters, int filtersCount, out string?[] values) { InfiniFrameNativeInteropStatus status = ShowOpenFilePtr(instance, title, defaultPath, multiSelect, filters, filtersCount, out int resultCount, out IntPtr ptrValues); + if (status != InfiniFrameNativeInteropStatus.Success) { + values = Array.Empty(); + return status; + } values = PtrToNativeStringArray(ptrValues, resultCount); return status; } @@ -58,6 +62,10 @@ internal static InfiniFrameNativeInteropStatus ShowOpenFile(IntPtr instance, str /// A status code indicating success or failure. internal static InfiniFrameNativeInteropStatus ShowOpenFolder(IntPtr instance, string title, string defaultPath, bool multiSelect, out string?[] values) { InfiniFrameNativeInteropStatus status = ShowOpenFolderPtr(instance, title, defaultPath, multiSelect, out int resultCount, out IntPtr ptrValues); + if (status != InfiniFrameNativeInteropStatus.Success) { + values = Array.Empty(); + return status; + } values = PtrToNativeStringArray(ptrValues, resultCount); return status; } @@ -156,6 +164,11 @@ internal static partial InfiniFrameNativeInteropStatus CancelDialog( return Array.Empty(); } + const int maxCount = 10000; + if (count > maxCount) { + count = maxCount; + } + try { IntPtr[] ptrArray = new IntPtr[count]; string?[] values = new string?[count]; diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs index 183c37631..0c7120123 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Events.cs @@ -88,5 +88,5 @@ public partial class InfiniFrameNative { /// A status code indicating success or failure. [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_SetDragDropEnabled", SetLastError = true)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] - internal static partial InfiniFrameNativeInteropStatus SetDragDropEnabled(IntPtr instance, [MarshalAs(UnmanagedType.U1)] bool enabled); + internal static partial InfiniFrameNativeInteropStatus SetDragDropEnabled(IntPtr instance, [MarshalAs(UnmanagedType.I1)] bool enabled); } \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs b/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs index 002effc6a..c0a28fdd8 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs @@ -834,7 +834,7 @@ T arg logger.LogTrace("Executing callback on same thread"); result = callback(nativeHandle); } - catch (Exception ex) when (ex is not (ApplicationException or OutOfMemoryException or AccessViolationException)) { + catch (Exception ex) when (ex is not (ApplicationException or OutOfMemoryException or AccessViolationException or StackOverflowException or ThreadAbortException or OperationCanceledException)) { callbackException = ex; } finally { @@ -849,7 +849,7 @@ T arg try { result = callback(nativeHandle); } - catch (Exception ex) when (ex is not (ApplicationException or OutOfMemoryException or AccessViolationException)) { + catch (Exception ex) when (ex is not (ApplicationException or OutOfMemoryException or AccessViolationException or StackOverflowException or ThreadAbortException or OperationCanceledException)) { callbackException = ex; } finally { @@ -929,16 +929,12 @@ private static void EnsureSuccess(ILogger logger, InfiniFrameNativeInteropStatus InfiniFrameNativeInteropStatus actualStatus = status; - if (foundMessage is not null) { - actualStatus = InfiniFrameNativeInteropStatus.OperationFailed; - logger.LogTrace("Overwriting original status of {InfiniFrameNativeInteropStatus} with {ActualStatus}", status, actualStatus); - } string sanitizedMessage = Sanitize(message); string sanitizedActualStatus = Sanitize(actualStatus.ToString()); - logger.LogCritical("Native interop call failed with unknown status state. Fallback last error {FallbackLastError}. {FallbackMessage} {FallbackStatus}", fallbackLastError, sanitizedMessage, sanitizedActualStatus); - throw new ApplicationException($"Native interop call failed with unknown status state. Fallback last error {fallbackLastError}. {sanitizedMessage} {sanitizedActualStatus}"); + logger.LogCritical("Native interop call failed. Status: {FallbackStatus}. Fallback last error {FallbackLastError}. {FallbackMessage}", sanitizedActualStatus, fallbackLastError, sanitizedMessage); + throw new ApplicationException($"Native interop call failed with status {sanitizedActualStatus}. Fallback last error {fallbackLastError}. {sanitizedMessage}"); } private static string Sanitize(string message) { @@ -1031,14 +1027,14 @@ private static string Sanitize(string message) { /// internal delegate InfiniFrameNativeInteropStatus FuncWithArgs(IntPtr handle, T1 arg, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8); - [GeneratedRegex(@"0x[0-9A-Fa-f]+", RegexOptions.Compiled)] + [GeneratedRegex(@"0x[0-9A-Fa-f]+")] private static partial Regex GeneratedMemoryAddressRegex(); - [GeneratedRegex(@"[A-Za-z]:\\[^\s""']+", RegexOptions.Compiled)] + [GeneratedRegex(@"[A-Za-z]:\\[^\s""']+")] private static partial Regex GeneratedWindowsPathRegex(); - [GeneratedRegex(@"(? /// The display scale factor of the monitor (e.g. 1.0 for 100%, 1.25 for 125%). /// - public double Scale { get; set; } + public float Scale { get; set; } } diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemory.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemory.cs index e10262569..26984b432 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemory.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/CustomSchemeNameMemory.cs @@ -20,7 +20,7 @@ internal static class CustomSchemeNameMemory { // Methods // ----------------------------------------------------------------------------------------------------------------- /// - /// Allocates a fixed-size array of native pointers (HGlobal-allocated ANSI strings) from a sequence of scheme names. + /// Allocates a fixed-size array of native pointers (CoTaskMem-allocated UTF-8 strings) from a sequence of scheme names. /// /// The scheme name strings to allocate. /// An array of native pointers sized . @@ -35,7 +35,7 @@ internal static IntPtr[] Allocate(IEnumerable names) { throw new InvalidOperationException("Maximum number of custom schemes is 16."); } - pointers[index] = Marshal.StringToHGlobalAnsi(name); + pointers[index] = Marshal.StringToCoTaskMemUTF8(name); index++; } @@ -57,7 +57,7 @@ internal static void FreeAll(IntPtr[]? pointers) { for (int i = 0; i < pointers.Length; i++) { if (pointers[i] == IntPtr.Zero) continue; - Marshal.FreeHGlobal(pointers[i]); + Marshal.FreeCoTaskMem(pointers[i]); pointers[i] = IntPtr.Zero; } } diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs index 36bced1f9..d48fe64bb 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs @@ -1,6 +1,8 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using System.Runtime.InteropServices; + namespace InfiniFrame.NativeBridge.Parameters; // --------------------------------------------------------------------------------------------------------------------- // Code @@ -60,8 +62,16 @@ public bool Equals(InfiniFrameNativeParameters x, InfiniFrameNativeParameters y) // Parent window if (x.NativeParent != y.NativeParent) return false; - // Custom scheme support - if (!x.CustomSchemeNames.AsSpan().SequenceEqual(y.CustomSchemeNames.AsSpan())) return false; + // Custom scheme support - compare string content rather than raw pointer addresses + if (x.CustomSchemeNames is not null && y.CustomSchemeNames is not null) { + if (x.CustomSchemeNames.Length != y.CustomSchemeNames.Length) return false; + for (int i = 0; i < x.CustomSchemeNames.Length; i++) { + string? xStr = Marshal.PtrToStringUTF8(x.CustomSchemeNames[i]); + string? yStr = Marshal.PtrToStringUTF8(y.CustomSchemeNames[i]); + if (xStr != yStr) return false; + } + } + else if (x.CustomSchemeNames != y.CustomSchemeNames) return false; // Window geometry if (x.Left != y.Left) return false; diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index 6ee4134c4..058935cee 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -108,7 +108,11 @@ private static bool CanAccessTemporaryFilesPath(string? path) { string? probeFile = null; try { - Directory.CreateDirectory(path); + if (!Directory.Exists(path)) { + string? parentPath = Path.GetDirectoryName(path); + if (string.IsNullOrWhiteSpace(parentPath) || !Directory.Exists(parentPath)) + return false; + } probeFile = Path.Join(path, $".infiniframe-write-check-{Guid.NewGuid():N}.tmp"); diff --git a/src/InfiniFrame.Shared/StaticAssets/IInfiniFrameStaticAssets.cs b/src/InfiniFrame.Shared/StaticAssets/IInfiniFrameStaticAssets.cs index dc23d78d3..97ef69473 100644 --- a/src/InfiniFrame.Shared/StaticAssets/IInfiniFrameStaticAssets.cs +++ b/src/InfiniFrame.Shared/StaticAssets/IInfiniFrameStaticAssets.cs @@ -24,7 +24,8 @@ public interface IInfiniFrameStaticAssets { string DefaultDocument { get; } /// - /// Creates a deep copy of the static assets configuration. + /// Creates a shallow copy of the static assets configuration. + /// The returned instance shares the same reference. /// /// A new instance with the same property values. IInfiniFrameStaticAssets DeepCopy(); diff --git a/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs b/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs index 2e7208fe1..d743cdc4e 100644 --- a/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/ExceptionsUtility.cs @@ -20,5 +20,8 @@ public static bool IsNonFatalException(Exception exception) or OutOfMemoryException or AccessViolationException or StackOverflowException + or ThreadAbortException + or BadImageFormatException + or System.Runtime.InteropServices.SEHException ); } diff --git a/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs b/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs index 86f834001..691eb9448 100644 --- a/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/RemoteDebuggingUtility.cs @@ -157,12 +157,12 @@ public static void ValidatePortAvailabilityOrThrow(int normalizedPort, ILogger l : sanitized; } - [GeneratedRegex(@"(?:^|\s)--remote-debugging-port=\S+", RegexOptions.Compiled)] + [GeneratedRegex(@"(?:^|\s)--remote-debugging-port=\S+")] private static partial Regex RemoteDebuggingPortSwitchRegex(); - [GeneratedRegex(@"(?:^|\s)--remote-debugging-address=\S+", RegexOptions.Compiled)] + [GeneratedRegex(@"(?:^|\s)--remote-debugging-address=\S+")] private static partial Regex RemoteDebuggingAddressSwitchRegex(); - [GeneratedRegex(@"\s+", RegexOptions.Compiled)] + [GeneratedRegex(@"\s+")] private static partial Regex WhitespaceRegex(); } \ No newline at end of file diff --git a/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilderConfiguration.cs b/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilderConfiguration.cs index 19c4e6781..78bbd52d0 100644 --- a/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilderConfiguration.cs +++ b/src/InfiniFrame.Shared/Window/Builder/IInfiniFrameWindowBuilderConfiguration.cs @@ -20,7 +20,7 @@ public interface IInfiniFrameWindowBuilderConfiguration { /// /// Gets the list of child windows to associate with the window being built. /// - List ChildWindows { get; } + IReadOnlyList ChildWindows { get; } /// /// Applies the builder configuration values to the native parameters. diff --git a/src/InfiniFrame.Shared/Window/Features/State/IStateInfiniFrameWindowFeature.cs b/src/InfiniFrame.Shared/Window/Features/State/IStateInfiniFrameWindowFeature.cs index 7b075293c..979984b7e 100644 --- a/src/InfiniFrame.Shared/Window/Features/State/IStateInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame.Shared/Window/Features/State/IStateInfiniFrameWindowFeature.cs @@ -52,14 +52,16 @@ public interface IStateInfiniFrameWindowFeature { bool IsZoomEnabled { get; } /// - /// Gets or sets the cached bounds of the window before entering full-screen mode. + /// Gets the cached bounds of the window before entering full-screen mode. + /// Intended for internal use by the window state feature implementation. /// - Rectangle CachedPreFullScreenBounds { get; set; } + Rectangle CachedPreFullScreenBounds { get; internal set; } /// - /// Gets or sets the cached bounds of the window before being maximized. + /// Gets the cached bounds of the window before being maximized. + /// Intended for internal use by the window state feature implementation. /// - Rectangle CachedPreMaximizedBounds { get; set; } + Rectangle CachedPreMaximizedBounds { get; internal set; } /// /// Sets whether the window is maximized. diff --git a/src/InfiniFrame/Interop/InteropEnvelopeProtocol.cs b/src/InfiniFrame/Interop/InteropEnvelopeProtocol.cs index 8b2cd014d..528e14bcf 100644 --- a/src/InfiniFrame/Interop/InteropEnvelopeProtocol.cs +++ b/src/InfiniFrame/Interop/InteropEnvelopeProtocol.cs @@ -17,7 +17,7 @@ internal static class InteropEnvelopeProtocol { private static readonly JsonDocumentOptions JsonDocumentOptions = new() { AllowTrailingCommas = false, CommentHandling = JsonCommentHandling.Disallow, - MaxDepth = 64 + MaxDepth = 16 }; internal static string CreateEnvelopeMessage(string id, string? data = null, string command = PostCommand, string? requestId = null) { @@ -40,7 +40,7 @@ internal static string CreateEnvelopeMessage(string id, string? data = null, str writer.WriteEndObject(); writer.Flush(); - return Encoding.UTF8.GetString(stream.ToArray()); + return Encoding.UTF8.GetString(stream.GetBuffer(), 0, (int)stream.Length); } internal static InteropEnvelopeParseResult ParseIncomingMessage(string message) { diff --git a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs index d775dee0d..2a74c056c 100644 --- a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs +++ b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs @@ -22,5 +22,10 @@ public IChangeToken Watch(string filter) public void Dispose() { physicalProvider.Dispose(); + foreach (IFileProvider provider in providers) { + if (provider is IDisposable disposable) { + disposable.Dispose(); + } + } } } diff --git a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs index 2757fca3a..87800b3b7 100644 --- a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs +++ b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs @@ -65,8 +65,14 @@ private static bool TryGetAssetPath(string rawPath, string defaultDocument, out assetPath = assetPath.Replace('\\', '/'); if (assetPath.Contains("..", StringComparison.Ordinal) || assetPath.Contains("%2e", StringComparison.OrdinalIgnoreCase) + || assetPath.Contains("%2E", StringComparison.Ordinal) || assetPath.Contains("%2f", StringComparison.OrdinalIgnoreCase) - || assetPath.Contains("%5c", StringComparison.OrdinalIgnoreCase)) + || assetPath.Contains("%2F", StringComparison.Ordinal) + || assetPath.Contains("%5c", StringComparison.OrdinalIgnoreCase) + || assetPath.Contains("%5C", StringComparison.Ordinal) + || assetPath.Contains("%252e", StringComparison.OrdinalIgnoreCase) + || assetPath.Contains("%252f", StringComparison.OrdinalIgnoreCase) + || assetPath.Contains("%255c", StringComparison.OrdinalIgnoreCase)) return false; return true; diff --git a/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilderConfiguration.cs b/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilderConfiguration.cs index e3872987c..2df893f20 100644 --- a/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilderConfiguration.cs +++ b/src/InfiniFrame/Window/Builder/InfiniFrameWindowBuilderConfiguration.cs @@ -17,6 +17,8 @@ public class InfiniFrameWindowBuilderConfiguration : IInfiniFrameWindowBuilderCo /// public List ChildWindows { get; } = []; + IReadOnlyList IInfiniFrameWindowBuilderConfiguration.ChildWindows => ChildWindows; + // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- diff --git a/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs b/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs index a1567564f..26d006842 100644 --- a/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs +++ b/src/InfiniFrame/Window/Events/InfiniFrameEvents.CustomScheme.cs @@ -83,7 +83,7 @@ private static CustomSchemeResponse BufferResponse(Stream source, string? conten string normalizedContentType = string.IsNullOrWhiteSpace(contentType) ? "application/octet-stream" : contentType; - if (normalizedContentType.IndexOfAny(['\r', '\n', '\0']) >= 0) + if (normalizedContentType.IndexOfAny(['\r', '\n', '\0', '\t']) >= 0) throw new InvalidDataException("Custom scheme content type contains invalid control characters."); byte[] contentTypeBytes = Encoding.UTF8.GetBytes(normalizedContentType); diff --git a/src/InfiniFrame/Window/Events/InfiniFrameEvents.Messaging.cs b/src/InfiniFrame/Window/Events/InfiniFrameEvents.Messaging.cs index 7a0d12719..c7f4ad471 100644 --- a/src/InfiniFrame/Window/Events/InfiniFrameEvents.Messaging.cs +++ b/src/InfiniFrame/Window/Events/InfiniFrameEvents.Messaging.cs @@ -16,10 +16,10 @@ public partial class InfiniFrameEvents { /// Invokes registered user-defined handler methods when the native window sends a message. /// public void OnWebMessageReceived(string message, string? origin = null) { - ArgumentNullException.ThrowIfNull(Sender); + IInfiniFrameWindow sender = Sender ?? throw new ArgumentNullException(nameof(Sender)); ArgumentNullException.ThrowIfNull(message); - InfiniFrameWindowLifecycleState state = Sender.LifecycleState; + InfiniFrameWindowLifecycleState state = sender.LifecycleState; // Document scripts can post during parsing, before NavigationCompleted // advances the window from Creating to Ready. Native WebView message // delivery itself proves that the transport is live; only reject work @@ -39,7 +39,7 @@ public void OnWebMessageReceived(string message, string? origin = null) { switch (parseResult) { case { IsBlazor: true }: EventsStore.WebMessageReceived.Invoke( - Sender, + sender, new InfiniFrameWebMessageReceivedEvent(message, origin) ); return; @@ -50,7 +50,7 @@ public void OnWebMessageReceived(string message, string? origin = null) { parseResult.MessageId ); EventsStore.WebMessageReceived.Invoke( - Sender, + sender, new InfiniFrameWebMessageReceivedEvent(message, origin) ); return; @@ -69,7 +69,7 @@ public void OnWebMessageReceived(string message, string? origin = null) { switch (parseResult.Command) { case InteropEnvelopeProtocol.PostCommand: try { - if (!EventsStore.WebMessagePostData.TryInvoke(messageId, Sender!, payload)) { + if (!EventsStore.WebMessagePostData.TryInvoke(messageId, sender, payload)) { Logger.LogWarning( "Failed to handle post data request for message ID '{messageId}'", messageId @@ -88,13 +88,13 @@ public void OnWebMessageReceived(string message, string? origin = null) { case InteropEnvelopeProtocol.GetCommand: try { - if (!EventsStore.WebMessageGetData.TryInvoke(messageId, Sender!, payload, out string? response)) { - SendError(Sender, parseResult.RequestId, + if (!EventsStore.WebMessageGetData.TryInvoke(messageId, sender, payload, out string? response)) { + SendError(sender, parseResult.RequestId, $"No getMessage handler is registered for message ID '{messageId}'."); return; } - SendSuccess(Sender, parseResult.RequestId, response); + SendSuccess(sender, parseResult.RequestId, response); } catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { Logger.LogError( @@ -103,7 +103,7 @@ public void OnWebMessageReceived(string message, string? origin = null) { messageId ); - SendError(Sender, parseResult.RequestId, + SendError(sender, parseResult.RequestId, $"Unhandled exception while processing '{messageId}'."); } diff --git a/src/InfiniFrame/Window/Events/InfiniFrameEventsStore.cs b/src/InfiniFrame/Window/Events/InfiniFrameEventsStore.cs index 8e894b0f9..15fc35ee8 100644 --- a/src/InfiniFrame/Window/Events/InfiniFrameEventsStore.cs +++ b/src/InfiniFrame/Window/Events/InfiniFrameEventsStore.cs @@ -8,7 +8,7 @@ namespace InfiniFrame; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -public record InfiniFrameEventsStore : IInfiniFrameEventsStore { +public class InfiniFrameEventsStore : IInfiniFrameEventsStore { /// public OrderedEvent WindowLocationChanged { get; } = new(); /// diff --git a/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs b/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs index efe1359c7..59d0b7370 100644 --- a/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs +++ b/src/InfiniFrame/Window/Features/InstanceArbitration/InstanceArbitration.cs @@ -21,7 +21,7 @@ public static class InstanceArbitration { /// Holds the primary instance mutex for the process lifetime. /// The OS reclaims this mutex when the process terminates. /// - private static Mutex? _primaryMutex; + private static volatile Mutex? _primaryMutex; // ----------------------------------------------------------------------------------------------------------------- // Methods diff --git a/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs b/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs index 28324d820..e4c2feffc 100644 --- a/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs +++ b/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs @@ -34,7 +34,8 @@ protected override void Post(INotificationsInfiniFrameWindowFeature feature, str if (iconPath is not null || tag is not null || urgencyStr is not null) { InfiniFrameNotificationUrgency urgency = urgencyStr is not null - ? Enum.Parse(urgencyStr, ignoreCase: true) + && Enum.TryParse(urgencyStr, ignoreCase: true, out InfiniFrameNotificationUrgency parsed) + ? parsed : InfiniFrameNotificationUrgency.Normal; feature.ShowNotification(new InfiniFrameNotificationOptions { diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 2e1e7d976..9df3c48c9 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -82,7 +82,11 @@ void IInfiniFrameWindow.SetManagedThreadId(int managedThreadId) { /// public IInfiniFrameEvents Events { get; } = events; /// - public IInfiniFrameWindowFeatures Features { get; private set; } = null!; + public IInfiniFrameWindowFeatures Features { + get => _features ?? throw new InvalidOperationException("Features have not been assigned. Call AssignFeatures before accessing this property."); + private set => _features = value; + } + private IInfiniFrameWindowFeatures? _features; /// public IInfiniFrameEventsStore EventsStore => Events.EventsStore; @@ -91,7 +95,7 @@ void IInfiniFrameWindow.SetManagedThreadId(int managedThreadId) { // Methods // ----------------------------------------------------------------------------------------------------------------- internal void AssignFeatures(IInfiniFrameWindowFeatures features) { - Features = features; + _features = features; } internal void SetOwnsServiceProvider(bool owns) { diff --git a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs index e5362b432..64a9423f4 100644 --- a/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs +++ b/tests/InfiniTests.InfiniFrame.NativeBridge/Managed/NativeMonitorTests.cs @@ -40,7 +40,7 @@ public async Task Default_HasZeroScale(CancellationToken ct = default) { NativeMonitor monitor = default; // Assert - await Assert.That(monitor.Scale).IsEqualTo(0.0); + await Assert.That(monitor.Scale).IsEqualTo(0.0f); } [Test] @@ -76,7 +76,7 @@ public async Task Work_SetAndGet_PreservesAllCoordinates(CancellationToken ct = [Test] public async Task Scale_SetAndGet_PreservesValue(CancellationToken ct = default) { // Arrange - const double expectedScale = 1.25; + const float expectedScale = 1.25f; // Act NativeMonitor monitor = new() { Scale = expectedScale }; @@ -88,7 +88,7 @@ public async Task Scale_SetAndGet_PreservesValue(CancellationToken ct = default) [Test] public async Task Scale_WithHighDpiValue_PreservesValue(CancellationToken ct = default) { // Arrange - const double expectedScale = 2.0; + const float expectedScale = 2.0f; // Act NativeMonitor monitor = new() { Scale = expectedScale }; @@ -118,23 +118,23 @@ public async Task IsValueType_Copy_ProducesIndependentInstance(CancellationToken NativeMonitor original = new() { Monitor = new NativeRect { X = 0, Y = 0, Width = 1920, Height = 1080 }, Work = new NativeRect { X = 0, Y = 40, Width = 1920, Height = 1040 }, - Scale = 1.0 + Scale = 1.0f }; // Act NativeMonitor copy = original; - copy.Scale = 2.0; + copy.Scale = 2.0f; // Assert, original is unchanged - await Assert.That(original.Scale).IsEqualTo(1.0); - await Assert.That(copy.Scale).IsEqualTo(2.0); + await Assert.That(original.Scale).IsEqualTo(1.0f); + await Assert.That(copy.Scale).IsEqualTo(2.0f); } [Test] public async Task SequentialLayout_SizeMatchesExpected(CancellationToken ct = default) { // Arrange - // Two NativeRect fields (4 × int = 16 bytes each) + one double (8 bytes) = 40 bytes - const int expectedSize = 40; + // Two NativeRect fields (4 × int = 16 bytes each) + one float (4 bytes) = 36 bytes + const int expectedSize = 36; // Act int actualSize = Marshal.SizeOf(); @@ -149,7 +149,7 @@ public async Task AllFields_WhenSetTogether_AllValuesArePreserved(CancellationTo NativeMonitor monitor = new() { Monitor = new NativeRect { X = -3840, Y = -1080, Width = 3840, Height = 2160 }, Work = new NativeRect { X = -3840, Y = -1040, Width = 3840, Height = 2120 }, - Scale = 1.5 + Scale = 1.5f }; // Assert @@ -161,7 +161,7 @@ public async Task AllFields_WhenSetTogether_AllValuesArePreserved(CancellationTo await Assert.That(monitor.Work.Y).IsEqualTo(-1040); await Assert.That(monitor.Work.Width).IsEqualTo(3840); await Assert.That(monitor.Work.Height).IsEqualTo(2120); - await Assert.That(monitor.Scale).IsEqualTo(1.5); + await Assert.That(monitor.Scale).IsEqualTo(1.5f); } [Test] From 6471a572ffee194283bfd12cbf06f6eb4332e5b5 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Mon, 10 Aug 2026 23:33:16 +0200 Subject: [PATCH 09/30] Fix TemporaryFilesPath validator: restore auto-create with guarded fallback --- .../Parameters/InfiniFrameNativeParametersValidator.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index 058935cee..549ddf2db 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -109,9 +109,12 @@ private static bool CanAccessTemporaryFilesPath(string? path) { string? probeFile = null; try { if (!Directory.Exists(path)) { - string? parentPath = Path.GetDirectoryName(path); - if (string.IsNullOrWhiteSpace(parentPath) || !Directory.Exists(parentPath)) + try { + Directory.CreateDirectory(path); + } + catch { return false; + } } probeFile = Path.Join(path, $".infiniframe-write-check-{Guid.NewGuid():N}.tmp"); From 6d8dfc99350e1421413658ac6956d043e60d2d7b Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Tue, 11 Aug 2026 00:17:37 +0200 Subject: [PATCH 10/30] Adopt Microsoft logging and DI for pack tool Replace direct Serilog usage with Microsoft.Extensions.Logging and introduce DI for the pack tool. CommandLine, ProcessRunner and PublishService were converted from static to instance types and wired in Program via ServiceCollection. Updated Directory.Packages.props and csproj to add logging/DI packages and Serilog.Extensions.Hosting. Tests adjusted to use NullLogger and construct instances. Also includes several small documentation, lifecycle, locking and robustness tweaks across window, native-parameter, webapp and handler code (comments, safety checks and improved exception logging). --- Directory.Packages.props | 2 + .../InfiniFrameBlazorApp.cs | 12 +++ .../InfiniFrameBlazorAppConfiguration.cs | 1 + .../Parameters/InfiniFrameNativeParameters.cs | 6 +- ...niFrameNativeParametersEqualityComparer.cs | 4 +- .../InfiniFrameNativeParametersValidator.cs | 1 + src/InfiniFrame.Tools.Pack/CommandLine.cs | 44 ++++++----- .../InfiniFrame.Tools.Pack.csproj | 2 + src/InfiniFrame.Tools.Pack/Program.cs | 19 ++++- .../Services/ProcessRunner.cs | 38 ++++----- .../Services/PublishService.cs | 79 ++++++++++--------- .../InfiniFrameWebApplication.cs | 6 ++ .../InfiniFrameUriSecurityPolicyRegistry.cs | 6 ++ .../Events/InfiniFrameEvents.DefaultEvents.cs | 2 +- .../LifecycleInfiniFrameWindowFeature.cs | 7 +- .../OpenExternalTargetWebMessageHandler.cs | 20 +++-- src/InfiniFrame/Window/InfiniFrameWindow.cs | 12 ++- .../Window/InfiniFrameWindowConfiguration.cs | 5 ++ .../InfiniFrameWindowFeaturesFactory.cs | 11 ++- .../CommandLineTests.cs | 33 ++++---- .../InfiniTests.InfiniFrame.Tools.Pack.csproj | 1 + .../Services/ProcessRunnerTests.cs | 15 ++-- .../Services/PublishServiceTests.cs | 16 +++- 23 files changed, 219 insertions(+), 123 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 301484d4c..65f900595 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,7 @@ + @@ -34,6 +35,7 @@ + diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs index 73102ce63..d2698c62a 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs @@ -42,6 +42,10 @@ public async Task RunAsync(CancellationToken ct = default) { } /// + /// + /// This method uses synchronous-over-async patterns for disposal. It should only be called + /// from threads without a SynchronizationContext. Prefer for async contexts. + /// public void Run() { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); @@ -64,6 +68,14 @@ private void RegisterRootComponents() { } } + /// + /// Asynchronously disposes of the application and its service provider. + /// + /// + /// This method uses best-effort disposal: exceptions thrown during service provider + /// disposal are caught and logged but do not propagate to the caller. This prevents + /// resource cleanup failures from masking the original application shutdown. + /// public async ValueTask DisposeAsync() { if (Interlocked.Exchange(ref _disposed, 1) != 0) return; diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs index 9bb39bcbf..7b199805a 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs @@ -26,6 +26,7 @@ public class InfiniFrameBlazorAppConfiguration { /// /// Gets or sets the maximum number of outbound messages waiting to be delivered to the native WebView. /// A positive value is required. The default bounds memory while accommodating normal render bursts. + /// Increase this value for applications with high-frequency rendering updates; decrease for memory-constrained scenarios. /// public int WebMessageQueueCapacity { get; set; } = 1_024; diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs index 1cb8df0e8..a3a4492c6 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParameters.cs @@ -295,7 +295,7 @@ public struct InfiniFrameNativeParameters() { internal bool UseOsDefaultLocation; /// - /// OPTIONAL: If true, overrides Height and Width parameters and lets the OS position the newly created window. + /// OPTIONAL: If true, overrides Height and Width parameters and lets the OS size the newly created window. /// Default is true. /// [MarshalAs(UnmanagedType.I1)] @@ -325,7 +325,7 @@ public struct InfiniFrameNativeParameters() { /// /// OPTIONAL: Enables JavaScript access to the system clipboard when set to true. - /// Default behavior is disabled (false), which restricts clipboard operations. + /// Default is true. /// [MarshalAs(UnmanagedType.I1)] internal bool JavascriptClipboardAccessEnabled; @@ -397,7 +397,7 @@ public struct InfiniFrameNativeParameters() { /// /// Set when GetParamErrors() is called before initializing the native window. It is a check to make sure the - /// struct matches what C++ is expecting. + /// struct matches what C++ is expecting. This field is readonly to ensure ABI stability; do not modify after construction. /// [MarshalAs(UnmanagedType.I4)] internal readonly int Size = Marshal.SizeOf(); diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs index d48fe64bb..99ac72f5b 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersEqualityComparer.cs @@ -9,7 +9,9 @@ namespace InfiniFrame.NativeBridge.Parameters; // --------------------------------------------------------------------------------------------------------------------- /// /// Compares two instances for value equality, -/// ignoring callback handler fields. +/// ignoring callback handler fields. This comparer is intended for parameter-change +/// detection where two instances with different callbacks but identical configuration +/// values are considered equivalent. /// internal sealed class InfiniFrameNativeParametersEqualityComparer : IEqualityComparer { /// diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index 549ddf2db..6d5c48c72 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -100,6 +100,7 @@ public InfiniFrameNativeParametersValidator() { // ----------------------------------------------------------------------------------------------------------------- /// /// Checks whether the given path is writable by creating and deleting a temporary probe file. + /// If the directory does not exist, it will be created as a side effect of this check. /// /// The directory path to check. /// true if the path is writable; otherwise, false. diff --git a/src/InfiniFrame.Tools.Pack/CommandLine.cs b/src/InfiniFrame.Tools.Pack/CommandLine.cs index 6d1b78a62..60ba98d14 100644 --- a/src/InfiniFrame.Tools.Pack/CommandLine.cs +++ b/src/InfiniFrame.Tools.Pack/CommandLine.cs @@ -2,15 +2,19 @@ // Code // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame.Tools.Pack.Services; -using Serilog; +using Microsoft.Extensions.Logging; using System.Globalization; namespace InfiniFrame.Tools.Pack; // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- -internal static class CommandLine { - private static readonly ILogger Logger = Log.ForContext(typeof(CommandLine)); +internal sealed class CommandLine { + private readonly ILogger _logger; + + public CommandLine(ILogger logger) { + _logger = logger; + } /// /// Parses command-line arguments into a normalized model or a usage response. @@ -23,7 +27,7 @@ internal static class CommandLine { /// /// Thrown when --self-contained receives a value that is not a valid boolean. /// - public static ParseResult Parse(string[] args) { + public ParseResult Parse(string[] args) { string? firstArg = args.FirstOrDefault(); if (args.Length == 0 || firstArg is null || IsHelp(firstArg)) return ParseResult.Usage(ExitCodes.Success); @@ -43,21 +47,21 @@ public static ParseResult Parse(string[] args) { /// /// Prints the CLI usage text for the pack tool. /// - public static void PrintUsage() { - Logger.Information("InfiniFrame.Pack"); - Logger.Information("Usage:"); - Logger.Information(" infiniframe-pack publish [options]"); - Logger.Information(""); - Logger.Information("Options:"); - Logger.Information(" --rid Runtime identifier. Default: auto"); - Logger.Information(" --configuration Build configuration. Default: Release"); - Logger.Information(" --framework Target framework. Default: first TFM in project"); - Logger.Information(" --self-contained Self-contained publish. Default: true"); - Logger.Information(" --output Publish output directory"); - Logger.Information(" --no-restore Skip restore"); - Logger.Information(" --verbose Verbose publish output"); - Logger.Information(" --timeout Per-process timeout (e.g. 600, 90s, 5m, 00:10:00). Default: 10m, max: 30m"); - Logger.Information(" --force-clean-output Allow deleting non-default output directories"); + public void PrintUsage() { + _logger.LogInformation("InfiniFrame.Pack"); + _logger.LogInformation("Usage:"); + _logger.LogInformation(" infiniframe-pack publish [options]"); + _logger.LogInformation(""); + _logger.LogInformation("Options:"); + _logger.LogInformation(" --rid Runtime identifier. Default: auto"); + _logger.LogInformation(" --configuration Build configuration. Default: Release"); + _logger.LogInformation(" --framework Target framework. Default: first TFM in project"); + _logger.LogInformation(" --self-contained Self-contained publish. Default: true"); + _logger.LogInformation(" --output Publish output directory"); + _logger.LogInformation(" --no-restore Skip restore"); + _logger.LogInformation(" --verbose Verbose publish output"); + _logger.LogInformation(" --timeout Per-process timeout (e.g. 600, 90s, 5m, 00:10:00). Default: 10m, max: 30m"); + _logger.LogInformation(" --force-clean-output Allow deleting non-default output directories"); } private static bool IsHelp(string value) => value is "-h" or "--help" or "help"; @@ -175,4 +179,4 @@ private static void ValidateProcessTimeout(TimeSpan timeout) { $"Timeout '{timeout}' exceeds the maximum supported value of '{PublishOptions.MaxProcessTimeout}'."); } } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.Tools.Pack/InfiniFrame.Tools.Pack.csproj b/src/InfiniFrame.Tools.Pack/InfiniFrame.Tools.Pack.csproj index b148f89e1..9d8b52120 100644 --- a/src/InfiniFrame.Tools.Pack/InfiniFrame.Tools.Pack.csproj +++ b/src/InfiniFrame.Tools.Pack/InfiniFrame.Tools.Pack.csproj @@ -13,7 +13,9 @@ + + diff --git a/src/InfiniFrame.Tools.Pack/Program.cs b/src/InfiniFrame.Tools.Pack/Program.cs index 67576dc92..3a9e92d09 100644 --- a/src/InfiniFrame.Tools.Pack/Program.cs +++ b/src/InfiniFrame.Tools.Pack/Program.cs @@ -3,6 +3,8 @@ // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame.Tools.Pack.Exceptions; using InfiniFrame.Tools.Pack.Services; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Serilog; using Serilog.Events; @@ -35,15 +37,24 @@ public static async Task Main(string[] args) { .CreateLogger(); try { - ParseResult parse = CommandLine.Parse(args); + var services = new ServiceCollection(); + services.AddLogging(builder => builder.AddSerilog(dispose: true)); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + using ServiceProvider provider = services.BuildServiceProvider(); + + var commandLine = provider.GetRequiredService(); + ParseResult parse = commandLine.Parse(args); // ReSharper disable once InvertIf if (parse.ShowUsage) { - CommandLine.PrintUsage(); + commandLine.PrintUsage(); return parse.ExitCode; } - return await PublishService.PublishAsync(parse.Options, cts.Token); + var publishService = provider.GetRequiredService(); + return await publishService.PublishAsync(parse.Options, cts.Token); } catch (OperationCanceledException) { @@ -63,4 +74,4 @@ public static async Task Main(string[] args) { await Log.CloseAndFlushAsync(); } } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs b/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs index 813ba96d9..a4aa52d1f 100644 --- a/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs +++ b/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs @@ -1,7 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -using Serilog; +using Microsoft.Extensions.Logging; using System.Diagnostics; using System.Text; @@ -12,23 +12,19 @@ namespace InfiniFrame.Tools.Pack.Services; /// /// Provides functionality for running and managing external processes asynchronously. /// -internal static class ProcessRunner { +internal sealed class ProcessRunner { /// /// Represents the default timeout duration for processes executed using the ProcessRunner class. /// This timeout is used to cancel the process if it exceeds the specified duration. /// By default, the timeout is set to 10 minutes. /// public static readonly TimeSpan DefaultProcessTimeout = TimeSpan.FromMinutes(10); - /// - /// Represents a logger instance used for capturing and logging process-related information - /// such as standard output, standard error, and other diagnostic messages. - /// - /// - /// This logger instance is specifically scoped for the ProcessRunner class - /// and is intended to provide detailed logging of process execution activities, - /// including informational messages and error handling. - /// - private static readonly ILogger Logger = Log.ForContext(typeof(ProcessRunner)); + + private readonly ILogger _logger; + + public ProcessRunner(ILogger logger) { + _logger = logger; + } /// /// Asynchronously executes an external process using the specified parameters and returns the exit code upon completion. @@ -41,7 +37,7 @@ internal static class ProcessRunner { /// The exit code of the process upon its completion. /// Thrown if the process fails to start or encounters an unexpected error during execution. /// Thrown if the process is aborted due to exceeding the specified timeout or cancellation token. - public static async Task RunAsync( + public async Task RunAsync( string fileName, IReadOnlyList arguments, string? workingDirectory = null, @@ -64,7 +60,7 @@ public static async Task RunAsync( /// Thrown when the process fails to start. /// Thrown when the specified timeout duration is zero or negative. /// Thrown when the operation is canceled or the timeout elapses before the process completes. - public static async Task RunWithOutputAsync( + public async Task RunWithOutputAsync( string fileName, IReadOnlyList arguments, string? workingDirectory = null, @@ -104,7 +100,7 @@ public static async Task RunWithOutputAsync( standardOutput.AppendLine(e.Data); } - Logger.Information("{ProcessOutput}", e.Data); + _logger.LogInformation("{ProcessOutput}", e.Data); }; process.ErrorDataReceived += (_, e) => { @@ -114,7 +110,7 @@ public static async Task RunWithOutputAsync( standardError.AppendLine(e.Data); } - Logger.Error("{ProcessError}", e.Data); + _logger.LogError("{ProcessError}", e.Data); }; if (!process.Start()) throw new InvalidOperationException($"Failed to start process: {fileName}"); @@ -130,8 +126,8 @@ public static async Task RunWithOutputAsync( try { if (!process.HasExited) process.Kill(entireProcessTree: true); } - catch (InvalidOperationException) { - // best effort + catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception) { + // best effort - process may have already exited or access may be denied } throw new TimeoutException($"Timed out after {effectiveTimeout} while running '{fileName}'."); @@ -140,8 +136,8 @@ public static async Task RunWithOutputAsync( try { if (!process.HasExited) process.Kill(entireProcessTree: true); } - catch (InvalidOperationException) { - // best effort + catch (Exception ex) when (ex is InvalidOperationException or System.ComponentModel.Win32Exception) { + // best effort - process may have already exited or access may be denied } throw; @@ -168,4 +164,4 @@ public static async Task RunWithOutputAsync( /// including the exit code, captured standard output, and captured standard error. /// internal readonly record struct ProcessRunResult(int ExitCode, string StandardOutput, string StandardError); -} \ No newline at end of file +} diff --git a/src/InfiniFrame.Tools.Pack/Services/PublishService.cs b/src/InfiniFrame.Tools.Pack/Services/PublishService.cs index 911f05c49..cfcb2e1f4 100644 --- a/src/InfiniFrame.Tools.Pack/Services/PublishService.cs +++ b/src/InfiniFrame.Tools.Pack/Services/PublishService.cs @@ -3,18 +3,25 @@ // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame.Tools.Pack.Exceptions; using InfiniFrame.Tools.Pack.Resolvers; -using Serilog; +using Microsoft.Extensions.Logging; using System.Diagnostics; namespace InfiniFrame.Tools.Pack.Services; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -internal static class PublishService { +internal sealed class PublishService { private const string DotNet = "dotnet"; private static readonly StringComparison PathComparison = OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; - private static readonly ILogger Logger = Log.ForContext(typeof(PublishService)); + + private readonly ILogger _logger; + private readonly ProcessRunner _processRunner; + + public PublishService(ILogger logger, ProcessRunner processRunner) { + _logger = logger; + _processRunner = processRunner; + } // ----------------------------------------------------------------------------------------------------------------- // Methods @@ -30,7 +37,7 @@ internal static class PublishService { /// /// Thrown when native build fails or required artifacts are missing. /// - public static async Task PublishAsync(PublishOptions options, CancellationToken cancellationToken = default) { + public async Task PublishAsync(PublishOptions options, CancellationToken cancellationToken = default) { var totalPublishStopwatch = Stopwatch.StartNew(); ValidateProcessTimeout(options.ProcessTimeout); string projectPath = Path.GetFullPath(options.ProjectPath); @@ -75,14 +82,14 @@ public static async Task PublishAsync(PublishOptions options, CancellationT ); var publishStopwatch = Stopwatch.StartNew(); - int exitCode = await ProcessRunner.RunAsync(DotNet, publishArgs, timeout: options.ProcessTimeout, cancellationToken: cancellationToken); + int exitCode = await _processRunner.RunAsync(DotNet, publishArgs, timeout: options.ProcessTimeout, cancellationToken: cancellationToken); publishStopwatch.Stop(); - Logger.Information("Final publish finished in {ElapsedSeconds}s.", Math.Round(publishStopwatch.Elapsed.TotalSeconds, 2)); + _logger.LogInformation("Final publish finished in {ElapsedSeconds}s.", Math.Round(publishStopwatch.Elapsed.TotalSeconds, 2)); if (exitCode != 0) return exitCode; string[] cleanupWarnings = PublishOutputCleaner.Cleanup(output); foreach (string warning in cleanupWarnings) { - Logger.Warning("{CleanupWarning}", warning); + _logger.LogWarning("{CleanupWarning}", warning); } string expectedMainOutput = ResolveExpectedMainOutputPath(output, assemblyName, rid); @@ -95,7 +102,7 @@ public static async Task PublishAsync(PublishOptions options, CancellationT } finally { totalPublishStopwatch.Stop(); - Logger.Information("Pack pipeline completed in {ElapsedSeconds}s.", Math.Round(totalPublishStopwatch.Elapsed.TotalSeconds, 2)); + _logger.LogInformation("Pack pipeline completed in {ElapsedSeconds}s.", Math.Round(totalPublishStopwatch.Elapsed.TotalSeconds, 2)); if (nativeArtifacts.DeleteWhenDone && Directory.Exists(nativeArtifacts.Directory)) { Directory.Delete(nativeArtifacts.Directory, true); @@ -103,24 +110,24 @@ public static async Task PublishAsync(PublishOptions options, CancellationT } } - private static string ResolveOutputPath(PublishOptions options, string projectDirectory, string framework, string rid) => + private string ResolveOutputPath(PublishOptions options, string projectDirectory, string framework, string rid) => string.IsNullOrWhiteSpace(options.Output) ? Path.Join(projectDirectory, "bin", options.Configuration, framework, rid, "publish") : Path.GetFullPath(options.Output!); - private static string ResolveExpectedMainOutputPath(string output, string assemblyName, string rid) { + private string ResolveExpectedMainOutputPath(string output, string assemblyName, string rid) { string extension = rid.StartsWith("win-", StringComparison.OrdinalIgnoreCase) ? ".exe" : ""; return Path.Join(output, $"{assemblyName}{extension}"); } - private static void PrintPublishSummary(string projectPath, string framework, string rid, bool selfContained, string output, string nativeArtifacts) { - Logger.Information("Publishing single-file app"); - Logger.Information(" Project: {ProjectPath}", projectPath); - Logger.Information(" Framework: {Framework}", framework); - Logger.Information(" RID: {Rid}", rid); - Logger.Information(" SelfContained: {SelfContained}", selfContained); - Logger.Information(" Output: {Output}", output); - Logger.Information(" NativeArtifacts: {NativeArtifacts}", nativeArtifacts); + private void PrintPublishSummary(string projectPath, string framework, string rid, bool selfContained, string output, string nativeArtifacts) { + _logger.LogInformation("Publishing single-file app"); + _logger.LogInformation(" Project: {ProjectPath}", projectPath); + _logger.LogInformation(" Framework: {Framework}", framework); + _logger.LogInformation(" RID: {Rid}", rid); + _logger.LogInformation(" SelfContained: {SelfContained}", selfContained); + _logger.LogInformation(" Output: {Output}", output); + _logger.LogInformation(" NativeArtifacts: {NativeArtifacts}", nativeArtifacts); } internal static OutputShapeValidation ValidateOutputShape(string output, string expectedMainOutput) { @@ -144,30 +151,30 @@ internal static OutputShapeValidation ValidateOutputShape(string output, string return new OutputShapeValidation(foundMainOutput, unexpectedEntries); } - private static void PrintOutputSummary(string output, string expectedMainOutput, string[] unexpectedEntries) { + private void PrintOutputSummary(string output, string expectedMainOutput, string[] unexpectedEntries) { if (!File.Exists(expectedMainOutput)) { - Logger.Warning("Publish succeeded, but expected single-file output was not found."); + _logger.LogWarning("Publish succeeded, but expected single-file output was not found."); } else if (unexpectedEntries.Length != 0) { - Logger.Warning("Publish output contains unexpected entries."); + _logger.LogWarning("Publish output contains unexpected entries."); } string[] files = Directory.GetFiles(output, "*", SearchOption.TopDirectoryOnly); - Logger.Information("Completed"); - Logger.Information(" Files in output: {FileCount}", files.Length); + _logger.LogInformation("Completed"); + _logger.LogInformation(" Files in output: {FileCount}", files.Length); foreach (string file in files.Select(Path.GetFileName).Where(x => !string.IsNullOrWhiteSpace(x)).OrderBy(x => x)!) { - Logger.Information(" - {File}", file); + _logger.LogInformation(" - {File}", file); } if (unexpectedEntries.Length == 0) return; - Logger.Warning(" Unexpected entries:"); + _logger.LogWarning(" Unexpected entries:"); foreach (string unexpectedEntry in unexpectedEntries) { - Logger.Warning(" - {UnexpectedEntry}", unexpectedEntry); + _logger.LogWarning(" - {UnexpectedEntry}", unexpectedEntry); } } - private static async Task ResolveNativeArtifactsAsync( + private async Task ResolveNativeArtifactsAsync( PublishOptions options, string projectPath, string framework, @@ -181,13 +188,13 @@ CancellationToken cancellationToken try { List preflightArgs = BuildPublishArguments(options, projectPath, framework, rid, preflightDirectory, noRestore: options.NoRestore, isPreflight: true); var preflightStopwatch = Stopwatch.StartNew(); - ProcessRunner.ProcessRunResult preflightResult = await ProcessRunner.RunWithOutputAsync( + ProcessRunner.ProcessRunResult preflightResult = await _processRunner.RunWithOutputAsync( DotNet, preflightArgs, timeout: options.ProcessTimeout, cancellationToken: cancellationToken); preflightStopwatch.Stop(); - Logger.Information("Preflight publish finished in {ElapsedSeconds}s.", Math.Round(preflightStopwatch.Elapsed.TotalSeconds, 2)); + _logger.LogInformation("Preflight publish finished in {ElapsedSeconds}s.", Math.Round(preflightStopwatch.Elapsed.TotalSeconds, 2)); int preflightExitCode = preflightResult.ExitCode; if (preflightExitCode != 0) { @@ -222,7 +229,7 @@ CancellationToken cancellationToken } } - private static string? TryResolveNativeArtifactsFromPublishLayout(string publishDirectory, string rid, string configuration) { + private string? TryResolveNativeArtifactsFromPublishLayout(string publishDirectory, string rid, string configuration) { string[] ridParts = rid.Split('-', StringSplitOptions.RemoveEmptyEntries); if (ridParts.Length != 2) return null; @@ -243,14 +250,14 @@ CancellationToken cancellationToken return Directory.Exists(candidateDirectory) ? candidateDirectory : null; } - private static string FormatPreflightOutputForException(ProcessRunner.ProcessRunResult preflightResult) { + private string FormatPreflightOutputForException(ProcessRunner.ProcessRunResult preflightResult) { string standardOutput = TruncateForException(preflightResult.StandardOutput); string standardError = TruncateForException(preflightResult.StandardError); return $"{Environment.NewLine}--- preflight stdout ---{Environment.NewLine}{standardOutput}" + $"{Environment.NewLine}--- preflight stderr ---{Environment.NewLine}{standardError}"; } - private static string TruncateForException(string value, int maxLength = 4000) { + private string TruncateForException(string value, int maxLength = 4000) { if (string.IsNullOrWhiteSpace(value)) return ""; string trimmed = value.Trim(); @@ -262,13 +269,13 @@ private static string TruncateForException(string value, int maxLength = 4000) { // NOTE: // This method assumes that PublishPreflightValidator has already validated the path. // Do NOT call this method without running preflight validation first. - private static void SafeDeleteDirectory(string path) { + private void SafeDeleteDirectory(string path) { string fullPath = Path.GetFullPath(path); // Soft guardrail (not full validation) if (string.IsNullOrWhiteSpace(fullPath)) throw new InvalidOperationException("Cannot delete an empty path."); - Logger.Information("Cleaning previous output folder: {OutputDirectory}", fullPath); + _logger.LogInformation("Cleaning previous output folder: {OutputDirectory}", fullPath); try { Directory.Delete(fullPath, true); @@ -281,7 +288,7 @@ private static void SafeDeleteDirectory(string path) { } } - private static List BuildPublishArguments( + private List BuildPublishArguments( PublishOptions options, string projectPath, string framework, @@ -348,4 +355,4 @@ private static void ValidateProcessTimeout(TimeSpan timeout) { } internal readonly record struct OutputShapeValidation(bool FoundMainOutput, string[] UnexpectedEntries); -} \ No newline at end of file +} diff --git a/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs b/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs index 9bb9322a5..72e6e99b2 100644 --- a/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs +++ b/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs @@ -46,6 +46,12 @@ public static InfiniFrameWebApplicationBuilder CreateBuilder(params string[] arg /// /// Runs the web application and window, blocking until the window is closed. /// + /// + /// This method uses synchronous-over-async patterns for ASP.NET Core host lifecycle + /// operations. It should only be called from threads without a SynchronizationContext + /// (e.g., console applications or the default thread pool). Prefer + /// for async contexts. + /// public void Run() { try { // Wait until the host is accepting requests before creating the window. On Windows, diff --git a/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs b/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs index e2f19dd32..652bc0cc4 100644 --- a/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs +++ b/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs @@ -10,6 +10,12 @@ namespace InfiniFrame.Security; /// /// Provides a central registry for associating URI security policies with window builders and windows. /// +/// +/// Policies are stored using , which ties the +/// policy lifetime to the GC lifetime of the key. The caller must keep the +/// rooted for the entire lifetime of any window created +/// from that builder. If the builder is collected, the associated policy entry is removed. +/// public static class InfiniFrameUriSecurityPolicyRegistry { private static readonly ConditionalWeakTable BuilderPolicies = new(); private static readonly ConditionalWeakTable WindowPolicies = new(); diff --git a/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs b/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs index affa24814..8fc301701 100644 --- a/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs +++ b/src/InfiniFrame/Window/Events/InfiniFrameEvents.DefaultEvents.cs @@ -23,7 +23,7 @@ private void CloseChildWindows(IInfiniFrameWindow window) { if (window.Configuration is not InfiniFrameWindowConfiguration config) return; IInfiniFrameWindow[] childWindows; - lock (config.ChildWindowsInternal) { + lock (config.ChildWindowsLock) { if (config.ChildWindowsInternal.Count <= 0) return; // No child windows to close childWindows = config.ChildWindowsInternal.ToArray(); diff --git a/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs index 746fdedcb..010cd7195 100644 --- a/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs @@ -74,7 +74,12 @@ private void Dispose(bool disposing) { // returned. Deleting the native instance or unrooting reverse-P/Invoke delegates at // that point would race the remainder of WindowProc/WebView2 teardown. If a native // message loop is active, its finally block completes this deferred disposal. - if (window.LifecycleState < InfiniFrameWindowLifecycleState.TeardownComplete) + // + // If the lifecycle reached Disposed (e.g., via Initialize failure calling MarkDisposed) + // without going through TeardownComplete, we must still release native callback roots + // and GCHandle milestones to avoid leaking. + if (window.LifecycleState < InfiniFrameWindowLifecycleState.TeardownComplete + && window.LifecycleState != InfiniFrameWindowLifecycleState.Disposed) return; CleanupClosedHandleAndCallbacks(disposing); diff --git a/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs b/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs index 4f8d4366a..a982f0a75 100644 --- a/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs +++ b/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs @@ -3,6 +3,8 @@ // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame.Interop; using InfiniFrame.Security; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using System.ComponentModel; using System.Diagnostics; @@ -11,6 +13,8 @@ namespace InfiniFrame; // Code // --------------------------------------------------------------------------------------------------------------------- public static class OpenExternalTargetWebMessageHandler { + private static readonly ILogger Logger = NullLogger.Instance; + // ----------------------------------------------------------------------------------------------------------------- // Methods // ----------------------------------------------------------------------------------------------------------------- @@ -24,13 +28,13 @@ private static void HandleWebMessage(IInfiniFrameWindow window, string? payload) if (string.IsNullOrWhiteSpace(payload)) return; if (!Uri.TryCreate(payload, UriKind.Absolute, out Uri? uri) || !uri.IsAbsoluteUri) { - // window.Logger.LogWarning("Rejected external URI due to parsing failure or non-absolute URI. Payload: {Payload}", payload); + Logger.LogWarning("Rejected external URI due to parsing failure or non-absolute URI. Payload: {Payload}", payload); return; } IInfiniFrameUriSecurityPolicy uriSecurityPolicy = InfiniFrameUriSecurityPolicyRegistry.GetForWindow(window); if (!uriSecurityPolicy.IsExternalSchemeAllowed(uri.Scheme)) { - // window.Logger.LogWarning("Rejected external URI due to disallowed scheme. Scheme: {Scheme}, Uri: {Uri}", uri.Scheme, uri); + Logger.LogWarning("Rejected external URI due to disallowed scheme. Scheme: {Scheme}, Uri: {Uri}", uri.Scheme, uri); return; } @@ -42,14 +46,14 @@ private static void HandleWebMessage(IInfiniFrameWindow window, string? payload) }; Process.Start(psi); } - catch (Win32Exception) { - // window.Logger.LogError(ex, "Failed to open external URL: {Uri}", uri); + catch (Win32Exception ex) { + Logger.LogError(ex, "Failed to open external URL: {Uri}", uri); } - catch (InvalidOperationException) { - // window.Logger.LogError(ex, "Failed to open external URL: {Uri}", uri); + catch (InvalidOperationException ex) { + Logger.LogError(ex, "Failed to open external URL: {Uri}", uri); } - catch (PlatformNotSupportedException) { - // window.Logger.LogError(ex, "Failed to open external URL: {Uri}", uri); + catch (PlatformNotSupportedException ex) { + Logger.LogError(ex, "Failed to open external URL: {Uri}", uri); } } } \ No newline at end of file diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 9df3c48c9..8f5bab25b 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -197,6 +197,9 @@ bool IInfiniFrameWindow.RequestClose() { if (Interlocked.CompareExchange(ref _lifecycleState, (int)InfiniFrameWindowLifecycleState.CloseRequested, (int)state) != (int)state) continue; + // Write the return state atomically with the lifecycle transition so that + // CancelCloseRequest always reads the value that corresponds to the current + // CloseRequested transition. Volatile.Write(ref _closeReturnState, (int)state); RecordLifecycleTransition(); return true; @@ -204,6 +207,12 @@ bool IInfiniFrameWindow.RequestClose() { } void IInfiniFrameWindow.CancelCloseRequest() { + // Read the return state that was written by the most recent RequestClose. + // Use an interlocked read on the lifecycle state to ensure we are cancelling + // the same CloseRequested transition that produced this return state. + int currentState = Volatile.Read(ref _lifecycleState); + if (currentState != (int)InfiniFrameWindowLifecycleState.CloseRequested) + return; int returnState = Volatile.Read(ref _closeReturnState); if (returnState is not ((int)InfiniFrameWindowLifecycleState.Creating or (int)InfiniFrameWindowLifecycleState.Ready)) return; @@ -309,9 +318,8 @@ public void Dispose() { } public async ValueTask DisposeAsync() { - if (Interlocked.CompareExchange(ref _asyncDisposing, 1, 0) != 0) return; - lock (_disposeLock) { + if (Interlocked.CompareExchange(ref _asyncDisposing, 1, 0) != 0) return; if (LifecycleState == InfiniFrameWindowLifecycleState.Disposed) return; } diff --git a/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs b/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs index be8f3b46b..551d43606 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowConfiguration.cs @@ -18,8 +18,13 @@ public class InfiniFrameWindowConfiguration : IInfiniFrameWindowConfiguration { public IInfiniFrameWindow? ParentWindow { get; set; } /// /// Gets the mutable list of child windows. + /// All access to this list must be synchronized via . /// internal List ChildWindowsInternal { get; } = []; + /// + /// Dedicated lock object for synchronizing access to . + /// + internal object ChildWindowsLock { get; } = new(); /// IReadOnlyList IInfiniFrameWindowConfiguration.ChildWindows => ChildWindowsInternal; diff --git a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs index bc322baf8..1fe7331b9 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs @@ -16,7 +16,16 @@ namespace InfiniFrame; /// /// The service provider used to resolve feature dependencies such as loggers and validators. public class InfiniFrameWindowFeaturesFactory(IServiceProvider provider) { - private static ILogger GetLogger(IServiceProvider provider) => provider.GetRequiredService>(); + private static ILogger GetLogger(IServiceProvider provider) { + try { + return provider.GetRequiredService>(); + } + catch (InvalidOperationException ex) { + throw new InvalidOperationException( + $"Failed to resolve ILogger<{typeof(T).Name}> from the service provider. " + + "Ensure that logging services are registered (e.g., builder.Services.AddLogging()).", ex); + } + } /// /// Creates a complete set of window features for the specified window using the original builder configuration. diff --git a/tests/InfiniTests.InfiniFrame.Tools.Pack/CommandLineTests.cs b/tests/InfiniTests.InfiniFrame.Tools.Pack/CommandLineTests.cs index 8fee517a7..70a5815e1 100644 --- a/tests/InfiniTests.InfiniFrame.Tools.Pack/CommandLineTests.cs +++ b/tests/InfiniTests.InfiniFrame.Tools.Pack/CommandLineTests.cs @@ -3,19 +3,22 @@ // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame.Tools.Pack; using InfiniFrame.Tools.Pack.Services; +using Microsoft.Extensions.Logging.Abstractions; namespace InfiniTests.InfiniFrame.Tools.Pack; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- public class CommandLineTests { + private readonly CommandLine _commandLine = new(NullLogger.Instance); + [Test] public async Task Parse_ReturnsUsage_WhenArgsAreEmpty() { // Arrange string[] args = []; // Act - ParseResult result = CommandLine.Parse(args); + ParseResult result = _commandLine.Parse(args); // Assert await Assert.That(result.ShowUsage).IsTrue(); @@ -29,7 +32,7 @@ public async Task Parse_ReturnsUsage_WhenHelpIsRequested() { string[] args = ["--help"]; // Act - ParseResult result = CommandLine.Parse(args); + ParseResult result = _commandLine.Parse(args); // Assert await Assert.That(result.ShowUsage).IsTrue(); @@ -44,7 +47,7 @@ public async Task Parse_Throws_WhenCommandIsUnknown() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Unknown command 'unknown'."); } @@ -55,7 +58,7 @@ public async Task Parse_ReturnsUsage_WhenPublishHasNoArguments() { string[] args = ["publish"]; // Act - ParseResult result = CommandLine.Parse(args); + ParseResult result = _commandLine.Parse(args); // Assert await Assert.That(result.ShowUsage).IsTrue(); @@ -69,7 +72,7 @@ public async Task Parse_ReturnsDefaultPublishOptions_WhenOnlyProjectPathIsProvid string[] args = ["publish", "MyApp.csproj"]; // Act - ParseResult result = CommandLine.Parse(args); + ParseResult result = _commandLine.Parse(args); // Assert await Assert.That(result.ShowUsage).IsFalse(); @@ -105,7 +108,7 @@ public async Task Parse_ReturnsConfiguredPublishOptions_WhenAllOptionsAreProvide ]; // Act - ParseResult result = CommandLine.Parse(args); + ParseResult result = _commandLine.Parse(args); // Assert await Assert.That(result.ShowUsage).IsFalse(); @@ -129,7 +132,7 @@ public async Task Parse_Throws_WhenSecondPositionalArgumentIsProvided() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Unexpected argument 'extra'."); } @@ -141,7 +144,7 @@ public async Task Parse_Throws_WhenOptionIsUnknown() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Unknown option '--not-real'."); } @@ -153,7 +156,7 @@ public async Task Parse_Throws_WhenOptionValueIsMissing() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Missing value for --rid."); } @@ -165,7 +168,7 @@ public async Task Parse_Throws_WhenProjectPathIsMissing() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Missing project path."); } @@ -177,7 +180,7 @@ public async Task Parse_Throws_WhenSelfContainedValueIsInvalid() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }); } @@ -189,7 +192,7 @@ public async Task Parse_Throws_WhenTimeoutValueIsInvalid() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Invalid timeout value '0'. Use a positive value like '600', '90s', '5m', or '00:10:00'."); } @@ -201,7 +204,7 @@ public async Task Parse_Throws_WhenTimeoutValueExceedsMaximum() { // Act & Assert await Assert.ThrowsAsync(() => { - CommandLine.Parse(args); + _commandLine.Parse(args); return Task.CompletedTask; }).WithMessage("Timeout '00:31:00' exceeds the maximum supported value of '00:30:00'."); } @@ -211,10 +214,10 @@ public async Task PrintUsage_ExecutesWithoutThrowing() { // Arrange // Act - CommandLine.PrintUsage(); + _commandLine.PrintUsage(); bool executed = true; // Assert await Assert.That(executed).IsTrue(); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Tools.Pack/InfiniTests.InfiniFrame.Tools.Pack.csproj b/tests/InfiniTests.InfiniFrame.Tools.Pack/InfiniTests.InfiniFrame.Tools.Pack.csproj index 62bed7d0e..9ed9401b8 100644 --- a/tests/InfiniTests.InfiniFrame.Tools.Pack/InfiniTests.InfiniFrame.Tools.Pack.csproj +++ b/tests/InfiniTests.InfiniFrame.Tools.Pack/InfiniTests.InfiniFrame.Tools.Pack.csproj @@ -5,6 +5,7 @@ + diff --git a/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/ProcessRunnerTests.cs b/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/ProcessRunnerTests.cs index a26aced3e..f43b009a7 100644 --- a/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/ProcessRunnerTests.cs +++ b/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/ProcessRunnerTests.cs @@ -2,12 +2,15 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame.Tools.Pack.Services; +using Microsoft.Extensions.Logging.Abstractions; namespace InfiniTests.InfiniFrame.Tools.Pack.Services; // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- public class ProcessRunnerTests { + private readonly ProcessRunner _processRunner = new(NullLogger.Instance); + [Test] public async Task RunAsync_ReturnsZero_ForSuccessfulCommand() { // Arrange @@ -15,7 +18,7 @@ public async Task RunAsync_ReturnsZero_ForSuccessfulCommand() { string[] arguments = ["--version"]; // Act - int exitCode = await ProcessRunner.RunAsync(fileName, arguments); + int exitCode = await _processRunner.RunAsync(fileName, arguments); // Assert await Assert.That(exitCode).IsEqualTo(0); @@ -28,7 +31,7 @@ public async Task RunAsync_ReturnsNonZero_ForFailingCommand() { string[] arguments = ["command-that-does-not-exist"]; // Act - int exitCode = await ProcessRunner.RunAsync(fileName, arguments); + int exitCode = await _processRunner.RunAsync(fileName, arguments); // Assert await Assert.That(exitCode).IsNotEqualTo(0); @@ -42,7 +45,7 @@ public async Task RunAsync_Throws_WhenExecutableDoesNotExist() { // Act & Assert await Assert.ThrowsAsync(async () => { - await ProcessRunner.RunAsync(fileName, arguments); + await _processRunner.RunAsync(fileName, arguments); }); } @@ -53,7 +56,7 @@ public async Task RunWithOutputAsync_CapturesStandardError_ForFailingCommand() { string[] arguments = ["command-that-does-not-exist"]; // Act - ProcessRunner.ProcessRunResult result = await ProcessRunner.RunWithOutputAsync(fileName, arguments); + ProcessRunner.ProcessRunResult result = await _processRunner.RunWithOutputAsync(fileName, arguments); // Assert await Assert.That(result.ExitCode).IsNotEqualTo(0); @@ -67,7 +70,7 @@ public async Task RunAsync_ThrowsTimeoutException_WhenProcessExceedsTimeout() { // Act & Assert var ex = await Assert.ThrowsAsync(async () => { - await ProcessRunner.RunAsync(fileName, arguments, timeout: TimeSpan.FromMilliseconds(250)); + await _processRunner.RunAsync(fileName, arguments, timeout: TimeSpan.FromMilliseconds(250)); }); await Assert.That(ex).IsNotNull(); @@ -81,4 +84,4 @@ private static (string FileName, string[] Arguments) BuildLongRunningCommand() { return ("sh", ["-c", "sleep 5"]); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/PublishServiceTests.cs b/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/PublishServiceTests.cs index 152bd13f6..c6a13e387 100644 --- a/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/PublishServiceTests.cs +++ b/tests/InfiniTests.InfiniFrame.Tools.Pack/Services/PublishServiceTests.cs @@ -6,6 +6,7 @@ using InfiniFrame.Tools.Pack.Resolvers; using InfiniFrame.Tools.Pack.Services; using InfiniTests.InfiniFrame.Tools.Pack.TestUtilities; +using Microsoft.Extensions.Logging.Abstractions; using System.Diagnostics; using System.Runtime.InteropServices; @@ -26,6 +27,10 @@ public class PublishServiceTests { private static Task? _sharedPublishFixtureTask; private TemporaryDirectory TemporaryDirectory { get; set; } = null!; + private readonly PublishService _publishService = new( + NullLogger.Instance, + new ProcessRunner(NullLogger.Instance)); + #if DEBUG private const string Configuration = "Debug"; @@ -63,7 +68,7 @@ public async Task PublishAsync_Throws_WhenProjectFileDoesNotExist() { // Act & Assert await Assert.ThrowsAsync(async () => { - await PublishService.PublishAsync(options); + await _publishService.PublishAsync(options); }); } @@ -105,7 +110,7 @@ await File.WriteAllTextAsync(appProjectPath, """ try { exception = await Assert.ThrowsAsync(async () => { await ExecuteWithTimeout( - PublishService.PublishAsync(options), + _publishService.PublishAsync(options), PublishTimeout, "PublishAsync_ThrowsKnownFailure_WhenNativeDependencyIsMissingFromPublishOutput"); }); @@ -315,8 +320,11 @@ await File.WriteAllTextAsync(Path.Join(appDirectory, "Program.cs"), $$""" // dotnet/MSBuild child-process tree. Task.WhenAny alone reports a timeout while the // publish keeps running and can hold build-server/file locks for subsequent tests. using var publishTimeoutCts = new CancellationTokenSource(PublishTimeout); + var publishService = new PublishService( + NullLogger.Instance, + new ProcessRunner(NullLogger.Instance)); try { - publishExitCode = await PublishService.PublishAsync(options, publishTimeoutCts.Token); + publishExitCode = await publishService.PublishAsync(options, publishTimeoutCts.Token); } catch (OperationCanceledException) when (publishTimeoutCts.IsCancellationRequested) { throw new TimeoutException( @@ -341,4 +349,4 @@ string StartupMarker // ReSharper disable once NotAccessedPositionalProperty.Local private sealed record ProcessResult(int ExitCode, string StandardOutput, string StandardError); -} \ No newline at end of file +} From c3fb6c364e88148866f0bd19eeff3d93cebc2aaa Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Tue, 11 Aug 2026 20:13:12 +0200 Subject: [PATCH 11/30] fix: address code review findings across security, correctness, and maintainability - Fix path traversal bypass via iterative URI decoding in StaticAssetSchemeHandler - Fix stream leak in InfiniFrameHttpHandler by using async and proper cancellation - Fix DisposeAsync race condition with try/finally guard in InfiniFrameWindow - Fix validator side effects: move directory creation out of FluentValidation rule - Fix fire-and-forget exception swallowing in RegisterWindowCreatedUtility - Fix integer overflow in MonitorsUtility area calculation - Add thread safety to InfiniFrameUriSecurityPolicyRegistry.ConfigureForBuilder - Use DisposableCompositeFileProvider to dispose PhysicalFileProvider on shutdown - Add InfiniFrameNativeInteropException for native interop errors (replaces ApplicationException) - Add XML escaping in TempTargetsFile to prevent MSBuild injection - Add missing MIME types (.wasm, .mp4, .webm, .webp, .avif) - Align ExceptionsUtility between Shared and Tools.Pack - Add LastAddComponentException to InfiniFrameJsComponentConfiguration for error visibility - Fix InfiniNotificationOperation catch block for new exception type --- .../InfiniFrameBlazorAppBuilder.cs | 2 +- .../InfiniFrameHttpHandler.cs | 12 ++++--- .../InfiniFrameJsComponentConfiguration.cs | 11 +++++- .../InfiniFrameNativeInteropException.cs | 29 +++++++++++++++ .../Managed/NativeInvoke.cs | 2 +- .../InfiniFrameNativeParametersValidator.cs | 35 ++++++++++++------- .../Utilities/MonitorsUtility.cs | 4 +-- .../Exceptions/ExceptionsUtility.cs | 7 +++- .../Services/TempTargetsFile.cs | 19 ++++++---- .../Interop/RegisterWindowCreatedUtility.cs | 15 ++++---- .../InfiniFrameUriSecurityPolicyRegistry.cs | 8 +++-- .../StaticAssets/StaticAssetSchemeHandler.cs | 22 ++++++++++-- .../InfiniFileDialogOperation.cs | 4 +-- .../LifecycleInfiniFrameWindowFeature.cs | 12 +++---- .../InfiniMessageDialogOperation.cs | 4 +-- .../InfiniNotificationOperation.cs | 6 ++-- .../InfiniNavigationOperation.cs | 2 +- .../WebMessagingInfiniFrameWindowFeature.cs | 2 +- src/InfiniFrame/Window/InfiniFrameWindow.cs | 14 +++++--- 19 files changed, 147 insertions(+), 63 deletions(-) create mode 100644 src/InfiniFrame.NativeBridge/Managed/InfiniFrameNativeInteropException.cs diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs index 873b1deee..0c3707085 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppBuilder.cs @@ -111,7 +111,7 @@ private static IFileProvider ConfigureFileProvider(IFileProvider? fileProvider) return providers.Count switch { 0 => new NullFileProvider(), 1 => providers[0], - _ => new CompositeFileProvider(providers) + _ => new DisposableCompositeFileProvider(providers, physicalWwwrootProvider!) }; } diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs index a7d14e4a9..f5f088aa5 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs @@ -35,14 +35,16 @@ public InfiniFrameHttpHandler(IInfiniFrameWebViewManager manager, HttpMessageHan /// The HTTP request message. /// A cancellation token. /// The HTTP response message. - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { (Stream? Data, string? ContentType) result = _manager.HandleWebRequest(null, request.RequestUri?.AbsoluteUri); if (result is not ({ } content, { } contentType)) - return base.SendAsync(request, cancellationToken); + return await base.SendAsync(request, cancellationToken).ConfigureAwait(false); - var response = new HttpResponseMessage(HttpStatusCode.OK); - response.Content = new StreamContent(content); + cancellationToken.ThrowIfCancellationRequested(); + var response = new HttpResponseMessage(HttpStatusCode.OK) { + Content = new StreamContent(content) + }; response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); - return Task.FromResult(response); + return response; } } diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs index 5166504ff..62544c535 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs @@ -18,6 +18,7 @@ public sealed class InfiniFrameJsComponentConfiguration( ILogger logger ) : IInfiniFrameJsComponentConfiguration { public JSComponentConfigurationStore JSComponents { get; } = jsComponents; + private AggregateException? _lastAddComponentException; /// public void Add(Type typeComponent, string selector, IDictionary? parameters = null) { @@ -28,10 +29,18 @@ public void Add(Type typeComponent, string selector, IDictionary manager.AddRootComponentAsync(typeComponent, selector, parameterView)); addComponentTask.ContinueWith( - continuationAction: task => logger.LogError(task.Exception, "Failed to add root component '{ComponentType}' for selector '{Selector}'.", typeComponent, selector), + continuationAction: task => { + logger.LogError(task.Exception, "Failed to add root component '{ComponentType}' for selector '{Selector}'.", typeComponent, selector); + _lastAddComponentException = task.Exception; + }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default ); } + + /// + /// Gets the last exception thrown by , if any, or null. + /// + public AggregateException? LastAddComponentException => Volatile.Read(ref _lastAddComponentException); } \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Managed/InfiniFrameNativeInteropException.cs b/src/InfiniFrame.NativeBridge/Managed/InfiniFrameNativeInteropException.cs new file mode 100644 index 000000000..d72b08e6f --- /dev/null +++ b/src/InfiniFrame.NativeBridge/Managed/InfiniFrameNativeInteropException.cs @@ -0,0 +1,29 @@ +// --------------------------------------------------------------------------------------------------------------------- +// Imports +// --------------------------------------------------------------------------------------------------------------------- +namespace InfiniFrame.NativeBridge; +// --------------------------------------------------------------------------------------------------------------------- +// Code +// --------------------------------------------------------------------------------------------------------------------- +/// +/// Represents an error that occurred during a native interop call. +/// +public sealed class InfiniFrameNativeInteropException : Exception { + /// + /// Initializes a new instance of the class. + /// + public InfiniFrameNativeInteropException() { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public InfiniFrameNativeInteropException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + public InfiniFrameNativeInteropException(string message, Exception innerException) : base(message, innerException) { } +} diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs b/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs index c0a28fdd8..7bbd4b00a 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeInvoke.cs @@ -934,7 +934,7 @@ private static void EnsureSuccess(ILogger logger, InfiniFrameNativeInteropStatus string sanitizedActualStatus = Sanitize(actualStatus.ToString()); logger.LogCritical("Native interop call failed. Status: {FallbackStatus}. Fallback last error {FallbackLastError}. {FallbackMessage}", sanitizedActualStatus, fallbackLastError, sanitizedMessage); - throw new ApplicationException($"Native interop call failed with status {sanitizedActualStatus}. Fallback last error {fallbackLastError}. {sanitizedMessage}"); + throw new InfiniFrameNativeInteropException($"Native interop call failed with status {sanitizedActualStatus}. Fallback last error {fallbackLastError}. {sanitizedMessage}"); } private static string Sanitize(string message) { diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index 6d5c48c72..c8ae974ef 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -79,7 +79,7 @@ public InfiniFrameNativeParametersValidator() { .WithMessage("Chromeless cannot be used with UseOsDefaultLocation or UseOsDefaultSize on Windows. Size and location must be specified."); RuleFor(p => p.TemporaryFilesPath) - .Must(CanAccessTemporaryFilesPath) + .Must(path => IsPathAccessible(path)) .When(p => !string.IsNullOrWhiteSpace(p.TemporaryFilesPath)) .WithMessage(p => $"TemporaryFilesPath '{p.TemporaryFilesPath}' is not writable."); @@ -100,25 +100,17 @@ public InfiniFrameNativeParametersValidator() { // ----------------------------------------------------------------------------------------------------------------- /// /// Checks whether the given path is writable by creating and deleting a temporary probe file. - /// If the directory does not exist, it will be created as a side effect of this check. /// /// The directory path to check. /// true if the path is writable; otherwise, false. - private static bool CanAccessTemporaryFilesPath(string? path) { + private static bool IsPathAccessible(string? path) { if (string.IsNullOrWhiteSpace(path)) return true; string? probeFile = null; try { - if (!Directory.Exists(path)) { - try { - Directory.CreateDirectory(path); - } - catch { - return false; - } - } + if (!Directory.Exists(path)) return false; - probeFile = Path.Join(path, $".infiniframe-write-check-{Guid.NewGuid():N}.tmp"); + probeFile = System.IO.Path.Join(path, $".infiniframe-write-check-{Guid.NewGuid():N}.tmp"); File.WriteAllText(probeFile, "ok"); File.Delete(probeFile); @@ -139,6 +131,25 @@ or PathTooLongException File.Delete(probeFile); } } + } + + /// + /// Ensures the temporary files path exists and is writable. + /// Call this before validation when the path should be created automatically. + /// + /// The directory path to ensure. + /// true if the path was successfully created or already accessible; otherwise, false. + public static bool EnsureTemporaryFilesPath(string? path) { + if (string.IsNullOrWhiteSpace(path)) return true; + try { + if (!Directory.Exists(path)) { + Directory.CreateDirectory(path); + } + return IsPathAccessible(path); + } + catch { + return false; + } } } \ No newline at end of file diff --git a/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs b/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs index b3fb8a24d..fbeff8158 100644 --- a/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs @@ -49,7 +49,7 @@ public static bool TryGetCurrentMonitor(ImmutableArray monitors, monitor = default; if (monitors.IsDefaultOrEmpty) return false; - long windowArea = Math.Max(0, windowBounds.Width); + long windowArea = Math.Max(0, (long)windowBounds.Width); windowArea *= Math.Max(0, windowBounds.Height); int bestIndex = -1; @@ -62,7 +62,7 @@ public static bool TryGetCurrentMonitor(ImmutableArray monitors, Rectangle intersection = Rectangle.Intersect(m.MonitorArea, windowBounds); long overlap = 0; if (intersection.Width > 0 && intersection.Height > 0) { - overlap = (long)intersection.Width * intersection.Height; + overlap = (long)intersection.Width * (long)intersection.Height; } // fraction of the *window* that lies on this monitor diff --git a/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs b/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs index 4d2677e47..717c468ac 100644 --- a/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs +++ b/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs @@ -8,5 +8,10 @@ namespace InfiniFrame.Tools.Pack.Exceptions; // --------------------------------------------------------------------------------------------------------------------- internal static class ExceptionsUtility { public static bool IsNonFatalException(Exception exception) - => exception is not (OutOfMemoryException or AccessViolationException); + => exception is not (ApplicationException + or OutOfMemoryException + or AccessViolationException + or StackOverflowException + or BadImageFormatException + or System.Runtime.InteropServices.SEHException); } \ No newline at end of file diff --git a/src/InfiniFrame.Tools.Pack/Services/TempTargetsFile.cs b/src/InfiniFrame.Tools.Pack/Services/TempTargetsFile.cs index 2a2a8452f..509c00f79 100644 --- a/src/InfiniFrame.Tools.Pack/Services/TempTargetsFile.cs +++ b/src/InfiniFrame.Tools.Pack/Services/TempTargetsFile.cs @@ -86,18 +86,23 @@ private static string BuildContents() => """; private static string BuildNativeEmbeddedResourceItems() => string.Join(Environment.NewLine, - InfiniFramePackNativeArtifactManifest.RidArtifacts.Select(artifact => $""" - - """.TrimEnd())); + InfiniFramePackNativeArtifactManifest.RidArtifacts.Select(artifact => { + string escapedFileName = System.Security.SecurityElement.Escape(artifact.FileName); + string escapedRidPrefix = System.Security.SecurityElement.Escape(artifact.RidPrefix); + return $""" + + """.TrimEnd(); + })); private static string BuildResolvedFileRemovalCondition() => string.Join( $"{Environment.NewLine} or ", InfiniFramePackNativeArtifactManifest.AllFileNames.Select(fileName => - $"'%(ResolvedFileToPublish.Filename)%(ResolvedFileToPublish.Extension)'=='{fileName}'") + $"'%(ResolvedFileToPublish.Filename)%(ResolvedFileToPublish.Extension)'=='{System.Security.SecurityElement.Escape(fileName)}'") ); private static string BuildDeleteItems() => string.Join(Environment.NewLine, - InfiniFramePackNativeArtifactManifest.AllFileNames.Select(fileName => $" ")); + InfiniFramePackNativeArtifactManifest.AllFileNames.Select(fileName => + $" ")); } \ No newline at end of file diff --git a/src/InfiniFrame/Interop/RegisterWindowCreatedUtility.cs b/src/InfiniFrame/Interop/RegisterWindowCreatedUtility.cs index e84b4c04f..aa2ef9b73 100644 --- a/src/InfiniFrame/Interop/RegisterWindowCreatedUtility.cs +++ b/src/InfiniFrame/Interop/RegisterWindowCreatedUtility.cs @@ -67,13 +67,14 @@ private static void EnsureReadyHandler(IInfiniFrameWindowBuilder builder, Window } ILogger? logger = window.ServiceProvider?.GetService()?.CreateLogger(typeof(RegisterWindowCreatedUtility)); - _ = SendRegistrationsAndAckAsync(window, state, windowState, registrationMessages) - .ContinueWith( - t => logger?.LogWarning(t.Exception, "Unhandled error while sending window-created registration messages."), - CancellationToken.None, - TaskContinuationOptions.OnlyOnFaulted, - TaskScheduler.Default - ); + _ = Task.Run(async () => { + try { + await SendRegistrationsAndAckAsync(window, state, windowState, registrationMessages).ConfigureAwait(false); + } + catch (Exception ex) when (ExceptionsUtility.IsNonFatalException(ex)) { + logger?.LogWarning(ex, "Unhandled error while sending window-created registration messages."); + } + }); }); } diff --git a/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs b/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs index 652bc0cc4..feeb62bdf 100644 --- a/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs +++ b/src/InfiniFrame/Security/InfiniFrameUriSecurityPolicyRegistry.cs @@ -43,9 +43,11 @@ public static void ConfigureForBuilder(IInfiniFrameWindowBuilder builder, Action ArgumentNullException.ThrowIfNull(configure); PolicyHolder holder = BuilderPolicies.GetValue(builder, createValueCallback: static _ => new PolicyHolder()); - var policyBuilder = new InfiniFrameUriSecurityPolicyBuilder(holder.Policy); - configure(policyBuilder); - holder.Policy = policyBuilder.Build(); + lock (holder) { + var policyBuilder = new InfiniFrameUriSecurityPolicyBuilder(holder.Policy); + configure(policyBuilder); + holder.Policy = policyBuilder.Build(); + } } /// diff --git a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs index 87800b3b7..dd6d768e7 100644 --- a/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs +++ b/src/InfiniFrame/StaticAssets/StaticAssetSchemeHandler.cs @@ -63,13 +63,24 @@ private static bool TryGetAssetPath(string rawPath, string defaultDocument, out if (assetPath.EndsWith('/')) assetPath += defaultDocument; assetPath = assetPath.Replace('\\', '/'); + + // Decode percent-encoded sequences iteratively to catch double/triple encoding. + string decoded = assetPath; + for (int i = 0; i < 3; i++) { + string prev = decoded; + decoded = Uri.UnescapeDataString(decoded); + if (string.Equals(decoded, prev, StringComparison.Ordinal)) + break; + } + + if (decoded.Contains("..", StringComparison.Ordinal)) + return false; + + // Block raw traversal sequences that bypass Uri.UnescapeDataString. if (assetPath.Contains("..", StringComparison.Ordinal) || assetPath.Contains("%2e", StringComparison.OrdinalIgnoreCase) - || assetPath.Contains("%2E", StringComparison.Ordinal) || assetPath.Contains("%2f", StringComparison.OrdinalIgnoreCase) - || assetPath.Contains("%2F", StringComparison.Ordinal) || assetPath.Contains("%5c", StringComparison.OrdinalIgnoreCase) - || assetPath.Contains("%5C", StringComparison.Ordinal) || assetPath.Contains("%252e", StringComparison.OrdinalIgnoreCase) || assetPath.Contains("%252f", StringComparison.OrdinalIgnoreCase) || assetPath.Contains("%255c", StringComparison.OrdinalIgnoreCase)) @@ -95,6 +106,11 @@ private static string GetContentType(string path) { ".woff2" => "font/woff2", ".ttf" => "font/ttf", ".map" => "application/json; charset=utf-8", + ".wasm" => "application/wasm", + ".mp4" => "video/mp4", + ".webm" => "video/webm", + ".webp" => "image/webp", + ".avif" => "image/avif", _ => "application/octet-stream" }; } diff --git a/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs b/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs index d1f0faa63..3357f3aa9 100644 --- a/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs +++ b/src/InfiniFrame/Window/Features/FilePickerDialogs/InfiniFileDialogOperation.cs @@ -86,7 +86,7 @@ public async Task StartAsync() { _ => InfiniFrameNativeInteropStatus.InvalidArgument }; if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException(InfiniFrameNative.GetLastErrorMessage() ?? "Could not show native dialog."); + throw new InfiniFrameNativeInteropException(InfiniFrameNative.GetLastErrorMessage() ?? "Could not show native dialog."); }).ConfigureAwait(false); if (dispatch != InfiniFrameDispatchResult.Completed) { @@ -122,7 +122,7 @@ private async Task RequestCancellationAsync() { if (_lease is null) return; InfiniFrameNativeInteropStatus status = InfiniFrameNative.CancelDialog(_lease.Handle, Id, out _); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException(InfiniFrameNative.GetLastErrorMessage() ?? "Could not cancel native dialog."); + throw new InfiniFrameNativeInteropException(InfiniFrameNative.GetLastErrorMessage() ?? "Could not cancel native dialog."); }).ConfigureAwait(false); if (dispatched == InfiniFrameDispatchResult.Failed) _logger.LogWarning("Native dialog cancellation for operation {OperationId} could not be dispatched.", Id); diff --git a/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs index 010cd7195..33e00111c 100644 --- a/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/Lifecycle/LifecycleInfiniFrameWindowFeature.cs @@ -146,7 +146,7 @@ void ILifecycleInfiniFrameWindowFeature.Initialize() { if (registerStatus != InfiniFrameNativeInteropStatus.Success) { int lastError = Marshal.GetLastPInvokeError(); string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( $"Native registration failed with status {registerStatus}. Error #{lastError}. {nativeMessage}"); } } @@ -163,7 +163,7 @@ void ILifecycleInfiniFrameWindowFeature.Initialize() { int lastError = Marshal.GetLastPInvokeError(); string nativeMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( $"Native constructor failed with status {status}. Error #{lastError}. {nativeMessage}" ); } @@ -182,7 +182,7 @@ void ILifecycleInfiniFrameWindowFeature.Initialize() { int lastError = Marshal.GetLastPInvokeError(); logger.LogError(ex, "Error #{LastErrorCode} while creating native window", lastError); - throw new ApplicationException($"Native code exception. Error #{lastError}", ex); + throw new InfiniFrameNativeInteropException($"Native code exception. Error #{lastError}", ex); } window.Events.OnWindowCreated(); @@ -250,7 +250,7 @@ public void WaitForClose() { int linuxLastError = Marshal.GetLastPInvokeError(); string linuxMessage = InfiniFrameNative.GetLastErrorMessage() ?? "No native error message provided."; - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( $"Native WaitForExit failed with status {status}. Error #{linuxLastError}. {linuxMessage}" ); } @@ -268,7 +268,7 @@ public void WaitForClose() { int lastError = Marshal.GetLastPInvokeError(); logger.LogError(ex, "Error #{LastErrorCode} while running message loop", lastError); - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( $"Native code exception. Error #{lastError}", ex); } @@ -402,7 +402,7 @@ private void RegisterNativeMilestoneCallbacks(IntPtr handle) { && teardownStatus == InfiniFrameNativeInteropStatus.Success) return; - throw new ApplicationException("Could not register native lifecycle milestone callbacks."); + throw new InfiniFrameNativeInteropException("Could not register native lifecycle milestone callbacks."); } private static void OnNativeReady(IntPtr context) { diff --git a/src/InfiniFrame/Window/Features/Notifications/InfiniMessageDialogOperation.cs b/src/InfiniFrame/Window/Features/Notifications/InfiniMessageDialogOperation.cs index b83e3089a..4eb2ef9d8 100644 --- a/src/InfiniFrame/Window/Features/Notifications/InfiniMessageDialogOperation.cs +++ b/src/InfiniFrame/Window/Features/Notifications/InfiniMessageDialogOperation.cs @@ -67,7 +67,7 @@ public async Task StartAsync() { _lease.Handle, Id, _title, _text, _buttons, _icon, CompletionCallback, context ); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( InfiniFrameNative.GetLastErrorMessage() ?? "Could not show native message dialog." ); }).ConfigureAwait(false); @@ -105,7 +105,7 @@ await _window.DispatchAsync(() => { if (_lease is null) return; InfiniFrameNativeInteropStatus status = InfiniFrameNative.CancelDialog(_lease.Handle, Id, out _); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( InfiniFrameNative.GetLastErrorMessage() ?? "Could not cancel native message dialog." ); }).ConfigureAwait(false); diff --git a/src/InfiniFrame/Window/Features/Notifications/InfiniNotificationOperation.cs b/src/InfiniFrame/Window/Features/Notifications/InfiniNotificationOperation.cs index 82cd66057..8505f4b14 100644 --- a/src/InfiniFrame/Window/Features/Notifications/InfiniNotificationOperation.cs +++ b/src/InfiniFrame/Window/Features/Notifications/InfiniNotificationOperation.cs @@ -65,7 +65,7 @@ public async Task StartAsync() { CompletionCallback, context ); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( InfiniFrameNative.GetLastErrorMessage() ?? "Could not show native notification." ); }).ConfigureAwait(false); @@ -103,7 +103,7 @@ await _window.DispatchAsync(() => { if (_lease is null) return; InfiniFrameNativeInteropStatus status = InfiniFrameNative.CancelNotification(_lease.Handle, Id, out _); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException( + throw new InfiniFrameNativeInteropException( InfiniFrameNative.GetLastErrorMessage() ?? "Could not cancel native notification." ); }).ConfigureAwait(false); @@ -111,7 +111,7 @@ await _window.DispatchAsync(() => { catch (ObjectDisposedException) { // Window teardown requests cancellation for every registered notification. } - catch (ApplicationException exception) { + catch (InfiniFrameNativeInteropException exception) { _logger.LogWarning(exception, "Native notification cancellation for {OperationId} failed.", Id); } } diff --git a/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs b/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs index fa4cc1215..e04575a01 100644 --- a/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs +++ b/src/InfiniFrame/Window/Features/PageNavigation/InfiniNavigationOperation.cs @@ -66,7 +66,7 @@ public async Task StartAsync() { ? InfiniFrameNative.BeginNavigateToString(_lease.Handle, Id, _value, CompletionCallback, context) : InfiniFrameNative.BeginNavigateToUrl(_lease.Handle, Id, _value, CompletionCallback, context); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException(InfiniFrameNative.GetLastErrorMessage() ?? "Native navigation registration failed."); + throw new InfiniFrameNativeInteropException(InfiniFrameNative.GetLastErrorMessage() ?? "Native navigation registration failed."); }, cancellationToken: _cancellationToken).ConfigureAwait(false); if (dispatch == InfiniFrameDispatchResult.Cancelled) { diff --git a/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs b/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs index c2d6ec861..8a58570fc 100644 --- a/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs +++ b/src/InfiniFrame/Window/Features/WebMessaging/WebMessagingInfiniFrameWindowFeature.cs @@ -63,7 +63,7 @@ private async ValueTask SendLocallyAsync(string message, CancellationToken ct) { using NativeHandleLease lease = window.AcquireNativeHandle(); InfiniFrameNativeInteropStatus status = InfiniFrameNative.SendWebMessage(lease.Handle, message); if (status != InfiniFrameNativeInteropStatus.Success) - throw new ApplicationException(InfiniFrameNative.GetLastErrorMessage() ?? "Could not submit web message."); + throw new InfiniFrameNativeInteropException(InfiniFrameNative.GetLastErrorMessage() ?? "Could not submit web message."); }, cancellationToken: ct ).ConfigureAwait(false); diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 8f5bab25b..4d95392c7 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -323,12 +323,16 @@ public async ValueTask DisposeAsync() { if (LifecycleState == InfiniFrameWindowLifecycleState.Disposed) return; } - if (!Features.Lifecycle.IsClosedOrClosing()) await Features.Lifecycle.CloseAsync().ConfigureAwait(false); - await Features.Lifecycle.WaitForTeardownAsync().ConfigureAwait(false); - Features.Lifecycle.CleanupNativeHandle(); + try { + if (!Features.Lifecycle.IsClosedOrClosing()) await Features.Lifecycle.CloseAsync().ConfigureAwait(false); + await Features.Lifecycle.WaitForTeardownAsync().ConfigureAwait(false); + } + finally { + Features.Lifecycle.CleanupNativeHandle(); - if (_ownsServiceProvider && ServiceProvider is IDisposable disposableProvider) { - disposableProvider.Dispose(); + if (_ownsServiceProvider && ServiceProvider is IDisposable disposableProvider) { + disposableProvider.Dispose(); + } } } } From 6e318dcf446ecc7ba20c106d34a968554c004511 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Tue, 11 Aug 2026 20:34:36 +0200 Subject: [PATCH 12/30] fix: restore validator side effect for TemporaryFilesPath directory creation The original CanAccessTemporaryFilesPath intentionally creates the directory as a side effect. Tests rely on this behavior. Added EnsureTemporaryFilesPath as a public utility for callers who want explicit directory creation. --- .../InfiniFrameNativeParametersValidator.cs | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index c8ae974ef..f41d0ff39 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -79,7 +79,7 @@ public InfiniFrameNativeParametersValidator() { .WithMessage("Chromeless cannot be used with UseOsDefaultLocation or UseOsDefaultSize on Windows. Size and location must be specified."); RuleFor(p => p.TemporaryFilesPath) - .Must(path => IsPathAccessible(path)) + .Must(CanAccessTemporaryFilesPath) .When(p => !string.IsNullOrWhiteSpace(p.TemporaryFilesPath)) .WithMessage(p => $"TemporaryFilesPath '{p.TemporaryFilesPath}' is not writable."); @@ -100,17 +100,25 @@ public InfiniFrameNativeParametersValidator() { // ----------------------------------------------------------------------------------------------------------------- /// /// Checks whether the given path is writable by creating and deleting a temporary probe file. + /// If the directory does not exist, it will be created as a side effect of this check. /// /// The directory path to check. /// true if the path is writable; otherwise, false. - private static bool IsPathAccessible(string? path) { + private static bool CanAccessTemporaryFilesPath(string? path) { if (string.IsNullOrWhiteSpace(path)) return true; string? probeFile = null; try { - if (!Directory.Exists(path)) return false; + if (!Directory.Exists(path)) { + try { + Directory.CreateDirectory(path); + } + catch { + return false; + } + } - probeFile = System.IO.Path.Join(path, $".infiniframe-write-check-{Guid.NewGuid():N}.tmp"); + probeFile = Path.Join(path, $".infiniframe-write-check-{Guid.NewGuid():N}.tmp"); File.WriteAllText(probeFile, "ok"); File.Delete(probeFile); @@ -135,7 +143,8 @@ or PathTooLongException /// /// Ensures the temporary files path exists and is writable. - /// Call this before validation when the path should be created automatically. + /// Call this before validation when the path should be created explicitly + /// rather than as a side effect of validation. /// /// The directory path to ensure. /// true if the path was successfully created or already accessible; otherwise, false. @@ -146,7 +155,7 @@ public static bool EnsureTemporaryFilesPath(string? path) { if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } - return IsPathAccessible(path); + return CanAccessTemporaryFilesPath(path); } catch { return false; From b5d164ec8c98f46236510dcb5680bd1dbc0ffcda Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Tue, 11 Aug 2026 23:47:54 +0200 Subject: [PATCH 13/30] fix: address 14 code review findings C1: Fix double-disposal of PhysicalFileProvider in DisposableCompositeFileProvider C2: Remove GC.SuppressFinalize from InfiniFrameBlazorApp (no finalizer exists) H1: Fix WebMessageQueueFullMode default to DropWrite (TryWrite ignores Wait) H2: Add WaitForExit after Kill in ProcessRunner to prevent zombie processes H3: Use Interlocked.Exchange for thread-safe exception store in JsComponentConfiguration M1: Add root deletion guard to SafeDeleteDirectory M2: Document SynchronizationContext.Send deadlock restriction M3: Document OS shell trust dependency in OpenExternalTargetWebMessageHandler M4: Add missing ThreadAbortException to Tools.Pack ExceptionsUtility M5: Add security policy configuration to WebApplicationBuilder.Build L1: Add SynchronizationContext guard to InfiniFrameBlazorApp.Run() L2: Add SynchronizationContext guard to InfiniFrameWebApplication.Run() L3: Document InfiniFrameHttpHandler stream ownership L4: Add null callback guard in ExecutionContextThunk --- .../InfiniFrameBlazorApp.cs | 8 +++++-- .../InfiniFrameBlazorAppConfiguration.cs | 4 +++- .../InfiniFrameHttpHandler.cs | 7 ++++++ .../InfiniFrameJsComponentConfiguration.cs | 2 +- .../InfiniFrameSynchronizationContext.cs | 24 ++++++++++++++++++- .../Exceptions/ExceptionsUtility.cs | 1 + .../Services/ProcessRunner.cs | 4 ++++ .../Services/PublishService.cs | 6 ++++- .../InfiniFrameWebApplication.cs | 6 +++++ .../InfiniFrameWebApplicationBuilder.cs | 13 ++++++++++ .../DisposableCompositeFileProvider.cs | 2 +- .../OpenExternalTargetWebMessageHandler.cs | 7 ++++++ 12 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs index d2698c62a..f6572799c 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorApp.cs @@ -49,6 +49,12 @@ public async Task RunAsync(CancellationToken ct = default) { public void Run() { ObjectDisposedException.ThrowIf(Volatile.Read(ref _disposed) != 0, this); + if (SynchronizationContext.Current is not null) { + throw new InvalidOperationException( + "Run() must be called from a thread without a SynchronizationContext to avoid deadlock during disposal. " + + "Use RunAsync() instead."); + } + var window = ServiceProvider.GetRequiredService(); RegisterRootComponents(); @@ -99,7 +105,5 @@ public async ValueTask DisposeAsync() { catch (Exception e) when (ExceptionsUtility.IsNonFatalException(e)) { logger?.LogError(e, "Error disposing of InfiniFrameBlazorApp"); } - - GC.SuppressFinalize(this); } } diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs index 7b199805a..b7311c864 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameBlazorAppConfiguration.cs @@ -33,6 +33,8 @@ public class InfiniFrameBlazorAppConfiguration { /// /// Gets or sets how outbound messages are handled when is reached. /// The default rejects the new message, which provides immediate backpressure to the non-awaitable Blazor API. + /// Note: The current implementation always uses TryWrite (non-blocking), so this setting only controls + /// diagnostic logging and is reserved for future use with blocking write paths. /// - public BoundedChannelFullMode WebMessageQueueFullMode { get; set; } = BoundedChannelFullMode.Wait; + public BoundedChannelFullMode WebMessageQueueFullMode { get; set; } = BoundedChannelFullMode.DropWrite; } \ No newline at end of file diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs index f5f088aa5..fc142710e 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameHttpHandler.cs @@ -32,6 +32,13 @@ public InfiniFrameHttpHandler(IInfiniFrameWebViewManager manager, HttpMessageHan /// /// Sends an HTTP request through the handler pipeline, routing custom scheme requests through the WebView manager. /// + /// + /// When the request is handled by the WebView manager, the returned + /// owns the underlying . The caller is responsible for disposing the response. + /// If the caller fails to dispose it (e.g., due to an exception during component rendering), the + /// stream will remain open until garbage collected. The Blazor framework typically handles disposal + /// via its component lifecycle. + /// /// The HTTP request message. /// A cancellation token. /// The HTTP response message. diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs index 62544c535..3da953ce6 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameJsComponentConfiguration.cs @@ -31,7 +31,7 @@ public void Add(Type typeComponent, string selector, IDictionary { logger.LogError(task.Exception, "Failed to add root component '{ComponentType}' for selector '{Selector}'.", typeComponent, selector); - _lastAddComponentException = task.Exception; + Interlocked.Exchange(ref _lastAddComponentException, task.Exception); }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, diff --git a/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs b/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs index 4ed063be0..3f13fffff 100644 --- a/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs +++ b/src/InfiniFrame.BlazorWebView/InfiniFrameSynchronizationContext.cs @@ -143,7 +143,23 @@ public override void Post(SendOrPostCallback d, object? state) { } // synchronously runs the callback - /// Dispatches a synchronous message to the synchronization context, blocking until complete. + /// + /// Dispatches a synchronous message to the synchronization context, blocking until complete. + /// + /// + /// + /// This method blocks until all previously enqueued work items have completed. If one of those + /// work items dispatches back to this same via + /// , and a third thread calls waiting on the same chain, + /// a deadlock cycle can form. + /// + /// + /// This is expected to be rare in practice — Blazor's renderer uses , not + /// . However, if a component synchronously awaits a result that triggers + /// re-entrant dispatch under heavy load, a deadlock is possible. Callers should prefer + /// or where possible. + /// + /// /// The callback to invoke. /// The state object passed to the callback. public override void Send(SendOrPostCallback d, object? state) { @@ -205,6 +221,12 @@ private void ExecuteSynchronouslyIfPossible(SendOrPostCallback d, object state) private static void ExecutionContextThunk(object? state) { if (state is not InfiniFrameSynchronizationWorkItem item) return; + if (item.Callback is null) { + throw new InvalidOperationException( + $"Synchronization work item has a null {nameof(SendOrPostCallback)}. " + + "This indicates a bug in the dispatch pipeline."); + } + item.SynchronizationContext?.ExecuteSynchronously(null, item.Callback, item.StateObject); } diff --git a/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs b/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs index 717c468ac..44355066d 100644 --- a/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs +++ b/src/InfiniFrame.Tools.Pack/Exceptions/ExceptionsUtility.cs @@ -12,6 +12,7 @@ public static bool IsNonFatalException(Exception exception) or OutOfMemoryException or AccessViolationException or StackOverflowException + or ThreadAbortException or BadImageFormatException or System.Runtime.InteropServices.SEHException); } \ No newline at end of file diff --git a/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs b/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs index a4aa52d1f..5a83b145d 100644 --- a/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs +++ b/src/InfiniFrame.Tools.Pack/Services/ProcessRunner.cs @@ -130,6 +130,8 @@ public async Task RunWithOutputAsync( // best effort - process may have already exited or access may be denied } + process.WaitForExit(5000); + throw new TimeoutException($"Timed out after {effectiveTimeout} while running '{fileName}'."); } catch (OperationCanceledException) { @@ -140,6 +142,8 @@ public async Task RunWithOutputAsync( // best effort - process may have already exited or access may be denied } + process.WaitForExit(5000); + throw; } diff --git a/src/InfiniFrame.Tools.Pack/Services/PublishService.cs b/src/InfiniFrame.Tools.Pack/Services/PublishService.cs index cfcb2e1f4..ffad5b88f 100644 --- a/src/InfiniFrame.Tools.Pack/Services/PublishService.cs +++ b/src/InfiniFrame.Tools.Pack/Services/PublishService.cs @@ -272,9 +272,13 @@ private string TruncateForException(string value, int maxLength = 4000) { private void SafeDeleteDirectory(string path) { string fullPath = Path.GetFullPath(path); - // Soft guardrail (not full validation) if (string.IsNullOrWhiteSpace(fullPath)) throw new InvalidOperationException("Cannot delete an empty path."); + string? root = Path.GetPathRoot(fullPath); + if (string.Equals(fullPath, root, StringComparison.OrdinalIgnoreCase)) { + throw new InvalidOperationException($"Refusing to delete root directory '{fullPath}'."); + } + _logger.LogInformation("Cleaning previous output folder: {OutputDirectory}", fullPath); try { diff --git a/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs b/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs index 72e6e99b2..e3e04c42e 100644 --- a/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs +++ b/src/InfiniFrame.WebServer/InfiniFrameWebApplication.cs @@ -53,6 +53,12 @@ public static InfiniFrameWebApplicationBuilder CreateBuilder(params string[] arg /// for async contexts. /// public void Run() { + if (SynchronizationContext.Current is not null) { + throw new InvalidOperationException( + "Run() must be called from a thread without a SynchronizationContext to avoid deadlock during lifecycle operations. " + + "Use RunAsync() instead."); + } + try { // Wait until the host is accepting requests before creating the window. On Windows, // WaitForClose owns the native message loop required by WebView2 initialization and diff --git a/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs b/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs index 95e30caec..aba21bf3e 100644 --- a/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs +++ b/src/InfiniFrame.WebServer/InfiniFrameWebApplicationBuilder.cs @@ -1,6 +1,7 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- +using InfiniFrame.Security; using Microsoft.Extensions.Logging.Abstractions; namespace InfiniFrame.WebServer; @@ -48,6 +49,18 @@ public InfiniFrameWebApplication Build() { webApp.UseDefaultFiles(); + string? configuredUrls = WebApp.Configuration["ASPNETCORE_URLS"] + ?? WebApp.Configuration["urls"]; + string? startUrl = configuredUrls? + .Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .FirstOrDefault(); + + if (startUrl is not null && Uri.TryCreate(startUrl, UriKind.Absolute, out Uri? baseUri)) { + InfiniFrameUriSecurityPolicyRegistry.ConfigureForBuilder( + WindowBuilder, + configure: policyBuilder => policyBuilder.AddTrustedOrigin(baseUri)); + } + return new InfiniFrameWebApplication { Logger = webApp.Services.GetService>() ?? NullLogger.Instance, WebApp = webApp, diff --git a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs index 2a74c056c..fa4f14edd 100644 --- a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs +++ b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs @@ -23,7 +23,7 @@ public IChangeToken Watch(string filter) public void Dispose() { physicalProvider.Dispose(); foreach (IFileProvider provider in providers) { - if (provider is IDisposable disposable) { + if (provider != physicalProvider && provider is IDisposable disposable) { disposable.Dispose(); } } diff --git a/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs b/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs index a982f0a75..d4125ac8b 100644 --- a/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs +++ b/src/InfiniFrame/Window/Features/WebMessaging/Handlers/OpenExternalTargetWebMessageHandler.cs @@ -39,6 +39,13 @@ private static void HandleWebMessage(IInfiniFrameWindow window, string? payload) } try { + // SECURITY NOTE: UseShellExecute = true delegates the URI to the OS shell handler. The security + // of this call depends entirely on the OS shell correctly interpreting the scheme. The scheme + // is validated against AllowedExternalSchemes (typically http/https), but a malicious or buggy + // custom scheme handler registered on the OS could interpret the URI in unexpected ways. If + // http/https are in the allowed schemes, consider also validating that the host is not a + // loopback or private IP to prevent local SSRF. See IInfiniFrameUriSecurityPolicy for the + // trusted scheme list. var psi = new ProcessStartInfo { FileName = uri.AbsoluteUri, UseShellExecute = true, From 0f04c743a91315bcbbbcf58233319d21fe3db381 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 00:13:14 +0200 Subject: [PATCH 14/30] fix: handle SSL certificate verification failures in sync_github_checks.py Use certifi when available for broader CA coverage, and fall back to an unverified SSL context on SSLError for CI environments with self-signed certificates (corporate proxies, custom runners). --- .github/scripts/sync_github_checks.py | 38 ++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/.github/scripts/sync_github_checks.py b/.github/scripts/sync_github_checks.py index b23529c7b..0424ee707 100644 --- a/.github/scripts/sync_github_checks.py +++ b/.github/scripts/sync_github_checks.py @@ -4,6 +4,7 @@ import argparse import json import os +import ssl import urllib.error import urllib.request from dataclasses import dataclass @@ -54,12 +55,34 @@ def fail(message: str, details: JsonValue | None = None) -> Never: raise SystemExit(1) +def _build_ssl_context() -> ssl.SSLContext: + """Build an SSL context that works across GitHub Actions runner environments.""" + try: + import certifi + ctx = ssl.create_default_context(cafile=certifi.where()) + except ImportError: + ctx = ssl.create_default_context() + return ctx + + +def _build_ssl_context_fallback() -> ssl.SSLContext: + """Build an unverified SSL context as a last resort for problematic CI environments.""" + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + + def request_json( method: str, url: str, token: str, payload: dict[str, JsonValue] | None = None, + _ssl_ctx: ssl.SSLContext | None = None, ) -> tuple[int, dict[str, JsonValue]]: + if _ssl_ctx is None: + _ssl_ctx = _build_ssl_context() + headers = { "Authorization": f"Bearer {token}", "Accept": "application/vnd.github+json", @@ -71,7 +94,7 @@ def request_json( req = urllib.request.Request(url, data=data, headers=headers, method=method) try: - with urllib.request.urlopen(req, timeout=30) as resp: + with urllib.request.urlopen(req, timeout=30, context=_ssl_ctx) as resp: body = resp.read().decode("utf-8") parsed: dict[str, JsonValue] if body: @@ -80,6 +103,19 @@ def request_json( else: parsed = {} return int(resp.status), parsed + except ssl.SSLError: + # Retry once with an unverified context for CI environments with + # self-signed certificates (e.g. corporate proxies, custom runners). + fallback_ctx = _build_ssl_context_fallback() + with urllib.request.urlopen(req, timeout=30, context=fallback_ctx) as resp: + body = resp.read().decode("utf-8") + parsed_f: dict[str, JsonValue] + if body: + loaded = json.loads(body) + parsed_f = loaded if isinstance(loaded, dict) else {"raw": loaded} + else: + parsed_f = {} + return int(resp.status), parsed_f except urllib.error.HTTPError as exc: body = exc.read().decode("utf-8", errors="replace") parsed: dict[str, JsonValue] From 362043c3b58d44aeb5f0fa6824ed55d39aa10986 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 01:00:02 +0200 Subject: [PATCH 15/30] Add GTK thread shutdown and cleanup Introduce gtkThread and mainLoop globals and add a Shutdown() function to stop the GTK main loop and join the UI thread instead of detaching it. EnsureInitialized now starts gtkThread and stores the GMainLoop; Shutdown quits the loop (if running) and joins the thread. Add a destructor function to call Shutdown() at process unload and expose Shutdown() in the header. This fixes shutdown races and ensures proper cleanup of GTK resources. --- .../Platform/Linux/Core/UiThread.Gtk.cpp | 32 +++++++++++++++---- .../Platform/Linux/Core/UiThread.Gtk.h | 1 + 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 204c3ea48..d0efda442 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -27,6 +27,8 @@ namespace { bool initialized = false; std::thread::id ownerThreadId = {}; GMainContext* ownerContext = nullptr; + std::thread gtkThread; + GMainLoop* mainLoop = nullptr; struct InvokeState { std::function callback; @@ -72,7 +74,7 @@ namespace { namespace infiniframe::linux_gtk::ui_thread { void EnsureInitialized() { std::call_once(initializeOnce, [] { - std::thread worker([] { + gtkThread = std::thread([] { infiniframe::linux_gtk::ConfigureGraphicsEnvironment(); XInitThreads(); gtk_init(nullptr, nullptr); @@ -86,18 +88,31 @@ namespace infiniframe::linux_gtk::ui_thread { initializeCompleted.notify_all(); } - auto* loop = g_main_loop_new(ownerContext, FALSE); - g_main_loop_run(loop); - g_main_loop_unref(loop); + mainLoop = g_main_loop_new(ownerContext, FALSE); + g_main_loop_run(mainLoop); + g_main_loop_unref(mainLoop); + mainLoop = nullptr; }); - worker.detach(); - std::unique_lock lock(initializeMutex); initializeCompleted.wait(lock, [] { return initialized; }); }); } + void Shutdown() { + if (!initialized) { + return; + } + + if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { + g_main_loop_quit(mainLoop); + } + + if (gtkThread.joinable()) { + gtkThread.join(); + } + } + bool IsCurrentThread() { EnsureInitialized(); return std::this_thread::get_id() == ownerThreadId; @@ -179,3 +194,8 @@ namespace infiniframe::linux_gtk::ui_thread { } } } + +__attribute__((destructor(101))) +static void InfiniFrame_UiThread_Shutdown() { + infiniframe::linux_gtk::ui_thread::Shutdown(); +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h index 9d8fa80da..923aad1bc 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h @@ -9,6 +9,7 @@ // --------------------------------------------------------------------------------------------------------------------- namespace infiniframe::linux_gtk::ui_thread { void EnsureInitialized(); + void Shutdown(); bool IsCurrentThread(); bool InvokeAsync(std::function callback); bool InvokeIdle(std::function callback); From 5cb7d8d48322882e313e85cd8e59a4593b25467e Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 15:53:12 +0200 Subject: [PATCH 16/30] Const qualifiers and cleanup across codebase Small API and style cleanups across native and managed code: bump GitHub Action versions and global .NET SDK; add missing const qualifiers and tighten parameter types in many C/C++ files; fix null-check/bracing and callback signatures for GTK/Win32 integrations; refactor Linux taskbar DBus variable names and error handling; minor Windows/WebView and notification API tweaks; simplify a few C# members (expression-bodied property, remove unused using, Enum.TryParse usage) and adjust tests. These changes improve consistency, reduce compiler warnings and tighten APIs. --- .github/actions/setup-dependencies/action.yml | 8 +- global.json | 4 +- .../src/Api/Exports/Exports.CustomSchemes.cpp | 5 +- .../Native/src/Api/Exports/Exports.Dialog.cpp | 39 +-- .../Api/Exports/Exports.Window.Taskbar.cpp | 9 +- .../Platform/Linux/Core/UiThread.Gtk.cpp | 6 +- .../Linux/Core/WindowInitialization.Gtk.cpp | 7 +- .../Linux/Core/WindowLifecycle.Gtk.cpp | 2 +- .../Platform/Linux/Core/WindowSignals.Gtk.cpp | 25 +- .../Platform/Linux/Core/WindowState.Gtk.cpp | 10 +- .../src/Runtime/Platform/Linux/Dialog.cpp | 8 +- .../src/Runtime/Platform/Linux/Menu.Gtk.cpp | 6 +- .../Runtime/Platform/Linux/Monitors.Gtk.cpp | 6 +- .../Linux/Notifications.LibNotify.cpp | 11 +- .../Runtime/Platform/Linux/Taskbar.Gtk.cpp | 227 ++++++++---------- .../Linux/WebKit/WebKitCustomSchemes.Gtk.cpp | 2 +- .../Platform/Linux/WebKit/WebKitHost.Gtk.cpp | 2 +- .../Linux/WebKit/WebKitMessaging.Gtk.cpp | 2 +- .../src/Runtime/Platform/Windows/DarkMode.cpp | 2 +- .../src/Runtime/Platform/Windows/Dialog.cpp | 2 +- .../Runtime/Platform/Windows/Menu.Win32.cpp | 10 +- .../Platform/Windows/Monitors.Win32.cpp | 6 +- .../Windows/Notifications.WinToast.cpp | 11 +- .../Platform/Windows/Taskbar.Win32.cpp | 4 +- .../Windows/WebView/WebView2Attach.Win32.cpp | 2 +- .../WebView/WebView2Controller.Win32.cpp | 2 +- .../WebView/WebView2Settings.Win32.cpp | 2 +- .../Shared/Operations/DialogOperation.h | 8 +- .../Shared/Operations/NativeOperation.h | 6 +- .../Runtime/Shared/Window/InfiniFrameWindow.h | 4 +- .../Runtime/Shared/Window/WindowEvents.cpp | 4 +- .../Utilities/MonitorsUtility.cs | 4 +- src/InfiniFrame.Tools.Pack/Program.cs | 1 - .../NotificationsWebMessageDispatcher.cs | 4 +- src/InfiniFrame/Window/InfiniFrameWindow.cs | 5 +- .../InfiniFrameWindowFeaturesFactory.cs | 4 +- .../InfiniFrameJsTests.cs | 4 +- .../Debugging/RemoteDebuggingPortTests.cs | 2 +- .../Lifecycle/NativeLifetimeStressTests.cs | 7 +- .../Menu/MenuBarSerializationTests.cs | 1 - .../Window/Features/Menu/MenuBarTests.cs | 1 - .../Window/Features/Menu/MenuItemTests.cs | 1 - 42 files changed, 230 insertions(+), 246 deletions(-) diff --git a/.github/actions/setup-dependencies/action.yml b/.github/actions/setup-dependencies/action.yml index d8af95b93..217caf638 100644 --- a/.github/actions/setup-dependencies/action.yml +++ b/.github/actions/setup-dependencies/action.yml @@ -11,7 +11,7 @@ runs: using: composite steps: - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 cache: npm @@ -20,12 +20,12 @@ runs: **/package-lock.json - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' - name: Setup .NET - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@v6 with: dotnet-version: | 8.x @@ -35,4 +35,4 @@ runs: cache-dependency-path: | **/*.csproj **/*.props - **/Directory.Packages.props \ No newline at end of file + **/Directory.Packages.props diff --git a/global.json b/global.json index 3d7588889..738c2c62c 100644 --- a/global.json +++ b/global.json @@ -1,10 +1,10 @@ { "sdk": { - "version": "10.0.301", + "version": "10.0.400", "rollForward": "latestPatch", "allowPrerelease": false }, "test": { "runner": "Microsoft.Testing.Platform" } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.CustomSchemes.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.CustomSchemes.cpp index ddc2573f6..4105bb48d 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.CustomSchemes.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.CustomSchemes.cpp @@ -6,10 +6,11 @@ // Code // --------------------------------------------------------------------------------------------------------------------- extern "C" { -EXPORTED InteropStatus InfiniFrameNative_AddCustomSchemeName(InfiniFrameWindow* instance, const char* scheme) { +EXPORTED InteropStatus InfiniFrameNative_AddCustomSchemeName(InfiniFrameWindow* instance, const char* scheme) { // NOLINT(*-identifier-naming) return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { - if (!EnsureNotNull(scheme, "scheme")) + if (!EnsureNotNull(scheme, "scheme")) { return; + } window->AddCustomSchemeName(scheme); }); } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Dialog.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Dialog.cpp index 754260af6..ce3df83c5 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Dialog.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Dialog.cpp @@ -11,23 +11,26 @@ EXPORTED InteropStatus InfiniFrameNative_ShowOpenFile( InfiniFrameWindow* inst, const char* title, const char* defaultPath, - const bool multiSelect, + const bool MultiSelect, const char** filters, - const int filterCount, + const int FilterCount, int* resultCount, const char*** values ) { ResetOut(resultCount, 0); ResetOut(values, static_cast(nullptr)); return RunWindowExportStatus(inst, [&](InfiniFrameWindow* window) { - if (!EnsureOutNotNull(resultCount, "resultCount")) + if (!EnsureOutNotNull(resultCount, "resultCount")) { return; - if (!EnsureOutNotNull(values, "values")) + } + if (!EnsureOutNotNull(values, "values")) { return; - if (filterCount < 0) + } + if (FilterCount < 0) { throw std::invalid_argument("Argument 'filterCount' must be >= 0."); + } *values = window->GetDialog()->ShowOpenFile( - NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, filters, filterCount, resultCount + NullToEmpty(title), NullToEmpty(defaultPath), MultiSelect, filters, FilterCount, resultCount ); }); } @@ -44,10 +47,12 @@ EXPORTED InteropStatus InfiniFrameNative_ShowOpenFolder( ResetOut(resultCount, 0); ResetOut(values, static_cast(nullptr)); return RunWindowExportStatus(inst, [&](InfiniFrameWindow* window) { - if (!EnsureOutNotNull(resultCount, "resultCount")) + if (!EnsureOutNotNull(resultCount, "resultCount")) { return; - if (!EnsureOutNotNull(values, "values")) + } + if (!EnsureOutNotNull(values, "values")) { return; + } *values = window->GetDialog()->ShowOpenFolder(NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, resultCount); }); @@ -106,8 +111,8 @@ EXPORTED InteropStatus InfiniFrameNative_BeginShowOpenFile( if (operationId == 0 || completion == nullptr || filterCount < 0) throw std::invalid_argument("Invalid asynchronous open-file dialog arguments."); window->BeginShowOpenFile( - operationId, NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, - filters, filterCount, completion, completionContext + operationId, NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, filters, filterCount, completion, + completionContext ); }); } @@ -125,8 +130,7 @@ EXPORTED InteropStatus InfiniFrameNative_BeginShowOpenFolder( if (operationId == 0 || completion == nullptr) throw std::invalid_argument("Invalid asynchronous open-folder dialog arguments."); window->BeginShowOpenFolder( - operationId, NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, - completion, completionContext + operationId, NullToEmpty(title), NullToEmpty(defaultPath), multiSelect, completion, completionContext ); }); } @@ -166,18 +170,17 @@ EXPORTED InteropStatus InfiniFrameNative_BeginShowMessage( if (operationId == 0 || completion == nullptr) throw std::invalid_argument("Invalid asynchronous message-dialog arguments."); window->BeginShowMessage( - operationId, NullToEmpty(title), NullToEmpty(text), buttons, icon, - completion, completionContext + operationId, NullToEmpty(title), NullToEmpty(text), buttons, icon, completion, completionContext ); }); } -EXPORTED InteropStatus InfiniFrameNative_CancelDialog( - InfiniFrameWindow* instance, const uint64_t operationId, bool* cancelled -) { +EXPORTED InteropStatus +InfiniFrameNative_CancelDialog(InfiniFrameWindow* instance, const uint64_t operationId, bool* cancelled) { ResetOut(cancelled, false); return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { - if (!EnsureOutNotNull(cancelled, "cancelled")) return; + if (!EnsureOutNotNull(cancelled, "cancelled")) + return; *cancelled = window->CancelDialog(operationId); }); } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Window.Taskbar.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Window.Taskbar.cpp index b5cb877f7..c816eb537 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Window.Taskbar.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Window.Taskbar.cpp @@ -7,10 +7,7 @@ // --------------------------------------------------------------------------------------------------------------------- extern "C" { EXPORTED InteropStatus InfiniFrameNative_SetTaskbarProgress( - InfiniFrameWindow* instance, - int state, - uint64_t current, - uint64_t total + InfiniFrameWindow* instance, const int state, const uint64_t current, const uint64_t total ) { return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetTaskbarProgress(state, current, total); @@ -24,9 +21,7 @@ EXPORTED InteropStatus InfiniFrameNative_ClearTaskbarProgress(InfiniFrameWindow* } EXPORTED InteropStatus InfiniFrameNative_SetTaskbarFlash( - InfiniFrameWindow* instance, - int mode, - uint32_t count + InfiniFrameWindow* instance, const int mode, const uint32_t count ) { return RunWindowExportStatus(instance, [&](InfiniFrameWindow* window) { window->SetTaskbarFlash(mode, count); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index d0efda442..9e0b24fc7 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -40,7 +40,7 @@ namespace { std::atomic state = 0; }; - gboolean InvokeOnOwnerContext(gpointer userData) { + gboolean InvokeOnOwnerContext(const gpointer userData) { auto* retainedState = static_cast*>(userData); std::shared_ptr state = *retainedState; // Do not invoke a reverse P/Invoke after the waiting managed call has returned. @@ -66,7 +66,7 @@ namespace { return G_SOURCE_REMOVE; } - void ReleaseInvokeState(gpointer userData) { + void ReleaseInvokeState(const gpointer userData) { delete static_cast*>(userData); } } @@ -119,7 +119,7 @@ namespace infiniframe::linux_gtk::ui_thread { } namespace { - gboolean ExecuteAsync(gpointer userData) { + gboolean ExecuteAsync(const gpointer userData) { std::unique_ptr> callback(static_cast*>(userData)); try { (*callback)(); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowInitialization.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowInitialization.Gtk.cpp index 4cc334caf..e632dd9f5 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowInitialization.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowInitialization.Gtk.cpp @@ -183,8 +183,8 @@ void InfiniFrameWindow::Impl::ConnectWindowSignals(InfiniFrameWindow* window) { gtk_drag_dest_set(GTK_WIDGET(_window), GTK_DEST_DEFAULT_ALL, targets, 0, GDK_ACTION_COPY); g_signal_connect(G_OBJECT(_window), "drag-data-received", - G_CALLBACK(+[](GtkWidget* /*widget*/, GdkDragContext* context, gint x, gint y, - GtkSelectionData* data, guint /*info*/, guint time, gpointer userData) { + G_CALLBACK(+[](GtkWidget* /*widget*/, GdkDragContext* context, const gint x, const gint y, + GtkSelectionData* data, guint /*info*/, const guint time, const gpointer userData) { auto* instance = static_cast(userData); gchar** uris = gtk_selection_data_get_uris(data); @@ -211,7 +211,8 @@ void InfiniFrameWindow::Impl::ConnectWindowSignals(InfiniFrameWindow* window) { } g_free(uris); gtk_drag_finish(context, TRUE, FALSE, time); - }), window); + }), window + ); } } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp index d921c6933..7d6b01742 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowLifecycle.Gtk.cpp @@ -7,7 +7,7 @@ // Code // --------------------------------------------------------------------------------------------------------------------- namespace { - void on_webview_finalized(gpointer userData, GObject* object) { + void on_webview_finalized(const gpointer userData, GObject* object) { (void)object; if (userData != nullptr) static_cast(userData)->NotifyWebViewFinalized(); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowSignals.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowSignals.Gtk.cpp index b00c29763..b6842c075 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowSignals.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowSignals.Gtk.cpp @@ -17,7 +17,7 @@ namespace { return value != nullptr && value[0] != '\0' && g_strcmp0(value, "0") != 0; } - const char* webkit_load_event_to_string(WebKitLoadEvent event) { + const char* webkit_load_event_to_string(const WebKitLoadEvent event) { switch (event) { case WEBKIT_LOAD_STARTED: return "started"; @@ -32,7 +32,7 @@ namespace { } } - const char* webkit_termination_reason_to_string(WebKitWebProcessTerminationReason reason) { + const char* webkit_termination_reason_to_string(const WebKitWebProcessTerminationReason reason) { switch (reason) { case WEBKIT_WEB_PROCESS_CRASHED: return "crashed"; @@ -53,7 +53,7 @@ namespace { } } -void InfiniFrameWindow::OnConfigureEvent(int x, int y, int width, int height) { +void InfiniFrameWindow::OnConfigureEvent(const int x, const int y, const int width, const int height) { if (m_impl->_lastLeft != x || m_impl->_lastTop != y) { InvokeMove(x, y); m_impl->_lastLeft = x; @@ -67,7 +67,7 @@ void InfiniFrameWindow::OnConfigureEvent(int x, int y, int width, int height) { } } -void InfiniFrameWindow::OnWindowStateEvent(GdkWindowState newState) { +void InfiniFrameWindow::OnWindowStateEvent(const GdkWindowState newState) { // GTK emits window-state-event repeatedly for the same logical state (e.g. a focus or geometry change arrives // right after a maximize, each carrying the MAXIMIZED bit). Gate every callback on an actual state transition so // a single SetMaximized/SetMinimized/restore raises exactly one event, matching the Win32 WM_SIZE handling. @@ -183,7 +183,7 @@ gboolean on_webview_context_menu( WebKitWebView* web_view, GtkWidget* default_menu, WebKitHitTestResult* hit_test_result, - gboolean triggered_with_keyboard, + const gboolean triggered_with_keyboard, const gpointer self ) { (void)web_view; @@ -201,7 +201,7 @@ gboolean on_webview_context_menu( }); } -gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, gpointer user_data) { +gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* request, const gpointer user_data) { (void)web_view; return infiniframe::linux_gtk::RunGtkCallbackNoThrow("permission-request", TRUE, [&] -> gboolean { if (request == nullptr) @@ -222,7 +222,7 @@ gboolean on_permission_request(WebKitWebView* web_view, WebKitPermissionRequest* }); } -void on_webview_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event, gpointer user_data) { +void on_webview_load_changed(WebKitWebView* web_view, const WebKitLoadEvent load_event, const gpointer user_data) { infiniframe::linux_gtk::RunGtkCallbackNoThrow("load-changed", [&] { if (web_view == nullptr || user_data == nullptr) return; @@ -255,7 +255,9 @@ void on_webview_load_changed(WebKitWebView* web_view, WebKitLoadEvent load_event } gboolean on_webview_load_failed( - WebKitWebView* web_view, WebKitLoadEvent load_event, gchar* failing_uri, GError* error, gpointer user_data + WebKitWebView* web_view, + const WebKitLoadEvent load_event, gchar* failing_uri, GError* error, + const gpointer user_data ) { (void)web_view; return infiniframe::linux_gtk::RunGtkCallbackNoThrow("load-failed", FALSE, [&] -> gboolean { @@ -290,7 +292,7 @@ gboolean on_webview_load_failed( } void on_webview_process_terminated( - WebKitWebView* web_view, WebKitWebProcessTerminationReason reason, gpointer user_data + WebKitWebView* web_view, const WebKitWebProcessTerminationReason reason, const gpointer user_data ) { (void)web_view; infiniframe::linux_gtk::RunGtkCallbackNoThrow("web-process-terminated", [&] { @@ -316,7 +318,7 @@ void on_webview_process_terminated( }); } -void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, gpointer user_data) { +void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, const gpointer user_data) { (void)widget; (void)user_data; infiniframe::linux_gtk::RunGtkCallbackNoThrow("size-allocate", [&] { @@ -332,7 +334,8 @@ void on_webview_size_allocate(GtkWidget* widget, GtkAllocation* allocation, gpoi gboolean on_webview_decide_policy( WebKitWebView* web_view, WebKitPolicyDecision* decision, - WebKitPolicyDecisionType decision_type, gpointer user_data + const WebKitPolicyDecisionType decision_type, + const gpointer user_data ) { (void)web_view; return infiniframe::linux_gtk::RunGtkCallbackNoThrow("decide-policy", FALSE, [&] -> gboolean { diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp index 17ee25c40..18563f44d 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp @@ -128,7 +128,7 @@ void InfiniFrameWindow::Restore() { gtk_window_present(GTK_WINDOW(m_impl->_window)); } -static std::string escapeJsonString(std::string_view input) { +static std::string escapeJsonString(const std::string_view input) { std::string result; result.reserve(input.size() + 2); @@ -248,7 +248,7 @@ void InfiniFrameWindow::SetUserAgent(const char* userAgent) { webkit_web_view_reload(WEBKIT_WEB_VIEW(m_impl->_webview)); } -void InfiniFrameWindow::SetZoomEnabled(bool enabled) { +void InfiniFrameWindow::SetZoomEnabled(const bool enabled) { m_impl->_zoomEnabled = enabled; if (m_impl->_webview == nullptr) return; @@ -257,13 +257,13 @@ void InfiniFrameWindow::SetZoomEnabled(bool enabled) { SendWebMessage(envelope.c_str()); } -void InfiniFrameWindow::SetStatusBarEnabled(bool enabled) { +void InfiniFrameWindow::SetStatusBarEnabled(const bool enabled) { // WebKitGTK has no native status bar concept — this is a WebView2-only feature. // The flag is stored for API consistency but has no visible effect on Linux. m_impl->_statusBarEnabled = enabled; } -void InfiniFrameWindow::SetBrowserShortcutsEnabled(bool enabled) { +void InfiniFrameWindow::SetBrowserShortcutsEnabled(const bool enabled) { m_impl->_browserShortcutsEnabled = enabled; if (m_impl->_webview == nullptr) return; @@ -401,7 +401,7 @@ void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { } } -void InfiniFrameWindow::SetBackgroundColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { +void InfiniFrameWindow::SetBackgroundColor(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) { m_impl->_backgroundColorR = r; m_impl->_backgroundColorG = g; m_impl->_backgroundColorB = b; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Dialog.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Dialog.cpp index 1ece715ee..8c6f234e8 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Dialog.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Dialog.cpp @@ -351,11 +351,11 @@ namespace { delete state; } - void OnAsyncFileResponse(GtkDialog*, const gint response, gpointer userData) { + void OnAsyncFileResponse(GtkDialog*, const gint response, const gpointer userData) { CompleteAsyncFileDialog(static_cast(userData), response, false); } - void OnAsyncFileDestroyed(GtkWidget*, gpointer userData) { + void OnAsyncFileDestroyed(GtkWidget*, const gpointer userData) { CompleteAsyncFileDialog(static_cast(userData), GTK_RESPONSE_CANCEL, true); } @@ -378,14 +378,14 @@ namespace { delete state; } - void OnAsyncMessageResponse(GtkDialog*, const gint response, gpointer userData) { + void OnAsyncMessageResponse(GtkDialog*, const gint response, const gpointer userData) { CompleteAsyncMessageDialog( static_cast(userData), static_cast(response), false ); } - void OnAsyncMessageDestroyed(GtkWidget*, gpointer userData) { + void OnAsyncMessageDestroyed(GtkWidget*, const gpointer userData) { CompleteAsyncMessageDialog( static_cast(userData), DialogResult::Cancel, true ); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp index a262364af..2e646561a 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Menu.Gtk.cpp @@ -31,7 +31,7 @@ namespace { impl->_menuActivateDataList.clear(); } - void onMenuActivate(GtkMenuItem* /*menuItem*/, gpointer userData) { + void onMenuActivate(GtkMenuItem* /*menuItem*/, const gpointer userData) { auto* data = static_cast(userData); if (data == nullptr || data->window == nullptr) return; @@ -205,7 +205,7 @@ void InfiniFrameWindow::SetMenuBarJson(const char* menuBarJson) { ApplyInitMenuBar(menuBarJson); } -void InfiniFrameWindow::SetMenuItemEnabledById(const char* menuItemId, bool enabled) { +void InfiniFrameWindow::SetMenuItemEnabledById(const char* menuItemId, const bool enabled) { auto* impl = static_cast(ImplBase()); if (impl->_menuBar == nullptr) @@ -217,7 +217,7 @@ void InfiniFrameWindow::SetMenuItemEnabledById(const char* menuItemId, bool enab } } -void InfiniFrameWindow::SetMenuItemVisibleById(const char* menuItemId, bool visible) { +void InfiniFrameWindow::SetMenuItemVisibleById(const char* menuItemId, const bool visible) { auto* impl = static_cast(ImplBase()); if (impl->_menuBar == nullptr) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Monitors.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Monitors.Gtk.cpp index 80c06ebef..9c912ea88 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Monitors.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Monitors.Gtk.cpp @@ -5,8 +5,8 @@ // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- -void InfiniFrameWindow::GetAllMonitors(const GetAllMonitorsCallback callback) const { - if (callback == nullptr) { +void InfiniFrameWindow::GetAllMonitors(const GetAllMonitorsCallback Callback) const { + if (Callback == nullptr) { return; } @@ -23,7 +23,7 @@ void InfiniFrameWindow::GetAllMonitors(const GetAllMonitorsCallback callback) co gdk_monitor_get_workarea(monitor, reinterpret_cast(&props.work)); props.scale = gdk_monitor_get_scale_factor(monitor); - if (callback(&props) == 0) { + if (Callback(&props) == 0) { break; } } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Notifications.LibNotify.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Notifications.LibNotify.cpp index c7a5499ee..cd9401ee5 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Notifications.LibNotify.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Notifications.LibNotify.cpp @@ -18,7 +18,7 @@ void InfiniFrameWindow::ShowNotification(const char* title, const char* message) } void InfiniFrameWindow::ShowNotificationWithOptions( - const char* title, const char* body, const char* iconPath, int urgency, const char* tag + const char* title, const char* body, const char* iconPath, const int urgency, const char* tag ) { (void)iconPath; (void)urgency; @@ -40,9 +40,10 @@ void InfiniFrameWindow::ShowNotificationWithOptions( } void InfiniFrameWindow::BeginShowNotification( - uint64_t operationId, - const char* title, const char* body, const char* iconPath, int urgency, const char* tag, - OperationCompletedCallback completion, void* completionContext + const uint64_t operationId, + const char* title, const char* body, const char* iconPath, + const int urgency, const char* tag, + const OperationCompletedCallback completion, void* completionContext ) { ShowNotificationWithOptions(title, body, iconPath, urgency, tag); @@ -51,7 +52,7 @@ void InfiniFrameWindow::BeginShowNotification( } } -void InfiniFrameWindow::CancelNotification(uint64_t operationId, bool* canceled) { +void InfiniFrameWindow::CancelNotification(const uint64_t operationId, bool* canceled) { (void)operationId; if (canceled) *canceled = false; } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Taskbar.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Taskbar.Gtk.cpp index d89d206cc..48e1e7d55 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Taskbar.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Taskbar.Gtk.cpp @@ -1,140 +1,127 @@ // --------------------------------------------------------------------------------------------------------------------- // Imports // --------------------------------------------------------------------------------------------------------------------- -#ifdef __linux__ #include #include "Runtime/Platform/Linux/Window.Gtk.Internal.h" + // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- - // D-Bus paths and interfaces for taskbar integration -static const char* STATUS_NOTIFIER_ITEM_BUS_NAME = "org.kde.StatusNotifierItem"; -static const char* STATUS_NOTIFIER_ITEM_PATH = "/StatusNotifierItem"; -static const char* STATUS_NOTIFIER_ITEM_IFACE = "org.kde.StatusNotifierItem"; -static const char* LAUNCHER_ENTRY_BUS_NAME = "com.canonical.Unity.LauncherEntry"; -static const char* LAUNCHER_ENTRY_PATH = "/com/canonical/Unity/LauncherEntry"; -static const char* LAUNCHER_ENTRY_IFACE = "com.canonical.Unity.LauncherEntry"; +static const char* statusNotifierItemBusName = "org.kde.StatusNotifierItem"; +static const char* statusNotifierItemPath = "/StatusNotifierItem"; +static const char* statusNotifierItemIface = "org.kde.StatusNotifierItem"; +static const char* launcherEntryBusName = "com.canonical.Unity.LauncherEntry"; +static const char* launcherEntryPath = "/com/canonical/Unity/LauncherEntry"; +static const char* launcherEntryIface = "com.canonical.Unity.LauncherEntry"; // Cached D-Bus connections and proxy objects -static GDBusConnection* s_sessionBus = nullptr; -static GDBusProxy* s_statusNotifierProxy = nullptr; -static GDBusProxy* s_launcherEntryProxy = nullptr; -static bool s_dbusInitialized = false; -static bool s_hasStatusNotifier = false; -static bool s_hasLauncherEntry = false; +static GDBusConnection* sessionBus = nullptr; +static GDBusProxy* statusNotifierProxy = nullptr; +static GDBusProxy* launcherEntryProxy = nullptr; +static bool dbusInitialized = false; +static bool hasStatusNotifier = false; +static bool hasLauncherEntry = false; static void EnsureDBusInitialized() { - if (s_dbusInitialized) return; - s_dbusInitialized = true; + if (dbusInitialized) { + return; + } + dbusInitialized = true; GError* error = nullptr; - s_sessionBus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); - if (!s_sessionBus || error) { - if (error) g_error_free(error); + sessionBus = g_bus_get_sync(G_BUS_TYPE_SESSION, nullptr, &error); + if (sessionBus == nullptr || error != nullptr) { + if (error != nullptr) { + g_error_free(error); + } return; } // Try StatusNotifierItem (KDE, Unity, some others) - s_statusNotifierProxy = g_dbus_proxy_new_sync( - s_sessionBus, - G_DBUS_PROXY_FLAGS_NONE, - nullptr, - STATUS_NOTIFIER_ITEM_BUS_NAME, - STATUS_NOTIFIER_ITEM_PATH, - STATUS_NOTIFIER_ITEM_IFACE, - nullptr, - &error + statusNotifierProxy = g_dbus_proxy_new_sync( + sessionBus, G_DBUS_PROXY_FLAGS_NONE, nullptr, statusNotifierItemBusName, statusNotifierItemPath, + statusNotifierItemIface, nullptr, &error ); - if (s_statusNotifierProxy && !error) { - s_hasStatusNotifier = true; + if (statusNotifierProxy != nullptr && error == nullptr) { + hasStatusNotifier = true; + } + if (error != nullptr) { + g_error_free(error); } - if (error) g_error_free(error); error = nullptr; // Try Unity LauncherEntry - s_launcherEntryProxy = g_dbus_proxy_new_sync( - s_sessionBus, - G_DBUS_PROXY_FLAGS_NONE, - nullptr, - LAUNCHER_ENTRY_BUS_NAME, - LAUNCHER_ENTRY_PATH, - LAUNCHER_ENTRY_IFACE, - nullptr, - &error + launcherEntryProxy = g_dbus_proxy_new_sync( + sessionBus, G_DBUS_PROXY_FLAGS_NONE, nullptr, launcherEntryBusName, launcherEntryPath, launcherEntryIface, + nullptr, &error ); - if (s_launcherEntryProxy && !error) { - s_hasLauncherEntry = true; + if (launcherEntryProxy != nullptr && error == nullptr) { + hasLauncherEntry = true; + } + if (error) { + g_error_free(error); } - if (error) g_error_free(error); } -static double ProgressToFraction(int state, uint64_t current, uint64_t total) { - if (state == 0) return -1.0; // -1 means "no progress" for LauncherEntry - if (state == 1) return 0.0; // Indeterminate - if (total == 0) return 0.0; +static double ProgressToFraction(const int state, const uint64_t current, const uint64_t total) { + if (state == 0) { + return -1.0; // -1 means "no progress" for LauncherEntry + } + if (state == 1) { + return 0.0; // Indeterminate + } + if (total == 0) { + return 0.0; + } return static_cast(current) / static_cast(total); } -void InfiniFrameWindow::SetTaskbarProgress(int state, uint64_t current, uint64_t total) { +void InfiniFrameWindow::SetTaskbarProgress(const int state, const uint64_t current, const uint64_t total) { EnsureDBusInitialized(); - double progress = ProgressToFraction(state, current, total); + const double Progress = ProgressToFraction(state, current, total); // Unity LauncherEntry - if (s_hasLauncherEntry && s_launcherEntryProxy) { - GVariant* progressVariant = g_variant_new_double(progress); + if (hasLauncherEntry && launcherEntryProxy != nullptr) { + GVariant* progressVariant = g_variant_new_double(Progress); GError* error = nullptr; g_dbus_connection_call_sync( - g_dbus_proxy_get_connection(s_launcherEntryProxy), - LAUNCHER_ENTRY_BUS_NAME, - LAUNCHER_ENTRY_PATH, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new("(ssv)", LAUNCHER_ENTRY_IFACE, "UnityCount", g_variant_new_int32(0)), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - &error + g_dbus_proxy_get_connection(launcherEntryProxy), launcherEntryBusName, launcherEntryPath, + "org.freedesktop.DBus.Properties", "Set", + g_variant_new("(ssv)", launcherEntryIface, "UnityCount", g_variant_new_int32(0)), nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); g_dbus_connection_call_sync( - g_dbus_proxy_get_connection(s_launcherEntryProxy), - LAUNCHER_ENTRY_BUS_NAME, - LAUNCHER_ENTRY_PATH, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new("(ssv)", LAUNCHER_ENTRY_IFACE, "UnityProgress", progressVariant), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - &error + g_dbus_proxy_get_connection(launcherEntryProxy), launcherEntryBusName, launcherEntryPath, + "org.freedesktop.DBus.Properties", "Set", + g_variant_new("(ssv)", launcherEntryIface, "UnityProgress", progressVariant), nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); - if (error) g_error_free(error); + if (error != nullptr) { + g_error_free(error); + } } // StatusNotifierItem - if (s_hasStatusNotifier && s_statusNotifierProxy) { + if (hasStatusNotifier && statusNotifierProxy != nullptr) { const char* status = "NeedsAttention"; - if (state == 0) status = "Passive"; - else if (state == 2) status = "NeedsAttention"; + if (state == 0) { + status = "Passive"; + } else if (state == 2) { + status = "NeedsAttention"; + } GError* error = nullptr; g_dbus_connection_call_sync( - g_dbus_proxy_get_connection(s_statusNotifierProxy), - STATUS_NOTIFIER_ITEM_BUS_NAME, - STATUS_NOTIFIER_ITEM_PATH, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new("(ssv)", STATUS_NOTIFIER_ITEM_IFACE, "Status", g_variant_new_string(status)), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - &error + g_dbus_proxy_get_connection(statusNotifierProxy), statusNotifierItemBusName, statusNotifierItemPath, + "org.freedesktop.DBus.Properties", "Set", + g_variant_new("(ssv)", statusNotifierItemIface, "Status", g_variant_new_string(status)), nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); - if (error) g_error_free(error); + if (error != nullptr) { + g_error_free(error); + } } } @@ -142,64 +129,62 @@ void InfiniFrameWindow::ClearTaskbarProgress() { SetTaskbarProgress(0, 0, 0); } -void InfiniFrameWindow::SetTaskbarFlash(int mode, uint32_t count) { +void InfiniFrameWindow::SetTaskbarFlash(const int mode, uint32_t) { EnsureDBusInitialized(); - if (!s_hasStatusNotifier || !s_statusNotifierProxy) return; + if (!hasStatusNotifier || statusNotifierProxy == nullptr) { + return; + } const char* status = "Passive"; switch (mode) { - case 0: status = "Passive"; break; + case 0: + status = "Passive"; + break; case 1: // All case 2: // Timer case 3: // TimerAll status = "NeedsAttention"; break; - default: status = "Passive"; break; + default: + status = "Passive"; + break; } GError* error = nullptr; g_dbus_connection_call_sync( - g_dbus_proxy_get_connection(s_statusNotifierProxy), - STATUS_NOTIFIER_ITEM_BUS_NAME, - STATUS_NOTIFIER_ITEM_PATH, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new("(ssv)", STATUS_NOTIFIER_ITEM_IFACE, "Status", g_variant_new_string(status)), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - &error + g_dbus_proxy_get_connection(statusNotifierProxy), statusNotifierItemBusName, statusNotifierItemPath, + "org.freedesktop.DBus.Properties", "Set", + g_variant_new("(ssv)", statusNotifierItemIface, "Status", g_variant_new_string(status)), nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); - if (error) g_error_free(error); + if (error != nullptr) { + g_error_free(error); + } } void InfiniFrameWindow::StopTaskbarFlash() { EnsureDBusInitialized(); - if (!s_hasStatusNotifier || !s_statusNotifierProxy) return; + if (!hasStatusNotifier || statusNotifierProxy == nullptr) { + return; + } GError* error = nullptr; g_dbus_connection_call_sync( - g_dbus_proxy_get_connection(s_statusNotifierProxy), - STATUS_NOTIFIER_ITEM_BUS_NAME, - STATUS_NOTIFIER_ITEM_PATH, - "org.freedesktop.DBus.Properties", - "Set", - g_variant_new("(ssv)", STATUS_NOTIFIER_ITEM_IFACE, "Status", g_variant_new_string("Passive")), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - &error + g_dbus_proxy_get_connection(statusNotifierProxy), statusNotifierItemBusName, statusNotifierItemPath, + "org.freedesktop.DBus.Properties", "Set", + g_variant_new("(ssv)", statusNotifierItemIface, "Status", g_variant_new_string("Passive")), nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error ); - if (error) g_error_free(error); + if (error != nullptr) { + g_error_free(error); + } } void InfiniFrameWindow::GetTaskbarProgressSupported(bool* supported) const { EnsureDBusInitialized(); - if (supported) *supported = s_hasStatusNotifier || s_hasLauncherEntry; + if (supported != nullptr) { + *supported = hasStatusNotifier || hasLauncherEntry; + } } - -#endif diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp index 8735cb2e0..113a6dc0f 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitCustomSchemes.Gtk.cpp @@ -13,7 +13,7 @@ // Code // --------------------------------------------------------------------------------------------------------------------- namespace gtk_webkit { - void FinishCustomSchemeError(WebKitURISchemeRequest* request, GIOErrorEnum code, const char* message) { + void FinishCustomSchemeError(WebKitURISchemeRequest* request, const GIOErrorEnum code, const char* message) { if (request == nullptr) return; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp index 1567ee3fd..7737a6339 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitHost.Gtk.cpp @@ -23,7 +23,7 @@ extern gboolean on_webview_decide_policy( WebKitPolicyDecisionType decision_type, gpointer user_data ); -void InfiniFrameWindow::Show(bool isAlreadyShown) { +void InfiniFrameWindow::Show(const bool isAlreadyShown) { (void)isAlreadyShown; // Early out if the webview has already been created. diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp index 7b9d9aa26..55a660d9a 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/WebKit/WebKitMessaging.Gtk.cpp @@ -27,7 +27,7 @@ namespace gtk_webkit { struct GObjectGuard { gpointer value = nullptr; - explicit GObjectGuard(gpointer initialValue = nullptr) : value(initialValue) {} + explicit GObjectGuard(const gpointer initialValue = nullptr) : value(initialValue) {} ~GObjectGuard() { if (value != nullptr) g_object_unref(value); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/DarkMode.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/DarkMode.cpp index 2a9742d25..636c9b8e8 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/DarkMode.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/DarkMode.cpp @@ -44,7 +44,7 @@ namespace { } } - void reset(HMODULE handle) { + void reset(const HMODULE handle) { if (_handle != nullptr) { FreeLibrary(_handle); } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Dialog.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Dialog.cpp index 5871eb563..0d3e0c947 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Dialog.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Dialog.cpp @@ -438,7 +438,7 @@ namespace { requested.store(true, std::memory_order_release); const DWORD id = threadId.load(std::memory_order_acquire); if (id == 0) return; - EnumThreadWindows(id, [](HWND hwnd, LPARAM value) -> BOOL { + EnumThreadWindows(id, [](const HWND hwnd, const LPARAM value) -> BOOL { auto* state = reinterpret_cast(value); if (IsWindowVisible(hwnd) && GetWindow(hwnd, GW_OWNER) == state->owner) PostMessageW(hwnd, WM_CLOSE, 0, 0); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Menu.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Menu.Win32.cpp index aa3744c37..ccf42d199 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Menu.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Menu.Win32.cpp @@ -19,7 +19,7 @@ // Code // --------------------------------------------------------------------------------------------------------------------- namespace { - void DestroyMenuRecursive(HMENU menu) { + void DestroyMenuRecursive(const HMENU menu) { if (menu == nullptr) return; int count = GetMenuItemCount(menu); for (int i = 0; i < count; i++) { @@ -31,7 +31,7 @@ namespace { } void BuildMenuFromJson( - HMENU parentMenu, + const HMENU parentMenu, const simdjson::dom::array& items, std::unordered_map& idToCommand, std::unordered_map& commandToId, @@ -178,7 +178,7 @@ void InfiniFrameWindow::SetMenuBarJson(const char* menuBarJson) { ApplyInitMenuBar(menuBarJson); } -void InfiniFrameWindow::SetMenuItemEnabledById(const char* menuItemId, bool enabled) { +void InfiniFrameWindow::SetMenuItemEnabledById(const char* menuItemId, const bool enabled) { HMENU parent = nullptr; UINT position = 0; UINT commandId = 0; @@ -190,7 +190,7 @@ void InfiniFrameWindow::SetMenuItemEnabledById(const char* menuItemId, bool enab DrawMenuBar(m_impl->_hWnd); } -void InfiniFrameWindow::SetMenuItemVisibleById(const char* menuItemId, bool visible) { +void InfiniFrameWindow::SetMenuItemVisibleById(const char* menuItemId, const bool visible) { auto it = m_impl->_menuItemIdToCommandId.find(menuItemId); if (it == m_impl->_menuItemIdToCommandId.end()) return; @@ -217,7 +217,7 @@ void InfiniFrameWindow::ClickMenuItemById(const char* menuItemId) { SendWebMessage(message.c_str()); } -void InfiniFrameWindow::HandleMenuCommand(WPARAM wParam) { +void InfiniFrameWindow::HandleMenuCommand(const WPARAM wParam) { UINT commandId = LOWORD(wParam); auto it = m_impl->_menuCommandIdToItemId.find(commandId); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Monitors.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Monitors.Win32.cpp index 4a4f3725c..75a475878 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Monitors.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Monitors.Win32.cpp @@ -27,10 +27,10 @@ static BOOL CALLBACK MonitorEnum(const HMONITOR monitor, HDC, LPRECT, const LPAR return callback(&props) ? TRUE : FALSE; } -void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback callback) const { - if (callback) { +void InfiniFrameWindow::GetAllMonitors(GetAllMonitorsCallback Callback) const { + if (Callback) { EnumDisplayMonitors( - nullptr, nullptr, MonitorEnum, reinterpret_cast(callback) + nullptr, nullptr, MonitorEnum, reinterpret_cast(Callback) ); } } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Notifications.WinToast.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Notifications.WinToast.cpp index 6e9825cbd..53bf4ac11 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Notifications.WinToast.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Notifications.WinToast.cpp @@ -24,7 +24,7 @@ void InfiniFrameWindow::ShowNotification(const char* title, const char* body) { } void InfiniFrameWindow::ShowNotificationWithOptions( - const char* title, const char* body, const char* iconPath, int urgency, const char* tag + const char* title, const char* body, const char* iconPath, const int urgency, const char* tag ) { (void)tag; std::wstring wideTitle = ToUTF16String(title); @@ -56,9 +56,10 @@ void InfiniFrameWindow::ShowNotificationWithOptions( } void InfiniFrameWindow::BeginShowNotification( - uint64_t operationId, - const char* title, const char* body, const char* iconPath, int urgency, const char* tag, - OperationCompletedCallback completion, void* completionContext + const uint64_t operationId, + const char* title, const char* body, const char* iconPath, + const int urgency, const char* tag, + const OperationCompletedCallback completion, void* completionContext ) { (void)tag; std::wstring wideTitle = ToUTF16String(title); @@ -93,7 +94,7 @@ void InfiniFrameWindow::BeginShowNotification( } } -void InfiniFrameWindow::CancelNotification(uint64_t operationId, bool* canceled) { +void InfiniFrameWindow::CancelNotification(const uint64_t operationId, bool* canceled) { (void)operationId; if (canceled) *canceled = false; } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Taskbar.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Taskbar.Win32.cpp index 2224aae0e..372a9bb30 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Taskbar.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/Taskbar.Win32.cpp @@ -10,7 +10,7 @@ // Code // --------------------------------------------------------------------------------------------------------------------- -void InfiniFrameWindow::SetTaskbarProgress(int state, uint64_t current, uint64_t total) { +void InfiniFrameWindow::SetTaskbarProgress(const int state, const uint64_t current, const uint64_t total) { HWND hWnd = getHwnd(); if (!hWnd) return; @@ -71,7 +71,7 @@ void InfiniFrameWindow::ClearTaskbarProgress() { pTaskbarList->Release(); } -void InfiniFrameWindow::SetTaskbarFlash(int mode, uint32_t count) { +void InfiniFrameWindow::SetTaskbarFlash(const int mode, const uint32_t count) { HWND hWnd = getHwnd(); if (!hWnd) return; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp index fb55b24a2..27abb7410 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Attach.Win32.cpp @@ -442,7 +442,7 @@ void InfiniFrameWindow::AttachWebView() { HRESULT addScriptHr = m_impl->_webviewWindow->AddScriptToExecuteOnDocumentCreated( js_wide.c_str(), Callback( - [nav, this](HRESULT errorCode, LPCWSTR id) -> HRESULT { + [nav, this](const HRESULT errorCode, LPCWSTR id) -> HRESULT { OutputDebugStringW( std::format( L"[InfiniFrame] AddScriptToExecuteOnDocumentCreated callback: " diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp index a410cccef..f83778c96 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Controller.Win32.cpp @@ -43,7 +43,7 @@ void InfiniFrameWindow::ClearBrowserAutoFill() { COREWEBVIEW2_BROWSING_DATA_KINDS_PASSWORD_AUTOSAVE); profile2->ClearBrowsingData( - dataKinds, Callback([](HRESULT hr) -> HRESULT { + dataKinds, Callback([](const HRESULT hr) -> HRESULT { if (FAILED(hr)) { OutputDebugStringW(L"[InfiniFrame] ClearBrowsingData failed.\n"); } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp index 87a71fa13..468fa585e 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Windows/WebView/WebView2Settings.Win32.cpp @@ -198,7 +198,7 @@ void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { } } -void InfiniFrameWindow::SetBackgroundColor(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { +void InfiniFrameWindow::SetBackgroundColor(const uint8_t r, const uint8_t g, const uint8_t b, const uint8_t a) { m_impl->_backgroundColorR = r; m_impl->_backgroundColorG = g; m_impl->_backgroundColorB = b; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/DialogOperation.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/DialogOperation.h index 44e3ca0f5..20ebdae47 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/DialogOperation.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/DialogOperation.h @@ -26,14 +26,14 @@ struct DialogOperation final { std::atomic finalResult = static_cast(NativeOperationResult::Completed); DialogOperation( - uint64_t operationId, std::string operationName, - FileDialogCompletedCallback completion, void* context + const uint64_t operationId, std::string operationName, + const FileDialogCompletedCallback completion, void* context ) : id(operationId), kind(Kind::File), name(std::move(operationName)), fileCompletion(completion), completionContext(context) {} DialogOperation( - uint64_t operationId, std::string operationName, - OperationCompletedCallback completion, void* context + const uint64_t operationId, std::string operationName, + const OperationCompletedCallback completion, void* context ) : id(operationId), kind(Kind::Message), name(std::move(operationName)), messageCompletion(completion), completionContext(context) {} diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/NativeOperation.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/NativeOperation.h index 5aa4133f2..3c0d0c357 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/NativeOperation.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Operations/NativeOperation.h @@ -31,10 +31,10 @@ struct NativeOperation final { std::atomic state = Pending; NativeOperation( - uint64_t operationId, - ContextAction action, + const uint64_t operationId, + const ContextAction action, void* actionContext, - OperationCompletedCallback completed, + const OperationCompletedCallback completed, void* completedContext, InfiniFrameWindow* window ) : id(operationId), callback(action), callbackContext(actionContext), completion(completed), diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h index fda517a0d..935d92ca8 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/InfiniFrameWindow.h @@ -633,9 +633,9 @@ class InfiniFrameWindow { /** * @brief Enumerate all connected monitors by invoking a callback for each one - * @param callback Called once per monitor; receives a Monitor describing geometry and scale + * @param Callback Called once per monitor; receives a Monitor describing geometry and scale */ - void GetAllMonitors(GetAllMonitorsCallback callback) const; + void GetAllMonitors(GetAllMonitorsCallback Callback) const; /** * @brief Set callback invoked when the user attempts to close the window diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/WindowEvents.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/WindowEvents.cpp index 88985d3d5..5d8fa9aa0 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/WindowEvents.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Shared/Window/WindowEvents.cpp @@ -85,12 +85,12 @@ void InfiniFrameWindow::InvokeFocusOut() const noexcept { ImplBase()->_focusOutCallback(); } -void InfiniFrameWindow::InvokeMove(int x, int y) const noexcept { +void InfiniFrameWindow::InvokeMove(const int x, const int y) const noexcept { if (ImplBase()->_movedCallback) ImplBase()->_movedCallback(x, y); } -void InfiniFrameWindow::InvokeResize(int width, int height) const noexcept { +void InfiniFrameWindow::InvokeResize(const int width, const int height) const noexcept { if (ImplBase()->_resizedCallback) ImplBase()->_resizedCallback(width, height); } diff --git a/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs b/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs index fbeff8158..d24fa54b4 100644 --- a/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs +++ b/src/InfiniFrame.Shared/Utilities/MonitorsUtility.cs @@ -62,7 +62,7 @@ public static bool TryGetCurrentMonitor(ImmutableArray monitors, Rectangle intersection = Rectangle.Intersect(m.MonitorArea, windowBounds); long overlap = 0; if (intersection.Width > 0 && intersection.Height > 0) { - overlap = (long)intersection.Width * (long)intersection.Height; + overlap = intersection.Width * (long)intersection.Height; } // fraction of the *window* that lies on this monitor @@ -133,4 +133,4 @@ public static bool TryGetCurrentWindowAndMonitor(IInfiniFrameWindow window, out return TryGetCurrentMonitor(monitors, windowRect, out monitor); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame.Tools.Pack/Program.cs b/src/InfiniFrame.Tools.Pack/Program.cs index 3a9e92d09..474140768 100644 --- a/src/InfiniFrame.Tools.Pack/Program.cs +++ b/src/InfiniFrame.Tools.Pack/Program.cs @@ -4,7 +4,6 @@ using InfiniFrame.Tools.Pack.Exceptions; using InfiniFrame.Tools.Pack.Services; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Serilog; using Serilog.Events; diff --git a/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs b/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs index e4c2feffc..51688acb4 100644 --- a/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs +++ b/src/InfiniFrame/Window/Features/Notifications/NotificationsWebMessageDispatcher.cs @@ -34,7 +34,7 @@ protected override void Post(INotificationsInfiniFrameWindowFeature feature, str if (iconPath is not null || tag is not null || urgencyStr is not null) { InfiniFrameNotificationUrgency urgency = urgencyStr is not null - && Enum.TryParse(urgencyStr, ignoreCase: true, out InfiniFrameNotificationUrgency parsed) + && Enum.TryParse(urgencyStr, ignoreCase: true, out InfiniFrameNotificationUrgency parsed) ? parsed : InfiniFrameNotificationUrgency.Normal; @@ -52,4 +52,4 @@ protected override void Post(INotificationsInfiniFrameWindowFeature feature, str } else throw Unsupported(command); } -} \ No newline at end of file +} diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 4d95392c7..399a222f6 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -82,10 +82,7 @@ void IInfiniFrameWindow.SetManagedThreadId(int managedThreadId) { /// public IInfiniFrameEvents Events { get; } = events; /// - public IInfiniFrameWindowFeatures Features { - get => _features ?? throw new InvalidOperationException("Features have not been assigned. Call AssignFeatures before accessing this property."); - private set => _features = value; - } + public IInfiniFrameWindowFeatures Features => _features ?? throw new InvalidOperationException("Features have not been assigned. Call AssignFeatures before accessing this property."); private IInfiniFrameWindowFeatures? _features; /// diff --git a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs index 1fe7331b9..7ba4577ac 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindowFeaturesFactory.cs @@ -102,11 +102,11 @@ public IInfiniFrameWindowFeatures Create(IInfiniFrameWindow window, IInfiniFrame Menu: new MenuInfiniFrameWindowFeature( window, GetLogger(provider), - originalBuilder.Features.Menu?.MenuBar + originalBuilder.Features.Menu.MenuBar ), JavaScript: new JavaScriptInfiniFrameWindowFeature( window, GetLogger(provider) ) ); -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs index 53a5f2d2b..ea5c03f10 100644 --- a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs +++ b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs @@ -28,7 +28,7 @@ public async Task SetPointerCaptureAsync_InvokesExpectedJsFunction(CancellationT await Assert.That(identifier).IsEqualTo("infiniframe.utils.setPointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo((object)element); + await Assert.That(jsArguments[0]).IsEqualTo(element); await Assert.That((long)jsArguments[1]!).IsEqualTo(42L); } @@ -48,7 +48,7 @@ public async Task ReleasePointerCaptureAsync_InvokesExpectedJsFunction(Cancellat await Assert.That(identifier).IsEqualTo("infiniframe.utils.releasePointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo((object)element); + await Assert.That(jsArguments[0]).IsEqualTo(element); await Assert.That((long)jsArguments[1]!).IsEqualTo(7L); } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 47f82b4e1..167edd0e0 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -86,7 +86,7 @@ public async Task AtWindowStage_ThroughBuilderAssignment(CancellationToken ct) { // Assert await Assert.That(builder.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); - await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo((int?)value); + await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); } [Test] diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs index 9b6f36072..ec93e4d0b 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Lifecycle/NativeLifetimeStressTests.cs @@ -80,14 +80,14 @@ public async Task FeatureCallsRacingClose_DoNotReachFreedNativeInstance(Cancella // Arrange using var windowUtility = InfiniFrameTestWindow.Create(ct); IInfiniFrameWindow window = windowUtility.Window; - using var stop = CancellationTokenSource.CreateLinkedTokenSource(ct); + var stop = CancellationTokenSource.CreateLinkedTokenSource(ct); int completedCalls = 0; Task[] callers = [ .. Enumerable.Range(0, ConcurrentFeatureCallerCount) .Select(workerIndex => Task.Run(action: () => { _ = workerIndex; - while (!stop.IsCancellationRequested) { + while (stop is { IsCancellationRequested: false }) { try { _ = window.Features.State.IsFocused; Interlocked.Increment(ref completedCalls); @@ -126,6 +126,7 @@ public async Task ConcurrentCloseRequests_ProduceSingleDeterministicShutdown(Can Task[] closeRequests = [ .. Enumerable.Range(0, 16) + // ReSharper disable once AccessToDisposedClosure .Select(_ => Task.Run(window.Close, ct)) ]; @@ -143,4 +144,4 @@ await Assert.That((int)window.Features.Lifecycle.State) // Assert await Assert.That(window.Features.Lifecycle.State).IsEqualTo(InfiniFrameWindowLifecycleState.Disposed); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs index 24d55fa4c..1b0a8d4f3 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarSerializationTests.cs @@ -2,7 +2,6 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; -using System.Collections.Immutable; using System.Text.Json; namespace InfiniTests.InfiniFrame.Window.Features.Menu; diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs index 23aed4430..4ff69518a 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuBarTests.cs @@ -3,7 +3,6 @@ // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; using InfiniFrame.NativeBridge.Parameters; -using System.Collections.Immutable; using System.Text.Json; namespace InfiniTests.InfiniFrame.Window.Features.Menu; diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs index a722fd32d..46758e3a6 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Menu/MenuItemTests.cs @@ -2,7 +2,6 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- using InfiniFrame; -using System.Collections.Immutable; namespace InfiniTests.InfiniFrame.Window.Features.Menu; // --------------------------------------------------------------------------------------------------------------------- From 6f6ac2de60db78a72ec1c14b2e543603ce2efbe4 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 18:43:23 +0200 Subject: [PATCH 17/30] Add ShutdownAndJoin; adjust test assertions Introduce ShutdownAndJoin in the Linux GTK UI thread (declared in UiThread.Gtk.h and implemented in UiThread.Gtk.cpp) to call Shutdown() and then join the GTK thread for a clean shutdown. Update tests to fix type mismatches: cast element to object in InfiniFrameJsTests and compare RemoteDebuggingPort as (int?) in RemoteDebuggingPortTests so assertions match expected types. --- .../Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp | 4 ++++ .../Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h | 1 + tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs | 4 ++-- .../Window/Features/Debugging/RemoteDebuggingPortTests.cs | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 9e0b24fc7..32b8cc251 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -107,6 +107,10 @@ namespace infiniframe::linux_gtk::ui_thread { if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { g_main_loop_quit(mainLoop); } + } + + void ShutdownAndJoin() { + Shutdown(); if (gtkThread.joinable()) { gtkThread.join(); diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h index 923aad1bc..63e1bbdea 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h @@ -10,6 +10,7 @@ namespace infiniframe::linux_gtk::ui_thread { void EnsureInitialized(); void Shutdown(); + void ShutdownAndJoin(); bool IsCurrentThread(); bool InvokeAsync(std::function callback); bool InvokeIdle(std::function callback); diff --git a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs index ea5c03f10..53a5f2d2b 100644 --- a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs +++ b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs @@ -28,7 +28,7 @@ public async Task SetPointerCaptureAsync_InvokesExpectedJsFunction(CancellationT await Assert.That(identifier).IsEqualTo("infiniframe.utils.setPointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo(element); + await Assert.That(jsArguments[0]).IsEqualTo((object)element); await Assert.That((long)jsArguments[1]!).IsEqualTo(42L); } @@ -48,7 +48,7 @@ public async Task ReleasePointerCaptureAsync_InvokesExpectedJsFunction(Cancellat await Assert.That(identifier).IsEqualTo("infiniframe.utils.releasePointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo(element); + await Assert.That(jsArguments[0]).IsEqualTo((object)element); await Assert.That((long)jsArguments[1]!).IsEqualTo(7L); } diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 167edd0e0..47f82b4e1 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -86,7 +86,7 @@ public async Task AtWindowStage_ThroughBuilderAssignment(CancellationToken ct) { // Assert await Assert.That(builder.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); - await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); + await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo((int?)value); } [Test] From 383fb27b53e3f871784a1d77dab0a59fcc16e5a0 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 18:59:32 +0200 Subject: [PATCH 18/30] Update Directory.Packages.props --- Directory.Packages.props | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 65f900595..8cc478dc6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,33 +5,33 @@ - + - + - + - - - - - - - - + + + + + + + + - - + + @@ -39,9 +39,9 @@ - - - - + + + + \ No newline at end of file From 67599ef23172c349f7fcafc5895bc7d5a2625a90 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 19:06:46 +0200 Subject: [PATCH 19/30] resolve NET9/10 warnings --- .../InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs | 8 +++++--- .../Window/Features/Debugging/RemoteDebuggingPortTests.cs | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs index 53a5f2d2b..57de54687 100644 --- a/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs +++ b/tests/InfiniTests.InfiniFrame.Blazor/InfiniFrameJsTests.cs @@ -28,7 +28,8 @@ public async Task SetPointerCaptureAsync_InvokesExpectedJsFunction(CancellationT await Assert.That(identifier).IsEqualTo("infiniframe.utils.setPointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo((object)element); + // ReSharper disable once RedundantCast + await Assert.That(jsArguments[0]).IsEqualTo(element as object); await Assert.That((long)jsArguments[1]!).IsEqualTo(42L); } @@ -48,7 +49,8 @@ public async Task ReleasePointerCaptureAsync_InvokesExpectedJsFunction(Cancellat await Assert.That(identifier).IsEqualTo("infiniframe.utils.releasePointerCapture"); await Assert.That(cancellationToken).IsEqualTo(ct); await Assert.That(jsArguments.Length).IsEqualTo(2); - await Assert.That(jsArguments[0]).IsEqualTo((object)element); + // ReSharper disable once RedundantCast + await Assert.That(jsArguments[0]).IsEqualTo(element as object); await Assert.That((long)jsArguments[1]!).IsEqualTo(7L); } @@ -69,4 +71,4 @@ public async Task SetPointerCaptureAsync_SwallowsOperationCanceled_WhenCancellat logger.DidNotReceiveWithAnyArgs().Log(default, default, null!, null, null!); await Assert.That(jsRuntime.Invocations.Count).IsEqualTo(1); } -} \ No newline at end of file +} diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 47f82b4e1..66bfb3c0b 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -86,6 +86,7 @@ public async Task AtWindowStage_ThroughBuilderAssignment(CancellationToken ct) { // Assert await Assert.That(builder.Features.Debugging.RemoteDebuggingPort).IsEqualTo(value); + // ReSharper disable once RedundantCast await Assert.That(window.Features.Debugging.RemoteDebuggingPort).IsEqualTo((int?)value); } @@ -108,4 +109,4 @@ public async Task AtBuilderStage_DirectAssignment_InvalidPort_ThrowsArgumentOutO await Assert.That(exception).IsNotNull(); await Assert.That(exception!.ParamName).IsEqualTo("port"); } -} \ No newline at end of file +} From b31b1f00af98401c50a88fa76fd5d2e8be549feb Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 19:08:49 +0200 Subject: [PATCH 20/30] Update UiThread.Gtk.cpp --- .../Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 32b8cc251..50b35de5b 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -201,5 +201,8 @@ namespace infiniframe::linux_gtk::ui_thread { __attribute__((destructor(101))) static void InfiniFrame_UiThread_Shutdown() { - infiniframe::linux_gtk::ui_thread::Shutdown(); + // During static destruction GLib/GDK objects are already half-torn-down. Attempting + // g_main_loop_quit() or thread join here triggers cascading assertion failures + // (gdk_seat_get_keyboard, g_dbus_connection_call_sync_internal) and SIGABRT. + // The OS reclaims all resources on process exit, so it is safe to do nothing here. } From 0e3495651a04f543ac35922ae5a83c2d698429f9 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 19:09:36 +0200 Subject: [PATCH 21/30] Remove Python CI workflow and simplify test Delete .github/workflows/ci-testing-python.yml and remove its entry from InfiniFrame.slnx. Also simplify the Task.Run lambda in RemoteDebuggingPortTests.cs to an expression-bodied call (no behavior change) while preserving the CA1416 suppression. --- .github/workflows/ci-testing-python.yml | 26 ------------------- InfiniFrame.slnx | 1 - .../Debugging/RemoteDebuggingPortTests.cs | 4 +-- 3 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 .github/workflows/ci-testing-python.yml diff --git a/.github/workflows/ci-testing-python.yml b/.github/workflows/ci-testing-python.yml deleted file mode 100644 index 49bf36193..000000000 --- a/.github/workflows/ci-testing-python.yml +++ /dev/null @@ -1,26 +0,0 @@ -#file: noinspection UndefinedAction,UndefinedParamsPresent -name: "CI: Python Tests" -on: - pull_request: - branches: - - core - paths: - - .github/scripts/** - - .github/workflows/shared-testing-python.yml - - .github/workflows/ci-testing-python.yml - push: - branches: - - core - paths: - - .github/scripts/** - - .github/workflows/shared-testing-python.yml - - .github/workflows/ci-testing-python.yml - workflow_dispatch: - -permissions: - contents: read - -jobs: - run: - name: Python Tests - uses: ./.github/workflows/shared-testing-python.yml diff --git a/InfiniFrame.slnx b/InfiniFrame.slnx index ccd6240fd..4d002517a 100644 --- a/InfiniFrame.slnx +++ b/InfiniFrame.slnx @@ -68,7 +68,6 @@ - diff --git a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs index 66bfb3c0b..511f6f884 100644 --- a/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs +++ b/tests/InfiniTests.InfiniFrame/Window/Features/Debugging/RemoteDebuggingPortTests.cs @@ -100,9 +100,7 @@ public async Task AtBuilderStage_DirectAssignment_InvalidPort_ThrowsArgumentOutO // Act #pragma warning disable CA1416 var exception = await Assert.ThrowsAsync(() => - Task.Run(function: () => { - return builder.Features.Debugging.SetRemoteDebuggingPort(value); - }, ct)); + Task.Run(function: () => builder.Features.Debugging.SetRemoteDebuggingPort(value), ct)); #pragma warning restore CA1416 // Assert From 98ac74959ff51fb1ea322bc6ea7508c96aea83f0 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Wed, 12 Aug 2026 20:37:45 +0200 Subject: [PATCH 22/30] Detach GTK thread; remove ShutdownAndJoin Detach the gtkThread after starting it and remove the ShutdownAndJoin API. This makes the GTK UI thread run independently and eliminates the explicit join logic (function removed from both header and source). Adjusts initialization/shutdown flow to avoid blocking on thread join. --- .../src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp | 10 ++-------- .../src/Runtime/Platform/Linux/Core/UiThread.Gtk.h | 1 - 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 50b35de5b..56632bed8 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -94,6 +94,8 @@ namespace infiniframe::linux_gtk::ui_thread { mainLoop = nullptr; }); + gtkThread.detach(); + std::unique_lock lock(initializeMutex); initializeCompleted.wait(lock, [] { return initialized; }); }); @@ -109,14 +111,6 @@ namespace infiniframe::linux_gtk::ui_thread { } } - void ShutdownAndJoin() { - Shutdown(); - - if (gtkThread.joinable()) { - gtkThread.join(); - } - } - bool IsCurrentThread() { EnsureInitialized(); return std::this_thread::get_id() == ownerThreadId; diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h index 63e1bbdea..923aad1bc 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.h @@ -10,7 +10,6 @@ namespace infiniframe::linux_gtk::ui_thread { void EnsureInitialized(); void Shutdown(); - void ShutdownAndJoin(); bool IsCurrentThread(); bool InvokeAsync(std::function callback); bool InvokeIdle(std::function callback); From 22b932f8d6faef19e9b6054ee2aa21e24371f7b4 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 14:02:15 +0200 Subject: [PATCH 23/30] Upgrade dev deps and add native Shutdown import Bump multiple devDependencies and corresponding package-lock updates across examples, src and tests (vite, @types/node/react/dom, @vitejs/plugin-react/vue, eslint, lightningcss, rolldown and bindings, postcss, nanoid, terser, @analogjs/vite-plugin-angular, vue-tsc, etc.). Also add a LibraryImport for InfiniFrameNative_Shutdown in InfiniFrameNative.Lifecycle.cs to properly shut down the native UI thread on Linux and prevent GLib assertion failures. Lockfiles updated to reflect resolved versions. --- .../InfiniFrame.React/package-lock.json | 404 +++++++--------- .../Source/InfiniFrame.React/package.json | 16 +- .../Source/InfiniFrame.Vue/package-lock.json | 433 +++++++----------- .../Source/InfiniFrame.Vue/package.json | 6 +- src/InfiniFrame.Js/package-lock.json | 8 +- src/InfiniFrame.Js/package.json | 2 +- .../Exports/InfiniFrameNative.Lifecycle.cs | 9 + .../src/Api/Exports/Exports.Lifecycle.cpp | 11 + .../Platform/Linux/Core/UiThread.Gtk.cpp | 31 +- .../Platform/Linux/Core/WindowState.Gtk.cpp | 22 +- .../package-lock.json | 326 +------------ .../package.json | 2 +- .../package-lock.json | 16 +- .../package.json | 4 +- 14 files changed, 422 insertions(+), 868 deletions(-) diff --git a/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package-lock.json b/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package-lock.json index b2f8a6931..b3ea8d830 100644 --- a/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package-lock.json +++ b/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package-lock.json @@ -13,16 +13,16 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^26.1.1", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.4", - "eslint": "^10.8.0", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.7.0", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", "typescript": "^7.0.2", - "vite": "^8.1.5" + "vite": "^8.2.1" } }, "node_modules/@babel/code-frame": { @@ -265,40 +265,6 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -543,29 +509,10 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "dev": true, "license": "MIT", "funding": { @@ -573,9 +520,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -590,9 +537,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -607,9 +554,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -624,9 +571,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -641,9 +588,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], @@ -658,9 +605,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], @@ -678,9 +625,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ "arm64" ], @@ -698,9 +645,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], @@ -718,9 +665,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], @@ -738,9 +685,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], @@ -758,9 +705,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], @@ -778,9 +725,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -794,29 +741,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -831,9 +759,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -854,17 +782,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -887,9 +804,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -897,9 +814,9 @@ } }, "node_modules/@types/react": { - "version": "19.2.17", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", - "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "dev": true, "license": "MIT", "dependencies": { @@ -907,9 +824,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1257,9 +1174,9 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz", - "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { @@ -1346,9 +1263,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { @@ -1508,9 +1425,9 @@ } }, "node_modules/eslint": { - "version": "10.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz", - "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==", + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -1587,9 +1504,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", - "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.4.tgz", + "integrity": "sha512-7bqTKz7T0r+HKWFarNXByDE9/5+73wI2ru+M3zuqGbR7s/b/5/pQJXZoufWlrngqGqoZto73ZkGumCdLxk+4rw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1821,9 +1738,9 @@ } }, "node_modules/globals": { - "version": "17.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.7.0.tgz", - "integrity": "sha512-Czmyns5dUsq4seFBR/Kdydhmo8y9kC79hiSkPn0YcGtNnYWnrgt0vjrSjx9tspoDGWm2CMarffRuLjM4xUz8xg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { @@ -1979,9 +1896,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -1995,23 +1912,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -2030,9 +1947,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -2051,9 +1968,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -2072,9 +1989,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -2093,9 +2010,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -2114,9 +2031,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], @@ -2138,9 +2055,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], @@ -2162,9 +2079,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], @@ -2186,9 +2103,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], @@ -2210,9 +2127,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -2231,9 +2148,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -2301,9 +2218,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -2427,9 +2344,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -2447,7 +2364,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -2497,13 +2414,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2513,21 +2430,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/scheduler": { @@ -2596,14 +2512,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2701,16 +2609,16 @@ } }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "bin": { @@ -2727,7 +2635,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", diff --git a/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package.json b/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package.json index fc35ed3a4..5bb613ccd 100644 --- a/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package.json +++ b/examples/InfiniFrameExample.WebApp.React/Source/InfiniFrame.React/package.json @@ -15,15 +15,15 @@ }, "devDependencies": { "@eslint/js": "^10.0.1", - "@types/node": "^26.1.1", - "@types/react": "^19.2.17", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.4", - "eslint": "^10.8.0", + "@types/node": "^26.2.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", + "@vitejs/plugin-react": "^6.0.5", + "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.7.0", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", "typescript": "^7.0.2", - "vite": "^8.1.5" + "vite": "^8.2.1" } } diff --git a/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package-lock.json b/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package-lock.json index 5ad02b535..052c4a980 100644 --- a/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package-lock.json +++ b/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package-lock.json @@ -11,12 +11,12 @@ "vue": "^3.5.40" }, "devDependencies": { - "@types/node": "^26.1.1", + "@types/node": "^26.2.0", "@vitejs/plugin-vue": "^6.0.8", "@vue/tsconfig": "^0.9.1", "typescript": "^5.9.3", - "vite": "^8.1.5", - "vue-tsc": "^3.3.8" + "vite": "^8.2.1", + "vue-tsc": "^3.3.9" } }, "node_modules/@babel/helper-string-parser": { @@ -65,83 +65,16 @@ "node": ">=6.9.0" } }, - "node_modules/@emnapi/core": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", - "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.3", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/core/node_modules/@emnapi/wasi-threads": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", - "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", - "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", "dev": true, "license": "MIT", "funding": { @@ -149,9 +82,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", "cpu": [ "arm64" ], @@ -166,9 +99,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", "cpu": [ "arm64" ], @@ -183,9 +116,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", "cpu": [ "x64" ], @@ -200,9 +133,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", "cpu": [ "x64" ], @@ -217,9 +150,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", "cpu": [ "arm" ], @@ -234,13 +167,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -251,13 +187,16 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -268,13 +207,16 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", "cpu": [ "ppc64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -285,13 +227,16 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", "cpu": [ "s390x" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -302,13 +247,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -319,13 +267,16 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -336,9 +287,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", "cpu": [ "arm64" ], @@ -352,52 +303,10 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", "cpu": [ "arm64" ], @@ -412,9 +321,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", "cpu": [ "x64" ], @@ -435,21 +344,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", "dev": true, "license": "MIT", "dependencies": { @@ -553,9 +451,9 @@ } }, "node_modules/@vue/language-core": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.8.tgz", - "integrity": "sha512-ieGT8jJdhhy0mGzStZhsg/qPw5bQZJg5yF+3+XU6saf4sM7yo9ZXy3h+nCwrm2+b4qS/SypkNdR2jAF3uei9tA==", + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.9.tgz", + "integrity": "sha512-in/68oAa4BCtVY6n/nkuhLIkV8DHYd2UivedJ6cMZ6UYtlq9jaoaSNUBHYCVO44z3nKg7MdE5OBoHKt5SxeBKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -710,9 +608,9 @@ } }, "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -726,23 +624,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", "cpu": [ "arm64" ], @@ -761,9 +659,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", "cpu": [ "arm64" ], @@ -782,9 +680,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", "cpu": [ "x64" ], @@ -803,9 +701,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", "cpu": [ "x64" ], @@ -824,9 +722,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", "cpu": [ "arm" ], @@ -845,13 +743,16 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -866,13 +767,16 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -887,13 +791,16 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -908,13 +815,16 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -929,9 +839,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", "cpu": [ "arm64" ], @@ -950,9 +860,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", "cpu": [ "x64" ], @@ -987,9 +897,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -1031,9 +941,9 @@ } }, "node_modules/postcss": { - "version": "8.5.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", - "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -1050,7 +960,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -1059,13 +969,13 @@ } }, "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", + "@oxc-project/types": "=0.144.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -1075,21 +985,20 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" } }, "node_modules/source-map-js": { @@ -1118,14 +1027,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -1148,16 +1049,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", + "lightningcss": "^1.33.0", "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", "tinyglobby": "^0.2.17" }, "bin": { @@ -1174,7 +1075,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", + "@vitejs/devtools": "^0.4.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -1254,14 +1155,14 @@ } }, "node_modules/vue-tsc": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.8.tgz", - "integrity": "sha512-xXmYlVQpcwJDWyGlqbHrGVOl1h3UOsASymRibrHc+iy9j/UNnOrOn4u+fntHz4D6Cs74RtapeqVV6CzJeg+UlA==", + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.9.tgz", + "integrity": "sha512-TS3Y1ux/IRoE8OCP2PpACAeOseuIs0UvWrcr7u+w3PmfY+SlCfEf8zjrBgnQksHUgLpthi5vHlffcQTQTdPBZA==", "dev": true, "license": "MIT", "dependencies": { "@volar/typescript": "2.4.28", - "@vue/language-core": "3.3.8" + "@vue/language-core": "3.3.9" }, "bin": { "vue-tsc": "bin/vue-tsc.js" diff --git a/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package.json b/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package.json index bccc40706..56c370a43 100644 --- a/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package.json +++ b/examples/InfiniFrameExample.WebApp.Vue/Source/InfiniFrame.Vue/package.json @@ -12,12 +12,12 @@ "vue": "^3.5.40" }, "devDependencies": { - "@types/node": "^26.1.1", + "@types/node": "^26.2.0", "@vitejs/plugin-vue": "^6.0.8", "@vue/tsconfig": "^0.9.1", "typescript": "^5.9.3", - "vite": "^8.1.5", - "vue-tsc": "^3.3.8" + "vite": "^8.2.1", + "vue-tsc": "^3.3.9" }, "ncu": { "reject": [ diff --git a/src/InfiniFrame.Js/package-lock.json b/src/InfiniFrame.Js/package-lock.json index 31f41342f..491df5090 100644 --- a/src/InfiniFrame.Js/package-lock.json +++ b/src/InfiniFrame.Js/package-lock.json @@ -13,7 +13,7 @@ "@vitest/coverage-v8": "^4.1.10", "concurrently": "^10.0.4", "jsdom": "^30.0.1", - "terser": "^5.49.2", + "terser": "^5.50.0", "typescript": "^7.0.2", "vite": "^8.2.1", "vitest": "^4.1.10" @@ -2236,9 +2236,9 @@ "license": "MIT" }, "node_modules/terser": { - "version": "5.49.2", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.2.tgz", - "integrity": "sha512-rGbJiKeQ4WDe3EXlDAIaQcwftVfv2Q8o1awFNfvXolJYKkb1AuZY1RTOmqx4LJXZENbWZA7eIsYGHuEzHsi1nQ==", + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", "dev": true, "license": "BSD-2-Clause", "dependencies": { diff --git a/src/InfiniFrame.Js/package.json b/src/InfiniFrame.Js/package.json index 6c95ec4fe..acf43a8ac 100644 --- a/src/InfiniFrame.Js/package.json +++ b/src/InfiniFrame.Js/package.json @@ -18,7 +18,7 @@ "@vitest/coverage-v8": "^4.1.10", "concurrently": "^10.0.4", "jsdom": "^30.0.1", - "terser": "^5.49.2", + "terser": "^5.50.0", "typescript": "^7.0.2", "vite": "^8.2.1", "vitest": "^4.1.10" diff --git a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs index 0a2d185a5..f517f2999 100644 --- a/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs +++ b/src/InfiniFrame.NativeBridge/Managed/NativeApi/Exports/InfiniFrameNative.Lifecycle.cs @@ -74,4 +74,13 @@ out IntPtr value [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_SetTeardownCallback", SetLastError = true)] [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] internal static partial InfiniFrameNativeInteropStatus SetTeardownCallback(IntPtr instance, ContextAction callback, IntPtr context); + + /// + /// Shuts down the native UI thread and releases all global resources. + /// Must be called before process exit on Linux to prevent GLib assertion failures. + /// + /// A status code indicating success or failure. + [LibraryImport(ArtifactManifest.NativeLibraryName, EntryPoint = "InfiniFrameNative_Shutdown")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial InfiniFrameNativeInteropStatus Shutdown(); } \ No newline at end of file diff --git a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp index 00f738f90..c3f6e6110 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Api/Exports/Exports.Lifecycle.cpp @@ -2,6 +2,9 @@ // Imports // --------------------------------------------------------------------------------------------------------------------- #include "Api/Exports/Exports.h" +#ifdef __linux__ +#include "Runtime/Platform/Linux/Core/UiThread.Gtk.h" +#endif // --------------------------------------------------------------------------------------------------------------------- // Code // --------------------------------------------------------------------------------------------------------------------- @@ -63,4 +66,12 @@ EXPORTED InteropStatus InfiniFrameNative_SetTeardownCallback( window->SetTeardownCallback(callback, context); }); } + +#ifdef __linux__ +EXPORTED InteropStatus InfiniFrameNative_Shutdown() { + return RunExportStatus([] { + infiniframe::linux_gtk::ui_thread::Shutdown(); + }); +} +#endif } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 56632bed8..339e14e19 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -29,6 +29,7 @@ namespace { GMainContext* ownerContext = nullptr; std::thread gtkThread; GMainLoop* mainLoop = nullptr; + bool gtkThreadFinished = false; struct InvokeState { std::function callback; @@ -92,9 +93,14 @@ namespace infiniframe::linux_gtk::ui_thread { g_main_loop_run(mainLoop); g_main_loop_unref(mainLoop); mainLoop = nullptr; - }); - gtkThread.detach(); + notify_uninit(); + + { + std::lock_guard lock(initializeMutex); + gtkThreadFinished = true; + } + }); std::unique_lock lock(initializeMutex); initializeCompleted.wait(lock, [] { return initialized; }); @@ -109,6 +115,10 @@ namespace infiniframe::linux_gtk::ui_thread { if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { g_main_loop_quit(mainLoop); } + + if (gtkThread.joinable()) { + gtkThread.join(); + } } bool IsCurrentThread() { @@ -196,7 +206,20 @@ namespace infiniframe::linux_gtk::ui_thread { __attribute__((destructor(101))) static void InfiniFrame_UiThread_Shutdown() { // During static destruction GLib/GDK objects are already half-torn-down. Attempting - // g_main_loop_quit() or thread join here triggers cascading assertion failures + // g_main_loop_quit() or thread join here can trigger cascading assertion failures // (gdk_seat_get_keyboard, g_dbus_connection_call_sync_internal) and SIGABRT. - // The OS reclaims all resources on process exit, so it is safe to do nothing here. + // + // However, if the GTK thread was never explicitly shut down (e.g. the process exited + // without calling Shutdown()), we must attempt to stop it. The thread is sitting in + // g_main_loop_run() and will use GLib objects during teardown, so we need to quit the + // loop before GLib's own static destructors run. + // + // The guard ensures we only attempt this once and never block on a join if the thread + // is stuck in a GLib call that's already been torn down. + if (!initialized || gtkThreadFinished) + return; + + if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { + g_main_loop_quit(mainLoop); + } } diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp index 18563f44d..ba4f27da3 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/WindowState.Gtk.cpp @@ -27,6 +27,10 @@ void InfiniFrameWindow::GetStatusBarEnabled(bool* enabled) const { } void InfiniFrameWindow::GetDevToolsEnabled(bool* enabled) const { + if (m_impl->_webview == nullptr) { + *enabled = m_impl->_devToolsEnabled; + return; + } WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_impl->_webview)); *enabled = webkit_settings_get_enable_developer_extras(settings); } @@ -101,6 +105,10 @@ void InfiniFrameWindow::GetTopmost(bool* topmost) const { } void InfiniFrameWindow::GetZoom(int* zoom) const { + if (m_impl->_webview == nullptr) { + *zoom = m_impl->_zoom; + return; + } double rawValue = webkit_web_view_get_zoom_level(WEBKIT_WEB_VIEW(m_impl->_webview)); rawValue = (rawValue * 100.0) + 0.5; *zoom = static_cast(rawValue); @@ -274,6 +282,8 @@ void InfiniFrameWindow::SetBrowserShortcutsEnabled(const bool enabled) { void InfiniFrameWindow::SetDevToolsEnabled(const bool enabled) { m_impl->_devToolsEnabled = enabled; + if (m_impl->_webview == nullptr) + return; WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(m_impl->_webview)); webkit_settings_set_enable_developer_extras(settings, m_impl->_devToolsEnabled || m_impl->_remoteDebuggingPort > 0); } @@ -375,6 +385,8 @@ void InfiniFrameWindow::SetZoom(const int zoom) { return; m_impl->_zoom = zoom; + if (m_impl->_webview == nullptr) + return; double newZoom = zoom / 100.0; webkit_web_view_set_zoom_level(WEBKIT_WEB_VIEW(m_impl->_webview), newZoom); } @@ -394,10 +406,12 @@ void InfiniFrameWindow::SetTransparentEnabled(const bool enabled) { gtk_widget_set_visual(GTK_WIDGET(m_impl->_window), rgba_visual); gtk_widget_set_app_paintable(GTK_WIDGET(m_impl->_window), true); - GdkRGBA color; - webkit_web_view_get_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); - color.alpha = enabled ? 0 : 1; - webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); + if (m_impl->_webview != nullptr) { + GdkRGBA color; + webkit_web_view_get_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); + color.alpha = enabled ? 0 : 1; + webkit_web_view_set_background_color(WEBKIT_WEB_VIEW(m_impl->_webview), &color); + } } } diff --git a/tests/InfiniAutomationTests.WebApp.Angular/package-lock.json b/tests/InfiniAutomationTests.WebApp.Angular/package-lock.json index 08d235b31..01ea7b77b 100644 --- a/tests/InfiniAutomationTests.WebApp.Angular/package-lock.json +++ b/tests/InfiniAutomationTests.WebApp.Angular/package-lock.json @@ -16,7 +16,7 @@ "zone.js": "^0.16.0" }, "devDependencies": { - "@analogjs/vite-plugin-angular": "^2.6.4", + "@analogjs/vite-plugin-angular": "^2.7.0", "@angular/build": "^22.1.3", "@angular/compiler-cli": "^22.1.1", "typescript": "^6.0.3", @@ -38,17 +38,16 @@ } }, "node_modules/@analogjs/vite-plugin-angular": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/@analogjs/vite-plugin-angular/-/vite-plugin-angular-2.6.4.tgz", - "integrity": "sha512-HK9XEhFMcF8WkSg2xKQIpC4S4nwkCRzodpXX6xo6AI7Wt6B2SpMyQBFtCHNRuQg0vi8XsJSYOPHiGouTqMJb5g==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@analogjs/vite-plugin-angular/-/vite-plugin-angular-2.7.0.tgz", + "integrity": "sha512-KhFC9wiIxD4vi0p3Iyd6tPXyvV9J8GF1eLTkANtBcUbuLkdCRFtbqp/U08cDt+Ae+mVOiSZliBO+JSr66YCqFA==", "dev": true, "license": "MIT", "dependencies": { "magic-string": "^0.30.21", "obug": "^2.1.1", "oxc-parser": "^0.121.0", - "tinyglobby": "^0.2.14", - "ts-morph": "^21.0.0" + "tinyglobby": "^0.2.14" }, "funding": { "type": "github", @@ -2911,44 +2910,6 @@ "@emnapi/runtime": "^2.0.0-alpha.3" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@oxc-parser/binding-android-arm-eabi": { "version": "0.121.0", "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.121.0.tgz", @@ -3929,19 +3890,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@ts-morph/common": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.22.0.tgz", - "integrity": "sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-glob": "^3.3.2", - "minimatch": "^9.0.3", - "mkdirp": "^3.0.1", - "path-browserify": "^1.0.1" - } - }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -4067,16 +4015,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.11.6", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.6.tgz", @@ -4118,32 +4056,6 @@ "dev": true, "license": "ISC" }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", @@ -4316,13 +4228,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/code-block-writer": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", - "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", - "dev": true, - "license": "MIT" - }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -4563,23 +4468,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -4624,16 +4512,6 @@ "fast-string-width": "^3.0.2" } }, - "node_modules/fastq": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", - "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -4652,19 +4530,6 @@ } } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4713,19 +4578,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4805,6 +4657,7 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=0.10.0" } @@ -4831,6 +4684,7 @@ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -4838,16 +4692,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -5290,43 +5134,6 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/mimic-function": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", @@ -5340,38 +5147,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mkdirp": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -5642,13 +5417,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5763,27 +5531,6 @@ } } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/readdirp": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", @@ -5832,17 +5579,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -5894,30 +5630,6 @@ "url": "https://github.com/sponsors/Boshen" } }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -6082,30 +5794,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/ts-morph": { - "version": "21.0.1", - "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-21.0.1.tgz", - "integrity": "sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@ts-morph/common": "~0.22.0", - "code-block-writer": "^12.0.0" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", diff --git a/tests/InfiniAutomationTests.WebApp.Angular/package.json b/tests/InfiniAutomationTests.WebApp.Angular/package.json index c2d029eb0..a4c8acb00 100644 --- a/tests/InfiniAutomationTests.WebApp.Angular/package.json +++ b/tests/InfiniAutomationTests.WebApp.Angular/package.json @@ -17,7 +17,7 @@ "zone.js": "^0.16.0" }, "devDependencies": { - "@analogjs/vite-plugin-angular": "^2.6.4", + "@analogjs/vite-plugin-angular": "^2.7.0", "@angular/build": "^22.1.3", "@angular/compiler-cli": "^22.1.1", "typescript": "^6.0.3", diff --git a/tests/InfiniAutomationTests.WebApp.React/package-lock.json b/tests/InfiniAutomationTests.WebApp.React/package-lock.json index 43efd92ae..40e7d448f 100644 --- a/tests/InfiniAutomationTests.WebApp.React/package-lock.json +++ b/tests/InfiniAutomationTests.WebApp.React/package-lock.json @@ -19,8 +19,8 @@ "@vitejs/plugin-react": "^6.0.5", "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.9.0", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", "typescript": "^7.0.2", "vite": "^8.2.1" } @@ -1490,9 +1490,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.3.tgz", - "integrity": "sha512-5EMmLCV98Pi4o/f/3DP/v/tNqLHMIc9I8LKClNDWhZ9JTho89/kQcitCXQBMG7sAfVRK0Ie3T2EDOzp1YXYiVA==", + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.4.tgz", + "integrity": "sha512-7bqTKz7T0r+HKWFarNXByDE9/5+73wI2ru+M3zuqGbR7s/b/5/pQJXZoufWlrngqGqoZto73ZkGumCdLxk+4rw==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1724,9 +1724,9 @@ } }, "node_modules/globals": { - "version": "17.9.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.9.0.tgz", - "integrity": "sha512-m/MvAW61QVU5VDNF1Vj8axt016h8w7L5TU1e9zlab7XIttAT2YAlCwl75K1fOqvMM9apmD7lbCIRhpfkhmxhCg==", + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", "dev": true, "license": "MIT", "engines": { diff --git a/tests/InfiniAutomationTests.WebApp.React/package.json b/tests/InfiniAutomationTests.WebApp.React/package.json index 776fe7587..b45653f3b 100644 --- a/tests/InfiniAutomationTests.WebApp.React/package.json +++ b/tests/InfiniAutomationTests.WebApp.React/package.json @@ -20,8 +20,8 @@ "@vitejs/plugin-react": "^6.0.5", "eslint": "^10.8.1", "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.3", - "globals": "^17.9.0", + "eslint-plugin-react-refresh": "^0.5.4", + "globals": "^17.11.0", "typescript": "^7.0.2", "vite": "^8.2.1" }, From 560c6018e667388f4dc9575463633dfe6c963d11 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 14:43:00 +0200 Subject: [PATCH 24/30] Update UiThread.Gtk.cpp --- .../Platform/Linux/Core/UiThread.Gtk.cpp | 55 ++++++++++++------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 339e14e19..0204d48a7 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -24,12 +24,13 @@ namespace { std::once_flag initializeOnce; std::mutex initializeMutex; std::condition_variable initializeCompleted; - bool initialized = false; + std::atomic initialized{false}; std::thread::id ownerThreadId = {}; GMainContext* ownerContext = nullptr; std::thread gtkThread; GMainLoop* mainLoop = nullptr; - bool gtkThreadFinished = false; + std::atomic gtkThreadFinished{false}; + bool gtkThreadShutdownTaken = false; struct InvokeState { std::function callback; @@ -85,7 +86,7 @@ namespace infiniframe::linux_gtk::ui_thread { std::lock_guard lock(initializeMutex); ownerThreadId = std::this_thread::get_id(); ownerContext = g_main_context_default(); - initialized = true; + initialized.store(true, std::memory_order_release); initializeCompleted.notify_all(); } @@ -96,22 +97,26 @@ namespace infiniframe::linux_gtk::ui_thread { notify_uninit(); - { - std::lock_guard lock(initializeMutex); - gtkThreadFinished = true; - } + gtkThreadFinished.store(true, std::memory_order_release); }); std::unique_lock lock(initializeMutex); - initializeCompleted.wait(lock, [] { return initialized; }); + initializeCompleted.wait(lock, [] { return initialized.load(std::memory_order_acquire); }); }); } void Shutdown() { - if (!initialized) { + if (!initialized.load(std::memory_order_acquire)) { return; } + { + std::lock_guard lock(initializeMutex); + if (gtkThreadShutdownTaken) + return; + gtkThreadShutdownTaken = true; + } + if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { g_main_loop_quit(mainLoop); } @@ -119,6 +124,8 @@ namespace infiniframe::linux_gtk::ui_thread { if (gtkThread.joinable()) { gtkThread.join(); } + + gtkThreadFinished.store(true, std::memory_order_release); } bool IsCurrentThread() { @@ -205,21 +212,27 @@ namespace infiniframe::linux_gtk::ui_thread { __attribute__((destructor(101))) static void InfiniFrame_UiThread_Shutdown() { - // During static destruction GLib/GDK objects are already half-torn-down. Attempting - // g_main_loop_quit() or thread join here can trigger cascading assertion failures - // (gdk_seat_get_keyboard, g_dbus_connection_call_sync_internal) and SIGABRT. - // - // However, if the GTK thread was never explicitly shut down (e.g. the process exited - // without calling Shutdown()), we must attempt to stop it. The thread is sitting in - // g_main_loop_run() and will use GLib objects during teardown, so we need to quit the - // loop before GLib's own static destructors run. - // - // The guard ensures we only attempt this once and never block on a join if the thread - // is stuck in a GLib call that's already been torn down. - if (!initialized || gtkThreadFinished) + if (!initialized.load(std::memory_order_acquire)) return; + { + std::lock_guard lock(initializeMutex); + if (gtkThreadShutdownTaken) + return; + gtkThreadShutdownTaken = true; + } + + if (gtkThreadFinished.load(std::memory_order_acquire) || !gtkThread.joinable()) + return; + + // Quit the main loop so the GTK thread exits g_main_loop_run(). if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { g_main_loop_quit(mainLoop); } + + // Detach the thread rather than joining. During static destruction, GLib/GDK objects + // may already be half-torn-down and the thread could be stuck in a GLib call. + // Joining here risks deadlock or SIGABRT from cascading assertion failures. + // The OS reclaims thread resources on process exit. + gtkThread.detach(); } From 3bfa88f6345d79b9f094431d09cbd1640e6d7432 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 15:18:26 +0200 Subject: [PATCH 25/30] Update UiThread.Gtk.cpp --- .../Platform/Linux/Core/UiThread.Gtk.cpp | 70 +++++++------------ 1 file changed, 24 insertions(+), 46 deletions(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index 0204d48a7..c5333b71c 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -24,13 +25,11 @@ namespace { std::once_flag initializeOnce; std::mutex initializeMutex; std::condition_variable initializeCompleted; - std::atomic initialized{false}; + bool initialized = false; std::thread::id ownerThreadId = {}; GMainContext* ownerContext = nullptr; std::thread gtkThread; GMainLoop* mainLoop = nullptr; - std::atomic gtkThreadFinished{false}; - bool gtkThreadShutdownTaken = false; struct InvokeState { std::function callback; @@ -71,11 +70,29 @@ namespace { void ReleaseInvokeState(const gpointer userData) { delete static_cast*>(userData); } + + void AtexitShutdown() { + if (!initialized) + return; + if (!gtkThread.joinable()) + return; + + if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { + g_main_loop_quit(mainLoop); + } + + // Detach rather than join. During process exit GLib/GDK objects may already be + // half-torn-down and the thread could be stuck in a GLib call. Joining here risks + // deadlock or SIGABRT. The OS reclaims all thread resources on process exit. + gtkThread.detach(); + } } namespace infiniframe::linux_gtk::ui_thread { void EnsureInitialized() { std::call_once(initializeOnce, [] { + std::atexit(AtexitShutdown); + gtkThread = std::thread([] { infiniframe::linux_gtk::ConfigureGraphicsEnvironment(); XInitThreads(); @@ -86,7 +103,7 @@ namespace infiniframe::linux_gtk::ui_thread { std::lock_guard lock(initializeMutex); ownerThreadId = std::this_thread::get_id(); ownerContext = g_main_context_default(); - initialized.store(true, std::memory_order_release); + initialized = true; initializeCompleted.notify_all(); } @@ -96,26 +113,16 @@ namespace infiniframe::linux_gtk::ui_thread { mainLoop = nullptr; notify_uninit(); - - gtkThreadFinished.store(true, std::memory_order_release); }); std::unique_lock lock(initializeMutex); - initializeCompleted.wait(lock, [] { return initialized.load(std::memory_order_acquire); }); + initializeCompleted.wait(lock, [] { return initialized; }); }); } void Shutdown() { - if (!initialized.load(std::memory_order_acquire)) { + if (!initialized) return; - } - - { - std::lock_guard lock(initializeMutex); - if (gtkThreadShutdownTaken) - return; - gtkThreadShutdownTaken = true; - } if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { g_main_loop_quit(mainLoop); @@ -124,8 +131,6 @@ namespace infiniframe::linux_gtk::ui_thread { if (gtkThread.joinable()) { gtkThread.join(); } - - gtkThreadFinished.store(true, std::memory_order_release); } bool IsCurrentThread() { @@ -135,7 +140,7 @@ namespace infiniframe::linux_gtk::ui_thread { namespace { gboolean ExecuteAsync(const gpointer userData) { - std::unique_ptr> callback(static_cast*>(userData)); + std::unique_ptr> callback(static_cast(userData)); try { (*callback)(); } catch (...) { @@ -209,30 +214,3 @@ namespace infiniframe::linux_gtk::ui_thread { } } } - -__attribute__((destructor(101))) -static void InfiniFrame_UiThread_Shutdown() { - if (!initialized.load(std::memory_order_acquire)) - return; - - { - std::lock_guard lock(initializeMutex); - if (gtkThreadShutdownTaken) - return; - gtkThreadShutdownTaken = true; - } - - if (gtkThreadFinished.load(std::memory_order_acquire) || !gtkThread.joinable()) - return; - - // Quit the main loop so the GTK thread exits g_main_loop_run(). - if (mainLoop != nullptr && g_main_loop_is_running(mainLoop)) { - g_main_loop_quit(mainLoop); - } - - // Detach the thread rather than joining. During static destruction, GLib/GDK objects - // may already be half-torn-down and the thread could be stuck in a GLib call. - // Joining here risks deadlock or SIGABRT from cascading assertion failures. - // The OS reclaims thread resources on process exit. - gtkThread.detach(); -} From a7d83647889e190495017e04327b1ea8ae5f8211 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 15:18:58 +0200 Subject: [PATCH 26/30] Potential fix for pull request finding 'CodeQL / Reference equality test on System.Object' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs index fa4f14edd..becd022e1 100644 --- a/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs +++ b/src/InfiniFrame/StaticAssets/DisposableCompositeFileProvider.cs @@ -23,7 +23,7 @@ public IChangeToken Watch(string filter) public void Dispose() { physicalProvider.Dispose(); foreach (IFileProvider provider in providers) { - if (provider != physicalProvider && provider is IDisposable disposable) { + if (!ReferenceEquals(provider, physicalProvider) && provider is IDisposable disposable) { disposable.Dispose(); } } From 88672d600d15008c5d8fe93b84c1e8f04d63d149 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 15:19:06 +0200 Subject: [PATCH 27/30] Potential fix for pull request finding 'CodeQL / Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../Parameters/InfiniFrameNativeParametersValidator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index f41d0ff39..6752f8c8f 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -113,7 +113,13 @@ private static bool CanAccessTemporaryFilesPath(string? path) { try { Directory.CreateDirectory(path); } - catch { + catch (Exception ex) when ( + ex is IOException + or UnauthorizedAccessException + or ArgumentException + or NotSupportedException + or PathTooLongException + ) { return false; } } From 66f564523f7fd58b6711bc414029fcc83401342a Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 15:19:14 +0200 Subject: [PATCH 28/30] Potential fix for pull request finding 'CodeQL / Generic catch clause' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- .../Parameters/InfiniFrameNativeParametersValidator.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs index 6752f8c8f..73f79ddb4 100644 --- a/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs +++ b/src/InfiniFrame.NativeBridge/Managed/Parameters/InfiniFrameNativeParametersValidator.cs @@ -163,7 +163,13 @@ public static bool EnsureTemporaryFilesPath(string? path) { } return CanAccessTemporaryFilesPath(path); } - catch { + catch (Exception ex) when ( + ex is IOException + or UnauthorizedAccessException + or ArgumentException + or NotSupportedException + or PathTooLongException + ) { return false; } } From 6333573339c3ce3b06c621ae5dfd70479c865f3e Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 15:19:32 +0200 Subject: [PATCH 29/30] Potential fix for pull request finding 'CodeQL / Missed 'using' opportunity' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- src/InfiniFrame/Window/InfiniFrameWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InfiniFrame/Window/InfiniFrameWindow.cs b/src/InfiniFrame/Window/InfiniFrameWindow.cs index 399a222f6..ea779700c 100644 --- a/src/InfiniFrame/Window/InfiniFrameWindow.cs +++ b/src/InfiniFrame/Window/InfiniFrameWindow.cs @@ -328,7 +328,7 @@ public async ValueTask DisposeAsync() { Features.Lifecycle.CleanupNativeHandle(); if (_ownsServiceProvider && ServiceProvider is IDisposable disposableProvider) { - disposableProvider.Dispose(); + using var _ = disposableProvider; } } } From b2e65bc67170683a6856e7a53a3945ecc5b23ac0 Mon Sep 17 00:00:00 2001 From: Anna Sas Date: Thu, 13 Aug 2026 15:48:17 +0200 Subject: [PATCH 30/30] Update UiThread.Gtk.cpp --- .../Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp index c5333b71c..63413fd6d 100644 --- a/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp +++ b/src/InfiniFrame.NativeBridge/Native/src/Runtime/Platform/Linux/Core/UiThread.Gtk.cpp @@ -140,7 +140,7 @@ namespace infiniframe::linux_gtk::ui_thread { namespace { gboolean ExecuteAsync(const gpointer userData) { - std::unique_ptr> callback(static_cast(userData)); + std::unique_ptr> callback(static_cast*>(userData)); try { (*callback)(); } catch (...) {