Skip to content

Feat: Initial work on application-level native lifecycle management - #418

Open
AnnaSasDev wants to merge 29 commits into
corefrom
406-feature-a-full-natively-supported-multiwindow-setup
Open

Feat: Initial work on application-level native lifecycle management#418
AnnaSasDev wants to merge 29 commits into
corefrom
406-feature-a-full-natively-supported-multiwindow-setup

Conversation

@AnnaSasDev

@AnnaSasDevAnnaSasDev commented Sep 1, 2026

Copy link
Copy Markdown
Member

No description provided.

This change introduces a native application abstraction across the managed and native layers, including ApplicationInitParameters, application ctor/dtor/run/shutdown exports, and platform-specific registration/message-loop implementations. Windows, macOS, and Linux window creation now accepts an application handle, tracks windows against the owning app, and shuts down via the app-managed loop. The app is exposed as a shared IInfiniFrameApplication service, and legacy platform registration methods are marked obsolete.
@AnnaSasDevAnnaSasDev linked an issue Sep 1, 2026 that may be closed by this pull request
Introduce a managed NativeApplicationHandle(IntPtr) and wire up InfiniFrameApplication implementation across platforms. Move per-platform Impl into shared InfiniFrameApplicationImpl.h, add application sources to CMake, and deprecate legacy exports/Register methods. Fix Windows RegisterClassEx error handling and adjust WinToast app-id selection logic. Update C# InfiniFrameApplication dispose checks and use the new NativeApplicationHandle constructor. Tests updated to include an IInfiniFrameApplication mock and MockFactory gains CreateApplicationMock. Also reformat a .DotSettings resource file.
Remove deprecated platform registration exports (register_mac, register_win32) and the legacy Shutdown export. Require an InfiniFrameApplication for window creation across platforms (throws on null ApplicationHandle) and add argument/size validation for init params. Remove legacy Win32 global hInstance/message-loop path and simplify lifecycle/wait logic to rely on application-owned loops. Adjust macOS window setup (remove Register). Improve Windows notification AppUserModelId selection to prefer explicit application ID. Switch managed string marshalling to UTF-8 with a MarshalStringUtf8 helper for application init parameters.
This change adds explicit application-owned window tracking and lifecycle management. IInfiniFrameApplication now exposes window counts/events and supports CloseAll, TrackWindow, and UntrackWindow, with the implementation keeping a thread-safe registry of active windows.
It also fixes service registration and initialization ordering by configuring AddInfiniFrame with optional ApplicationConfiguration, ensuring the app singleton is initialized before window creation, and registering InfiniFrameWindow where needed. On Windows, the native runtime now exposes whether the app message loop is active and pumps messages while waiting for shutdown when no app loop is running.
The legacy ApplicationBuilder was removed, and new tests cover window tracking, event firing, and close-all behavior.
Remove the IsMessageLoopRunning API and implementation. Refactor InfiniFrameWindow::WaitForExit to always use a timed condition_variable wait (50ms) and a local message pump that dispatches thread/window messages, reposts WM_QUIT for other loops, and avoids MsgWaitForMultipleObjects. Clean up now-unused includes. This simplifies lifecycle waiting logic and ensures WM_USER_INVOKE and WM_QUIT are handled reliably across threads.
This commit introduces application-level configurations for settings such as `WebView2RuntimePath` and `WindowsAppUserModelId`, migrating them out of window-specific configurations. It replaces deprecated APIs with updated extension methods like `WithBlazor` and `WithWebServer`. Lifecycle events and DI service registrations are enhanced for better chaining, while obsolete methods and properties are marked accordingly. Additionally, native implementations are updated to streamline initialization and cleanup processes, simplifying platform-specific logic.
…jects
This commit moves Blazor and WebServer integration extensions from the core InfiniFrame project into new dedicated projects (`InfiniFrame.BlazorWebView` and `InfiniFrame.WebServer`). It updates the APIs, deprecates outdated methods, adjusts test cases, and improves application lifecycle and DI handling.
Prevent reinitialization of InfiniFrameApplication by returning early if already initialized (first config wins). Register logging in InfiniFrameWindowBuilder's service collection. Update WindowsAppUserModelId tests to reflect that the value is stored on the builder/application feature (assert feature value and initialize the application with the AppUserModelId in tests) and remove reliance on native window parameters.
Updated the JS package and Vite configs for ESM compatibility by adding `type: "module"` and replacing `__dirname` with `import.meta.dirname`. This ensures the Vite build resolves the entry file correctly under Node's ESM runtime. Also cleaned up the Windows loader library XML comment formatting in the native bridge manifest.
Introduce a shared IServiceCollection/ServiceProvider for the application so windows and Blazor apps resolve from the same DI container. Blazor builder services are merged into the application's ServiceCollection and Blazor build accepts an existing IServiceProvider. InfiniFrameApplication now exposes internal ServiceCollection and lazily builds ServiceProvider in Run(), merging window builder services while preserving the IInfiniFrameApplication singleton. InfiniFrameWindowBuilder registers IInfiniFrameWindow transient. Also added some DI/using adjustments and TryAddSingleton for unhandled exception source.
Prevent premature native C++ object destruction during teardown by adding an explicit safe-to-destroy signal and guarded destructor invocation.
- Add IInfiniFrameWindow.MarkNativeHandleSafeToDestroy and implement it on InfiniFrameWindow to mark the NativeWindowHandle safe.
- NativeWindowHandle now defers calling InfiniFrameNative.Destructor until MarkSafeToDestroy is set and ensures destructor runs only once using Interlocked. ReleaseHandle calls TryDestroy but always returns true to avoid finalizer retries.
- LifecycleInfiniFrameWindowFeature: finalizer (disposing == false) no longer releases native handle; explicit Dispose waits for teardown work to complete before releasing the native handle and now signals the handle safe to destroy when teardown completes. This avoids use-after-free of C++ objects accessed by thread-pool teardown work items.
- Update tests to implement the new interface method.
This change ensures core InfiniFrame services are registered even when Initialize() was skipped before Build(), and it marks native handles as safe to destroy before releasing them during teardown/disposal. The page navigation test was also adjusted to assert URL presence consistently instead of requiring exact equality when no URL exists.
This change avoids a circular dependency in InfiniFrameWindowBuilder by creating the window directly through ActivatorUtilities instead of resolving it from the service provider. It also broadens Playwright startup exception capture, adds explicit timeouts to browser-related tests, fixes a typo in test attributes, and updates blank-page URL assertions for fresh windows.
Two Blazor WebView test cases were timing out under slower CI conditions. This change adds explicit NUnit timeouts to the custom element registration and app teardown tests to prevent hangs and make failures fail faster.
- Replace `_nativeDestructionScheduled` with `std::atomic` for thread safety.
- Add `_deletionQueued` atomic flag to avoid duplicate deletions.
- Refactor deferred destruction logic using `compare_exchange_strong` for guaranteed atomicity.
- Improve error handling for menu JSON parsing on Linux platforms.
@AnnaSasDevAnnaSasDev changed the title Feat: Add application-level native lifecycle managementFeat: Initial work on application-level native lifecycle managementSep 5, 2026
- Add `_destroying` atomic flag to prevent use-after-free in managed callbacks during teardown.
- Update destructor and callbacks to check `_destroying` for safe object handling.
- Adjust flaky GetCurrentUrlTests to stabilize assertions across slower runners with retries.
macOS crash fix:
ScheduleTeardownCompletion() used CFRunLoopPerformBlock to defer
SignalTeardown(), but ScheduleDeferredDestruction() dispatches
'delete this' via dispatch_async to the main queue. On macOS, GCD
dispatch sources fire BEFORE CFRunLoopPerformBlock blocks in the
same run-loop iteration, so 'delete this' ran first, destroying
m_impl and its mutexes, then SignalTeardown() tried to lock the
already-destroyed _milestoneMutex -> EINVAL -> SIGABRT/SIGSEGV.
Fix: Use dispatch_async (not CFRunLoopPerformBlock) for
SignalTeardown(). Both SignalTeardown and 'delete this' are now on
the same serial main queue. Dispatch SignalTeardown FIRST (before
the Complete*ForClose() calls that may trigger managed disposal),
so FIFO ordering guarantees it runs before 'delete this'.
Flaky test fix:
CustomElement_Registers_Renders_AndUpdatesFromAttributes had the
10s assembly default timeout but does 3 sequential Playwright
polling phases. Added [Timeout(30_000)] matching its sibling test.
Previous approaches (CFRunLoopPerformBlock, dispatch_async) failed
because delete this from ScheduleDeferredDestruction races with the
deferred SignalTeardown block on the main queue.
The fix: call SignalTeardown() synchronously AFTER InvokeClosed()
returns. At that point m_impl is still alive because delete this is
always deferred via dispatch_async. SignalTeardown locks
_milestoneMutex safely, then the function returns and the run loop
processes the deferred delete this.
The REAL root cause of the macOS mutex crash was in ScheduleOperation()
which used CFRunLoopPerformBlock to schedule NativeOperation::Execute()
blocks. On macOS, GCD dispatch sources (used by ScheduleDeferredDestruction
→ dispatch_async(delete this)) are drained BEFORE CFRunLoopPerformBlock
blocks in the same run-loop iteration. This meant delete this could
destroy m_impl (and all its mutexes) BEFORE a pending Execute() block
ran, causing use-after-free when Execute() tried to lock _operationMutex.
Fix: Use dispatch_async (not CFRunLoopPerformBlock) in ScheduleOperation().
Both operation execution and delete this are now on the same serial main
queue with FIFO ordering. Operations enqueued before delete this always
execute first while m_impl is still alive.
Also reverted the previous ScheduleTeardownCompletion/CompleteCloseAfterWebKitTeardown
changes since they were addressing a secondary issue, not the root cause.
InstallDiagnostics() was never called, so INFINIFRAME_NATIVE_DIAGNOSTICS
had no effect and no stack traces were printed on crash. Install it in
InfiniFrameNative_ctor so the signal handler is active from the first
window creation. This will give us actual stack traces to pinpoint the
exact crash location.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@AnnaSasDev