Repository navigation
Feat: support external host providers with document-isolated Blazor pages - #444
JoarEliasson wants to merge 11 commits into
Conversation
Adds a small, engine-neutral public contract in InfiniFrame.Hosting with which an application selects an external host provider before Build(). The provider owns the native windows and the browser; InfiniFrame keeps the IInfiniFrameWindow facade, the window lifecycle, its own dispatcher, document-aware Blazor delivery and resource dispatch. - InfiniFrame.Shared: IInfiniFrameHostProvider, IInfiniFrameHost, IInfiniFrameWindowHost and its optional operations, IInfiniFrameWindowHostEvents, request, capability and value types, and InfiniFrameHostContract (version 1.0). - InfiniFrame.Window: the internal hosted window facade, its FIFO dispatcher and runtime. - InfiniFrame.Application: UseHostProvider and the hosted run and disposal sequence. - InfiniFrame.BlazorWebView and InfiniFrame.Js: Blazor integration and the bridge script. For hosted windows, Blazor output is bound to the browser document whose AttachPage created the current page context. The binding is recorded on Blazor's dispatcher, in order with work already queued there, so output still pending for a replaced page stays with that page's document. Resource requests keep the status code of the static content provider (for example 404) instead of collapsing to a generic failure. Two internal interfaces connect the window and the WebView manager for this: IInfiniFrameDocumentWindow and IInfiniFrameStatusResourceSource. Default backends are unchanged.
Adds InfiniTests.InfiniFrame.Hosting: a deterministic scripted provider whose actions run on their own thread, and contract tests for application start-up and shutdown, window lifecycle and close, dispatch, documents and messages, resources, capabilities, Blazor delivery, and lifecycle and threading edge cases. No real browser is involved. Also exposes the number of active native callback roots internally, so that tests can check that every window releases its root.
Microsoft's WebViewManager shares one IPC sender between all page contexts it creates and routes inbound messages to its newest page context. After a full reload, a replaced page could therefore still run JavaScript in the next document, navigate it, or complete its replies. For hosted windows only, a coordinator now gives every browser document its own framework WebViewManager, created by that document's first AttachPage. Its output is bound to that document for good, only that document's messages reach it, and its disposal is started exactly once when the document retires. A retired page's new JS interop calls fail with JSDisconnectedException, as on a disconnected Blazor Server circuit. Calls that were already pending are neither completed nor cancelled; they end with their caller's token or timeout. At close, every hosted page scope is now released, including the last page's. Public signatures are unchanged. In hosted mode, root components must be added through IInfiniFrameWebViewManager: the inherited WebViewManager members act on the outer manager, which hosts no page. Default windows are unchanged.
…tcomes A retiring page's disposal is awaited for 10 s. A disposal still running then is not cancelled and no longer awaited, and the page scope is released, with its own bound. How such a disposal ends, if it ends, is now counted (late completion, fault or cancellation) and logged. A TimeoutException thrown by the disposal itself counts as a failure, not as the bound passing, and shutdown timeouts are counted separately. The tests suspend a component's disposal across a shortened bound, also with shutdown in that interval. They record that application shutdown does not wait for Blazor page disposal: the window-closed handler starts it without awaiting it.
…lient is disposed The page-scoped HttpClient wrapped the application's singleton InfiniFrameHttpHandler with the default disposeHandler: true. Disposing the first page scope therefore disposed the shared handler, and every later page's non-app:// requests failed with ObjectDisposedException. The client now leaves the handler to the application's root provider, which disposes it once.
…pture SetPointerCaptureAsync and ReleasePointerCaptureAsync now catch JSDisconnectedException alone and log it at Debug level: a page that is gone has nothing to capture or release. Genuine JS errors on a live page are still logged as errors, and other exceptions still propagate.
|
Will look at this during the week. Currently blocked by #445 as there seems to be an issue with the linux package dependencies |
|
On first glance this does look promesing! (the reason why I am mentioning this is because last PR was a major rework of the underlying systems to allow for an |
|
Thanks for taking a look! I'd be happy to adapt this to the design you have in mind for InfiniFrame.
Perhaps we could start by sharing the lifecycle and event code. For closing, I was thinking we could separate calling the handlers from applying their decision to the relevant close attempt, while leaving native interop and provider communication in their own adapters. Is that along the lines you're thinking, or would you prefer the built-in native backend to use the host-provider abstraction too, including application startup and shutdown? I'll hold off on structural changes until you've had a chance to review. If that sounds reasonable, I could work through the changes and add tests around creation and readiness, callback threading, and close cancellation. I'm also happy to look into #445 and see if I can help. |
|
|
||
| Select one provider per application builder. A process chooses either the default native backend or external | ||
| hosting before native initialization; mixing the modes or switching between them is unsupported. Default-engine | ||
| settings such as the WebView2 runtime path, notification registration, AppUserModelId, and default notification icon |
There was a problem hiding this comment.
Clarify that WebView2 is default on Windows and GTK WebKit on Linux
There was a problem hiding this comment.
Yes, that should be explicit here. Without UseHostProvider, InfiniFrame uses WebView2 on Windows, WebKitGTK on Linux and WKWebView on macOS. They’re listed in the README, but it would help to spell them out here too.
There was a problem hiding this comment.
I’ve added the defaults to the provider guide: WebView2 on Windows, WebKitGTK on Linux and WKWebView on macOS when UseHostProvider isn’t called.
| // On the thread pool, so neither this caller's synchronization context nor its thread is captured by the | ||
| // provider's own awaits while this thread waits. | ||
| InfiniFrameHostedRuntime hosted = _hosted; | ||
| Task.Run(() => RunHostedAsync(hosted, CancellationToken.None)).GetAwaiter().GetResult(); |
There was a problem hiding this comment.
not sure this is a good enough async await pattern because of the lack thereof
There was a problem hiding this comment.
Since there is no await inside RunHostedAsync there will be no observation of a crash (?).
Do we need this pattern to avoid deadlock or can we change to awaiting instead?
There was a problem hiding this comment.
I used Task.Run here because this is the synchronous Run() entry point, so it blocks until the application finishes. The idea was to keep hosted startup away from the caller’s synchronization context while that thread is waiting. Microsoft describes the deadlock concern in the ConfigureAwait FAQ.
RunHostedAsync does await internally. Task.Run follows the task it returns, and GetAwaiter().GetResult() passes any exception back to the caller. There’s also a test covering a reported companion-process failure through this path.
For an async caller, I’d use await app.RunAsync(), which already awaits the helper directly. I think the distinction between the two entry points could be clearer here.
There was a problem hiding this comment.
I’ve made the blocking behavior clearer in the code and guide. Run() waits for the complete hosted operation and propagates reported startup and host failures to its caller. Async callers can use await app.RunAsync() directly.
The Task.Run overload follows the task returned by the delegate, and GetAwaiter().GetResult() propagates its exception.
I’ve also added a regression for startup that yields while the caller’s scheduler is blocked. It checks that startup uses the default scheduler and that the same exception reaches the caller. The reported host-failure test now covers both Run() and RunAsync().
Explain the blocking Run bridge and retain its task unwrapping and exception propagation. Cover asynchronous startup under a blocked caller scheduler and reported host failure through both Run and RunAsync.
|
I’ve clarified the browser defaults and the blocking The hosting project and its dependencies build for .NET 8, 9 and 10 with zero warnings or errors. On Windows x64/.NET 10, all 131 hosting tests pass, with no skips. I also checked that replacing the thread-pool bridge with a direct blocking call makes the new regression fail on its scheduler assertion and still clean up. I haven’t rerun the native, JavaScript, real-browser or cross-platform suites for this clarification. With #446 merged, could you run the full suite with |
Summary
Allow an application to select an external browser/window provider with
UseHostProvider(...)beforeBuild(), while continuing to use InfiniFrame's window API and Blazor integration. This makes it possible to ship a browser engine in an independent provider package, including a CEF companion process, without putting that engine's implementation or native payload in InfiniFrame.The hosted path keeps Blazor work attached to the browser document that produced it. After a reload, the replaced page cannot send JavaScript or navigation to the next document or consume its replies. The PR includes the lifecycle, dispatch, resource handling, teardown, and shared HTTP-handler ownership needed to support that behavior as one change.
Type of Change
Affected Modules / Scope
InfiniFrame.Application, InfiniFrame.Window, InfiniFrame.Shared, InfiniFrame.BlazorWebView, InfiniFrame.Blazor, InfiniFrame.Js, hosting/regression tests, and the documentation site.
Changes Introduced
InfiniFrame.Hostingcontract with provider selection, capability validation, native lifecycle events, document IDs, messaging, and resource responses.Why there are two repositories
JoarEliasson/InfiniFrame→InfiniLore/InfiniFrame:core) contain the reusable framework extension point and its integration. InfiniFrame owns the application's window facade, lifecycle, managed dispatch, resource delivery, and Blazor document isolation.The CEF provider is supporting implementation evidence, not an additional dependency of this PR. Its README describes the restricted local-content policy and its Windows x64 / Linux x64 WSL2-X11 coverage; it does not support macOS.
Validation
Validated at
68259eb87311on Windows x64:UseAppHost=falseand run the assemblies through the installed runtimes.InfiniFrameEnableTestExportsenabled for the managed native tests. The JavaScript bridge builds from source and passes 425/425 tests.window-features-architecture#taskbar-progress-and-flashbroken-anchor warning, present in the base branch.The earlier combined implementation also passed the separate CEF consumer scenarios: 146/146 on Windows and 141/141 on Linux (Ubuntu 22.04 under WSL2/X11, reproduced from the repository on 8 November). Those browser runs used framework commit
9246583cc61f; the subsequent lifecycle-readiness fixes are covered by the new deterministic regressions listed above. Default-backend controls recorded two expected late-work failures (26/28), since hosted document isolation does not change default-backend page ownership.The full cross-platform matrix has not been run locally. This repository's
CI: Full Test Suiteworkflow accepts apr_numberthrough maintainer dispatch; it does not automatically run the full matrix on a pull-request event.Compatibility and lifecycle notes
Provider selection is explicit. Applications using the default backends keep that path. A process selects default or external hosting before native initialization and cannot mix or switch the modes. Default-engine settings and unsupported hosted capabilities are rejected.
For hosted pages, use the registered
IInfiniFrameWebViewManagerto change root components or dispatch into the current page. Inherited concreteWebViewManagermembers act on the outer coordinator. A retired page's new JS calls fail withJSDisconnectedException; already-pending calls need their caller's cancellation token or timeout to finish within a bound.Teardown bounds stop waiting, not arbitrary provider/component code. Late outcomes are observed. Host disposal may overlap a shutdown that ignores cancellation past its deadline; the contract states that exception explicitly. Blazor retirement can continue after the application run returns, so late component disposal can encounter already-disposed services. The guide describes these limits.
Related Issues
No issue linked.
Checklist