From 33858fb12958a1a59e7cdb6b9ee5c0936fbb891e Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Fri, 19 Jun 2026 11:21:04 -0700 Subject: [PATCH 1/2] fix(cef): re-enable agent-control on a tile after disabling it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-enabling agent-control on a previously-disabled tile hung: the 2nd per-browser Target.getTargetInfo probe never returned a targetId, so enableAgentControl timed out. cef_host's DoResolveTargetId reused a FIXED DevTools message id (kTargetInfoMsgId 0x7e57) on the browser's embedder DevTools session, and Chromium's session requires monotonically-increasing command ids — so the 2nd+ probe on the same browser was silently dropped. (Concurrent distinct browsers were fine: separate sessions.) Give each probe a fresh, increasing id (Slot::target_info_msg, seeded to kTargetInfoMsgId) and match the result against the slot's current id in the observer. Pairs with the already-landed resolveTargetId epoch guard so the resolve's stale 5s timer can't clobber the re-resolve. Restore the probe's re-enable lifecycle checks from a diagnostic to hard assertions. Verified live: 13/13 — re-enable yields a fresh grant, drives its own target again, the torn-down grant stays dead; probe completes in ~4s (was hanging ~25s). Co-Authored-By: Claude Opus 4.8 --- example/lib/multiview_probe.dart | 45 ++++++++++--------- .../flutter_cef_macos/native/cef_host/main.mm | 20 ++++++++- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/example/lib/multiview_probe.dart b/example/lib/multiview_probe.dart index 66619f4..ef496d1 100644 --- a/example/lib/multiview_probe.dart +++ b/example/lib/multiview_probe.dart @@ -19,8 +19,8 @@ // 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 -// A's grant (its endpoint goes dead) while B keeps driving; re-enabling A mints -// a FRESH port+token and the torn-down grant stays dead (no reuse). +// 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 // a wedged client + no sibling starvation): a faithful repro needs a real CDP driver @@ -181,24 +181,29 @@ class _ProbeAppState extends State { _check('F: sibling B unaffected by A teardown (still drives its own target)', bUnaffected); - // DIAGNOSTIC (non-fatal) — re-enable after disable. This currently FAILS due - // to a PRE-EXISTING limitation (NOT a P2-step2 regression — the targetId - // resolution path is untouched by P2-step2): the 2nd Target.getTargetInfo - // resolve for the SAME browser never returns kOpTargetId, so enableAgentControl - // times out. Root cause is in cef_host's per-browser targetId re-resolution and - // is not yet isolated (one candidate: DoResolveTargetId reuses a fixed DevTools - // message id, kTargetInfoMsgId 0x7e57, on that browser's session; concurrent - // A+B resolve fine because they are distinct sessions). Recorded as a diagnostic - // so it doesn't mask the P2-step2 result; tracked for a separate cef_host fix. - // Short timeout so the suite isn't held up by the known failure. - setState(() => _status = 're-enable after teardown (diagnostic)…'); - final gA2 = await _enable(_a, timeout: const Duration(seconds: 6)); - out['reenableDiag'] = { - 'reenabledOk': gA2 != null, - 'note': gA2 != null - ? 'pre-existing re-enable limitation appears FIXED' - : 'pre-existing cef_host re-resolve limitation (see comment) — not a P2-step2 regression', - }; + // F — re-enable after disable (a tile gets driven again later): yields a + // FRESH, independent grant (new port+token) and the torn-down one stays dead. + // Needs cef_host's per-probe monotonic DevTools id (a fixed id dropped the 2nd + // resolve on the same browser) + resolveTargetId's epoch-guarded timer. + setState(() => _status = 're-enable after teardown…'); + final gA2 = await _enable(_a); + out['grantA2'] = + gA2 == null ? null : {'port': gA2.port, 'token8': gA2.token.substring(0, 8)}; + _check('F: A re-enables after disable (fresh grant)', gA2 != null); + if (gA2 != null) { + _check('F: re-enabled grant differs from the torn-down one', + gA2.port != gA.port || gA2.token != gA.token); + final ta2 = await _targets(gA2.wsUrl); + _check('F: re-enabled relay drives A again (one own target)', + ta2.length == 1 && ta2.first == ta.first); + bool oldStillDead = false; + try { + await _targets(gA.wsUrl); + } catch (_) { + oldStillDead = true; + } + _check('F: the torn-down grant stays dead after re-enable', oldStillDead); + } } } catch (e, st) { out['fatal'] = '$e'; diff --git a/packages/flutter_cef_macos/native/cef_host/main.mm b/packages/flutter_cef_macos/native/cef_host/main.mm index d414ad3..bcb84b9 100644 --- a/packages/flutter_cef_macos/native/cef_host/main.mm +++ b/packages/flutter_cef_macos/native/cef_host/main.mm @@ -207,6 +207,12 @@ // browser's CDP targetId (Target.getTargetInfo). Kept alive for the slot's life; // UI-thread only. Lazily set on the first kOpResolveTargetId. CefRefPtr devtools_reg; + // CEF-2b: the DevTools message id of the LAST Target.getTargetInfo probe on this + // browser. A FRESH, monotonically-increasing id per probe (seeded to + // kTargetInfoMsgId) — Chromium's DevTools session requires increasing command ids, + // so reusing a fixed id silently drops the 2nd+ probe, which hung a re-enable of + // agent-control (disable then enable again). UI-thread only, like dialog_next. + int target_info_msg = 0; }; // Routing map from a wire browser id to its Slot. MUTATED ONLY ON THE CEF UI @@ -1359,7 +1365,12 @@ explicit TargetIdObserver(uint32_t wire_id) : wire_id_(wire_id) {} void OnDevToolsMethodResult(CefRefPtr browser, int message_id, bool success, const void* result, size_t result_size) override { - if (message_id != kTargetInfoMsgId || !success || !result || result_size == 0) + // Match this browser's CURRENT probe id (ids now increment per resolve, so a + // fixed constant would miss every probe after the first). UI-thread, like the + // resolve that set it. + auto slot = LookupWireId(wire_id_); + if (!slot || message_id != slot->target_info_msg || !success || !result || + result_size == 0) return; std::string json(static_cast(result), result_size); // Anchor to the targetInfo object first, so a differently-named *targetId* field @@ -1383,10 +1394,15 @@ void DoResolveTargetId(const std::shared_ptr& slot) { slot->devtools_reg = host->AddDevToolsMessageObserver(new TargetIdObserver(slot->browser_id)); } + // Fresh, increasing id per probe (see Slot::target_info_msg) so a re-resolve on the + // SAME browser isn't dropped by the DevTools session's monotonic-id requirement. + slot->target_info_msg = slot->target_info_msg < kTargetInfoMsgId + ? kTargetInfoMsgId + : slot->target_info_msg + 1; // Target.getTargetInfo with no params: executed on a specific browser's DevTools // agent (a page target), it returns THAT page's own targetInfo — so this resolves // exactly this browser's targetId, with no cross-tile ambiguity. - host->ExecuteDevToolsMethod(kTargetInfoMsgId, "Target.getTargetInfo", nullptr); + host->ExecuteDevToolsMethod(slot->target_info_msg, "Target.getTargetInfo", nullptr); } void DoImeSetComposition(const std::shared_ptr& slot, From 153a3e45dfba85ebc3dfd0a52545ad9601bd8c24 Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Fri, 19 Jun 2026 11:27:47 -0700 Subject: [PATCH 2/2] ci(cef): pin Flutter to 3.38.8 (the consumer's engine), not floating stable CI floated to `stable` (now 3.44.2), failing `flutter analyze` because the plugin's DeltaTextInputClient doesn't implement TextInputClient.onFocusReceived (a framework interface member added after 3.41). The package's consumer, work_canvas, ships on a custom 3.38.8 engine (its .fvmrc), so floating stable exercises a Flutter no consumer uses and breaks on every new interface method. Pin to 3.38.8 to test the real target. main's CI was already red on this before this branch. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 048d1fd..d6e89a0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,6 +13,11 @@ jobs: - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 with: + # Pin to the Flutter version the consumer (work_canvas) ships against + # (its .fvmrc / custom engine), not floating `stable`. Floating stable + # breaks CI on every new framework interface method the pinned engine + # doesn't carry (e.g. TextInputClient.onFocusReceived, added after 3.41). + flutter-version: 3.38.8 channel: stable - run: flutter --version - name: Install dependencies