diff --git a/PORTING.md b/PORTING.md index 7ad82b7..57d8152 100644 --- a/PORTING.md +++ b/PORTING.md @@ -107,17 +107,16 @@ Reference: `packages/flutter_cef_macos/native/cef_host/` (`main.mm` is the browser process, `process_helper.mm` the CEF child processes). The CEF client / app / handler logic, the browser-control functions, the navigation scheme allowlist, and the **IPC opcode protocol** are platform-agnostic C++ and can be -reused verbatim. Only these seams are macOS-specific (file:line are into -`main.mm` at the time of writing): +reused verbatim. Only these seams are macOS-specific: | Seam | macOS reference | What your platform provides | | --- | --- | --- | -| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops (~lines 346–450); `g_surface` (~133). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture. Note (verified against CEF 144 `cef_types_win.h` + Flutter engine `external_texture_d3d.cc`): CEF's `OnAcceleratedPaint` delivers an **NT** handle (no keyed mutex, non-owning, pool-released when the callback returns — open it synchronously via `ID3D11Device1::OpenSharedResource1`), while Flutter's ANGLE compositor requires a **legacy** `D3D11_RESOURCE_MISC_SHARED` handle (`EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE`); the two are mutually exclusive on one texture, so exactly one `CopyResource` NT→legacy is structurally mandatory (macOS also blits — `CompositeMetalLocked`). **Linux**: shared memory or a DMA-buf, presented via the platform texture. Either way the model is **producer-allocates**: `cef_host` mints the shared surface and announces it via `opPresent` — the host does NOT pass a surface id in the create (the macOS field name differs too: `shared_texture_io_surface` vs Windows `shared_texture_handle`, so the accelerated-paint glue takes a per-OS `#if`, not a typedef). | -| **IPC transport** — a framed bidirectional byte stream to the host. Wire format: 4-byte big-endian length prefix, then `[u32 browserId][opcode][payload]` (`browserId 0` = process-level: `opReady`, process logs, `opShutdown`). | `WriteAll`/`ReadAll` (207–235), `SendFrame` (~237–249), `ConnectUnixSocket` (1341+), the read loop (~1140). Unix domain socket. | **Windows**: a named pipe. **Linux**: a Unix domain socket (reuse as-is). Keep the same length-prefixed framing and the `browserId` dimension. | -| **App / run loop** — give CEF a host application + message loop, and a per-profile cache dir. | `CefHostApplication : NSApplication` (304–330); `@autoreleasepool` + `sharedApplication` (1420+); `--profile-dir` → `settings.root_cache_path` cache; `_NSGetExecutablePath` (1335). | **Windows/Linux**: the platform's CEF message-loop integration (`CefRunMessageLoop` or OS loop) and the caller-supplied `--profile-dir` (per-profile, persistent) as the cache path. | +| **Shared surface** — receive painted frames and present them to the host texture. CEF delivers either software `OnPaint` (CPU buffer) or `OnAcceleratedPaint` (a shared GPU texture handle). | `OnPaint` / `OnAcceleratedPaint` + the `IOSurface*` ops on each slot's `surface` (`render_handler.mm`). macOS uses an IOSurface-backed `CVPixelBuffer`. | **Windows**: a shared D3D11 texture. Note (verified against CEF 144 `cef_types_win.h` + Flutter engine `external_texture_d3d.cc`): CEF's `OnAcceleratedPaint` delivers an **NT** handle (no keyed mutex, non-owning, pool-released when the callback returns — open it synchronously via `ID3D11Device1::OpenSharedResource1`), while Flutter's ANGLE compositor requires a **legacy** `D3D11_RESOURCE_MISC_SHARED` handle (`EGL_D3D_TEXTURE_2D_SHARE_HANDLE_ANGLE`); the two are mutually exclusive on one texture, so exactly one `CopyResource` NT→legacy is structurally mandatory (macOS also blits — `CompositeMetalLocked`). **Linux**: shared memory or a DMA-buf, presented via the platform texture. Either way the model is **producer-allocates**: `cef_host` mints the shared surface and announces it via `opPresent` — the host does NOT pass a surface id in the create (the macOS field name differs too: `shared_texture_io_surface` vs Windows `shared_texture_handle`, so the accelerated-paint glue takes a per-OS `#if`, not a typedef). | +| **IPC transport** — a framed bidirectional byte stream to the host. Wire format: 4-byte big-endian length prefix, then `[u32 browserId][opcode][payload]` (`browserId 0` = process-level: `opReady`, process logs, `opShutdown`). | `WriteAll`/`ReadAll` and `SendFrame` (`ipc.mm`), `ConnectUnixSocket` (`main.mm`), the read loop (`IpcReadLoop`, `ipc_reader.mm`). Unix domain socket. | **Windows**: a named pipe. **Linux**: a Unix domain socket (reuse as-is). Keep the same length-prefixed framing and the `browserId` dimension. | +| **App / run loop** — give CEF a host application + message loop, and a per-profile cache dir. | `CefHostApplication : NSApplication`; `@autoreleasepool` + `sharedApplication`; `--profile-dir` → `settings.root_cache_path` cache; `_NSGetExecutablePath` (all in `main.mm`). | **Windows/Linux**: the platform's CEF message-loop integration (`CefRunMessageLoop` or OS loop) and the caller-supplied `--profile-dir` (per-profile, persistent) as the cache path. | | **Agent-control CDP pipe (optional)** — for `agentControl`, the host launches `cef_host` so Chromium's `--remote-debugging-pipe` (NUL-framed CDP JSON) rides **inherited file descriptors 3=read / 4=write** instead of a TCP port. | `CefProfileHost.launchViaPosixSpawn` (Swift): two `pipe()` pairs `dup2`'d onto fds 3/4 via `posix_spawn_file_actions`, parent ends marked `FD_CLOEXEC`; `cef_host` appends `remote-debugging-pipe` in `OnBeforeCommandLineProcessing`. The relay (`CdpRelay.swift`) + per-tile Target filter are platform-agnostic; only the fd placement is OS-specific. | **Linux**: reuse the `posix_spawn_file_actions` fd-3/4 recipe verbatim. **Windows**: a different model — `CreatePipe` + `SetHandleInformation(HANDLE_FLAG_INHERIT)` + `STARTUPINFOEX` `PROC_THREAD_ATTRIBUTE_HANDLE_LIST`, and Chromium takes the two inherited pipe **HANDLE values** via the separate switch `--remote-debugging-io-pipes=,` (uint32-serialized HANDLEs, passed **in addition to** the bare `--remote-debugging-pipe` — verified against `content_switches.cc` at chromium 144.0.7559.254; `--remote-debugging-pipe` itself takes no arguments on any platform). The default (non-agent) launch also needs a native `exec`/`CreateProcess` (Foundation.Process is macOS-only). | -| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (1426/1433). | **Windows**: CEF 144 ships **no `cef_sandbox.lib`** — the current model (`cef_sandbox_win.h`, CEF issue #3824) is: build your app as a **DLL** exporting `RunWinMain`/`RunConsoleMain` and launch it through the prebuilt `bootstrap.exe`/`bootstrapc.exe` from the CEF distribution, which establishes the sandbox before entering your code. **Linux**: the SUID / user-namespace sandbox helper. | -| **Framework / resource path** — point CEF at the CEF binary distribution. | `CefScopedLibraryLoader::LoadInMain`/`LoadInHelper`; `framework_dir_path` / `main_bundle_path` (1453/1466). | The equivalent paths for your bundle layout. | +| **Sandbox** — bring the child processes into the Chromium sandbox in a signed/release build. | `process_helper.mm`: `CefScopedSandboxContext` (release only); `settings.no_sandbox` toggled by `CEF_HOST_ADHOC` (`main.mm`). | **Windows**: CEF 144 ships **no `cef_sandbox.lib`** — the current model (`cef_sandbox_win.h`, CEF issue #3824) is: build your app as a **DLL** exporting `RunWinMain`/`RunConsoleMain` and launch it through the prebuilt `bootstrap.exe`/`bootstrapc.exe` from the CEF distribution, which establishes the sandbox before entering your code. **Linux**: the SUID / user-namespace sandbox helper. | +| **Framework / resource path** — point CEF at the CEF binary distribution. | `CefScopedLibraryLoader::LoadInMain`/`LoadInHelper`; `framework_dir_path` / `main_bundle_path` (`main.mm`). | The equivalent paths for your bundle layout. | | **Build + bundle + sign** | `native/build_cef_host.sh` (CMake, `CEF_MULTI_PROCESS` / `CEF_HOST_ADHOC` flags), `tool/bundle_cef_host.sh`. | A platform build that produces `cef_host` + the CEF runtime, and a bundling step into the host app. | The fetched CEF distribution already ships per-platform binaries and a CMake @@ -127,8 +126,8 @@ platform-specific link libs and the surface/transport sources. ## 4. Recommended: extract a `core/` + `platform/` split *with* your port -The macOS `main.mm` currently keeps the portable CEF logic and the macOS glue in -one translation unit. The seams above are the natural cut line: +The macOS host currently keeps the portable CEF logic and the macOS glue in +the same files. The seams above are the natural cut line: ``` native/cef_host/ diff --git a/example/lib/agentcontrol_probe.dart b/example/lib/agentcontrol_probe.dart index 2e41f66..ad499ff 100644 --- a/example/lib/agentcontrol_probe.dart +++ b/example/lib/agentcontrol_probe.dart @@ -1,4 +1,4 @@ -// Agent-control (P9) end-to-end probe — drives the Windows token-gated loopback +// Agent-control end-to-end probe — drives the Windows token-gated loopback // CDP relay through a real CDP WebSocket client, entirely in-process (no // Playwright / node / python needed: dart:io's WebSocket does the RFC-6455 // handshake and forwards a custom Authorization header). diff --git a/example/lib/cull_wedge_probe.dart b/example/lib/cull_wedge_probe.dart index 64ae433..5e2f8ff 100644 --- a/example/lib/cull_wedge_probe.dart +++ b/example/lib/cull_wedge_probe.dart @@ -2,8 +2,8 @@ // transitions that used to wedge it permanently blank (only relaunch recovered): // * setVisible(false) → resize while hidden → setVisible(true) // * setVisible(false) → setVisible(true) after the off-screen frame is evicted -// The native fix (F-1): DoSetVisible(true) forces a full repaint on the hidden→visible -// edge; (F-2) DoResize defers its paint while hidden; (F-4) the resize watchdog never +// The native fix: DoSetVisible(true) forces a full repaint on the hidden→visible +// edge; DoResize defers its paint while hidden; the resize watchdog never // force-promotes a hidden (never-painted) surface. Without these, the page below stays // BLANK after "Wedge cycle"; with them it reappears (gradient + the ticking clock proves // the frame is FRESH, not a stale cached one). @@ -55,7 +55,7 @@ class _WedgeAppState extends State { _controller.onPageStarted = (_) => _controller.loadHtmlString(_html); // Self-driving evidence run: a few seconds after first paint, run several wedge // cycles back-to-back then settle SHOWN, so a screenshot of the final state proves the - // page repainted (F-1) rather than wedged blank — no clicking needed. + // page repainted on show rather than wedged blank — no clicking needed. Future.delayed(const Duration(seconds: 4), _runAutoCycles); } diff --git a/example/lib/interaction_soak_probe.dart b/example/lib/interaction_soak_probe.dart index 1d858d9..6c0ec08 100644 --- a/example/lib/interaction_soak_probe.dart +++ b/example/lib/interaction_soak_probe.dart @@ -82,7 +82,7 @@ class _AppState extends State { t.controller.setVisible(false); case 2: // logical resize WHILE the tile may be hidden t.big = !t.big; - case 3: // un-cull (show) — F-1 must repaint, size-gate must promote + case 3: // un-cull (show) — show must repaint, size-gate must promote t.visible = true; t.controller.setVisible(true); } diff --git a/example/lib/jsbridge_smoke.dart b/example/lib/jsbridge_smoke.dart index bd5dcb4..180b869 100644 --- a/example/lib/jsbridge_smoke.dart +++ b/example/lib/jsbridge_smoke.dart @@ -1,4 +1,4 @@ -// P7 JS-bridge smoke harness (Windows integration proof) — reload-tolerant. +// JS-bridge smoke harness (Windows integration proof) — reload-tolerant. // // loadHtmlString settles with an extra page reload, so linear orchestration on // one page session is fragile. Instead: confirm() + the download click run in diff --git a/example/lib/main.dart b/example/lib/main.dart index 6dadeac..f0c3b08 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -372,7 +372,7 @@ and committed text — including emoji — should appear intact.

onPressed: _controller.openDevTools, ), // macOS-only: showEmojiPicker drives the AppKit Character - // Palette; there is no supported Win32 equivalent (PLAN §6), + // Palette; there is no supported Win32 equivalent, // so the button is hidden per-platform. if (Platform.isMacOS) IconButton( diff --git a/example/lib/multiview_probe.dart b/example/lib/multiview_probe.dart index 059b270..4f0a3bd 100644 --- a/example/lib/multiview_probe.dart +++ b/example/lib/multiview_probe.dart @@ -1,4 +1,4 @@ -// P2-step2 LIVE probe (cef-multiview PLAN Tests A + D + E) — flutter_cef side. +// Multi-view agent-control LIVE probe (checks A, D, E, F below) — flutter_cef side. // // Auto-running, headless-friendly self-test: mounts TWO CefWebViews on ONE shared // named profile (an isolated 'p2probe' — deliberately NOT Campus's real 'campus-web' @@ -18,11 +18,11 @@ // E. each relay's Target.getTargets returns ONLY its own target (A can't see B), // and presenting tile A's token to tile B's port is rejected. // F. concurrency + lifecycle: enabling both CONCURRENTLY brings up two isolated -// relays (the per-browserId dict, not the P1 scalar); disabling A kills only +// relays (one per browserId, not a single per-host slot); disabling A kills only // A's grant (its endpoint goes dead) while B keeps driving; and A can be // RE-ENABLED after disable — a fresh port+token, the torn-down grant stays dead. // -// Not covered here — reader-stall isolation (PLAN Test G, the SO_SNDTIMEO reaping of +// Not covered here — reader-stall isolation (the SO_SNDTIMEO reaping of // a wedged client + no sibling starvation): a faithful repro needs a real CDP driver // that completes the flatten auto-attach handshake and drives pipe-routed commands // (this probe's Target.getTargets is synthesized client-side and bypasses the shared @@ -125,8 +125,8 @@ class _ProbeAppState extends State { final out = {}; try { setState(() => _status = 'enabling agent-control on both views CONCURRENTLY…'); - // F — concurrent enable: fire BOTH at once (not sequentially). The P1 scalar - // relay/relayBrowserId would have lost this race; the per-browserId dict + + // F — concurrent enable: fire BOTH at once (not sequentially). A single + // per-host relay/relayBrowserId would have lost this race; the per-browserId dict + // cdpHandlerLock must bring up two isolated relays under simultaneous enable. final grants = await Future.wait([_enable(_a), _enable(_b)]); final gA = grants[0], gB = grants[1]; @@ -242,7 +242,7 @@ class _ProbeAppState extends State { children: [ Padding( padding: const EdgeInsets.all(8), - child: Text('P2-step2 probe — $_status', + child: Text('multiview probe — $_status', style: const TextStyle(fontWeight: FontWeight.w600)), ), Expanded( diff --git a/example/lib/profile_probe.dart b/example/lib/profile_probe.dart index 292a4d0..7c97e73 100644 --- a/example/lib/profile_probe.dart +++ b/example/lib/profile_probe.dart @@ -1,4 +1,4 @@ -// Windows profile + cookie END-TO-END probe (P6 foundation + P11 profile slice). +// Windows profile + cookie END-TO-END probe (shared hosts, named profiles, cookies). // // Auto-running, no-interaction self-test that drives the REAL integrated stack — // the public Dart API (CefWebController) -> the method channel -> the Windows @@ -34,7 +34,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_cef/flutter_cef.dart'; // A dev probe artifact — write evidence under the OS temp dir, not a -// maintainer-specific absolute path (PLAN P1: probe outputs -> systemTemp). +// maintainer-specific absolute path. final _evidenceDir = '${Directory.systemTemp.path}${Platform.pathSeparator}flutter_cef_profile_evidence'; const _sharedProfile = 'evi_shared'; diff --git a/example/lib/recreate_soak_probe.dart b/example/lib/recreate_soak_probe.dart index e9f9eaf..113c92f 100644 --- a/example/lib/recreate_soak_probe.dart +++ b/example/lib/recreate_soak_probe.dart @@ -1,5 +1,5 @@ // Recreate SOAK probe — mimics Campus's CefSessionController.recover() (the one pattern -// the other probes never exercised): on a paint-stall / F-6 stall Campus DISPOSES the +// the other probes never exercised): on a paint stall or liveness stall Campus DISPOSES the // controller, builds a FRESH one, and REMOUNTS the CefWebView against it (a generation // ValueKey bump). This probe drives that recreate cycle interleaved with zoom (renderScale) // and cull (setVisible) — the suspected source of "looks fine, then after interaction diff --git a/example/run_conformance_oracle.sh b/example/run_conformance_oracle.sh index 1e00ffe..b2b54ce 100755 --- a/example/run_conformance_oracle.sh +++ b/example/run_conformance_oracle.sh @@ -10,7 +10,7 @@ # • NO-RENDER : the harness never established / never adopted a surface # # This turns the manual "read the logs" check into a repeatable gate. Run after any change to -# CefWebSession.swift / CefProfileHost.swift / cef_host/main.mm. +# CefWebSession.swift / CefProfileHost*.swift / cef_host/*.mm. # # FLUTTER_CEF_HOST=/path/to/cef_host.app/Contents/MacOS/cef_host ./run_conformance_oracle.sh # diff --git a/example/run_leak_soak.sh b/example/run_leak_soak.sh index 819396d..3650ff2 100755 --- a/example/run_leak_soak.sh +++ b/example/run_leak_soak.sh @@ -1,6 +1,6 @@ #!/bin/bash -# LEAK-SOAK GATE — guards the producer-allocates IOSurface LIFETIME invariant (the property the -# three sev-8 audit findings probed and which the render oracle does NOT cover): cef_host mints a +# LEAK-SOAK GATE — guards the producer-allocates IOSurface LIFETIME invariant (which the render +# oracle does NOT cover): cef_host mints a # surface per paint/recreate and CFReleases the old; the consumer's CVPixelBuffer holds the only # remaining ref until it adopts the next id. If that ledger ever regresses (producer forgets to # release, or the consumer never drops the old ref), surfaces accumulate — invisible to the render diff --git a/lib/src/cef_web_controller.dart b/lib/src/cef_web_controller.dart index fcbd2b0..d2da73b 100644 --- a/lib/src/cef_web_controller.dart +++ b/lib/src/cef_web_controller.dart @@ -495,7 +495,7 @@ class CefWebController { onProcessGone?.call(reason); break; case 'paintStalled': - // C1: the browser came up but never delivered its first frame even after a + // The browser came up but never delivered its first frame even after a // re-kick — the texture is (still) blank with no other signal. Surface it so // the consumer can recover (e.g. recreate the view) instead of a silent blank. onPaintStalled?.call(); @@ -829,9 +829,9 @@ class CefWebController { ? allowedSchemes.map((s) => s.toLowerCase()).join(',') : null, enableCdp: enableCdp, - // Agent-control / pipe mode (CEF-1): the native side launches cef_host - // via posix_spawn with CDP over inherited fds 3/4 (--cdp-pipe) instead - // of a TCP --cdp-port. + // Agent-control / pipe mode: the native side launches cef_host with + // CDP over inherited pipes (fds 3/4 on macOS, two anonymous pipes on + // Windows) instead of a TCP --cdp-port. agentControl: agentControl, profile: profile != null && profile!.isNotEmpty ? profile : null, hostGroup: @@ -922,7 +922,7 @@ class CefWebController { return _platform.openAuthWindow(sessionId, url); } - /// CEF-2a — enable agent control for this tile and return a brokered, token-gated + /// Enable agent control for this tile and return a brokered, token-gated /// CDP endpoint a standard CDP client (e.g. `agent-browser`) can connect to. /// /// Requires the controller to have been created with `agentControl: true` (the @@ -959,7 +959,7 @@ class CefWebController { return (wsUrl: wsUrl, token: token, port: port); } - /// CEF-2a — tear down the agent-control relay (closes the listener and any client, + /// Tear down the agent-control relay (closes the listener and any client, /// invalidates the token). The tile itself keeps running. Idempotent. Future disableAgentControl() => _platform.disableAgentControl(sessionId); diff --git a/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt b/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt index 160d897..d7e32c2 100644 --- a/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt +++ b/packages/flutter_cef_windows/native/cef_host/CMakeLists.txt @@ -2,7 +2,7 @@ # plugin build (driven by build_cef_host.bat, which the plugin CMakeLists # invokes via add_custom_command). # -# Ship shape (docs/history/windows-port/SPIKES.md S2): the host is a client +# Ship shape: the host is a client # DLL `cef_host.dll` exporting RunConsoleMain, loaded by CEF's prebuilt # bootstrap. We ship bootstrapc.exe RENAMED to cef_host.exe beside the DLL (the # rename selects the module — no --module flag needed; clean Task Manager @@ -10,9 +10,9 @@ # sandbox_info the host forwards to CEF. # # CEF's own cmake config (find_package(CEF)) drives compiler flags (/MT): -# this target never links against Flutter, so the plugin's /MD rule (S6a) -# does not apply here. The client DLL must link delayimp.lib (CEF's -# /DELAYLOAD list — docs/history/windows-port/SPIKES.md S2). +# this target never links against Flutter, so the plugin's /MD rule (the CEF +# wrapper built against Flutter's runtime) does not apply here. The client +# DLL must link delayimp.lib (CEF's /DELAYLOAD list needs it). cmake_minimum_required(VERSION 3.21) project(cef_host_win LANGUAGES CXX) @@ -53,15 +53,15 @@ add_library(cef_host SHARED document_start.h ) SET_LIBRARY_TARGET_PROPERTIES(cef_host) -# NOMINMAX: cef_types_win.h pulls windows.h; without it std::min/max -> C2589 -# (S6a). CEF's own configs may set it too — harmless twice. +# NOMINMAX: cef_types_win.h pulls windows.h; without it std::min/max -> C2589. +# CEF's own configs may set it too — harmless twice. target_compile_definitions(cef_host PRIVATE NOMINMAX) target_include_directories(cef_host PRIVATE ${CEF_ROOT}) target_link_libraries(cef_host ${WRAPPER_LIB} "${CEF_ROOT}/Release/libcef.lib" delayimp.lib - # The OnAcceleratedPaint NT->legacy bridge blit (S1 pixel path). + # The OnAcceleratedPaint NT->legacy bridge blit (the GPU pixel path). d3d11.lib dxgi.lib ) diff --git a/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md b/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md index ba6ed97..0cf68ff 100644 --- a/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md +++ b/packages/flutter_cef_windows/native/cef_host/PROTOCOL.md @@ -2,15 +2,14 @@ The opcodes and protocol versions of both platforms are defined once, in `tool/protocol/spec.dart`, and generated into each package (§2). The rest of -this document was transcribed from the macOS implementation (line numbers are -from branch `feat/windows-port-p0` and have drifted): +this document was transcribed from the macOS implementation: -- `packages/flutter_cef_macos/native/cef_host/main.mm` — framing and read-loop - payload decoding. +- `packages/flutter_cef_macos/native/cef_host/` — framing (`ipc.mm`), read-loop + payload decoding (`ipc_reader.mm`), and the per-browser handlers + (`host_client.mm`, `browser_ops.mm`). - `packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift` — channel verb dispatch (`handle`) and native->Dart events (`emit` sites). -- `lib/src/cef_web_controller.dart` — the exact channel-arg maps Dart sends - (cited per verb below). +- `lib/src/cef_web_controller.dart` — the exact channel-arg maps Dart sends. The Windows host + plugin speak the macOS protocol with two payload differences, `kOpPresent` and `kOpShowDevTools` (§2), and their own protocol @@ -20,8 +19,8 @@ version. ## 1. IPC framing (byte stream over the named pipe) -Identical to macOS (main.mm:40-45, SendFrame main.mm:416-446, reader -main.mm:2338-2357): +Identical to macOS (`SendFrame` in `ipc.mm`, `IpcReadLoop` in +`ipc_reader.mm`): ``` [u32 bodyLen BE][u32 browserId BE][u8 opcode][payload...] @@ -30,15 +29,14 @@ main.mm:2338-2357): - `bodyLen` = 4 (browserId) + 1 (opcode) + payloadLen — counts every byte after the length prefix. - Guard on read: `5 <= bodyLen <= 64 MiB`, else the stream is desynced — - log + tear down the whole process (main.mm:2343-2351). + log + tear down the whole process. - `browserId` is the PLUGIN-assigned wire id (>= 1). `browserId 0` = process/profile level (`kOpReady`, process-level `kOpLog`, inbound - `kOpShutdown`) (main.mm:41-43). + `kOpShutdown`). - ALL multi-byte integers are BIG-ENDIAN, including `f64` (IEEE-754 double, - BE byte order — `ReadF64BE`/`WriteF64BE`, see main.mm ReadU32BE:476). + BE byte order — `ReadF64BE`/`WriteF64BE`, as in macOS `ipc.h`). - Writes are assembled into one contiguous frame and written atomically - under a write mutex, so a partial write never desyncs the peer - (main.mm:431-445). + under a write mutex, so a partial write never desyncs the peer. - Transport on Windows: named pipe `\\.\pipe\flutter_cef_<128-bit random hex>` (an unguessable name, so a same-user process can't squat it), `PIPE_TYPE_BYTE | PIPE_READMODE_BYTE`, single instance; plugin is the @@ -59,7 +57,7 @@ main.mm:2338-2357): `FILE_FLAG_OVERLAPPED`. A Unix socket fd is full-duplex, so the macOS reference never encounters this. - Unknown opcode at either end: log ONCE per opcode value and drop the - frame — never kill the stream (main.mm:2583-2598). + frame — never kill the stream. - Plugin writes run on the platform thread and wait at most 3 s for the host to take a frame. A write that fails or times out ends the host (every session gets `processGone("crashed")`): the host isn't reading, and part of @@ -157,56 +155,54 @@ macOS only, never reuse on Windows: 0x1e kOpMediaRequest, 0x1f kOpMediaState, 0x ## 3. Method-channel verbs (Dart -> plugin), channel `flutter_cef` -Verb names + arg keys verbatim from FlutterCefPlugin.swift:113-241 and +Verb names + arg keys verbatim from FlutterCefPlugin.swift (`handle`) and cef_web_controller.dart (invokeMethod sites). Every arg map carries -`sessionId` (String). SLICE = functional since the P1–P4 vertical slice; -P6 = functional since the profiles/cookies slice; P7 = functional since the -JS-bridge/dialogs/find/zoom/downloads slice (see §7); IMPL\* = native -implementation present but not yet verified on Windows OSR; PARITY = added -with the macOS-parity pass; UNSUPPORTED = replies +`sessionId` (String). `showDevTools` and the three `ime*` verbs are +implemented natively but not yet verified on Windows OSR. Verbs listed as +returning Error `unsupported` reply `Error("unsupported", " is not supported on Windows")`. -| Verb | Args (beyond sessionId) | Returns | Maps to | Slice? | Source | -|---|---|---|---|---|---| -| create | url:String, width:int (clamped to 1..16384), height:int (clamped to 1..16384), dpr:double, allowedSchemes:String? (csv of URL schemes, omit-when-empty; anything else is `bad_args`), enableCdp:bool? (omit-when-false), agentControl:bool? (omit-when-false), profile:String? (omit-when-empty), hostGroup:String? (omit-when-empty), authoredHtml:String? (serve as `url`), documentStartScripts:List?, channels:List? (JS channels registered before create) | `{textureId:int, width:int, height:int, cdpPort:int}` | spawn host (if needed) + [kOpSetDocumentStart 0x41] + [kOpSetAuthoredHtml 0x3f] + kOpCreateBrowser 0x13 | SLICE | Swift:255-446, controller:506-523 | -| navigate | url:String | null | 0x20 | SLICE | Swift:617-622, controller:566 | -| loadTrusted | url:String | null | 0x34 | SLICE (stub-ok) | Swift:626-631, controller:634 | -| loadAuthored | url:String, html:String | null | 0x3f then 0x34 | SLICE | Swift loadAuthored, controller loadHtmlString | -| resize | width:int, height:int, dpr:double | `{textureId:int}` (or null if unknown session) | 0x11 | SLICE | Swift:633-641, controller:874 | -| getFrameSurface | — | `{surfaceId:int, width:int, height:int}` (physical px) of the frame the texture is showing, or null before the first | plugin-local | SLICE | Swift:649-658 | -| dispose | — | null | 0x15 (last browser: 0x14 + host teardown) | SLICE | Swift:660-663, controller:530/948 | -| freezeSession | — | bool (false: unknown, already frozen, or host gone) | 0x15 (last browser: 0x14 + host teardown); the session and texture stay | PARITY | Swift freezeSession | -| thawSession | url:String? (default: the create url) | `{textureId:int}` or null when not frozen | host resolve/spawn + [0x41] + [0x3f] + 0x13 at the current size | PARITY | Swift thawSession | -| sessionStats | — | `{presentCount:int, lastPresentAgoMs:int?, firstPresentSeen:bool, frozen:bool}` (promoted presents only) or null | plugin-local | PARITY | Swift sessionStats | -| setAudioMuted | muted:bool | null | 0x3a | PARITY | Swift setAudioMuted | -| setFrameInterval | ms:int | null | 0x3b `{u16 ms}`; host clamps to [8, 250] and sets the windowless frame rate | PARITY | Swift setFrameInterval | -| chooseContextMenu, respondMediaRequest, setMediaSetting, openAuthWindow | — | Error `unsupported` | — | UNSUPPORTED | Swift | -| pointer | type:int, button:int, clickCount:int, modifiers:int, x:double, y:double, dx:double, dy:double | null | 0x10 | SLICE | Swift:730-740, controller:895 | -| key | type:int, modifiers:int, windowsKeyCode:int, nativeKeyCode:int, character:int | null | 0x12 | SLICE | Swift:742-752, controller:919 | -| reload | — | null | 0x21 | SLICE | Swift:122 | -| stop | — | null | 0x22 | SLICE | Swift:123 | -| goBack | — | null | 0x23 | SLICE | Swift:124 | -| goForward | — | null | 0x24 | SLICE | Swift:125 | -| executeJavaScript | code:String | null | 0x25 | SLICE | Swift:126-130 | -| setZoomLevel | level:double | null | 0x26 | P7 | Swift:131-133 | -| editCommand | command:int | null | 0x38 | SLICE | Swift:134-136 | -| setVisible | visible:bool | null | 0x35 | SLICE | Swift:137-139 | -| find | text:String, forward:bool, matchCase:bool, findNext:bool | null | 0x27 | P7 | Swift:140-148 | -| stopFind | clearSelection:bool | null | 0x28 | P7 | Swift:149-151 | -| respondJsDialog | id:int, ok:bool, text:String | null | 0x29 | P7 | Swift:152-158 | -| evalReturning | id:int, code:String | null | 0x2a | P7 | Swift:159-165 | -| addJavaScriptChannel | name:String | null | 0x2b | P7 | Swift:166-170 | -| setCookie | url, name, value, domain, path, sameSite : String; secure, httpOnly : bool | null | 0x2c | P6 | Swift:171-179 | -| clearCookies | — | null | 0x2d | P6 | Swift:180-182 | -| visitCookies | id:int, url:String | null | 0x2e | P6 | Swift:183-188 | -| deleteCookie | url:String, name:String | null | 0x2f | P6 | Swift:189-194 | -| showDevTools | — | null | 0x33 | IMPL\* | Swift:195-197 | -| enableAgentControl | — | `{wsUrl:String, token:String, port:int}` or FlutterError (`no_agent_control` when the session wasn't created with `agentControl:true`; `agent_control_unavailable` when it was, but joined a profile whose host started without it) | CDP relay (§8) | P9 | Swift:198-217, controller:595-605 | -| disableAgentControl | — | null | CDP relay (§8) | P9 | Swift:218-225, controller:609-610 | -| showEmojiPicker | — | Error `unsupported` | macOS-only (Character Palette) | UNSUPPORTED | Swift:226-230 | -| imeSetComposition | text:String | null | 0x30 | IMPL\* | Swift:231-233 | -| imeCommitText | text:String | null | 0x31 | IMPL\* | Swift:234-236 | -| imeCancelComposition | — | null | 0x32 | IMPL\* | Swift:237-239 | +| Verb | Args (beyond sessionId) | Returns | Maps to | +|---|---|---|---| +| create | url:String, width:int (clamped to 1..16384), height:int (clamped to 1..16384), dpr:double, allowedSchemes:String? (csv of URL schemes, omit-when-empty; anything else is `bad_args`), enableCdp:bool? (omit-when-false), agentControl:bool? (omit-when-false), profile:String? (omit-when-empty), hostGroup:String? (omit-when-empty), authoredHtml:String? (serve as `url`), documentStartScripts:List?, channels:List? (JS channels registered before create) | `{textureId:int, width:int, height:int, cdpPort:int}` | spawn host (if needed) + [kOpSetDocumentStart 0x41] + [kOpSetAuthoredHtml 0x3f] + kOpCreateBrowser 0x13 | +| navigate | url:String | null | 0x20 | +| loadTrusted | url:String | null | 0x34 | +| loadAuthored | url:String, html:String | null | 0x3f then 0x34 | +| resize | width:int, height:int, dpr:double | `{textureId:int}` (or null if unknown session) | 0x11 | +| getFrameSurface | — | `{surfaceId:int, width:int, height:int}` (physical px) of the frame the texture is showing, or null before the first | plugin-local | +| dispose | — | null | 0x15 (last browser: 0x14 + host teardown) | +| freezeSession | — | bool (false: unknown, already frozen, or host gone) | 0x15 (last browser: 0x14 + host teardown); the session and texture stay | +| thawSession | url:String? (default: the create url) | `{textureId:int}` or null when not frozen | host resolve/spawn + [0x41] + [0x3f] + 0x13 at the current size | +| sessionStats | — | `{presentCount:int, lastPresentAgoMs:int?, firstPresentSeen:bool, frozen:bool}` (promoted presents only) or null | plugin-local | +| setAudioMuted | muted:bool | null | 0x3a | +| setFrameInterval | ms:int | null | 0x3b `{u16 ms}`; host clamps to [8, 250] and sets the windowless frame rate | +| chooseContextMenu, respondMediaRequest, setMediaSetting, openAuthWindow | — | Error `unsupported` | — | +| pointer | type:int, button:int, clickCount:int, modifiers:int, x:double, y:double, dx:double, dy:double | null | 0x10 | +| key | type:int, modifiers:int, windowsKeyCode:int, nativeKeyCode:int, character:int | null | 0x12 | +| reload | — | null | 0x21 | +| stop | — | null | 0x22 | +| goBack | — | null | 0x23 | +| goForward | — | null | 0x24 | +| executeJavaScript | code:String | null | 0x25 | +| setZoomLevel | level:double | null | 0x26 | +| editCommand | command:int | null | 0x38 | +| setVisible | visible:bool | null | 0x35 | +| find | text:String, forward:bool, matchCase:bool, findNext:bool | null | 0x27 | +| stopFind | clearSelection:bool | null | 0x28 | +| respondJsDialog | id:int, ok:bool, text:String | null | 0x29 | +| evalReturning | id:int, code:String | null | 0x2a | +| addJavaScriptChannel | name:String | null | 0x2b | +| setCookie | url, name, value, domain, path, sameSite : String; secure, httpOnly : bool | null | 0x2c | +| clearCookies | — | null | 0x2d | +| visitCookies | id:int, url:String | null | 0x2e | +| deleteCookie | url:String, name:String | null | 0x2f | +| showDevTools | — | null | 0x33 | +| enableAgentControl | — | `{wsUrl:String, token:String, port:int}` or FlutterError (`no_agent_control` when the session wasn't created with `agentControl:true`; `agent_control_unavailable` when it was, but joined a profile whose host started without it) | CDP relay (§8) | +| disableAgentControl | — | null | CDP relay (§8) | +| showEmojiPicker | — | Error `unsupported` | macOS-only (Character Palette) | +| imeSetComposition | text:String | null | 0x30 | +| imeCommitText | text:String | null | 0x31 | +| imeCancelComposition | — | null | 0x32 | Unknown verbs reply `NotImplemented` (a `MissingPluginException` in Dart), as on macOS. @@ -217,34 +213,35 @@ Method names + payload keys verbatim from the Swift `emit` sites. Every payload carries `sessionId:String`. `invokeMethod` MUST run on the platform thread (marshal from the reader thread). -| Method | Payload (beyond sessionId) | From opcode | Source | -|---|---|---|---| -| cursor | cursor:int (cef_cursor_type_t) | 0x03 | Swift:359-361 | -| loadingState | isLoading:bool, canGoBack:bool, canGoForward:bool | 0x05 | Swift:362-367 | -| title | title:String | 0x06 | Swift:368-370 | -| url | url:String | 0x07 | Swift:371-373 | -| loadError | code:int, url:String, text:String (split payload at first '\n') | 0x08 | Swift:374-378 | -| consoleMessage | level:int, message:String | 0x09 | Swift:379-383 | -| pageStarted | url:String | 0x0a | Swift:384-386 | -| pageFinished | url:String | 0x0b | Swift:387-389 | -| progress | progress:int | 0x0c | Swift:390-392 | -| newWindow | url:String | 0x0d | Swift:393-395 | -| findResult | count:int, activeMatchOrdinal:int, isFinal:bool | 0x0e | Swift:396-401 | -| jsDialog | id:int, type:int, message:String, defaultText:String | 0x0f | Swift:402-407 | -| evalResult | payload:String ("id:json") | 0x16 | Swift:408-410 | -| channelMessage | payload:String ("name:message") | 0x17 | Swift:411-413 | -| download | suggestedName:String | 0x18 | Swift:414-416 | -| imeCompositionBounds | x:int, y:int, w:int, h:int | 0x19 | Swift:417-421 | -| cookies | id:int, json:String | 0x1a | Swift:422-424 | -| onSurface | surfaceId:int, width:int, height:int (physical px) — Windows: surfaceId = the bridge-handle token as int64 | 0x01 (on surface (re)alloc) | Swift:425-433 | -| processGone | reason:String — "crashed" (host death, a failed/timed-out pipe write, a renderer the liveness sweep found hung, or 0x42 for one crash-looping browser) \| "locked" (the host logged "profile-locked") \| "createFailed" (0x1d, or the host died before kOpReady) \| "respawnFailed" \| "protocolMismatch(host=vN)" | host death / 0x1d / 0x42 / handshake | Swift:490, 521, 531, 541, 601 | -| paintStalled | — | first-present watchdog: every grace (10 s, `FLUTTER_CEF_FIRSTPAINT_MS`) that ends with no promoted 0x01, with a 0x37 re-kick — the macOS cadence | Swift:554-558 | +| Method | Payload (beyond sessionId) | From opcode | +|---|---|---| +| cursor | cursor:int (cef_cursor_type_t) | 0x03 | +| loadingState | isLoading:bool, canGoBack:bool, canGoForward:bool | 0x05 | +| title | title:String | 0x06 | +| url | url:String | 0x07 | +| loadError | code:int, url:String, text:String (split payload at first '\n') | 0x08 | +| consoleMessage | level:int, message:String | 0x09 | +| pageStarted | url:String | 0x0a | +| pageFinished | url:String | 0x0b | +| progress | progress:int | 0x0c | +| newWindow | url:String | 0x0d | +| findResult | count:int, activeMatchOrdinal:int, isFinal:bool | 0x0e | +| jsDialog | id:int, type:int, message:String, defaultText:String | 0x0f | +| evalResult | payload:String ("id:json") | 0x16 | +| channelMessage | payload:String ("name:message") | 0x17 | +| download | suggestedName:String | 0x18 | +| imeCompositionBounds | x:int, y:int, w:int, h:int | 0x19 | +| cookies | id:int, json:String | 0x1a | +| onSurface | surfaceId:int, width:int, height:int (physical px) — Windows: surfaceId = the bridge-handle token as int64 | 0x01 (on surface (re)alloc) | +| processGone | reason:String — "crashed" (host death, a failed/timed-out pipe write, a renderer the liveness sweep found hung, or 0x42 for one crash-looping browser) \| "locked" (the host logged "profile-locked") \| "createFailed" (0x1d, or the host died before kOpReady) \| "respawnFailed" \| "protocolMismatch(host=vN)" | host death / 0x1d / 0x42 / handshake | +| paintStalled | — | first-present watchdog: every grace (10 s, `FLUTTER_CEF_FIRSTPAINT_MS`) that ends with no promoted 0x01, with a 0x37 re-kick — the macOS cadence | ## 5. Handshake + lifecycle rules (carry-over) - Plugin sends NOTHING until it receives `kOpReady`; it then checks `protocolVersion == kCefHostProtocolVersion` (§2) and refuses (teardown + - `processGone protocolMismatch`) on skew (main.mm:100-108, Swift:528-533). + `processGone protocolMismatch`) on skew (macOS: the `CefOp.ready` case in + `CefProfileHost+Ipc.swift`). Frames queued before ready flush in order, and a queued `kOpCreateBrowser` is rewritten with its view's size as of the flush: the host takes seconds to start, and the view may have been laid out again meanwhile. @@ -254,10 +251,12 @@ thread (marshal from the reader thread). - `kOpLog "profile-locked"` (then exit code 2) = profile already open elsewhere -> `processGone reason:"locked"`. The plugin latches the log line: the pipe EOF usually arrives before the exit code exists. -- Present size-gate (LAW 4): the plugin promotes a presented bridge - handle to the Flutter texture ONLY when `{srcW,srcH}` matches the expected - `round(logical*dpr)` for the current size (±1 px); until then it keeps - serving the previous texture (main.mm:640-665 rationale). Only a promoted +- Present size-gate: every `WasResized` discards CEF's frame pool, and frames + at the old size or scale still arrive after it. So the plugin promotes a + presented bridge handle to the Flutter texture ONLY when `{srcW,srcH}` + matches the expected `round(logical*dpr)` for the current size (±1 px); + until then it keeps serving the previous texture (the rationale is on + `SendPresentLocked` in the macOS `render_handler.mm`). Only a promoted present counts as painted (it ends the first-present watchdog and feeds `sessionStats`); a rejected one shows nothing. - No create pacer on Windows: the plugin sends every create at once and does @@ -277,18 +276,21 @@ thread (marshal from the reader thread). other browsers carry on. When two different browsers burst within 10 s of each other the host's children can't start, so the host exits and every session gets `processGone("crashed")`. -- Bridge-handle identity (LAW 3): the host-minted legacy handle is the - identity Flutter sees; never key anything on CEF's per-callback - `shared_texture_handle` values (SPIKES.md S4). +- Bridge-handle identity: the host-minted legacy handle is the identity + Flutter sees; never key anything on CEF's per-callback + `shared_texture_handle` values, which are fresh every callback and alias + across sizes and browsers. - The plugin holds an opened `ID3D11Texture2D` ComPtr on the current bridge - handle for as long as it feeds it to Flutter (LAW 6 / S1 belt-1). + handle for as long as it feeds it to Flutter, so the texture stays alive + even after the host releases the bridge. - cef_host args: `--ipc=` `--profile-dir=` `--ephemeral` `--allowed-schemes=` (§6) and — for agent control (§8) — `--cdp-io-pipes= - ,` (cf. macOS args main.mm:32-38; `--cdp-port` TCP CDP is still - post-slice — the Dart-side `enableCdp`+named-profile assert already blocks the - unsafe combination, so `cdpPort` stays 0 on Windows). + ,` (cf. the macOS args in the `main.mm` header; `--cdp-port` TCP + CDP is not implemented on Windows — the Dart-side `enableCdp`+named-profile + assert already blocks the unsafe combination, so `cdpPort` stays 0 on + Windows). -## 6. Profile model (P6 foundation + P11 profile slice) +## 6. Profile model The plugin owns ONE `cef_host` process per **profile key** and multiplexes N browsers over it (one wire browserId each) — the macOS @@ -298,27 +300,28 @@ ephemeral session in a host group, or `"~ephemeral~"+sessionId` for the default (throwaway) case, so an ungrouped ephemeral session gets its own host, a group's sessions share one throwaway host (torn down with the last of them), and every view with the same non-null `profile` shares one host → one cookie jar → one -login (macOS: FlutterCefPlugin.swift ephemeralKey, CefProfileHost.swift:1-12). +login (macOS: `ephemeralKey` in FlutterCefPlugin.swift, and the +`CefProfileHost` class comment). ### 6.1 Profile-dir resolution (plugin side) The `create` verb's `profile` arg (String, omit-when-empty — §3) selects the mode. The plugin resolves an on-disk cache dir and always passes it as -`--profile-dir=` (macOS `resolveProfileDir`, -FlutterCefPlugin.swift:697-728): +`--profile-dir=` (macOS `resolveProfileDir` in +FlutterCefPlugin.swift): - **Ephemeral** (`profile` absent/empty): a unique throwaway dir `%TEMP%\flutter_cef_ephem___`, created + removed once the host's whole process tree is gone (a startup sweep reclaims dirs whose owning pid is dead), and the host is launched WITH `--ephemeral` (macOS uses `flutter_cef_ephem_` + - `--ephemeral=1`, FlutterCefPlugin.swift:704-708 / CefProfileHost.swift:286-288). + `--ephemeral=1`). - **Named / persistent** (`profile` non-empty): a stable dir `%LOCALAPPDATA%\flutter_cef\profiles\`, launched WITHOUT `--ephemeral`. Sanitize the name to `[A-Za-z0-9._-]` (every other char → `_`), and neutralize an all-dots leaf (`.`/`..`/`...`) to `_` so it can't escape the - `profiles\` container (macOS FlutterCefPlugin.swift:710-717 — mirror this - exactly, including the all-dots guard). + `profiles\` container (macOS `resolveProfileDir` — mirror this exactly, + including the all-dots guard). NOTE — Windows path root differs from macOS DELIBERATELY: macOS uses `//flutter_cef/profiles/`; Windows uses @@ -331,8 +334,8 @@ FlutterCefPlugin.swift:697-728): - **DACL**: create the named-profile dir (and its `profiles\` ancestor) with a current-user-SID-protected DACL — the same pattern `ipc_pipe.cpp` uses for the - pipe (audit fix #3). This is the Windows analogue of macOS's `0700` - owner-only chmod (FlutterCefPlugin.swift:706/722/726). The ACE is inheritable + pipe. This is the Windows analogue of macOS's `0700` owner-only chmod in + `resolveProfileDir`. The ACE is inheritable (OICI) so the files Chromium creates inside inherit it. Unlike macOS, which re-chmods an existing leaf, Windows applies the DACL only when it creates the dir: a dir from a prior run keeps whatever DACL it has. @@ -340,25 +343,25 @@ FlutterCefPlugin.swift:697-728): ### 6.2 Host side (`--profile-dir` → `root_cache_path`) `cef_host` maps `--profile-dir` to `CefSettings.root_cache_path` and sets -`settings.persist_session_cookies = true` (macOS main.mm:2854-2869). One +`settings.persist_session_cookies = true` (as macOS `main.mm` does). One `root_cache_path` is shared by every browser in the process — that is what makes the login shared. `persist_session_cookies` keeps session cookies across relaunch (harmless for ephemeral, required for "stay signed in" on a named profile). The -`--ephemeral` flag (main.mm:2726, `is_ephemeral`) marks the throwaway case so the -host's guards (CDP-on-named-profile refusal, and on macOS the mock-keychain +`--ephemeral` flag (`is_ephemeral` in macOS `main.mm`) marks the throwaway case +so the host's guards (CDP-on-named-profile refusal, and on macOS the mock-keychain downgrade) fire only for a REAL persistent profile — `--profile-dir` is set for both. ### 6.3 At-rest encryption — Windows DPAPI (NO macOS-style downgrade) -**KEY Windows security fact (SPIKES.md S2):** OSCrypt on Windows encrypts the +**KEY Windows security fact:** OSCrypt on Windows encrypts the cookie store with **DPAPI**, which is **always available and signing-independent**. So the macOS rule "ad-hoc build → mock keychain → downgrade a named profile to ephemeral (unless `FLUTTER_CEF_ALLOW_INSECURE_PROFILE=1`)" has **NO Windows analogue** — there is no mock-keystore state and no downgrade. A named profile on Windows simply persists, encrypted at rest, regardless of code signing. Concretely: `kOpReady`'s `readyFlags` bit0 (ad-hoc/mock-keychain build) -is **macOS-only; the Windows host always sends 0** (§2, main.mm:1664-1682), and +is **macOS-only; the Windows host always sends 0** (§2), and there is no `onInsecureProfileRefused` / re-home-to-ephemeral path on Windows. **Caveat (state it, do not hide it):** DPAPI's user-tier protection is @@ -373,28 +376,28 @@ platforms (FileVault/BitLocker is the backstop). Cookies act on the profile's ONE process-wide `CefCookieManager` (the shared jar §6.1), so a write/clear is visible to every browser in the host. The four -command opcodes and the one result event are fully specified in §2 (byte layouts, -with main.mm cites) and reached from Dart via the verbs in §3/§4 — summarized here: +command opcodes and the one result event are fully specified in §2 (byte layouts) +and reached from Dart via the verbs in §3/§4 — summarized here: -| Dart (controller) | Verb (§3) | Opcode (§2) | main.mm | -|---|---|---|---| -| `setCookie(url,name,value,domain,path,secure,httpOnly,sameSite)` | `setCookie` | 0x2c `{utf8 url\0name\0value\0domain\0path\0secure\0httpOnly\0sameSite}` (pad to 8) | 2495-2510 | -| `clearCookies()` | `clearCookies` | 0x2d `{}` | 2511-2514 | -| `getCookies({url})` → `List` | `visitCookies` | 0x2e `{u32 id}{utf8 url}` (empty = all) → **0x1a** `{u32 id}{utf8 json-array}` event | 2515-2522 | -| `deleteCookie(url,name)` | `deleteCookie` | 0x2f `{utf8 url\0name}` | 2523-2531 | +| Dart (controller) | Verb (§3) | Opcode (§2) | +|---|---|---| +| `setCookie(url,name,value,domain,path,secure,httpOnly,sameSite)` | `setCookie` | 0x2c `{utf8 url\0name\0value\0domain\0path\0secure\0httpOnly\0sameSite}` (pad to 8) | +| `clearCookies()` | `clearCookies` | 0x2d `{}` | +| `getCookies({url})` → `List` | `visitCookies` | 0x2e `{u32 id}{utf8 url}` (empty = all) → **0x1a** `{u32 id}{utf8 json-array}` event | +| `deleteCookie(url,name)` | `deleteCookie` | 0x2f `{utf8 url\0name}` | `getCookies` is the only round-trip: the plugin assigns `id`, sends `visitCookies`, and resolves the Dart `Future` when the `cookies` event (from opcode 0x1a — §4) arrives carrying the same `id` and a JSON array (`CefCookie.fromJson` per element, -cef_web_controller.dart:741-767). The JSON array shape MUST match macOS -byte-for-byte (main.mm's `DoVisitCookies` serializer) so a page cannot detect a +in `_handleCookies`). The JSON array shape MUST match macOS byte-for-byte (the +`DoVisitCookies` serializer in macOS `browser_ops.mm`) so a page cannot detect a Windows-vs-macOS divergence. -## 7. JS bridge, JS dialogs, find, zoom, downloads (P7 verb parity) +## 7. JS bridge, JS dialogs, find, zoom, downloads All opcode numbers and byte layouts are in §2; this section documents the -**string packings** and the **per-session message-router routing** that the P7 -verbs depend on, transcribed from main.mm with line cites. The Dart side +**string packings** and the **per-session message-router routing** that these +verbs depend on, transcribed from the macOS host. The Dart side (cef_web_controller.dart, cross-platform, unchanged for Windows) parses exactly these shapes; the Windows host MUST reproduce them byte-for-byte so a page cannot detect a Windows-vs-macOS divergence. @@ -406,9 +409,9 @@ The JS bridge (channels + `runJavaScriptReturningResult`) rides one (`window.cefQuery` / `window.cefQueryCancel`). Browser-side and renderer-side MUST use the SAME config or queries never route. -- **macOS reference:** browser-side router lives in the `Handler` (main.mm — - `router_->OnProcessMessageReceived`, main.mm:1495-1501; `OnQuery`, - main.mm:1472-1494). The renderer half is a SEPARATE process +- **macOS reference:** browser-side router lives in `HostClient` + (`host_client.mm` — `router_->OnProcessMessageReceived` and `OnQuery`). + The renderer half is a SEPARATE process (`process_helper.mm`): `CefMessageRouterRendererSide` with the default config, wired in `OnContextCreated` / `OnContextReleased` / `OnProcessMessageReceived`. - **Windows:** there is no separate helper exe — the SAME `cef_host.exe` is @@ -423,15 +426,14 @@ Page → host. The shim is injected NATIVELY (there is no Dart-injected shim): - `addJavaScriptChannel(name)` → verb `addJavaScriptChannel` → **0x2b kOpAddChannel** `{utf8 name}` (§2). The host validates `name` is a JS - identifier (`IsValidChannelName`, main.mm:362-375; ≤64 chars, + identifier (`IsValidChannelName`; ≤64 chars, `[A-Za-z_$][A-Za-z0-9_$]*`) and registers it process-globally in `g_channels` - (`DoAddChannel`, main.mm:2019-2035). Invalid names are dropped + logged, never - fatal. Dart pre-validates with the same regex (cef_web_controller.dart:165, - 669-671) and re-registers every channel on `create()` - (cef_web_controller.dart:541-544) so add-before-mount works. -- **Shim injection** (`InjectChannelShim`, main.mm:377-384) — injected into the - **MAIN frame ONLY** on every `OnLoadStart` (main.mm:1337-1343) and into the - current frame at registration time (main.mm:2034). The injected source is + (`DoAddChannel`). Invalid names are dropped + logged, never + fatal. Dart pre-validates with the same regex (`_channelNameRe`) and + re-registers every channel on `create()` so add-before-mount works. +- **Shim injection** (`InjectChannelShim`) — injected into the + **MAIN frame ONLY** on every `OnLoadStart` and into the + current frame at registration time (`DoAddChannel`). The injected source is exactly: ```js window['']={postMessage:function(m){window.cefQuery({request:'ch::'+String(m), @@ -439,19 +441,19 @@ Page → host. The shim is injected NATIVELY (there is no Dart-injected shim): ``` - **Page → host delivery.** `window..postMessage(m)` calls `window.cefQuery` with `request = "ch::"`. `OnQuery` - (main.mm:1487-1492) refuses subframe queries (privileged bridge; 403), strips + refuses subframe queries (privileged bridge; 403), strips the `"ch:"` prefix (3 bytes), and sends **0x17 kOpChannelMsg** `{utf8 - "name:message"}` (main.mm:1489). The plugin emits `channelMessage + "name:message"}`. The plugin emits `channelMessage {payload:"name:message"}` (§4); Dart splits at the FIRST `:` and dispatches to - the registered handler (`_handleChannelMessage`, cef_web_controller.dart:336-340) + the registered handler (`_handleChannelMessage`) — colons in the message body are preserved. - **Per-session routing (mandatory — channel_probe_shared).** `OnQuery` stamps - `slot_->browser_id` (the originating browser) on kOpChannelMsg - (main.mm:1489), and the plugin fans the event to only that session's Dart + `slot_->browser_id` (the originating browser) on kOpChannelMsg, + and the plugin fans the event to only that session's Dart channel handler. A message from tile A's page reaches A's handler, never B's, even on a shared host. This is an information-sharing boundary (the channel NAME is process-global), not a message-spoofing one. -- `removeJavaScriptChannel(name)` is **Dart-local** (cef_web_controller.dart:683-685): +- `removeJavaScriptChannel(name)` is **Dart-local**: it stops delivery to the handler but does NOT tear down the page-side shim (which is process-global on a shared profile), so the page may still post — those messages are dropped. No opcode. @@ -459,75 +461,69 @@ Page → host. The shim is injected NATIVELY (there is no Dart-injected shim): ### 7.3 `runJavaScriptReturningResult` — the eval round-trip - `runJavaScriptReturningResult(code)` assigns an `id` and sends verb - `evalReturning` → **0x2a kOpEvalReturning** `{u32 id}{utf8 code}` (§2, - cef_web_controller.dart:655-662). The host (`DoEvalReturning`, - main.mm:2001-2018) SPLICES `code` into a `window.cefQuery` call (not `eval()`, - so it survives a strict page CSP) that JSON-stringifies + `evalReturning` → **0x2a kOpEvalReturning** `{u32 id}{utf8 code}` (§2). + The host (`DoEvalReturning`) SPLICES `code` into a `window.cefQuery` call + (not `eval()`, so it survives a strict page CSP) that JSON-stringifies `{ok:true,v:()}` on success or `{ok:false,v:String(e)}` on throw, with `request = "eval::"`. -- `OnQuery` (main.mm:1481-1486) refuses subframe queries (403), strips the +- `OnQuery` refuses subframe queries (403), strips the `"eval:"` prefix (5 bytes), and sends **0x16 kOpEvalResult** `{utf8 - "id:json"}` (main.mm:1483). The plugin emits `evalResult + "id:json"}`. The plugin emits `evalResult {payload:"id:json"}` (§4); Dart splits at the FIRST `:`, matches the pending completer by `id`, and JSON-decodes the tail: `ok:true` completes with `v`, - `ok:false` completes with an `Exception('')` (`_handleEvalResult`, - cef_web_controller.dart:297-313). -- In-flight evals are failed (never leaked) on `pageStarted` - (cef_web_controller.dart:224-228), `processGone`, and `dispose` - (cef_web_controller.dart:317-324). + `ok:false` completes with an `Exception('')` (`_handleEvalResult`). +- In-flight evals are failed (never leaked) on `pageStarted`, `processGone`, + and `dispose` (`_failPendingEvals`). - `executeJavaScript(code)` is the fire-and-forget sibling → **0x25 kOpExecuteJs** `{utf8 code}` (§2), no result event. ### 7.4 JS dialogs — alert / confirm / prompt -- Host → page request: `CefJSDialogHandler::OnJSDialog` (main.mm:1279-1303) +- Host → page request: `CefJSDialogHandler::OnJSDialog` assigns a per-slot `id`, stashes the `CefJSDialogCallback`, and sends **0x0f - kOpJsDialog** `{u32 id}{u32 type}{u32 msgLen}{msg utf8}{defaultText utf8}` - (main.mm:1291-1301). `type`: 0=alert, 1=confirm, 2=prompt (main.mm:1286-1288). + kOpJsDialog** `{u32 id}{u32 type}{u32 msgLen}{msg utf8}{defaultText utf8}`. + `type`: 0=alert, 1=confirm, 2=prompt. The plugin emits `jsDialog {id,type,message,defaultText}` (§4). -- Dart (`_handleJsDialog`, cef_web_controller.dart:345-382) routes by `type` to +- Dart (`_handleJsDialog`) routes by `type` to `onJavaScriptAlertDialog` / `onJavaScriptConfirmDialog` / `onJavaScriptTextInputDialog`, then replies verb `respondJsDialog {id, ok, text}` → **0x29 kOpJsDialogResp** `{u32 id}{u8 ok}{utf8 text}` (§2). Unset handlers fall closed to sensible defaults (alert dismissed, confirm→OK, prompt→defaultText). A throwing handler fails closed (`ok=false`) but is reported via `FlutterError.reportError` (not silently swallowed). -- Host applies the answer: `DoJsDialogResp` (main.mm:1994-2000) looks up the +- Host applies the answer: `DoJsDialogResp` looks up the stashed callback by `id` and calls `Continue(ok, text)`, which returns the page's `alert`/`confirm`/`prompt`. `OnBeforeUnloadDialog` always allows - navigation (main.mm:1304-1309). + navigation. ### 7.5 Find-in-page - `find(text, forward, matchCase, findNext)` → verb `find` → **0x27 kOpFind** - `{u8 fwd}{u8 matchCase}{u8 findNext}{utf8 text}` (§2, - cef_web_controller.dart:858-868; host read main.mm:2452-2459 → `DoFind`). + `{u8 fwd}{u8 matchCase}{u8 findNext}{utf8 text}` (§2; host → `DoFind`). - `stopFind(clearSelection)` → verb `stopFind` → **0x28 kOpStopFind** `{u8 - clearSelection}` (absent = 1) (§2, cef_web_controller.dart:871-872; host - main.mm:2460-2465 → `DoStopFind`, main.mm:1991-1993). -- Result event: `CefFindHandler::OnFindResult` (main.mm:1260-1275) sends **0x0e - kOpFindResult** `{u32 count}{u32 activeOrdinal}{u8 final}` = 9 bytes - (main.mm:1265-1274). The plugin emits `findResult + clearSelection}` (absent = 1) (§2; host → `DoStopFind`). +- Result event: `CefFindHandler::OnFindResult` sends **0x0e + kOpFindResult** `{u32 count}{u32 activeOrdinal}{u8 final}` = 9 bytes. + The plugin emits `findResult {count, activeMatchOrdinal, isFinal}` (§4); Dart delivers a `CefFindResult` - to `onFindResult` (cef_web_controller.dart:239-245). ⌘F/Ctrl+F is surfaced to - the host via `CefWebView.onFind` (cef_web_view.dart:571-579) — the widget has + to `onFindResult`. ⌘F/Ctrl+F is surfaced to + the host via `CefWebView.onFind` — the widget has no find bar of its own. ### 7.6 Content zoom - `setZoomLevel(level)` → verb `setZoomLevel` → **0x26 kOpSetZoom** `{f64 - level}` (§2, cef_web_controller.dart:822-823; host read main.mm:2434-2438 → - `DoSetZoom`). `level` is a Chromium zoom LEVEL; the factor is `1.2^level` - (0 = 100%). The view wires Ctrl/⌘ +/-/0 to step it (cef_web_view.dart:559-570). + level}` (§2; host → `DoSetZoom`). `level` is a Chromium zoom LEVEL; the + factor is `1.2^level` (0 = 100%). The view wires Ctrl/⌘ +/-/0 to step it. No result event. ### 7.7 Downloads -- `CefDownloadHandler::OnBeforeDownload` (main.mm:1251-1257) allows the download +- `CefDownloadHandler::OnBeforeDownload` allows the download (CEF blocks downloads without a handler) and sends **0x18 kOpDownload** `{utf8 - suggestedName}` (main.mm:1254). The plugin emits `download {suggestedName}` - (§4); Dart invokes `onDownload(suggestedName)` - (cef_web_controller.dart:255-257). Informational only (no reply verb). + suggestedName}`. The plugin emits `download {suggestedName}` + (§4); Dart invokes `onDownload(suggestedName)`. + Informational only (no reply verb). - Windows continues with `show_dialog=true`, like macOS's Save panel, so nothing is written without the user's say. The dialog opens on `%USERPROFILE%\Downloads\`, where the leaf is the page's suggested name @@ -535,7 +531,7 @@ Page → host. The shim is injected NATIVELY (there is no Dart-injected shim): dots/spaces and DOS device names neutralized) and given a ` (n)` suffix when that file exists. -## 8. Agent control — CDP-over-pipe + the token-gated loopback relay (P9) +## 8. Agent control — CDP-over-pipe + the token-gated loopback relay An external CDP client (Playwright via `connectOverCDP`, agent-browser) drives a live, logged-in tile **without** an open debug port: Chromium speaks CDP over an @@ -543,14 +539,14 @@ inherited pipe (`--remote-debugging-pipe` + `--remote-debugging-io-pipes`, NUL-framed JSON), and a small token-gated LOOPBACK HTTP+WebSocket relay bridges a standard CDP client to that pipe. Transcribed from the macOS reference `CdpRelay.swift` (the canonical relay) + `CefProfileHost.swift` -(launchViaPosixSpawn / readCdpLoop / enableAgentControl); the Windows spawn is -the S3 recipe (`flutter_cef_spikes/s3`). **SINGLE-TILE scope:** one relay per -host (raw browser-level passthrough — the pipe carries exactly one page target); -the per-tile Target-domain filter + N-relay CDP-id multiplex (CEF-2b, -`CdpRelay.swift:560-884`) is a documented follow-up (the `scope_target_id` seam -in `windows/cdp_relay.h`). +(launchViaPosixSpawn / readCdpLoop / enableAgentControl); the Windows spawn +hands Chromium two inherited anonymous pipes instead of fds. **SINGLE-TILE +scope:** one relay per host (raw browser-level passthrough — the pipe carries +exactly one page target); the per-tile Target-domain filter + N-relay CDP-id +multiplex (the `scopeTargetId` machinery in `CdpRelay.swift`) is a documented +follow-up (the `scope_target_id` seam in `windows/cdp_relay.h`). -### 8.1 Launch — the CDP pipe (S3 recipe) +### 8.1 Launch — the CDP pipe The `create` arg `agentControl:true` (§3) switches the host launch mechanism. `HostProcess::Spawn(agent_control=true)` (windows/host_process.cpp): @@ -563,14 +559,14 @@ The `create` arg `agentControl:true` (§3) switches the host launch mechanism. inherits nothing: the IPC pipe is connected by NAME (`CreateFileW`) and the Job Object is assigned post-spawn — neither is an inherited handle. `bInheritHandles = TRUE` + `EXTENDED_STARTUPINFO_PRESENT` are added only on this path; the - non-agent spawn stays byte-identical (S2/S3 confirm bootstrap preserves the - handle list). + non-agent spawn stays byte-identical (the renamed bootstrap exe passes the + inherited handles through to the host DLL unchanged). - The child gets `--cdp-io-pipes=,` (decimal HANDLE values). cef_host's `OnBeforeCommandLineProcessing` (browser process only) translates it into Chromium's `--remote-debugging-pipe` + - `--remote-debugging-io-pipes=,`. Mirrors macOS main.mm's - `--cdp-pipe` → `remote-debugging-pipe` injection (fds 3/4 there; explicit HANDLE - values here). + `--remote-debugging-io-pipes=,`. Mirrors the macOS + `main.mm` `--cdp-pipe` → `remote-debugging-pipe` injection (fds 3/4 there; + explicit HANDLE values here). - The plugin keeps the two PARENT-side ends (`CdpTransport::read`/`write`) and runs an always-on `CdpReadLoop` that splits the NUL-framed CDP stream and fans each message to the current relay (mirrors `CefProfileHost.readCdpLoop` + @@ -615,10 +611,10 @@ multi-client `--remote-debugging-port`. ### 8.4 Deferred (N-tile Target multiplex) -Not implemented in the slice (single-tile is passthrough): `Target.getTargetInfo` +Not implemented on Windows (single-tile is passthrough): `Target.getTargetInfo` targetId resolution (`kOpResolveTargetId 0x36` → `kOpTargetId 0x1b`, still a logged drop host-side), the deny-by-default / fail-closed / flatten-only Target-domain filter, and the per-relay CDP-id rewrite/demux that lets N relays -share one browser-wide pipe. See `CdpRelay.swift:560-884` + -`CefProfileHost.swift:1460-1596` and the filter test vectors -`packages/flutter_cef_macos/macos/Classes/test/CdpRelayFilterTests.swift`. +share one browser-wide pipe. See `CdpRelay.swift` + `CefProfileHost+Cdp.swift` +and the filter test vectors +`packages/flutter_cef_macos/test/CdpRelayFilterTests.swift`. diff --git a/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h b/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h index c9edc0c..76e9827 100644 --- a/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h +++ b/packages/flutter_cef_windows/native/cef_host/cef_host_protocol.h @@ -18,11 +18,11 @@ namespace flutter_cef { -// Framing guard (main.mm:2347): minimum body = 4 (browserId) + 1 (op). +// Framing guard, as on macOS: minimum body = 4 (browserId) + 1 (op). constexpr uint32_t kMinBodyLen = 5; constexpr uint32_t kMaxBodyLen = 64u << 20; // 64 MiB -// ---- Big-endian codecs (mirror main.mm ReadU32BE/WriteU32BE/ReadF64BE) ---- +// ---- Big-endian codecs (mirror the macOS host's ipc.h) ---- inline uint32_t ReadU32BE(const uint8_t* p) { return (uint32_t(p[0]) << 24) | (uint32_t(p[1]) << 16) | diff --git a/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc b/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc index 573b87c..c49bb44 100644 --- a/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc +++ b/packages/flutter_cef_windows/native/cef_host/cef_host_win.cc @@ -3,24 +3,24 @@ // The Flutter plugin (packages/flutter_cef_windows/windows/) spawns one // cef_host per profile and drives N browsers in it over a named-pipe IPC. // The wire contract is PROTOCOL.md + cef_host_protocol.h in this directory — -// transcribed verbatim from the macOS reference -// packages/flutter_cef_macos/native/cef_host/main.mm, with ONE payload -// difference (LAW 10): kOpPresent carries {u64 bridgeHandle BE}{u32 srcW BE} +// transcribed verbatim from the macOS reference host +// (packages/flutter_cef_macos/native/cef_host/), with ONE payload +// difference: kOpPresent carries {u64 bridgeHandle BE}{u32 srcW BE} // {u32 srcH BE} where bridgeHandle is the DXGI LEGACY shared handle of the // host-minted D3D11_RESOURCE_MISC_SHARED bridge texture. // -// Ship shape (LAW 8, SPIKES.md S2): this builds as cef_host.dll exporting +// Ship shape: this builds as cef_host.dll exporting // RunConsoleMain, loaded by CEF's prebuilt bootstrapc.exe shipped RENAMED to // cef_host.exe beside it. bootstrapc's sandbox_info is forwarded to BOTH // CefExecuteProcess and CefInitialize, so the renderer, GPU and utility // children run sandboxed. FLUTTER_CEF_NO_SANDBOX=1 turns the sandbox off. // -// THE LAWS this file keeps (specs/windows-port/SPIKES.md): +// Rules this file keeps (measured on CEF 144 during the Windows port): // 1. external_begin_frame_enabled = FALSE; windowless_frame_rate = 60. -// (S4: with the external pump only the FIRST browser in the process ever -// paints. The macOS PumpBeginFrame pacer is NOT ported; everywhere main.mm -// calls SendExternalBeginFrame we use Invalidate(PET_VIEW) — with the -// internal frame timer ON that is sufficient to drive a repaint.) +// (With the external pump only the FIRST browser in the process ever +// paints. The macOS PumpBeginFrame pacer is NOT ported; everywhere the +// macOS host calls SendExternalBeginFrame we use Invalidate(PET_VIEW) — +// with the internal frame timer ON that is sufficient to drive a repaint.) // 2. OnAcceleratedPaint's NT handle is valid ONLY inside the callback: // OpenSharedResource1 + CopyResource to the legacy bridge INSIDE the // callback, synchronously. The NT handle is never stored. @@ -33,8 +33,9 @@ // consumer contract, PROTOCOL.md §5) can refuse stale-size frames. // 5. Bridge textures are D3D11_RESOURCE_MISC_SHARED (LEGACY handle via // IDXGIResource::GetSharedHandle, not NT) — what ANGLE/Flutter accepts. -// 6. (Plugin-side; supported here by keeping the retired bridge alive until -// the present announcing its replacement has been written to the pipe.) +// 6. The plugin holds its own opened reference to the bridge it shows; this +// side helps by keeping the retired bridge alive until the present +// announcing its replacement has been written to the pipe. // // Args (per-PROCESS / per-profile, as on macOS), read from the wide command // line as UTF-8: @@ -110,29 +111,31 @@ using namespace flutter_cef; // opcodes + BE codecs (cef_host_protocol.h) using Microsoft::WRL::ComPtr; // ---- Shared runtime state ---- -// The IPC pipe handle. Atomic for the same reason main.mm:166-172 makes the -// fd atomic: the reader thread, SendFrame (any CEF thread), and teardown all +// The IPC pipe handle. Atomic for the same reason the macOS host's g_ipc_fd +// (ipc.mm) is: the reader thread, SendFrame (any CEF thread), and teardown all // touch it. std::atomic g_ipc_pipe{INVALID_HANDLE_VALUE}; std::mutex g_ipc_write_mutex; // One hidden window per host process, passed to SetAsWindowless(parent) so -// dialogs/menus/IMM degrade gracefully (PLAN §4.3; S5 works either way but -// an HWND is the better default). +// dialogs/menus/IMM degrade gracefully (the WebAuthn passkey UI shows either +// way, but an HWND is the better default). HWND g_hidden_hwnd = nullptr; // Host-set navigation scheme allowlist (lowercased; --allowed-schemes=a,b). // Empty = allow all. `about` is always allowed. Enforced in -// HostClient::OnBeforeBrowse exactly like main.mm:335-342/1528-1565. +// HostClient::OnBeforeBrowse exactly like the macOS host (SchemeAllowed in +// host_state.mm, OnBeforeBrowse in host_client.mm). std::set g_allowed_schemes; -// Agent-control CDP-over-pipe (P9). "," decimal inherited-HANDLE -// values from the plugin's --cdp-io-pipes= switch (the S3 recipe): the browser -// READS CDP commands from and WRITES responses/events to . Set in -// RunConsoleMain (browser process only) BEFORE CefInitialize; OnBeforeCommand- -// LineProcessing then injects Chromium's --remote-debugging-pipe + -// --remote-debugging-io-pipes. Empty = agent control off (byte-identical to the -// pre-P9 launch). Mirrors macOS main.mm's --cdp-pipe translation. +// Agent-control CDP-over-pipe. "," decimal inherited-HANDLE +// values of two anonymous pipes, from the plugin's --cdp-io-pipes= switch: the +// browser READS CDP commands from and WRITES responses/events to +// . Set in RunConsoleMain (browser process only) BEFORE CefInitialize; +// OnBeforeCommandLineProcessing then injects Chromium's --remote-debugging-pipe +// + --remote-debugging-io-pipes. Empty = agent control off (byte-identical to +// a launch without agent control). Mirrors the macOS host's --cdp-pipe +// translation (OnBeforeCommandLineProcessing in main.mm). std::string g_cdp_io_pipes; // JS channels: on each MAIN-frame load OnLoadStart injects a @@ -219,7 +222,7 @@ void LogErr(const char* fmt, ...) { va_end(ap); } -// ---- Pipe I/O (framing per PROTOCOL.md §1 / main.mm:416-446) ---- +// ---- Pipe I/O (framing per PROTOCOL.md §1) ---- // // OVERLAPPED, EMPIRICALLY REQUIRED (found via pipe_probe, 2026-07-20): on a // SYNCHRONOUS pipe handle Windows serializes all I/O on the file object, so @@ -275,10 +278,10 @@ bool WriteAllPipe(HANDLE pipe, const void* buf, size_t len) { // Frame layout: [u32 bodyLen BE][u32 browserId BE][u8 opcode][payload]. // bodyLen = 4 + 1 + payloadLen. Assembled whole + written under the write -// mutex so a partial write never desyncs the peer (mirrors main.mm SendFrame). -// C3 ordering: the handle is snapshotted UNDER the write lock; teardown -// exchanges INVALID_HANDLE_VALUE + closes under this same lock, so a late -// paint-thread send can never write into a recycled handle. +// mutex so a partial write never desyncs the peer (mirrors the macOS host's +// SendFrame in ipc.mm). Ordering: the handle is snapshotted UNDER the write +// lock; teardown exchanges INVALID_HANDLE_VALUE + closes under this same lock, +// so a late paint-thread send can never write into a recycled handle. void SendFrame(uint32_t browser_id, uint8_t opcode, const void* payload, size_t payload_len) { if (g_ipc_pipe.load() == INVALID_HANDLE_VALUE) return; // racy early-out @@ -387,10 +390,10 @@ std::string TempDirUtf8() { // ---- Process-wide D3D11 device for the bridge-blit present path ---- // One device + immediate context for the whole cef_host process, created -// lazily on the first accelerated paint (mirrors main.mm's g_mtl_device -// singleton, main.mm:320-333). The immediate context is NOT thread-safe; -// OnAcceleratedPaint arrives on the CEF UI thread only, but g_d3d_mutex -// serializes it anyway (belt + suspenders, and future-proof). +// lazily on the first accelerated paint (mirrors the macOS host's +// g_mtl_device singleton in render_handler.mm). The immediate context is NOT +// thread-safe; OnAcceleratedPaint arrives on the CEF UI thread only, but +// g_d3d_mutex serializes it anyway (belt + suspenders, and future-proof). ComPtr g_d3d_device; ComPtr g_d3d_device1; ComPtr g_d3d_ctx; @@ -462,9 +465,9 @@ struct Slot; using BrowserOp = std::function; // Per-browser state: one cef_host process multiplexes N browsers, one Slot -// per plugin-assigned wire id (mirrors main.mm's Slot, with the IOSurface/Metal -// fields swapped for the D3D11 bridge and the macOS-only begin-frame-pump -// fields dropped per LAW 1). +// per plugin-assigned wire id (mirrors the macOS host's Slot in host_state.h, +// with the IOSurface/Metal fields swapped for the D3D11 bridge and the +// begin-frame-pump fields dropped: this host never uses external begin frames). // // The slot is registered by the IPC reader when the create frame arrives, so // every frame behind it (a resize, zoom, JS, input) finds it. The browser binds @@ -472,7 +475,7 @@ using BrowserOp = std::function; struct Slot { uint32_t browser_id = 0; // plugin-assigned wire id (>=1); NOT GetIdentifier(). CefRefPtr browser; - // H3 async-create dispose-loss guard: a dispose arriving while CreateBrowser + // Async-create dispose-loss guard: a dispose arriving while CreateBrowser // is in flight records intent here; OnAfterCreated honors it the instant the // browser binds. UI-thread-confined. bool close_requested = false; @@ -486,24 +489,26 @@ struct Slot { double created_dpr = 1.0; // Guards bridge / width / height / dpr for THIS browser. Per-slot so paints - // on independent browsers don't contend (main.mm surface_mutex). + // on independent browsers don't contend (as the macOS Slot's surface_mutex). std::mutex surface_mutex; - // The host-minted legacy MISC_SHARED bridge texture (LAWs 3/5) — the - // identity Flutter sees. Re-minted whenever CEF paints at a new size + // The host-minted legacy MISC_SHARED bridge texture (legacy handle because + // that is what ANGLE/Flutter accepts) — the identity Flutter sees, since + // CEF's own handle values alias. Re-minted whenever CEF paints at a new size // (producer-allocates: the bridge always matches the painted frame, so the // CopyResource below is always 1:1 — the wrong-size class is structurally - // gone, same rationale as main.mm EnsureSurfaceForPaint:749-785). + // gone, same rationale as the macOS host's EnsureSurfaceForPaint in + // render_handler.mm). ComPtr bridge; uint64_t bridge_handle = 0; // IDXGIResource::GetSharedHandle of `bridge` int bridge_w = 0; int bridge_h = 0; // g_d3d_epoch the current `bridge` was minted at. A mismatch means the device - // it lives on was lost/reset, so the bridge must be re-minted (#9). + // it lives on was lost/reset, so the bridge must be re-minted. uint64_t bridge_epoch = 0; - // Belt-1 friendliness: the OLD bridge is kept alive here across a re-mint - // until the kOpPresent announcing its replacement has been written to the - // pipe (the plugin also holds its own opened D3D reference — LAW 6 — this - // is the producer-side half of that belt). + // The OLD bridge is kept alive here across a re-mint until the kOpPresent + // announcing its replacement has been written to the pipe (the plugin also + // holds its own opened D3D reference to the bridge it shows; this is the + // producer-side half of keeping a shown texture alive). ComPtr retired_bridge; // Set under surface_mutex in OnBeforeClose BEFORE releasing `bridge`, so a // paint racing teardown doesn't re-mint a bridge for a closing browser. @@ -523,8 +528,9 @@ struct Slot { double dpr = 1.0; // Exact URLs armed for a host-trusted content load (kOpLoadTrusted / - // data:-file: create). Exact-URL matched + consumed in OnBeforeBrowse — - // see main.mm:222-233 for why it is URL-bound, not a one-shot flag. + // data:-file: create). Exact-URL matched + consumed in OnBeforeBrowse. It is + // URL-bound, not a one-shot flag, because OnBeforeBrowse arrives as a later + // task and a page navigation queued in the gap could consume a flag. // UI-thread only. std::multiset trusted_pending; @@ -533,11 +539,12 @@ struct Slot { uint32_t dialog_next = 1; // Visibility (kOpSetVisible -> WasHidden). UI-thread only. On Windows there - // is no begin-frame pump to gate (LAW 1); this drives WasHidden plus the - // hidden->visible repaint kick (the F-1 keystone, main.mm:1962-1985). + // is no begin-frame pump to gate (no external begin frames); this drives + // WasHidden plus the hidden->visible repaint kick (as the macOS host's + // DoSetVisible in browser_ops.mm). bool visible = true; - // F-1/F-2: a dpr change landing while hidden defers its screen-info - // re-assert to the hidden->visible edge. UI-thread only. + // A dpr change landing while hidden defers its screen-info re-assert to the + // hidden->visible edge, where the repaint kick picks it up. UI-thread only. bool needs_screen_info_on_show = false; // The JS channels this browser's consumer registered, before create (they @@ -626,9 +633,10 @@ class HostRenderHandler : public CefRenderHandler { } // The REAL display (DIP), not the tile: `window.screen == innerWidth` is a - // textbook headless/OSR fingerprint (see main.mm RealScreenDip:586-609 and - // commit 855042d). GetSystemMetrics returns px in this process's DPI - // context; divide by dpr for DIP. Falls back to a plausible frame. + // textbook headless/OSR fingerprint (see the macOS host's RealScreenDip in + // render_handler.mm and commit 855042d). GetSystemMetrics returns px in this + // process's DPI context; divide by dpr for DIP. Falls back to a plausible + // frame. static void RealScreenDip(double dpr, CefRect& full, CefRect& work) { const double s = dpr > 0.0 ? dpr : 1.0; const int pw = GetSystemMetrics(SM_CXSCREEN); @@ -647,7 +655,7 @@ class HostRenderHandler : public CefRenderHandler { } // Device scale so CEF renders logical*dpr (HiDPI-native) + real screen - // bounds and color depth (mirrors main.mm GetScreenInfo:614-625). + // bounds and color depth (mirrors the macOS host's GetScreenInfo). bool GetScreenInfo(CefRefPtr, CefScreenInfo& info) override { std::lock_guard lock(slot_->surface_mutex); info.device_scale_factor = static_cast(slot_->dpr); @@ -662,8 +670,8 @@ class HostRenderHandler : public CefRenderHandler { } // Plausible window frame at a non-zero offset, taller than the view by - // typical browser chrome (outerHeight > innerHeight like a real window) — - // main.mm GetRootScreenRect:633-638. + // typical browser chrome (outerHeight > innerHeight like a real window), as + // the macOS host's GetRootScreenRect does. bool GetRootScreenRect(CefRefPtr, CefRect& rect) override { std::lock_guard lock(slot_->surface_mutex); constexpr int kChromeH = 87; @@ -695,7 +703,7 @@ class HostRenderHandler : public CefRenderHandler { d.SampleDesc.Count = 1; d.Usage = D3D11_USAGE_DEFAULT; d.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE; - d.MiscFlags = D3D11_RESOURCE_MISC_SHARED; // LEGACY handle (LAW 5) + d.MiscFlags = D3D11_RESOURCE_MISC_SHARED; // LEGACY handle, for ANGLE ComPtr fresh; HRESULT hr = g_d3d_device->CreateTexture2D(&d, nullptr, &fresh); if (FAILED(hr)) { @@ -713,7 +721,7 @@ class HostRenderHandler : public CefRenderHandler { return false; } // Park the old bridge until the present carrying the NEW handle is sent - // (belt-1 friendliness — the plugin's own opened ref is the primary belt). + // (a courtesy — the plugin's own opened ref is what keeps it alive). slot_->retired_bridge = slot_->bridge; slot_->bridge = fresh; slot_->bridge_handle = @@ -726,9 +734,9 @@ class HostRenderHandler : public CefRenderHandler { } // Present the just-filled bridge, tagging the frame with the bridge handle - // (the identity, LAW 3) and the PHYSICAL px dims of the frame actually - // composited (the size-gate signal, LAW 4 / PROTOCOL.md §5). Caller holds - // slot_->surface_mutex. Windows payload (LAW 10): + // (the identity Flutter sees) and the PHYSICAL px dims of the frame actually + // composited (so the plugin's size-gate can refuse stale-size frames, + // PROTOCOL.md §5). Caller holds slot_->surface_mutex. Windows-only payload: // {u64 bridgeHandle BE}{u32 srcW BE}{u32 srcH BE} = 16 bytes. void SendPresentLocked(int srcW, int srcH) { uint8_t p[16]; @@ -737,7 +745,7 @@ class HostRenderHandler : public CefRenderHandler { WriteU32BE(p + 12, static_cast(srcH < 0 ? 0 : srcH)); SendFrame(slot_->browser_id, kOpPresent, p, 16); // The present announcing the new bridge is on the wire — the old bridge - // may now die (the plugin holds its own reference to it, LAW 6). + // may now die (the plugin holds its own reference to it). slot_->retired_bridge.Reset(); } @@ -795,7 +803,7 @@ class HostRenderHandler : public CefRenderHandler { } // Flush the device and check it survived; on loss reset it so the next paint - // re-creates it and every bridge (#9). False = don't present this frame. + // re-creates it and every bridge. False = don't present this frame. // Caller holds g_d3d_mutex. bool FlushAndCheckDeviceLocked(const char* where) { g_d3d_ctx->Flush(); @@ -822,11 +830,10 @@ class HostRenderHandler : public CefRenderHandler { slot_->popup_rect = rect; } - // GPU pixel path (LAW 2, the S1 cef_leg.cpp recipe): CEF's GPU process - // composites the page and hands an NT shared handle valid ONLY inside this - // callback. Open it on our device, copy it into the legacy bridge (the view) - // or the kept dropdown texture (PET_POPUP), Flush — all synchronously, never - // storing the NT handle. + // GPU pixel path: CEF's GPU process composites the page and hands an NT + // shared handle valid ONLY inside this callback. Open it on our device, copy + // it into the legacy bridge (the view) or the kept dropdown texture + // (PET_POPUP), Flush — all synchronously, never storing the NT handle. void OnAcceleratedPaint(CefRefPtr, PaintElementType type, const RectList&, const CefAcceleratedPaintInfo& info) override { @@ -848,13 +855,13 @@ class HostRenderHandler : public CefRenderHandler { int srcW = 0, srcH = 0; { std::lock_guard d3d(g_d3d_mutex); - // LAW 2: open + copy INSIDE the callback. `src` (the ComPtr) is - // released before we return; the NT handle itself is never kept. + // Open + copy INSIDE the callback (the NT handle dies with it). `src` + // (the ComPtr) is released before we return; the handle is never kept. ComPtr src; HRESULT hr = g_d3d_device1->OpenSharedResource1( nt, __uuidof(ID3D11Texture2D), &src); if (FAILED(hr)) { - // #9: distinguish a transient bad-handle miss from real device loss. On + // Distinguish a transient bad-handle miss from real device loss. On // loss, drop the cached device (+ bump the epoch) so the next paint // re-creates the device and every bridge — otherwise the dead device is // cached forever and the tile never repaints. @@ -898,7 +905,7 @@ class HostRenderHandler : public CefRenderHandler { CompositePopupLocked(); // CopyResource/Flush return void; device loss surfaces via // GetDeviceRemovedReason. If it went down mid-blit, reset and skip this - // frame — never present pixels from a dead device (#9). + // frame — never present pixels from a dead device. if (!FlushAndCheckDeviceLocked("blit")) return; } } @@ -1017,9 +1024,9 @@ std::string CrashBurstText() { void DoShutdown(); // defined below; the crash-loop exit reuses it -// Deny-default permission gate (verbatim port of main.mm:1068-1089): no -// per-site UI exists here, so every permission prompt and media-access -// request is denied up front. +// Deny-default permission gate (verbatim port of the macOS host's +// HostPermissionHandler in host_client.mm): no per-site UI exists here, so +// every permission prompt and media-access request is denied up front. class HostPermissionHandler : public CefPermissionHandler { public: bool OnRequestMediaAccessPermission( @@ -1045,7 +1052,8 @@ class HostPermissionHandler : public CefPermissionHandler { // it at 2 MB. So the plugin can hand us the HTML plus the URL it should appear // to come from, and we answer the MAIN-FRAME request for exactly that URL with // the HTML instead of the network. Everything else the page loads goes to the -// network as normal. Verbatim port of main.mm's g_authored. +// network as normal. Verbatim port of the macOS host's g_authored +// (authored_content.mm). // // Keyed by WIRE ID and written on the reader thread, not stored on the Slot: the // frame is sent immediately ahead of the create / load it belongs to and must be @@ -1170,10 +1178,11 @@ class HostClient : public CefClient, public: explicit HostClient(std::shared_ptr slot) : slot_(std::move(slot)) { // Browser-side message router (default config: window.cefQuery / - // cefQueryCancel) — the SAME config the renderer half uses (main.mm: - // 1228-1234). One router per HostClient, i.e. per browser: OnQuery below - // stamps slot_->browser_id so a page message is delivered ONLY to the - // originating session (per-session routing, channel_probe_shared). + // cefQueryCancel) — the SAME config the renderer half uses (as on macOS, + // host_client.mm + process_helper.mm). One router per HostClient, i.e. per + // browser: OnQuery below stamps slot_->browser_id so a page message is + // delivered ONLY to the originating session (per-session routing, + // channel_probe_shared). CefMessageRouterConfig config; router_ = CefMessageRouterBrowserSide::Create(config); router_->AddHandler(this, false); @@ -1223,7 +1232,7 @@ class HostClient : public CefClient, return true; } - // CefFindHandler (main.mm:1260-1275). + // CefFindHandler (as on macOS). void OnFindResult(CefRefPtr, int, int count, const CefRect&, int activeMatchOrdinal, bool finalUpdate) override { uint8_t p[9]; @@ -1233,7 +1242,7 @@ class HostClient : public CefClient, SendFrame(slot_->browser_id, kOpFindResult, p, 9); } - // CefJSDialogHandler (main.mm:1279-1315): forward alert/confirm/prompt; + // CefJSDialogHandler (as on macOS): forward alert/confirm/prompt; // the plugin answers via kOpJsDialogResp -> DoJsDialogResp -> Continue. bool OnJSDialog(CefRefPtr, const CefString&, JSDialogType dialog_type, const CefString& message_text, @@ -1300,7 +1309,7 @@ class HostClient : public CefClient, } } - // CefMessageRouter wiring (main.mm:1468-1501). The renderer half (HostApp + // CefMessageRouter wiring (as on macOS). The renderer half (HostApp // below) injects window.cefQuery; queries land here. Forward the request to // the plugin: "eval::" -> kOpEvalResult (a // runJavaScriptReturningResult result); "ch::" -> @@ -1352,7 +1361,7 @@ class HostClient : public CefClient, } // Route renderer->browser process messages through the message router - // (main.mm:1495-1501). This carries the cefQuery payloads that surface in + // (as on macOS). This carries the cefQuery payloads that surface in // OnQuery above. bool OnProcessMessageReceived(CefRefPtr browser, CefRefPtr frame, @@ -1373,7 +1382,7 @@ class HostClient : public CefClient, SendUtf8(slot_->browser_id, kOpPageStart, policy::TruncateUtf8(frame->GetURL().ToString(), policy::kMaxPagePayload)); - // SECURITY (main.mm:1339-1343): install the JS-channel shims ONLY into + // SECURITY (as on macOS): install the JS-channel shims ONLY into // the MAIN frame — injecting the privileged window. bridge into a // cross-origin subframe would hand an untrusted iframe that bridge. for (const auto& name : slot_->channels) InjectChannelShim(frame, name); @@ -1385,9 +1394,10 @@ class HostClient : public CefClient, SendUtf8(slot_->browser_id, kOpPageFinish, policy::TruncateUtf8(frame->GetURL().ToString(), policy::kMaxPagePayload)); - // C1 render floor (main.mm:1350-1363, minus the external begin-frame - // per LAW 1): re-assert size + damage when the main frame finishes so a - // coalesced/dropped first frame is re-driven. Hidden tiles stay paused. + // Render floor (the macOS host's OnLoadEnd, minus the external + // begin-frame this host never sends): re-assert size + damage when the + // main frame finishes so a coalesced/dropped first frame is re-driven + // instead of leaving a blank tile. Hidden tiles stay paused. if (browser && browser->GetHost() && slot_->visible) { auto h = browser->GetHost(); h->WasResized(); @@ -1428,11 +1438,11 @@ class HostClient : public CefClient, SendFrame(slot_->browser_id, kOpProgress, p, 4); } - // H3: the async create completes here. Bind the browser, ack kOpCreated, + // The async create completes here. Bind the browser, ack kOpCreated, // honor a deferred close, then catch the browser up on everything that // arrived while the create was in flight: visibility, a resize, and the ops - // kept in slot_->deferred, in order. No begin-frame pump to start (LAW 1 — - // CEF's internal frame timer drives paints). + // kept in slot_->deferred, in order. No begin-frame pump to start (CEF's + // internal frame timer drives paints). void OnAfterCreated(CefRefPtr browser) override { if (std::getenv("FLUTTER_CEF_DEBUG")) LogErr("[cef_host] OnAfterCreated wire=%u", slot_->browser_id); @@ -1469,12 +1479,13 @@ class HostClient : public CefClient, } } - // Popups. macOS (main.mm:1444-1451) splits by disposition: a SIZED popup - // (CEF_WOD_NEW_POPUP — the window.open-with-features shape OAuth/"Sign in with - // Google" uses) gets a REAL native window so window.opener/postMessage/ - // window.close work; everything else (target=_blank / plain new tab) diverts - // to kOpNewWindow and loads in-place. The native-window port is post-slice on - // Windows, so sized popups still divert in-tab — but that CANNOT complete the + // Popups. macOS (OnBeforePopup in host_client.mm) splits by disposition: a + // SIZED popup (CEF_WOD_NEW_POPUP — the window.open-with-features shape + // OAuth/"Sign in with Google" uses) gets a REAL native window so + // window.opener/postMessage/window.close work; everything else + // (target=_blank / plain new tab) diverts to kOpNewWindow and loads + // in-place. The native popup window is not ported to Windows yet, so sized + // popups still divert in-tab — but that CANNOT complete the // opener/postMessage handshake (it strands sign-in). Emit a distinct log per // CEF_WOD_NEW_POPUP so the regression is visible, not silent. bool OnBeforePopup(CefRefPtr, CefRefPtr, int, @@ -1488,11 +1499,11 @@ class HostClient : public CefClient, if (!logged.exchange(true)) SendLog(slot_->browser_id, "OnBeforePopup: sized popup (CEF_WOD_NEW_POPUP) diverted in-tab " - "— native OAuth-popup window is post-slice on Windows; " + "— Windows has no native OAuth-popup window yet, so " "window.open sign-in (opener/postMessage) will not complete " - "(macOS OpenNativeAuthPopup, main.mm:1444)"); + "(macOS opens one: OpenNativeAuthPopup)"); } - // Non-native case (matches macOS's non-popup branch, main.mm:1449-1451): + // Non-native case (matches macOS's non-popup branch): // load the target in this tile. if (!target_url.empty()) SendUtf8(slot_->browser_id, kOpNewWindow, @@ -1510,9 +1521,9 @@ class HostClient : public CefClient, return true; } - // Centralized per-browser teardown (main.mm:1510-1527): drop the routing - // entry, release the bridge under the slot lock (closing set FIRST so a - // racing paint can't re-mint), break the retain cycle. + // Centralized per-browser teardown (as the macOS OnBeforeClose): drop the + // routing entry, release the bridge under the slot lock (closing set FIRST so + // a racing paint can't re-mint), break the retain cycle. void OnBeforeClose(CefRefPtr browser) override { if (router_) router_->OnBeforeClose(browser); SetAuthoredDoc(slot_->browser_id, "", ""); @@ -1533,7 +1544,7 @@ class HostClient : public CefClient, } // Ctrl-key editing shortcuts as the FALLBACK they are in a real browser - // (main.mm OnKeyEvent). CefWebView sends Ctrl+C/X/V/A/Z/Y to the page as raw + // (as macOS OnKeyEvent). CefWebView sends Ctrl+C/X/V/A/Z/Y to the page as raw // keys, so an editor that owns its undo stack and selection (Monaco) handles // them in its keydown listener; Blink's own key bindings run the edit command // when the page doesn't. OnKeyEvent is called only for a key both left @@ -1575,13 +1586,13 @@ class HostClient : public CefClient, return new AuthoredRequestHandler(std::move(html)); } - // Navigation scheme allowlist (main.mm:1528-1565). Empty allowlist = allow + // Navigation scheme allowlist (as on macOS). Empty allowlist = allow // all. Main-frame only; kOpLoadTrusted's exact-URL exemptions are consumed // here. bool OnBeforeBrowse(CefRefPtr browser, CefRefPtr frame, CefRefPtr request, bool, bool) override { // Clean up any pending message-router queries for the frame about to - // navigate (main.mm:1563) — otherwise a channel query in flight across a + // navigate (as on macOS) — otherwise a channel query in flight across a // navigation can misfire or leak its callback. if (router_) router_->OnBeforeBrowse(browser, frame); if (g_allowed_schemes.empty()) return false; // allow @@ -1618,9 +1629,9 @@ class HostClient : public CefClient, // The one CefApp, used for BOTH the browser process (CefInitialize) and every // re-exec'd sub-process (CefExecuteProcess). CEF calls GetBrowserProcessHandler // in the browser process and GetRenderProcessHandler in the render process, so -// the SAME binary hosts both halves of CefMessageRouter (main.mm's split across -// main.mm + process_helper.mm collapses here — Windows re-runs cef_host.exe as -// the render subprocess, LAW 8). The renderer half owns a +// the SAME binary hosts both halves of CefMessageRouter (the macOS split across +// host_client.mm + the helper's process_helper.mm collapses here — Windows +// re-runs cef_host.exe as the render subprocess). The renderer half owns a // CefMessageRouterRendererSide with the DEFAULT config (must match the // browser-side HostClient config); it injects window.cefQuery into every frame // and relays cefQuery calls to the browser process. @@ -1635,7 +1646,7 @@ class HostApp : public CefApp, return this; } - // ---- Render-process half (process_helper.mm:24-57 counterpart) ---- + // ---- Render-process half (the macOS HelperApp counterpart) ---- // Render-process-only callback; create the renderer-side router here with the // default config (window.cefQuery / cefQueryCancel). void OnWebKitInitialized() override { @@ -1714,7 +1725,7 @@ class HostApp : public CefApp, const CefString& process_type, CefRefPtr command_line) override { (void)process_type; - // OSR establishment latency (main.mm:1578-1592): OSR views have no real + // OSR establishment latency (as on macOS): OSR views have no real // OS window, so Chromium backgrounds their renderers during first load. if (!std::getenv("FLUTTER_CEF_KEEP_BG_THROTTLE")) { command_line->AppendSwitch("disable-renderer-backgrounding"); @@ -1728,19 +1739,19 @@ class HostApp : public CefApp, command_line->AppendSwitch("disable-gpu-compositing"); } // Verbose Chromium logging (browser + propagated to children) only when - // explicitly debugging (macOS main.mm:1627-1631 pattern). + // explicitly debugging (the macOS host's pattern). if (std::getenv("FLUTTER_CEF_DEBUG")) { command_line->AppendSwitch("enable-logging"); command_line->AppendSwitchWithValue( "log-file", TempDirUtf8() + "cef_host_chromium.log"); command_line->AppendSwitchWithValue("v", "1"); } - // Agent-control CDP-over-pipe translation (S3, P9): the plugin passed the + // Agent-control CDP-over-pipe translation: the plugin passed the // two inherited pipe HANDLE values as --cdp-io-pipes=,; turn // them into Chromium's real switches. Browser process only (process_type // empty) — the renderer/GPU children never get the debugging pipe (and never - // inherited the handles). Mirrors macOS main.mm's --cdp-pipe injection. - // (disable-blink-features=AutomationControlled is post-slice.) + // inherited the handles). Mirrors the macOS --cdp-pipe injection in + // main.mm. (disable-blink-features=AutomationControlled is not ported.) if (process_type.empty() && !g_cdp_io_pipes.empty()) { command_line->AppendSwitch("remote-debugging-pipe"); command_line->AppendSwitchWithValue("remote-debugging-io-pipes", @@ -1749,9 +1760,9 @@ class HostApp : public CefApp, } // Announce readiness. Payload = {readyFlags, protocolVersion} - // (main.mm:1664-1682). Bit0 (ad-hoc/mock-keychain build) is a macOS-only - // concern — Windows sends 0. The plugin sends NOTHING until this arrives, - // then refuses on protocolVersion skew. + // (as the macOS OnContextInitialized). Bit0 (ad-hoc/mock-keychain build) is + // a macOS-only concern — Windows sends 0. The plugin sends NOTHING until this + // arrives, then refuses on protocolVersion skew. void OnContextInitialized() override { CEF_REQUIRE_UI_THREAD(); if (std::getenv("FLUTTER_CEF_DEBUG")) @@ -1787,15 +1798,15 @@ void DoCreateBrowser(std::shared_ptr slot, std::string url) { } CefWindowInfo window_info; // The hidden per-process WS_POPUP window as the windowless parent so - // dialogs/menus/IMM degrade gracefully (PLAN §4.3; null works too — S5). + // dialogs/menus/IMM degrade gracefully (null works too for WebAuthn). window_info.SetAsWindowless(g_hidden_hwnd); // GPU OSR: the GPU process composites and hands OnAcceleratedPaint an NT - // shared handle (the S1 pixel path). Without GPU compositing Chromium falls + // shared handle (the GPU pixel path). Without GPU compositing Chromium falls // back to OnPaint, which the render handler uploads into the same bridge. window_info.shared_texture_enabled = true; - // LAW 1: external_begin_frame_enabled stays FALSE (default). With the - // external pump only the FIRST browser in the process ever paints (S4); - // CEF's internal frame timer at windowless_frame_rate drives paints. + // external_begin_frame_enabled stays FALSE (default). With the external + // pump only the FIRST browser in the process ever paints; CEF's internal + // frame timer at windowless_frame_rate drives paints. window_info.external_begin_frame_enabled = false; CefBrowserSettings settings; settings.windowless_frame_rate = 60; @@ -1816,7 +1827,7 @@ void DoCreateBrowser(std::shared_ptr slot, std::string url) { slot->trusted_pending.insert(NormalizeAuthoredUrl(url)); } CefRefPtr client = new HostClient(slot); - // H3: ASYNC create. OnAfterCreated binds the browser and acks kOpCreated. + // ASYNC create. OnAfterCreated binds the browser and acks kOpCreated. // Document-start scripts + create-time JS channels ride into the renderer as // the browser's extra_info (see document_start.h) — the only channel that is // in place before the first document's scripts run. @@ -1824,7 +1835,7 @@ void DoCreateBrowser(std::shared_ptr slot, std::string url) { window_info, client, url, settings, TakeDocumentStartExtraInfo(wire_id, &slot->channels), nullptr); if (!dispatched) { - // H7: reclaim the slot + tell the plugin. + // Dispatch failed: reclaim the slot + tell the plugin. SendLog(wire_id, "createBrowser: CreateBrowser dispatch failed"); SendFrame(wire_id, kOpCreateFailed, nullptr, 0); SetAuthoredDoc(wire_id, "", ""); @@ -1855,16 +1866,16 @@ void DoDisposeBrowser(uint32_t wire_id) { if (slot->browser) { slot->browser->GetHost()->CloseBrowser(true); } else { - slot->close_requested = true; // H3 deferred-close intent + slot->close_requested = true; // OnAfterCreated closes it on bind slot->deferred.clear(); } } -// LAW 4: every WasResized discards CEF's frame pool; late frames at the old +// Every WasResized discards CEF's frame pool; late frames at the old // size still arrive and are refused by the plugin's size-gate (each present // carries its truthful dims). Producer-allocates: the bridge re-mints on the -// first NEW-size paint, not here (main.mm DoResize:1828-1874, with -// Invalidate standing in for SendExternalBeginFrame per LAW 1). +// first NEW-size paint, not here (the macOS DoResize in browser_ops.mm, with +// Invalidate standing in for SendExternalBeginFrame). void DoResize(const std::shared_ptr& slot, int w, int h, double dpr) { if (w < 1 || w > 16384 || h < 1 || h > 16384) { SendLog(slot->browser_id, "resize: out-of-range dims " + std::to_string(w) + @@ -1887,7 +1898,7 @@ void DoResize(const std::shared_ptr& slot, int w, int h, double dpr) { slot->browser->GetHost()->WasResized(); slot->browser->GetHost()->Invalidate(PET_VIEW); } else { - // F-2: hidden — no frame can result; defer the screen-info re-assert + + // Hidden — no frame can result; defer the screen-info re-assert + // repaint to the hidden->visible edge (DoSetVisible). if (dpr_changed) slot->needs_screen_info_on_show = true; } @@ -1925,9 +1936,9 @@ void DoEditCommand(Slot& slot, int command) { default: break; } } -// Off-screen render gating + the F-1 hidden->visible repaint keystone -// (main.mm DoSetVisible:1962-1985, Invalidate in place of the external -// begin-frame per LAW 1). +// Off-screen render gating + the hidden->visible repaint kick that stops a +// culled tile coming back blank (the macOS DoSetVisible in browser_ops.mm, +// Invalidate in place of the external begin-frame). void DoSetVisible(const std::shared_ptr& slot, bool visible) { const bool was_visible = slot->visible; slot->visible = visible; @@ -1957,7 +1968,7 @@ void DoJsDialogResp(const std::shared_ptr& slot, uint32_t id, bool ok, cb->Continue(ok, text); } -// runJavaScriptReturningResult (main.mm DoEvalReturning:2001-2018, verbatim). +// runJavaScriptReturningResult (the macOS DoEvalReturning, verbatim). // Evaluate the user expression and post its JSON result back through // window.cefQuery (-> HostClient::OnQuery -> kOpEvalResult "id:json", // correlated to the Dart Future). `code` is the trusted host's JS (same trust @@ -1975,7 +1986,7 @@ void DoEvalReturning(Slot& slot, uint32_t id, const std::string& code) { frame->ExecuteJavaScript(js, "", 0); } -// Registers a JS channel for one browser (UI thread; mirrors main.mm +// Registers a JS channel for one browser (UI thread; mirrors the macOS // DoAddChannel). The slot exists from its create frame on, before the browser // binds; a channel registered by then rides into the page at load. void DoAddChannel(const std::shared_ptr& slot, const std::string& name) { @@ -1990,7 +2001,7 @@ void DoAddChannel(const std::shared_ptr& slot, const std::string& name) { if (slot->browser) InjectChannelShim(slot->browser->GetMainFrame(), name); } -// ---- Cookies (global manager = the shared profile jar; main.mm:2040-2140) ---- +// ---- Cookies (global manager = the shared profile jar, as on macOS) ---- std::string JsonEscape(const std::string& s) { std::string out; @@ -2068,7 +2079,7 @@ const char* SameSiteToString(cef_cookie_same_site_t v) { } } -// COOKIE VERBS TAKE A WIRE ID, NOT A SLOT (parity with macOS main.mm). The jar is +// COOKIE VERBS TAKE A WIRE ID, NOT A SLOT (parity with macOS). The jar is // process-global, so nothing here needs the browser — the id only routes the // reply/log. Requiring a live slot on the reader thread silently DROPPED any cookie // verb that raced the create (the slot is registered by a later TID_UI task): a @@ -2123,7 +2134,7 @@ void DoShowDevTools(Slot& slot) { CefPoint()); } -// ---- IME (main.mm:2219-2248) ---- +// ---- IME (as on macOS) ---- void DoImeSetComposition(Slot& slot, const std::string& text) { CefString t(text); uint32_t len = static_cast(t.length()); @@ -2143,7 +2154,7 @@ void DoImeSetComposition(Slot& slot, const std::string& text) { } // type: 0=move 1=down 2=up 3=wheel 4=leave; button: 0=left 1=middle 2=right. -// x/y logical (DIP) view coords, exactly like macOS (main.mm:2251-2285). +// x/y logical (DIP) view coords, exactly like macOS. void DoPointer(Slot& slot, int type, int button, int click_count, uint32_t modifiers, double x, double y, double dx, double dy) { CefMouseEvent ev; @@ -2180,7 +2191,7 @@ void DoPointer(Slot& slot, int type, int button, int click_count, // type: 0=rawkeydown 2=keyup 3=char (cef_key_event_type_t). The Dart side // sends REAL Windows virtual-key codes in windowsKeyCode on Windows (and the // Unicode codepoint for char events) — native CefKeyEvent semantics, no -// translation needed (main.mm DoKey:2288-2305; character fields always set +// translation needed (as the macOS DoKey; character fields always set // per the CEF t=11650 de-dup note, harmless on Windows). void DoKey(Slot& slot, int type, uint32_t modifiers, int32_t windows_key_code, int32_t native_key_code, uint32_t character) { @@ -2195,7 +2206,8 @@ void DoKey(Slot& slot, int type, uint32_t modifiers, int32_t windows_key_code, slot.browser->GetHost()->SendKeyEvent(ev); } -// C1: watchdog repaint re-kick (main.mm DoInvalidate:2310-2318). LAW 1: no +// Force a repaint: the plugin's first-present watchdog sends kOpInvalidate +// when a browser's first frame never arrived (the macOS DoInvalidate). No // SendExternalBeginFrame — the internal frame timer honors Invalidate. void DoInvalidate(const std::shared_ptr& slot) { CEF_REQUIRE_UI_THREAD(); @@ -2223,7 +2235,7 @@ class ShutdownFlushCallback : public CefCompletionCallback { // Tear down the WHOLE process (kOpShutdown / pipe EOF): close every browser, // flush the cookie jar to disk, then quit the message loop. Per-slot cleanup -// runs in OnBeforeClose (main.mm DoShutdown:2324-2335). +// runs in OnBeforeClose (as the macOS DoShutdown in host_state.mm). // // WINDOWS COOKIE DURABILITY (no macOS analogue): unlike macOS — where the // implicit CefShutdown flush reliably persists the jar — a fast Windows teardown @@ -2234,7 +2246,7 @@ class ShutdownFlushCallback : public CefCompletionCallback { // flushed, so we FlushStore here and defer the quit into its completion — that is // what makes "stay signed in" survive relaunch. A bounded fallback quits anyway // if the callback never fires, so teardown can't wedge inside the plugin reaper's -// grace. macOS main.mm is unchanged (it does not need this). +// grace. The macOS host does not need this. void DoShutdown() { CEF_REQUIRE_UI_THREAD(); std::vector> slots; @@ -2257,10 +2269,10 @@ void DoShutdown() { } } -// Reader thread: decode frames, marshal onto the CEF UI thread (mirrors -// main.mm IpcReadLoop; payload layouts PROTOCOL.md §2). Per-browser ops go -// through PostBrowserOp, which holds an op that beats the browser's bind until -// OnAfterCreated instead of dropping it. +// Reader thread: decode frames, marshal onto the CEF UI thread (mirrors the +// macOS IpcReadLoop in ipc_reader.mm; payload layouts PROTOCOL.md §2). +// Per-browser ops go through PostBrowserOp, which holds an op that beats the +// browser's bind until OnAfterCreated instead of dropping it. void IpcReadLoop() { HANDLE pipe = g_ipc_pipe.load(); for (;;) { @@ -2592,8 +2604,8 @@ void IpcReadLoop() { } // Create the ONE hidden window this host passes to SetAsWindowless(parent). -// WS_POPUP, never shown. (PLAN §4.3: a null parent degrades dialogs/menus/ -// IMM; browser_platform_delegate_native_win.cc bails on !msg.hwnd.) +// WS_POPUP, never shown. (A null parent degrades dialogs/menus/IMM; +// browser_platform_delegate_native_win.cc bails on !msg.hwnd.) HWND CreateHiddenHostWindow() { WNDCLASSW wc = {}; wc.lpfnWndProc = DefWindowProcW; @@ -2608,7 +2620,7 @@ HWND CreateHiddenHostWindow() { // Entry point, invoked by CEF's bootstrap (bootstrapc.exe renamed to // cef_host.exe). sandbox_info is forwarded to both CefExecuteProcess and -// CefInitialize (SPIKES.md S2). +// CefInitialize, so the child processes run sandboxed. extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( int argc, char* argv[], @@ -2632,7 +2644,7 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( std::string profile_dir = GetSwitch(args, "--profile-dir="); std::string allowed = GetSwitch(args, "--allowed-schemes="); bool ephemeral = HasFlag(args, "--ephemeral"); - // Agent control (P9): "," inherited-HANDLE values. Stored in the + // Agent control: "," inherited-HANDLE values. Stored in the // file-global so OnBeforeCommandLineProcessing (called from CefInitialize // below) can inject the Chromium CDP-pipe switches. Empty when off. g_cdp_io_pipes = GetSwitch(args, "--cdp-io-pipes="); @@ -2672,7 +2684,7 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( } // Profile dir fallback: a per-pid ephemeral temp dir (defensive — the - // plugin always supplies --profile-dir, mirroring main.mm:34-35). + // plugin always supplies --profile-dir, mirroring the macOS main()). if (profile_dir.empty()) { profile_dir = TempDirUtf8() + "flutter_cef_ephem_" + std::to_string(GetCurrentProcessId()); @@ -2680,8 +2692,8 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( } CreateDirectoryW(Widen(profile_dir).c_str(), nullptr); // ok if it exists - // Cross-process single-writer lock on a PERSISTENT profile dir (C2, - // main.mm:2786-2806): exclusive-open /.flutter_cef.lock; on + // Cross-process single-writer lock on a PERSISTENT profile dir (as the + // macOS main()): exclusive-open /.flutter_cef.lock; on // contention emit the machine-parseable kOpLog "profile-locked" and exit 2 // (the plugin keys processGone("locked") on that pair). The handle is held // for the process lifetime — the OS drops it on exit. Ephemeral dirs are @@ -2730,11 +2742,12 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( // process is what makes login shared across the tiles on a profile // (CefCookieManager::GetGlobalManager -> this jar). persist_session_cookies // keeps session cookies across relaunch — set UNCONDITIONALLY, mirroring - // main.mm:2869 (harmless for an ephemeral host, whose dir the plugin's reaper - // deletes on teardown; required for a named profile's "stay signed in"). + // the macOS main() (harmless for an ephemeral host, whose dir the plugin's + // reaper deletes on teardown; required for a named profile's "stay signed + // in"). // - // AT-REST (SPIKES.md S2 / §7): Windows OSCrypt encrypts the cookie/login - // stores with DPAPI, which is ALWAYS available and signing-INDEPENDENT — so a + // AT-REST: Windows OSCrypt encrypts the cookie/login stores with DPAPI, + // which is ALWAYS available and signing-INDEPENDENT — so a // named profile persists directly here, with NO analogue of the macOS ad-hoc // "mock-keychain -> downgrade named profile to ephemeral" rule (there is no // readyFlags bit0 gate on Windows; OnContextInitialized sends 0). DPAPI is @@ -2748,11 +2761,11 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( else settings.log_severity = LOGSEVERITY_ERROR; - // no_sandbox and the sandbox_info passed here must agree (the S2-proven - // pair). Passing the real sandbox_info while no_sandbox=1 makes every child - // fail its mojo handshake ("Terminating current process after 15 seconds - // with no connection"), so the escape hatch passes nullptr. sandbox_info is - // always forwarded to CefExecuteProcess above: children must see it. + // no_sandbox and the sandbox_info passed here must agree. Passing the real + // sandbox_info while no_sandbox=1 makes every child fail its mojo handshake + // ("Terminating current process after 15 seconds with no connection"), so + // the escape hatch passes nullptr. sandbox_info is always forwarded to + // CefExecuteProcess above: children must see it. if (sandbox_info != nullptr && !sandboxed) LogErr("[cef_host] FLUTTER_CEF_NO_SANDBOX=1: running without the sandbox"); if (!CefInitialize(main_args, settings, app, @@ -2762,7 +2775,7 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( return 10; } - // Reader thread (same model as macOS main.mm: reader posts to TID_UI via + // Reader thread (same model as the macOS host: reader posts to TID_UI via // CefPostTask; CefRunMessageLoop owns the main thread). std::thread reader; if (g_ipc_pipe.load() != INVALID_HANDLE_VALUE) reader = std::thread(IpcReadLoop); @@ -2786,9 +2799,9 @@ extern "C" CEF_BOOTSTRAP_EXPORT int RunConsoleMain( // Teardown: invalidate the pipe FIRST (atomic exchange), THEN close — so a // late SendFrame from a CEF thread can't write into a recycled handle - // (mirrors main.mm:2910-2918 C3 ordering). Closing the pipe also unblocks - // the reader's ReadFile, bounding the join... except a reader blocked in - // ReadFile on a still-open pipe: cancel it explicitly first. + // (mirrors the macOS host's teardown ordering). Closing the pipe also + // unblocks the reader's ReadFile, bounding the join... except a reader + // blocked in ReadFile on a still-open pipe: cancel it explicitly first. { std::lock_guard lock(g_ipc_write_mutex); HANDLE old = g_ipc_pipe.exchange(INVALID_HANDLE_VALUE); diff --git a/packages/flutter_cef_windows/native/cef_host/fetch_cef.ps1 b/packages/flutter_cef_windows/native/cef_host/fetch_cef.ps1 index e7438e8..3b1a9f7 100644 --- a/packages/flutter_cef_windows/native/cef_host/fetch_cef.ps1 +++ b/packages/flutter_cef_windows/native/cef_host/fetch_cef.ps1 @@ -8,7 +8,7 @@ # %LOCALAPPDATA%/flutter_cef/. If neither exists, download the pinned # tarball from cef-builds.spotifycdn.com, verify it against the SHA-256 pinned # in cef_pin.txt (fail closed), extract with native tar.exe (bsdtar handles -# .tar.bz2, docs/history/windows-port/SPIKES.md S6), and cache it under +# .tar.bz2), and cache it under # %LOCALAPPDATA%/flutter_cef for later builds. The CMake side checks the # resolved tree's include/cef_version.h against the same pin. @@ -78,8 +78,7 @@ Info "fetch_cef: extracting" # Use the Windows system bsdtar by FULL PATH. A bare `tar` on a CI runner # resolves to Git's bundled MSYS GNU tar, which reads "C:\...tarball" as a # host:path remote spec ("Cannot connect to C:"). System32\tar.exe is libarchive -# (bsdtar), handles .tar.bz2 and drive-letter paths natively -# (docs/history/windows-port/SPIKES.md S6). +# (bsdtar), handles .tar.bz2 and drive-letter paths natively. $SystemTar = Join-Path $env:SystemRoot 'System32\tar.exe' if (-not (Test-Path $SystemTar)) { $SystemTar = 'tar.exe' } & $SystemTar -xf $tarball -C $tmp diff --git a/packages/flutter_cef_windows/native/cef_host/test/pipe_probe.cc b/packages/flutter_cef_windows/native/cef_host/test/pipe_probe.cc index 6bcf8ad..f28fc8b 100644 --- a/packages/flutter_cef_windows/native/cef_host/test/pipe_probe.cc +++ b/packages/flutter_cef_windows/native/cef_host/test/pipe_probe.cc @@ -1,8 +1,8 @@ // pipe_probe — standalone gate test for the Windows cef_host. // // Acts as the PLUGIN side of the IPC contract (PROTOCOL.md): creates the -// named pipe server, spawns cef_host.exe against it, then drives the slice -// vertical: kOpReady (version handshake) -> kOpCreateBrowser 1024x768@1.0 -> +// named pipe server, spawns cef_host.exe against it, then drives one browser +// end to end: kOpReady (version handshake) -> kOpCreateBrowser 1024x768@1.0 -> // kOpCreated -> first kOpPresent within 20s -> kOpNavigate to a second URL -> // url/title/loadState events -> kOpShutdown -> clean host exit. // diff --git a/packages/flutter_cef_windows/windows/CMakeLists.txt b/packages/flutter_cef_windows/windows/CMakeLists.txt index 330c249..f2c4fa7 100644 --- a/packages/flutter_cef_windows/windows/CMakeLists.txt +++ b/packages/flutter_cef_windows/windows/CMakeLists.txt @@ -1,6 +1,6 @@ # flutter_cef_windows plugin build. # -# S6a/S6b findings baked in (specs/windows-port/SPIKES.md): +# Toolchain findings from the Windows port, baked in: # - keep apply_standard_settings (CEF headers are NOT linked here — the # plugin talks to cef_host over IPC; CEF lives only in native/cef_host); # - NOMINMAX per target (any TU that ever includes cef_types_win.h needs it, @@ -49,7 +49,7 @@ apply_standard_settings(${PLUGIN_NAME}) set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) -# S6a: NOMINMAX everywhere windows.h is reachable. +# NOMINMAX everywhere windows.h is reachable. target_compile_definitions(${PLUGIN_NAME} PRIVATE NOMINMAX) target_include_directories(${PLUGIN_NAME} INTERFACE @@ -62,7 +62,7 @@ target_link_libraries(${PLUGIN_NAME} PRIVATE flutter_wrapper_plugin d3d11 dxgi - # Agent-control CDP relay (P9): winsock (loopback HTTP+WS server) + bcrypt + # Agent-control CDP relay: winsock (loopback HTTP+WS server) + bcrypt # (SHA-1 Sec-WebSocket-Accept + BCryptGenRandom token). cdp_relay.cpp. ws2_32 bcrypt @@ -121,7 +121,7 @@ add_dependencies(${PLUGIN_NAME} flutter_cef_windows_host) # Files the app needs beside its exe: the host pair + the CEF runtime. # (Flat CEF layout on Windows; libcef.dll etc. resolve from the exe dir. -# Set from what cef_leg (S1) needed to run windowless+GPU.) +# The set a windowless, GPU-composited CEF needs at run time.) set(flutter_cef_windows_bundled_libraries "${CEF_HOST_DIST_DIR}/cef_host.exe" "${CEF_HOST_DIST_DIR}/cef_host.dll" @@ -149,8 +149,8 @@ set(flutter_cef_windows_bundled_libraries # DESTINATION would resolve against CMAKE_INSTALL_PREFIX, whose genex is # NOT re-evaluated inside this subdirectory's install script). BINARY_NAME # is set by the app-level CMakeLists before generated_plugins.cmake runs. -# TODO(builder-plugin): ship the full locale set (or make it configurable) -# once the slice is green — en-US is the required minimum. +# TODO(builder-plugin): ship the full locale set (or make it configurable); +# en-US is the required minimum. install(FILES "${FLUTTER_CEF_CEF_ROOT}/Resources/locales/en-US.pak" DESTINATION "$/locales" COMPONENT Runtime) diff --git a/packages/flutter_cef_windows/windows/cdp_relay.cpp b/packages/flutter_cef_windows/windows/cdp_relay.cpp index 803bf25..7cb6034 100644 --- a/packages/flutter_cef_windows/windows/cdp_relay.cpp +++ b/packages/flutter_cef_windows/windows/cdp_relay.cpp @@ -160,8 +160,8 @@ bool CdpRelay::Start() { DLog("socket() failed"); return false; } - // SO_EXCLUSIVEADDRUSE, NOT SO_REUSEADDR (PLAN §4.5): on Windows SO_REUSEADDR - // is a port-HIJACK primitive — it lets another same-user socket bind our + // SO_EXCLUSIVEADDRUSE, NOT SO_REUSEADDR: on Windows SO_REUSEADDR is a + // port-HIJACK primitive — it lets another same-user socket bind our // already-bound loopback port and intercept the client's next connection // (stealing the bearer token off the upgrade). Exclusive-use forbids that. BOOL on = TRUE; @@ -537,9 +537,9 @@ void CdpRelay::FrameLoop(SOCKET fd) { assembling = !fin; if (fin) { if (assembling_text) { - // Single-tile passthrough: forward the CDP message verbatim. (The - // CEF-2b filterClientToPipe + rewriteOutgoingId hooks would go here - // for the N-tile follow-up — see the header comment.) + // Single-tile passthrough: forward the CDP message verbatim. + // (CdpRelay.swift's filterClientToPipe + rewriteOutgoingId would + // go here for the N-tile follow-up — see the header comment.) send_to_pipe_(std::string(msg.begin(), msg.end())); } msg.clear(); @@ -567,8 +567,8 @@ void CdpRelay::FrameLoop(SOCKET fd) { } void CdpRelay::DeliverToClient(const std::string& json) { - // Single-tile passthrough: deliver verbatim. (The CEF-2b demux + filter - // seam — demuxPipeToClient/filterPipeToClient — would gate this for N-tile.) + // Single-tile passthrough: deliver verbatim. (CdpRelay.swift's per-tile + // demuxPipeToClient/filterPipeToClient would gate this for N-tile.) std::vector payload(json.begin(), json.end()); std::lock_guard lock(client_lock_); if (client_sock_ == INVALID_SOCKET) return; diff --git a/packages/flutter_cef_windows/windows/cdp_relay.h b/packages/flutter_cef_windows/windows/cdp_relay.h index c1cd090..2483e2e 100644 --- a/packages/flutter_cef_windows/windows/cdp_relay.h +++ b/packages/flutter_cef_windows/windows/cdp_relay.h @@ -4,7 +4,7 @@ // packages/flutter_cef_macos/macos/Classes/CdpRelay.swift (the canonical // relay): it re-exposes cef_host's CDP-over-pipe (Chromium // --remote-debugging-pipe + --remote-debugging-io-pipes, NUL-framed JSON on -// two inherited anonymous pipes — the S3 recipe) to a standard CDP client +// two inherited anonymous pipes) to a standard CDP client // (Playwright via connectOverCDP / agent-browser) as a loopback HTTP+WebSocket // endpoint. The Swift protocol logic ports directly: the POSIX socket calls // (socket/bind/listen/accept/recv/send, fd) become winsock (WSAStartup, @@ -24,11 +24,11 @@ // - Single active client — a second concurrent ws upgrade is rejected 503. // - CSPRNG token (BCryptGenRandom), constant-time compared. // -// SINGLE-TILE SCOPE (P9): this relay is the raw browser-level PASSTHROUGH -// (CEF-2a) — the CDP pipe carries exactly ONE page target (the one tile), so a -// passthrough is functionally correct and safe (there is no sibling tile to -// hide). The per-tile Target-domain FILTER + N-relay CDP-id MULTIPLEX (CEF-2b — -// the `scopeTargetId`/`relayId` machinery in CdpRelay.swift:560-884) is the +// SINGLE-TILE SCOPE: this relay is the raw browser-level PASSTHROUGH — the +// CDP pipe carries exactly ONE page target (the one tile), so a passthrough is +// functionally correct and safe (there is no sibling tile to hide). The +// per-tile Target-domain FILTER + N-relay CDP-id MULTIPLEX (the +// `scopeTargetId` filter and `CdpPipeIds` remap in CdpRelay.swift) is the // documented follow-up for N-tile Target multiplexing and is NOT implemented // here. The `scope_target_id` seam below is left in place for it (empty = // passthrough today); see the class comment where the filter hooks would go. @@ -64,10 +64,10 @@ class CdpRelay : public std::enable_shared_from_this { // safe to call from the relay's frame-loop thread. using SendToPipe = std::function; - // `send_to_pipe` bridges client->pipe. `scope_target_id` is the CEF-2b - // per-tile filter seam: empty (the P9 single-tile default) is the raw - // browser-level passthrough; a non-empty targetId would engage the - // Target-domain filter (NOT implemented in the slice — see the file comment). + // `send_to_pipe` bridges client->pipe. `scope_target_id` is the per-tile + // filter seam: empty (the single-tile default) is the raw browser-level + // passthrough; a non-empty targetId would engage the Target-domain filter + // (NOT implemented yet — see the file comment). explicit CdpRelay(SendToPipe send_to_pipe, std::string scope_target_id = std::string()); ~CdpRelay(); @@ -120,7 +120,7 @@ class CdpRelay : public std::enable_shared_from_this { void WriteRaw(SOCKET fd, const std::string& s); const SendToPipe send_to_pipe_; - const std::string scope_target_id_; // CEF-2b seam (empty = passthrough) + const std::string scope_target_id_; // per-tile seam (empty = passthrough) std::string token_; uint16_t port_ = 0; diff --git a/packages/flutter_cef_windows/windows/flutter_cef_plugin.cpp b/packages/flutter_cef_windows/windows/flutter_cef_plugin.cpp index aad22c9..f0f8561 100644 --- a/packages/flutter_cef_windows/windows/flutter_cef_plugin.cpp +++ b/packages/flutter_cef_windows/windows/flutter_cef_plugin.cpp @@ -126,7 +126,7 @@ std::vector GetStringList(const flutter::EncodableMap& m, return out; } -// A `profile` arg is "named" only when present AND non-empty (Swift:288-289). +// A `profile` arg is "named" only when present AND non-empty (as on macOS). bool HasNamedProfile(const flutter::EncodableMap& m) { auto it = m.find(flutter::EncodableValue(std::string("profile"))); if (it == m.end()) return false; @@ -197,10 +197,10 @@ flutter::EncodableValue Ev(const char* s) { } // Sanitize a profile name to a filesystem-safe leaf: anything outside -// [A-Za-z0-9._-] -> '_' (macOS resolveProfileDir, FlutterCefPlugin.swift: -// 710-717). A leaf of all dots ("."/".."/"...") would resolve to the -// profiles/ container or its PARENT — a one-level containment escape whose -// protected-DACL apply would clobber a shared ancestor — so neutralize it. +// [A-Za-z0-9._-] -> '_' (as macOS resolveProfileDir does). A leaf of all dots +// ("."/".."/"...") would resolve to the profiles/ container or its PARENT — a +// one-level containment escape whose protected-DACL apply would clobber a +// shared ancestor — so neutralize it. std::wstring SanitizeProfileLeaf(const std::string& profile) { std::wstring out; out.reserve(profile.size()); @@ -265,8 +265,8 @@ void FlutterCefPlugin::RegisterWithRegistrar( FlutterCefPlugin::FlutterCefPlugin(flutter::PluginRegistrarWindows* registrar) : registrar_(registrar) { // Reclaim ephemeral profile dirs orphaned by a previous crash/kill before we - // start minting new ones (macOS sweepStaleEphemeralProfiles at plugin init, - // FlutterCefPlugin.swift:97-109). No host is live yet. + // start minting new ones (as macOS sweepStaleEphemeralProfiles does at plugin + // init). No host is live yet. SweepStaleEphemeralProfiles(); channel_ = std::make_unique>( registrar->messenger(), "flutter_cef", @@ -479,7 +479,7 @@ void FlutterCefPlugin::HandleMethodCall( } if (method == "pointer") { // {u8 type}{u8 button}{u8 clickCount}{u8 pad}{u32 modifiers} - // {f64 x}{f64 y}{f64 dx}{f64 dy} = 40 bytes (main.mm:2560-2570). + // {f64 x}{f64 y}{f64 dx}{f64 dy} = 40 bytes. if (s) { std::vector p; p.reserve(40); @@ -519,9 +519,9 @@ void FlutterCefPlugin::HandleMethodCall( if (s) { const bool visible = GetBool(args, "visible", true); SendOrQueue(s, kOpSetVisible, {visible ? uint8_t{1} : uint8_t{0}}); - // C1 watchdog: a hidden CEF browser produces no frames by design, so - // suspend the first-present watchdog while hidden and re-arm it on show - // if still blank (macOS noteVisibility, CefProfileHost.swift:706-729). + // First-present watchdog: a hidden CEF browser produces no frames by + // design, so suspend the watchdog while hidden and re-arm it on show if + // still blank (as macOS CefProfileHost.noteVisibility does). s->visible = visible; if (!visible) { CancelWatchdog(s); @@ -614,7 +614,7 @@ void FlutterCefPlugin::HandleMethodCall( result->Success(); return; } - // ---- Cookies (shared per-profile jar; verbs match main.mm byte-for-byte). + // ---- Cookies (shared per-profile jar; verbs match the macOS host exactly). // The four verbs operate on the process-wide CefCookieManager inside the // host, so on a shared persistent profile every session sees one jar (macOS // parity). getCookies (Dart) rides the visitCookies verb, correlated back by @@ -622,7 +622,7 @@ void FlutterCefPlugin::HandleMethodCall( if (method == "setCookie") { if (s) { // {utf8 url\0name\0value\0domain\0path\0secure\0httpOnly\0sameSite} - // (main.mm kOpSetCookie). Hosts older than the attribute fields read + // (kOpSetCookie). Hosts older than the attribute fields read // only the first five. const std::string payload = GetString(args, "url") + '\0' + GetString(args, "name") + '\0' + @@ -648,7 +648,7 @@ void FlutterCefPlugin::HandleMethodCall( } if (method == "visitCookies") { if (s) { - // {u32 id}{utf8 url} — url empty = all (main.mm kOpVisitCookies:154). + // {u32 id}{utf8 url} — url empty = all. std::vector p; AppendU32(p, static_cast(GetInt(args, "id", 0))); AppendUtf8(p, GetString(args, "url")); @@ -659,7 +659,7 @@ void FlutterCefPlugin::HandleMethodCall( } if (method == "deleteCookie") { if (s) { - // {utf8 url\0name} (main.mm kOpDeleteCookie:155). + // {utf8 url\0name}. const std::string payload = GetString(args, "url") + '\0' + GetString(args, "name"); std::vector p(payload.begin(), payload.end()); @@ -795,7 +795,7 @@ void FlutterCefPlugin::HandleCreate( spec.profile = GetString(args, "profile"); spec.host_group = GetString(args, "hostGroup"); // Agent control: CDP-over-pipe launch. Off by default; when set, the host - // is spawned with the two inherited CDP pipes (the S3 recipe). Independent + // is spawned with the two inherited CDP pipes. Independent // of enableCdp (TCP) — the pipe path never opens a listening port. spec.agent_control = GetBool(args, "agentControl", false); spec.channels = GetStringList(args, "channels"); @@ -1069,7 +1069,7 @@ FlutterCefPlugin::Host* FlutterCefPlugin::ResolveOrSpawnHost( const uint64_t gen = host->generation; // Reader thread: frames + EOF, posted to the platform thread. host_key + gen // are captured by value so a stale OLD-host event posted during the reaper - // grace of a same-profile respawn is dropped on drain (C1). + // grace of a same-profile respawn is dropped on drain. host->pipe->StartReader( [this, host_key, gen](uint32_t browser_id, uint8_t opcode, std::vector payload) { @@ -1404,7 +1404,7 @@ void FlutterCefPlugin::SendToHost(Host* host, uint32_t browser_id, PostEvent(std::move(e)); } -// ---- agent control (P9) ---- +// ---- agent control ---- // static void FlutterCefPlugin::CdpReadLoop(std::shared_ptr transport) { @@ -1465,7 +1465,7 @@ void FlutterCefPlugin::EnableAgentControl( "agentControl: true"); return; } - // SECURITY (P9 single-tile invariant, see HandleCreate): the browser-level + // SECURITY (single-tile invariant, see HandleCreate): the browser-level // relay would expose every sibling tile on this host to the agent. Until the // per-tile Target filter lands, refuse a grant while the host serves >1 tile. if (h->browsers.size() > 1) { @@ -1593,8 +1593,8 @@ void FlutterCefPlugin::HandleHostFrame(const std::string& host_key, const uint8_t host_version = payload.size() >= 2 ? payload[1] : 0; if (host_version != kCefHostProtocolVersion) { // Nothing flushed yet, so nothing mis-parsed. No auto-respawn (it would - // re-resolve the same binary and loop) — fail every session (Swift: - // 528-533). + // re-resolve the same binary and loop) — fail every session, as macOS + // does in its onProtocolMismatch handler. std::ostringstream reason; reason << "protocolMismatch(host=v" << static_cast(host_version) << ")"; @@ -1687,7 +1687,7 @@ void FlutterCefPlugin::HandleSessionFrame( break; case kOpLoadErr: { if (payload.size() < 4) break; - // {u32 code}{utf8 "url\ntext"} — split at the FIRST '\n' (Swift:374-378). + // {u32 code}{utf8 "url\ntext"} — split at the FIRST '\n', as on macOS. const std::string combined = PayloadString(payload, 4); const size_t nl = combined.find('\n'); const std::string err_url = @@ -1808,7 +1808,7 @@ void FlutterCefPlugin::HandleSessionFrame( break; case kOpCookies: // {u32 id}{utf8 json-array} — correlated back to the Dart getCookies - // Future by request id (main.mm kOpCookies:130; Swift onCookies:422-424). + // Future by request id (macOS: CefWebSession.onCookies). if (payload.size() >= 4) { EmitEvent("cookies", session_id, {{Ev("id"), flutter::EncodableValue(static_cast( @@ -1828,9 +1828,9 @@ void FlutterCefPlugin::HandleSessionFrame( if (!s->visible) SendOrQueue(s, kOpSetVisible, {uint8_t{0}}); break; case kOpCreateFailed: { - // H7: this browser's create failed; the host process is otherwise - // healthy — drop just this ONE session (emit processGone, close its - // browser, keep the host for its siblings; Swift:537-549). Copy the id + // This browser's create failed; the host process is otherwise healthy — + // drop just this ONE session (emit processGone, close its browser, keep + // the host for its siblings, as macOS onBrowserGone does). Copy the id // first: DisposeSession frees `s`, dangling session_id (an alias of // s->id). const std::string sid = session_id; @@ -1948,11 +1948,11 @@ std::wstring FlutterCefPlugin::MakeEphemeralProfileDir() { std::wstring FlutterCefPlugin::MakePersistentProfileDir( const std::string& profile) { // %LOCALAPPDATA%\flutter_cef\profiles\, created with a - // current-user-SID protected DACL (the #3 pipe hardening pattern applied to - // the profile tree). Mirrors macOS resolveProfileDir's stable 0700 dir under - // Application Support (FlutterCefPlugin.swift:702-728). + // current-user-SID protected DACL (the IPC pipe's hardening pattern applied + // to the profile tree). Mirrors macOS resolveProfileDir's stable 0700 dir + // under Application Support. // - // AT-REST NOTE (SPIKES.md S2 / §7): unlike macOS — where an ad-hoc (unsigned) + // AT-REST NOTE: unlike macOS — where an ad-hoc (unsigned) // build has no real Keychain, so OSCrypt falls back to a MOCK key and named // profiles are downgraded to ephemeral — Windows OSCrypt uses DPAPI, which is // ALWAYS available and signing-INDEPENDENT. So a named profile just persists diff --git a/packages/flutter_cef_windows/windows/flutter_cef_plugin.h b/packages/flutter_cef_windows/windows/flutter_cef_plugin.h index 7e55f83..d38013a 100644 --- a/packages/flutter_cef_windows/windows/flutter_cef_plugin.h +++ b/packages/flutter_cef_windows/windows/flutter_cef_plugin.h @@ -1,9 +1,8 @@ // flutter_cef Windows plugin — channel host. // // The verb/event contract is ../native/cef_host/PROTOCOL.md §3/§4 -// (transcribed from FlutterCefPlugin.swift). Architecture (P6 foundation + -// the profile slice of P11 — mirrors CefProfileHost.swift + FlutterCefPlugin -// .swift): +// (transcribed from FlutterCefPlugin.swift). Architecture (mirrors +// CefProfileHost.swift + FlutterCefPlugin.swift): // - ONE cef_host.exe per PROFILE. A non-empty `profile` create arg -> a // shared, persistent Host (keyed by profile name) reused by every session // that names it; an absent/empty profile -> an ephemeral Host, shared by a @@ -20,19 +19,20 @@ // parent Host by key. // - Reader/watcher threads never touch the MethodChannel: they post // HostEvents into a mutex+deque drained on the platform thread via a -// message-only HWND (PostMessage wakeup) — the slice-approved marshal. +// message-only HWND (PostMessage wakeup). // - Event routing: every host->plugin frame carries a wire browser_id. // browser_id 0 is process-level (kOpReady, kOpLog); >=1 routes to the -// Session bound to that id on that Host. The #1 generation guard now keys -// on the HOST's generation, so a dead host's straggler frames (posted +// Session bound to that id on that Host. The generation guard keys on +// the HOST's generation, so a dead host's straggler frames (posted // during the reaper grace after a same-profile respawn) can't reach a // session on the fresh host. // - Handshake: nothing is sent until kOpReady; protocolVersion must equal // kCefHostProtocolVersion (else processGone "protocolMismatch(host=vN)" for // every session). Verbs // issued before ready are queued on the Host and flushed on ready. -// - Present size-gate (LAW 4): a present is promoted only when its -// {srcW,srcH} matches round(logical*dpr) ±1 px for the CURRENT size. +// - Present size-gate: CEF still delivers late frames at the OLD size after +// a resize, so a present is promoted only when its {srcW,srcH} matches +// round(logical*dpr) ±1 px for the CURRENT size. // - Teardown is two-tier: dispose ONE browser = kOpDisposeBrowser, host // survives if other sessions remain; last session gone / host death = tear // down the whole Host (reader/watcher/Job/pipe) via a bounded reaper. @@ -74,7 +74,7 @@ namespace flutter_cef { class CdpRelay; // windows/cdp_relay.h — winsock, pulled in only by the .cpp -// Agent-control (P9) CDP-over-pipe transport for one host. Held via a +// Agent-control CDP-over-pipe transport for one host. Held via a // shared_ptr so the always-on CDP reader thread (which delivers pipe messages // to the current relay) can safely outlive a Host erase during teardown — the // reader captures its own shared_ptr, so the transport (and its handles) stay @@ -117,8 +117,8 @@ class FlutterCefPlugin : public flutter::Plugin { struct Host { // hosts_ map key: profile name, "~group~"+hostGroup or "~ephemeral~"+sessionId std::string key; - // Monotonic per-spawn identity (C1 host-object-identity analogue of macOS - // failHost, FlutterCefPlugin.swift:484-511). The reader/exit-watcher + // Monotonic per-spawn identity (the analogue of the host-object identity + // check in macOS failHost). The reader/exit-watcher // lambdas capture THIS value; a stale OLD-host event posted during the // reaper grace after a dispose+respawn of the SAME profile key carries a // generation that no longer matches the live host and is dropped. @@ -162,7 +162,7 @@ class FlutterCefPlugin : public flutter::Plugin { int width = 800; int height = 600; double dpr = 1.0; - // The size-gate expectation: round(logical * dpr) (LAW 4). + // The size-gate expectation: round(logical * dpr), in physical px. uint32_t expected_pw = 0; uint32_t expected_ph = 0; uint64_t current_handle = 0; // last promoted bridge handle @@ -221,7 +221,7 @@ class FlutterCefPlugin : public flutter::Plugin { Kind kind = Kind::kFrame; std::string host_key; // The generation of the Host that owned the poster. Dropped on drain if it - // no longer matches the live host's generation (C1). + // no longer matches the live host's generation. uint64_t generation = 0; uint32_t browser_id = 0; uint8_t opcode = 0; @@ -249,8 +249,8 @@ class FlutterCefPlugin : public flutter::Plugin { Host* HostForSession(const Session* session); // Resolve the live Host for `key`, or spawn a fresh one. nullptr on spawn // failure. An EXISTING host is reused verbatim (its --allowed-schemes etc. - // are process args fixed at its spawn — the reuse arg is ignored, macOS - // parity, FlutterCefPlugin.swift:456-471). + // are process args fixed at its spawn — the reuse arg is ignored, as in + // macOS resolveOrSpawnHost). Host* ResolveOrSpawnHost(const std::string& key, const std::wstring& profile_dir, bool ephemeral, const std::wstring& host_exe, @@ -278,7 +278,7 @@ class FlutterCefPlugin : public flutter::Plugin { // the last. void DetachFromHost(const std::string& host_key, uint32_t browser_id); - // Agent control (P9). enableAgentControl starts (idempotently) the token-gated + // Agent control. enableAgentControl starts (idempotently) the token-gated // loopback CDP relay for the session's host and replies // `{wsUrl, token, port}` (the macOS return shape exactly); it errors if the // host was not created with agentControl:true. disableAgentControl tears the @@ -324,7 +324,7 @@ class FlutterCefPlugin : public flutter::Plugin { void HandleSessionFrame(Session* session, uint8_t opcode, const std::vector& payload); - // C1 first-present watchdog (WM_TIMER on message_window_). + // First-present watchdog (WM_TIMER on message_window_). void ArmWatchdog(Session* session); void CancelWatchdog(Session* session); void OnWatchdogTimer(UINT_PTR timer_id); @@ -343,9 +343,9 @@ class FlutterCefPlugin : public flutter::Plugin { static std::wstring ResolveCefHostPath(); static std::wstring MakeEphemeralProfileDir(); // Persistent + shared profile dir: %LOCALAPPDATA%\flutter_cef\profiles\ - // , created with a current-user-SID protected DACL (the #3 - // pipe-hardening pattern). Empty string on an unusable name. See the .cpp - // for the DPAPI-at-rest note (SPIKES.md S2 / §7). + // , created with a current-user-SID protected DACL (the + // IPC pipe's hardening pattern). Empty string on an unusable name. See + // the .cpp for the DPAPI-at-rest note. static std::wstring MakePersistentProfileDir(const std::string& profile); // Recursively delete a directory tree (best-effort). Safe on a missing path. static void DeleteDirRecursive(const std::wstring& dir); diff --git a/packages/flutter_cef_windows/windows/host_process.cpp b/packages/flutter_cef_windows/windows/host_process.cpp index a95480a..66c6e51 100644 --- a/packages/flutter_cef_windows/windows/host_process.cpp +++ b/packages/flutter_cef_windows/windows/host_process.cpp @@ -54,7 +54,7 @@ bool HostProcess::Spawn(const std::wstring& cef_host_exe, cmd += L" --allowed-schemes=" + w; } - // ---- Agent control (P9): CDP-over-pipe plumbing (the S3 recipe) ---- + // ---- Agent control: CDP-over-pipe plumbing ---- // Two anonymous pipes; ONLY the child-side ends are inheritable and land in // the STARTUPINFOEX handle list, so this composes cleanly with the existing // spawn (which inherits nothing). cmd_pipe: parent writes CDP -> child reads. @@ -78,8 +78,8 @@ bool HostProcess::Spawn(const std::wstring& cef_host_exe, // Child argv: --remote-debugging-io-pipes wants , where read is // the handle the browser READS commands from (cmd_read) and write is the one // it WRITES responses to (out_write). Decimal HANDLE values, cast to - // unsigned 32-bit (S3: `(unsigned)(uintptr_t)` — handle values fit in 32 - // bits for Chromium's int-parsing of the switch). + // unsigned 32-bit (`(unsigned)(uintptr_t)`: handle values fit in 32 bits + // for Chromium's int-parsing of the switch). cmd += L" --cdp-io-pipes=" + std::to_wstring(static_cast( reinterpret_cast(cmd_read))) + diff --git a/packages/flutter_cef_windows/windows/host_process.h b/packages/flutter_cef_windows/windows/host_process.h index 97f20a3..0d5030d 100644 --- a/packages/flutter_cef_windows/windows/host_process.h +++ b/packages/flutter_cef_windows/windows/host_process.h @@ -5,7 +5,7 @@ // cef_host.exe --ipc= --profile-dir= [--ephemeral] // [--allowed-schemes=] [--cdp-io-pipes=,] // (named pipe: the child connects by NAME with CreateFileW, so no handle -// inheritance is needed — cf. S3, which inherited anonymous handles). +// inheritance is needed, unlike the agent-control CDP pipes below). // // Kill guarantees: // - Job Object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, assigned before the @@ -44,8 +44,8 @@ class HostProcess { // CefProfileHost.spawn). A list that isn't all valid schemes is refused. // Returns false on spawn failure. // - // AGENT CONTROL (P9): when `agent_control` is true, the spawn additionally - // sets up the CDP-over-pipe transport (the S3 recipe, mirroring macOS + // AGENT CONTROL: when `agent_control` is true, the spawn additionally + // sets up the CDP-over-pipe transport (mirroring macOS // launchViaPosixSpawn's fds 3/4): it CreatePipe()s two anonymous pipes, marks // ONLY the child-side ends inheritable, spawns cef_host with a // STARTUPINFOEX PROC_THREAD_ATTRIBUTE_HANDLE_LIST containing exactly those two @@ -58,7 +58,7 @@ class HostProcess { // `out_cdp_read` (we read CDP responses/events here; child writes) and // `out_cdp_write` (we write CDP commands here; child reads). The caller owns + // closes them. When `agent_control` is false the spawn is byte-identical to - // the pre-P9 path (no handle inheritance, no extra pipes). + // a plain spawn (no handle inheritance, no extra pipes). bool Spawn(const std::wstring& cef_host_exe, const std::wstring& pipe_name, const std::wstring& profile_dir, bool ephemeral, const std::string& allowed_schemes = std::string(), @@ -92,7 +92,7 @@ class HostProcess { private: HANDLE process_ = nullptr; // held hProcess (no pid dance — a HANDLE is - // not a recyclable global name, PLAN §4.2) + // not a recyclable global name) HANDLE job_ = nullptr; // kill-on-close Job Object }; diff --git a/packages/flutter_cef_windows/windows/ipc_pipe.cpp b/packages/flutter_cef_windows/windows/ipc_pipe.cpp index 01f9092..663f198 100644 --- a/packages/flutter_cef_windows/windows/ipc_pipe.cpp +++ b/packages/flutter_cef_windows/windows/ipc_pipe.cpp @@ -11,7 +11,7 @@ #include "cef_host_protocol.h" #include "current_user_sd.h" -// PLAN §4.2/§7.6 pipe hardening needs a CSPRNG (BCryptGenRandom) and the token/ +// The pipe hardening needs a CSPRNG (BCryptGenRandom) and the token/ // SID/SDDL APIs. Neither bcrypt.lib nor advapi32.lib is on this plugin's CMake // link line and that file is owned elsewhere — pull them in from the TU so the // build stays self-contained. @@ -46,7 +46,7 @@ IpcPipe::~IpcPipe() { // static std::wstring IpcPipe::NextPipeName() { - // 128-bit CSPRNG name (PLAN §4.2/§7.6): a predictable pid_counter name lets a + // 128-bit CSPRNG name: a predictable pid_counter name lets a // same-user process pre-create (squat) the pipe before cef_host connects. An // unguessable name closes the race window entirely. uint8_t rnd[16] = {}; @@ -71,7 +71,7 @@ std::wstring IpcPipe::NextPipeName() { bool IpcPipe::Create(const std::wstring& pipe_name) { pipe_name_ = pipe_name; - // Explicit protected DACL granting ONLY the current user (PLAN §7.6): the + // Explicit protected DACL granting ONLY the current user: the // default named-pipe DACL is broader than we want, and an anonymous descriptor // gives no guarantee. Refuse to create an unsecured pipe. PSECURITY_DESCRIPTOR sd = nullptr; @@ -169,8 +169,8 @@ void IpcPipe::ReaderMain(FrameHandler handler, if (!ReadFull(hdr, sizeof(hdr))) break; const uint32_t body_len = ReadU32BE(hdr); if (body_len < kMinBodyLen || body_len > kMaxBodyLen) { - // Desynced stream: unrecoverable, tear the whole transport down - // (main.mm:2343-2351 rule). + // Desynced stream: unrecoverable, tear the whole transport down (as the + // macOS host's IpcReadLoop in ipc_reader.mm does). PipeLog("bodyLen out of range — stream desynced, closing"); break; } diff --git a/packages/flutter_cef_windows/windows/ipc_pipe.h b/packages/flutter_cef_windows/windows/ipc_pipe.h index 1f60fe3..3acaad8 100644 --- a/packages/flutter_cef_windows/windows/ipc_pipe.h +++ b/packages/flutter_cef_windows/windows/ipc_pipe.h @@ -4,8 +4,8 @@ // constants header ../native/cef_host/cef_host_protocol.h. // // - Name: \\.\pipe\flutter_cef_<128-bit CSPRNG hex> (NextPipeName()). The -// unguessable name + FILE_FLAG_FIRST_PIPE_INSTANCE close the squat race -// (PLAN §4.2/§7.6): a predictable pid_counter name let a same-user process +// unguessable name + FILE_FLAG_FIRST_PIPE_INSTANCE close the squat race: +// a predictable pid_counter name let a same-user process // pre-create the pipe before cef_host connected. // - Server: CreateNamedPipeW(PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED | // FILE_FLAG_FIRST_PIPE_INSTANCE, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | @@ -25,13 +25,14 @@ // - Framing: [u32 bodyLen BE][u32 browserId BE][u8 op][payload], bodyLen = // 4+1+payloadLen, guard 5..64 MiB (kMinBodyLen/kMaxBodyLen). Outbound // frames are assembled contiguously and written whole under a write mutex -// (as main.mm's SendFrame does), with a bounded wait (kWriteTimeoutMs). +// (as SendFrame in the macOS host's ipc.mm does), with a bounded wait +// (kWriteTimeoutMs). // - The FrameHandler/DisconnectHandler run on the READER thread; the caller // must marshal to the platform thread before touching the MethodChannel. // - Teardown (Close): signal the stop event + CancelIoEx, bounded join. If // the reader will not stop, Close returns false and the caller must LEAK // this object (unique_ptr::release) — never free state a blocked reader -// may still touch (CefProfileHost.swift:999-1002 rule). +// may still touch (the rule CefProfileHost.shutdown follows on macOS). // // NOTE: deliberately flutter-free (windows.h + protocol header only) so it // can be exercised by a standalone harness without an engine. diff --git a/packages/flutter_cef_windows/windows/texture_bridge.cpp b/packages/flutter_cef_windows/windows/texture_bridge.cpp index 097db92..5e99d7f 100644 --- a/packages/flutter_cef_windows/windows/texture_bridge.cpp +++ b/packages/flutter_cef_windows/windows/texture_bridge.cpp @@ -36,8 +36,8 @@ struct TextureBridge::Entry { uint64_t handle = 0; // host-minted legacy shared handle — the identity uint32_t width = 0; uint32_t height = 0; - // LAW 6 / S1 belt-1: opened D3D11 reference on the CURRENT bridge handle, - // held for as long as it is fed to Flutter. + // Our own opened D3D11 reference on the CURRENT bridge handle, held for as + // long as it is fed to Flutter, so the texture outlives the host's release. Microsoft::WRL::ComPtr keepalive; std::unique_ptr variant; }; @@ -51,7 +51,7 @@ bool TextureBridge::EnsureDevice() { if (device_) return true; // Any D3D11 device can open + hold a legacy MISC_SHARED handle; the engine // opens the handle on ITS device via the descriptor. Ours only pins the - // resource alive (LAW 6). Default adapter matches the spike-proven setup. + // resource alive. The default adapter is the setup the port was tested on. UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; HRESULT hr = D3D11CreateDevice( nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, flags, nullptr, 0, @@ -91,7 +91,7 @@ int64_t TextureBridge::RegisterSessionTexture() { raw->desc.height = raw->height; raw->desc.visible_width = raw->width; raw->desc.visible_height = raw->height; - raw->desc.format = kFlutterDesktopPixelFormatNone; // LAW 5 + raw->desc.format = kFlutterDesktopPixelFormatNone; raw->desc.release_context = nullptr; raw->desc.release_callback = [](void*) {}; return &raw->desc; @@ -125,8 +125,8 @@ bool TextureBridge::Present(int64_t texture_id, uint64_t bridge_handle, changed = (entry->handle != bridge_handle); } - // `previous` keeps the old opened ref alive until AFTER the swap (LAW 6: - // release the previous only once the new one is being served). + // `previous` keeps the old opened ref alive until AFTER the swap: release + // the previous only once the new one is being served. Microsoft::WRL::ComPtr previous; if (changed) { if (!EnsureDevice()) return false; @@ -135,7 +135,7 @@ bool TextureBridge::Present(int64_t texture_id, uint64_t bridge_handle, reinterpret_cast(static_cast(bridge_handle)), IID_PPV_ARGS(opened.GetAddressOf())); if (FAILED(hr)) { - // #9: on device loss (removed/reset) the cached device is dead and every + // On device loss (removed/reset) the cached device is dead and every // OpenSharedResource on it keeps failing — drop it (+ this slot's opened // keepalive) so the next present re-creates the device via EnsureDevice // and reopens the host-re-minted handle fresh. A plain bad-handle miss @@ -193,7 +193,7 @@ void TextureBridge::Unregister(int64_t texture_id) { entry = std::move(it->second); entries_.erase(it); } - // Async unregister (PLAN §2 #7): the engine may still sample the texture / + // Async unregister: the engine may still sample the texture / // call the descriptor callback until the completion callback fires. The // captured shared_ptr keeps the entry (variant + descriptor + keep-alive // ComPtr) alive exactly until the engine runs or destroys the callback — diff --git a/packages/flutter_cef_windows/windows/texture_bridge.h b/packages/flutter_cef_windows/windows/texture_bridge.h index 14c3f0b..b05e101 100644 --- a/packages/flutter_cef_windows/windows/texture_bridge.h +++ b/packages/flutter_cef_windows/windows/texture_bridge.h @@ -1,24 +1,25 @@ // TextureBridge — owns the Flutter GPU texture a session presents into. // -// The real bridge, per PROTOCOL.md §5 and the LAWS: +// The real bridge, per PROTOCOL.md §5: // - Each session gets a flutter::GpuSurfaceTexture // (kFlutterDesktopGpuSurfaceTypeDxgiSharedHandle) whose descriptor callback -// serves the CURRENT host-minted bridge handle under a mutex -// (validated by the P0 S1 spike — see specs/windows-port/SPIKES.md). +// serves the CURRENT host-minted bridge handle under a mutex. // - Descriptor: struct_size SET, width/height AND visible_width/height, -// format = kFlutterDesktopPixelFormatNone (LAW 5). Returns nullptr before +// format = kFlutterDesktopPixelFormatNone. Returns nullptr before // the first present — the engine skips the frame (PopulateTexture checks // descriptor/handle for null and bails). // - Present() holds an opened ID3D11Texture2D ComPtr on the CURRENT bridge // handle for as long as it is fed to Flutter, releasing the previous one -// only AFTER the swap (LAW 6, S1 belt-1). +// only AFTER the swap, so the texture stays alive even once the host +// releases its bridge. // - Identity is the host-minted bridge handle value we are handed — never -// CEF's per-callback shared_texture_handle (LAW 3). The size-gate (LAW 4) -// is the CALLER's job (the plugin compares srcW/srcH against the expected -// round(logical*dpr) before calling Present). +// CEF's per-callback shared_texture_handle (those values alias across sizes +// and browsers). The size-gate is the CALLER's job (the plugin compares +// srcW/srcH against the expected round(logical*dpr) before calling +// Present). // - UnregisterTexture is ASYNC on Windows: the entry (variant + descriptor + // keep-alive ComPtr) is captured by the completion callback and freed when -// the engine releases it (PLAN §2 #7) — no graveyard, no leak-to-teardown. +// the engine releases it — no graveyard, no leak-to-teardown. #ifndef FLUTTER_PLUGIN_FLUTTER_CEF_TEXTURE_BRIDGE_H_ #define FLUTTER_PLUGIN_FLUTTER_CEF_TEXTURE_BRIDGE_H_ @@ -51,7 +52,8 @@ class TextureBridge { // the texture's backing surface and marks a frame available. // width/height are the frame's physical px (already size-gated by the // caller). On a handle change, opens the handle on our D3D11 device and - // holds the ComPtr (LAW 6), releasing the previous ref AFTER the swap. + // holds the ComPtr to keep the texture alive, releasing the previous ref + // AFTER the swap. // Returns false if the entry is unknown or the handle could not be opened // (previous texture keeps serving). *handle_changed reports whether the // backing surface identity changed (drives the onSurface event).