Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 25 additions & 20 deletions example/lib/multiview_probe.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,24 +181,29 @@ class _ProbeAppState extends State<ProbeApp> {
_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';
Expand Down
20 changes: 18 additions & 2 deletions packages/flutter_cef_macos/native/cef_host/main.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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<CefRegistration> 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
Expand Down Expand Up @@ -1359,7 +1365,12 @@ explicit TargetIdObserver(uint32_t wire_id) : wire_id_(wire_id) {}
void OnDevToolsMethodResult(CefRefPtr<CefBrowser> 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<const char*>(result), result_size);
// Anchor to the targetInfo object first, so a differently-named *targetId* field
Expand All @@ -1383,10 +1394,15 @@ void DoResolveTargetId(const std::shared_ptr<Slot>& 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>& slot,
Expand Down
Loading