镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Feat: support external host providers with document-isolated Blazor pages - #444

Open
JoarEliasson wants to merge 11 commits into
InfiniLore:corefrom
JoarEliasson:feature/external-host-provider
Open

JoarEliasson wants to merge 11 commits into
InfiniLore:corefrom
JoarEliasson:feature/external-host-provider

Conversation

@JoarEliasson

Copy link
Copy Markdown
Contributor

Summary

Allow an application to select an external browser/window provider with UseHostProvider(...) before Build(), 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

  • New feature
  • Bug fix
  • Documentation update
  • Test addition or update

Affected Modules / Scope

InfiniFrame.Application, InfiniFrame.Window, InfiniFrame.Shared, InfiniFrame.BlazorWebView, InfiniFrame.Blazor, InfiniFrame.Js, hosting/regression tests, and the documentation site.

Changes Introduced

  • Add a versioned, engine-neutral InfiniFrame.Hosting contract with provider selection, capability validation, native lifecycle events, document IDs, messaging, and resource responses.
  • Keep the public window facade and managed dispatcher in InfiniFrame. Preserve resource statuses and reject unsupported configuration explicitly.
  • Give each hosted browser document its own Blazor manager. Retire it once, refuse its late output, and record bounded-disposal outcomes.
  • Leave the singleton HTTP handler owned by the application's root provider when page-scoped clients are disposed. Treat expected pointer-capture disconnection as a Debug-level no-op while retaining genuine error reporting.
  • Cover duplicate and cancelled native-close requests, callbacks queued before shutdown, and bounded host teardown with deterministic regressions.
  • Add an external-host-provider guide, a compile-checked setup example, and a scripted provider that exercises the contract without a browser.

Why there are two repositories

  • This fork and PR (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.
  • InfiniFrame.CefHost is an independent preview implementation of that public contract. It owns the CEF companion process, communication, browser policy, native payload packaging/deployment, and runnable consumers. It currently builds against packages from this fork. Keeping it separate lets CEF updates and provider releases evolve independently of the framework.

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 68259eb87311 on Windows x64:

  • The managed test solution builds for .NET 8, 9, and 10, with zero warnings or errors. The .NET 8/9 local checks use UseAppHost=false and run the assemblies through the installed runtimes.
  • The ten .NET 10 managed regression suites record 2441 passed, 46 skipped, 0 failed. This includes 129/129 Hosting, 120/120 BlazorWebView, and 60/60 Blazor tests. Skips are retained in the reported totals, not counted as passes.
  • The Hosting suite also passes 129/129 on .NET 8 and 129/129 on .NET 9.
  • The Windows x64 native library builds from source with InfiniFrameEnableTestExports enabled for the managed native tests. The JavaScript bridge builds from source and passes 425/425 tests.
  • The documentation site builds and the new guide's C# example compiles. The site still reports the existing window-features-architecture#taskbar-progress-and-flash broken-anchor warning, present in the base branch.
  • The close/dispatch and host-shutdown fixes include deterministic tests that fail on their pre-fix code and pass after the fixes. Independent reviews found no remaining blocking issue in those changes.

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 Suite workflow accepts a pr_number through 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 IInfiniFrameWebViewManager to change root components or dispatch into the current page. Inherited concrete WebViewManager members act on the outer coordinator. A retired page's new JS calls fail with JSDisconnectedException; 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

  • Added regression tests for the public hosting contract and lifecycle races.
  • Added usage, capability, threading, and teardown documentation.
  • Local validation and its limitations are recorded above.
  • Maintainer-dispatched cross-platform CI and maintainer review.

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.
@ScarletBytesDev

Copy link
Copy Markdown
Member

Will look at this during the week. Currently blocked by #445 as there seems to be an issue with the linux package dependencies

@ScarletBytesDev

Copy link
Copy Markdown
Member

On first glance this does look promesing!
Though I wonder if there is not a better approach to some design decissions, like having wired the same window events through the host... I wonder if better design decissions that might affect the core ways of how windows work, hosted or not, might be a better design choice.

(the reason why I am mentioning this is because last PR was a major rework of the underlying systems to allow for an InfiniframeApplication and I wonder if IInfiniFrameWindow cannot simply be implemented by something like InfiniFrameWindow and InfiniFrameHostedWindow, where one implemts the other as a baseline. Braindump for now, again will look at this later in the week 😊

@JoarEliasson

Copy link
Copy Markdown
Contributor Author

Thanks for taking a look! I'd be happy to adapt this to the design you have in mind for InfiniFrame.

InfiniFrameWindow and InfiniFrameHostedWindow both implement IInfiniFrameWindow, and the hosted path uses the existing event store and subscriptions. I kept their lifecycle handling separate to avoid changing the default backend's behavior, but I agree there's room to share more of it.

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.

@JoarEliasson

Copy link
Copy Markdown
Contributor Author

I had a look at #445 and opened #446 with a proposed fix. Hopefully that helps get the Linux checks running again! I’ve kept it separate so it can be reviewed on its own.


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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clarify that WebView2 is default on Windows and GTK WebKit on Linux

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure this is a good enough async await pattern because of the lack thereof

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@JoarEliasson

Copy link
Copy Markdown
Contributor Author

I’ve clarified the browser defaults and the blocking Run() path in f46de3dd, with regression coverage for caller scheduling and failure propagation.

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 pr_number=444 when convenient? I’ve left the broader window/lifecycle design unchanged pending your review.

@ScarletBytesDev ScarletBytesDev linked an issue Nov 12, 2026 that may be closed by this pull request
2 tasks done

This branch has not been deployed

No deployments
Sign up for free to 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.

Feature | Support external host providers

3 participants