From 557c6914f362a56cfac626b865c761a4a0545216 Mon Sep 17 00:00:00 2001 From: wenkaifan0720 Date: Thu, 24 Sep 2026 03:25:10 -0700 Subject: [PATCH] Comments: drop audit tags (macOS); delete unused entitlements.browser.plist; CI guard The macOS plugin and host carried the same unresolvable references #53 removed from Windows/Dart: audit ids (C1-C3, H1-H9, R2, "audit P3"), the visibility/resize audit's fix ids (F-1..F-6, C-3), the agent-control plan's phase ids (CEF-1, CEF-2a/b, P2-step1/2), the persistent-profile contract's section numbers (F.3-F.5, A.4, H.6), a "#138 consolidation" and pointers to work_canvas's specs/cef-passkey/PLAN.md, another repo. Each tag is replaced by its reason in words, or dropped where the sentence already said it. The "[cef] C2 respawn ephemeral host failed" log line loses its tag too. entitlements.browser.plist was an auth-spike leftover: nothing signs with it. The build (CMakeLists.txt) and tool/bundle_cef_host.sh sign cef_host and every helper with the same file, entitlements.release.plist for a real identity. Its comment now says so, and why the passkey keychain group is not in it. The CMakeLists comment claiming both sets keep allow-unsigned-executable-memory and disable-library-validation is corrected (only the dev set does). Deleting the file changes the cef_host input hash. tool/check_comment_tags.sh fails on the unambiguous shapes (LAW n, the all-caps slice word, the spike log file name, file.ext:line citations, a comment that opens with a letter+digits label, a parenthesised label) outside docs/history/ and CHANGELOGs. CI's analyze-test job runs it. Co-Authored-By: Claude Opus 5.5 (1M context) --- .github/workflows/ci.yaml | 2 + CONTRIBUTING.md | 3 + example/lib/channel_probe_shared.dart | 12 ++-- example/lib/profile_probe.dart | 2 +- packages/flutter_cef_macos/CHANGELOG.md | 2 + .../macos/Classes/CdpRelay.swift | 56 ++++++++--------- .../macos/Classes/CefProfileHost+Cdp.swift | 12 ++-- .../Classes/CefProfileHost+CreatePacing.swift | 20 +++--- .../macos/Classes/CefProfileHost+Ipc.swift | 46 +++++++------- .../Classes/CefProfileHost+Liveness.swift | 4 +- .../macos/Classes/CefProfileHost.swift | 52 +++++++-------- .../macos/Classes/CefWebSession.swift | 14 ++--- .../macos/Classes/FlutterCefPlugin.swift | 63 +++++++++---------- .../macos/Classes/LivenessProbePolicy.swift | 2 +- .../macos/Classes/ResizeWatchdogPolicy.swift | 8 +-- .../native/build-cef-from-source.sh | 5 +- .../native/cef_host/CMakeLists.txt | 6 +- .../native/cef_host/browser_ops.mm | 20 +++--- .../cef_host/entitlements.browser.plist | 30 --------- .../cef_host/entitlements.release.plist | 12 ++-- .../native/cef_host/host_client.mm | 10 +-- .../native/cef_host/host_state.h | 8 +-- .../flutter_cef_macos/native/cef_host/ipc.mm | 2 +- .../native/cef_host/ipc_reader.mm | 2 +- .../flutter_cef_macos/native/cef_host/main.mm | 8 +-- .../test/CdpRelayFilterTests.swift | 14 ++--- .../test/LivenessProbePolicyTests.swift | 6 +- .../test/ResizeWatchdogPolicyTests.swift | 4 +- .../test/run_filter_tests.sh | 2 +- .../test/run_liveness_probe_tests.sh | 2 +- .../test/run_resize_watchdog_tests.sh | 2 +- tool/check_comment_tags.sh | 56 +++++++++++++++++ 32 files changed, 260 insertions(+), 227 deletions(-) delete mode 100644 packages/flutter_cef_macos/native/cef_host/entitlements.browser.plist create mode 100755 tool/check_comment_tags.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a03c905..98cd3b0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,6 +11,8 @@ jobs: runs-on: macos-14 steps: - uses: actions/checkout@v4 + - name: No audit tags or line citations in comments + run: tool/check_comment_tags.sh - uses: subosito/flutter-action@v2 with: # Pin to the Flutter version the consumer (work_canvas) ships against diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 69dbb61..336c7c3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -187,6 +187,9 @@ and memory stay bounded under recreate churn). changes over broad refactors. The Dart, Swift, and CMake all carry dense explanatory comments at the non-obvious seams — keep that up; a tricky fix should explain *why*, not just *what*. +- **Say the reason in words.** Comments carry no audit or port-plan tags and + no `file.ext:` citations; CI runs `tool/check_comment_tags.sh`, which + fails on them. - **Document threading assumptions in the native layers.** The Swift/native code spans the Flutter platform thread, CEF's UI thread, the GPU/Viz process, and the socket relay. When you touch a method that must run on (or hand off to) a diff --git a/example/lib/channel_probe_shared.dart b/example/lib/channel_probe_shared.dart index 51ab6e6..e8f206b 100644 --- a/example/lib/channel_probe_shared.dart +++ b/example/lib/channel_probe_shared.dart @@ -1,11 +1,11 @@ // SHARED-HOST page->host channel probe — the single-controller channel_probe.dart // PASSES, so the basic channel works. Campus's peer tile differs in that it runs -// on a SHARED cef_host (a named profile → one host for many sessions, per the -// #138 consolidation). This probe mounts TWO controllers on ONE named profile -// (= one shared cef_host), each registering the SAME channel name 'probeHost' -// (exactly like every Campus agent_ui uses 'campusHost'), each page posting a -// DISTINCT tag. It verifies each controller's handler receives ONLY its own tag -// — i.e. OnQuery's slot_->browser_id routing stays correct across sessions. +// on a SHARED cef_host (a named profile → one host for many sessions). This +// probe mounts TWO controllers on ONE named profile (= one shared cef_host), +// each registering the SAME channel name 'probeHost' (exactly like every Campus +// agent_ui uses 'campusHost'), each page posting a DISTINCT tag. It verifies +// each controller's handler receives ONLY its own tag — i.e. OnQuery's +// slot_->browser_id routing stays correct across sessions. // // Run: // FLUTTER_CEF_HOST=<.../cef_host.app/Contents/MacOS/cef_host> \ diff --git a/example/lib/profile_probe.dart b/example/lib/profile_probe.dart index 7c97e73..8bfa132 100644 --- a/example/lib/profile_probe.dart +++ b/example/lib/profile_probe.dart @@ -19,7 +19,7 @@ // (Each run's dispose() sends kOpShutdown -> host flushes the // on-disk SQLite cookie store; persist_session_cookies=1.) // -// Results are written to C:\dev\flutter_cef_spikes\profile_evidence\ as a JSON +// Results are written to %TEMP%\flutter_cef_profile_evidence\ as a JSON // transcript (authoritative) and rendered on screen (for the screenshot), and a // `CEF_PROBE_RESULT …` line is printed to stdout. // diff --git a/packages/flutter_cef_macos/CHANGELOG.md b/packages/flutter_cef_macos/CHANGELOG.md index b948c2e..abcf735 100644 --- a/packages/flutter_cef_macos/CHANGELOG.md +++ b/packages/flutter_cef_macos/CHANGELOG.md @@ -1,5 +1,7 @@ ## Unreleased +* Removed the unused `native/cef_host/entitlements.browser.plist`, which + changes the `cef_host` input hash. * Document-start scripts and create-time JS channels, sent in the browser's `extra_info` and installed by the renderer in `OnContextCreated`. * `hostGroup`: ephemeral sessions in one group share a `cef_host`. diff --git a/packages/flutter_cef_macos/macos/Classes/CdpRelay.swift b/packages/flutter_cef_macos/macos/Classes/CdpRelay.swift index a4f6ce4..ff3e5a4 100644 --- a/packages/flutter_cef_macos/macos/Classes/CdpRelay.swift +++ b/packages/flutter_cef_macos/macos/Classes/CdpRelay.swift @@ -2,10 +2,10 @@ import Foundation import CryptoKit import Security -/// The token-gated, per-tile-scoped localhost CDP relay (CEF-2a transport + CEF-2b -/// isolation). It re-exposes the CEF-1 CDP-over-pipe to a standard CDP client -/// (`agent-browser`) as a loopback HTTP+WebSocket endpoint, confined (when scoped) to -/// a single tile's CDP target — see the per-tile-isolation note below. +/// The token-gated, per-tile-scoped localhost CDP relay. It re-exposes cef_host's +/// CDP-over-pipe to a standard CDP client (`agent-browser`) as a loopback +/// HTTP+WebSocket endpoint, confined (when scoped) to a single tile's CDP target — +/// see the per-tile-isolation note below. /// /// Why a hand-rolled server (no SwiftNIO/Starscream): the security review demanded /// a minimal supply-chain surface, and the codebase already speaks raw BSD sockets @@ -34,11 +34,11 @@ import Security /// Strictly better than raw Chrome's fixed, always-open, multi-client /// `--remote-debugging-port`. /// -/// Per-tile isolation (CEF-2b): the CDP pipe is browser-wide, so when constructed +/// Per-tile isolation: the CDP pipe is browser-wide, so when constructed /// with a `scopeTargetId` the relay applies a Target-domain filter that exposes the /// client ONLY that tile's target (and its sub-targets) — sibling tiles in the same /// shared-profile process are hidden and unreachable. Constructed without a scope it -/// is a raw browser-level passthrough (CEF-2a; dev/test only). +/// is a raw browser-level passthrough (dev/test only). final class CdpRelay { /// Forwards a CDP message (one JSON line) to cef_host over the pipe. Captures the /// host weakly so the host↔relay ownership (host strongly holds the relay) is not @@ -54,7 +54,7 @@ final class CdpRelay { private var running = false private let stateLock = NSLock() - /// The single active ws client (CEF-2a supports one connection per relay; a + /// The single active ws client (one connection per relay; a /// second upgrade is rejected, avoiding any fd-replacement double-close race). /// Guarded by `clientLock`, which also serializes writes to it. private var clientFd: Int32 = -1 @@ -78,10 +78,10 @@ final class CdpRelay { /// or buggy client must not be able to make us allocate unbounded. private static let maxFrame = 64 << 20 - // CEF-2b: per-tile isolation filter. When `scopeTargetId` is set, the relay + // Per-tile isolation filter. When `scopeTargetId` is set, the relay // exposes the client ONLY this CDP target (the opted-in tile) and its descendant // sub-targets — sibling tiles in the same shared-profile process are hidden. When - // nil, the relay is a raw browser-level passthrough (CEF-2a; dev-only). The CDP + // nil, the relay is a raw browser-level passthrough (dev-only). The CDP // pipe is browser-wide, so this filter is the per-tile security boundary. private let scopeTargetId: String? private var ourSessionId: String? // learned from our target's attachedToTarget @@ -92,7 +92,7 @@ final class CdpRelay { private var ourBrowserContextId: String? private let filterLock = NSLock() - // CEF-2b multiplex: N relays share ONE browser-wide pipe with ONE CDP id space. + // Multiplex: N relays share ONE browser-wide pipe with ONE CDP id space. // Session-routed traffic is demuxed by sessionId, but BROWSER-LEVEL commands // (no sessionId — Playwright's connect handshake) would collide. We rewrite // EVERY outgoing command id to a pipe id taken from the host's shared @@ -103,7 +103,7 @@ final class CdpRelay { private let pipeIds: CdpPipeIds private var pipeIdToClientId: [Int: Int] = [:] private let multiplexLock = NSLock() - // H2: this relay's OWN Target.attachToTarget pipe id (used to learn our page's CDP + // This relay's OWN Target.attachToTarget pipe id (used to learn our page's CDP // session order-independently, instead of passively witnessing a fire-once // browser-wide attachedToTarget event we may register too late to see), plus the // client setAutoAttach ids to ack once we've attached. multiplexLock. @@ -279,7 +279,7 @@ final class CdpRelay { close(fd); return } - // CEF-2a: one active client per relay. Reject a second concurrent upgrade + // One active client per relay. Reject a second concurrent upgrade // (avoids any fd-replacement double-close race); the slot frees on disconnect. clientLock.lock() if clientFd >= 0 { @@ -311,7 +311,7 @@ final class CdpRelay { if owned { clientFd = -1 } clientLock.unlock() if owned { close(fd) } - // H2: the relay persists past this client — drop the in-flight attach so a late + // The relay persists past this client — drop the in-flight attach so a late // self-attach response isn't delivered to the next client (a stale ack / spurious // attachedToTarget), and its id mappings. A client that connected in the meantime // already reset them (noteClientConnected) and may have state of its own, so this @@ -477,7 +477,7 @@ final class CdpRelay { assembling = !fin if fin { if assemblingText, let s = String(bytes: msg, encoding: .utf8) { - if let out = filterClientToPipe(s) { sendToPipe(rewriteOutgoingId(out)) } // CEF-2b scope filter + id remap + if let out = filterClientToPipe(s) { sendToPipe(rewriteOutgoingId(out)) } // scope filter + id remap } msg.removeAll(keepingCapacity: true) assemblingText = false @@ -495,7 +495,7 @@ final class CdpRelay { } } - /// Deliver a CDP message from the pipe to the connected client. Applies the CEF-2b + /// Deliver a CDP message from the pipe to the connected client. Applies the /// multiplex demux + scope filter (drops sibling-tile traffic) before writing. /// Called off the CDP reader thread. func deliverToClient(_ json: String) { @@ -503,18 +503,18 @@ final class CdpRelay { sendRawToClient(out) } - /// CEF-2b pure decision seam (no socket IO — unit-testable): map one inbound pipe + /// Pure decision seam (no socket IO — unit-testable): map one inbound pipe /// message to the bytes this relay should hand its client, or nil to DROP it. /// /// Multiplex demux (scoped relays only): a pipe message with a top-level id and NO /// method is a command RESPONSE, owned by exactly the relay that issued that unique /// pipe id. Restore the client's original id, or drop if it's a sibling relay's - /// response. Events (method present) + the CEF-2a passthrough fall through to the + /// response. Events (method present) + the unscoped passthrough fall through to the /// scope filter unchanged. func demuxPipeToClient(_ json: String) -> String? { if scopeTargetId != nil, let m = parseJson(json), m["method"] == nil, let pipeId = m["id"] as? Int { - // H2: our OWN Target.attachToTarget response — learn the page session + hand the + // Our OWN Target.attachToTarget response — learn the page session + hand the // client the synthesized attachedToTarget; never forward the raw response. multiplexLock.lock(); let isSelfAttach = (pipeId == selfAttachPipeId); multiplexLock.unlock() if isSelfAttach { handleSelfAttachResponse(m); return nil } @@ -523,7 +523,7 @@ final class CdpRelay { var restored = m; restored["id"] = clientId return jsonString(restored) } - return filterPipeToClient(json) // events / browser-level / CEF-2a passthrough + return filterPipeToClient(json) // events / browser-level / unscoped passthrough } /// Write a raw (already-filtered / self-originated) JSON text frame to the client. @@ -572,7 +572,7 @@ final class CdpRelay { } } - // MARK: CEF-2b — per-tile Target-domain filter (deny-by-default, fail-closed) + // MARK: Per-tile Target-domain filter (deny-by-default, fail-closed) // // The CDP pipe is browser-wide, so this filter is THE per-tile security boundary — // built to be safe against a hostile client, not just to support Playwright: @@ -601,7 +601,7 @@ final class CdpRelay { // page work routed by that top-level sessionId (+ nested sub-target sessions). /// pipe → client. Returns the JSON to forward, or nil to drop. nil scope = - /// passthrough (CEF-2a, dev only). (Internal, not private, so the standalone + /// passthrough (dev only). (Internal, not private, so the standalone /// filter unit test — CdpRelayFilterTests.swift — can exercise it directly.) func filterPipeToClient(_ json: String) -> String? { guard let tid = scopeTargetId else { return json } @@ -618,7 +618,7 @@ final class CdpRelay { let childSession = params?["sessionId"] as? String if sid == nil { // browser-level attach of a top-level target (a tile) guard attachedTid == tid else { return nil } // sibling tile — hide - // H2: our active Target.attachToTarget (beginPageAttach) triggers THIS real + // Our active Target.attachToTarget (beginPageAttach) triggers THIS real // browser-level event for our page. FORWARD it as-is — it carries the genuine // targetInfo (incl. a non-empty browserContextId that Playwright's // CRBrowser._onAttachedToTarget asserts on; a synthesized empty one crashes the @@ -690,7 +690,7 @@ final class CdpRelay { } // Session-routed command (flatten): allow only for our (allowed) sessions. The - // brief lock is released before any error IO (H4). + // brief lock is released before any error IO. if let s = sid { filterLock.lock() let allowed = allowedSessions.contains(s) @@ -736,7 +736,7 @@ final class CdpRelay { guard (params?["flatten"] as? Bool) == true else { sendClientError(id, "non-flatten setAutoAttach is not permitted"); return nil } - // H2: a BROWSER-LEVEL setAutoAttach (no sessionId — reached here because the + // A BROWSER-LEVEL setAutoAttach (no sessionId — reached here because the // sessionId branch above didn't claim it) is browser-context-wide. Forwarding // it (a) lets us change a SIBLING tile's auto-attach params (cross-tile control // leak) and (b) relies on a fire-once attachedToTarget storm we'll miss if we @@ -804,7 +804,7 @@ final class CdpRelay { sendClientJson(["id": id, "result": ["targetInfos": [info]]]) } - /// H2: ensure this relay knows its page's CDP session, then hand the client the page's + /// Ensure this relay knows its page's CDP session, then hand the client the page's /// Target.attachedToTarget directly — independent of the browser-wide auto-attach /// storm (fire-once, and which we must not forward: it would change sibling tiles' /// auto-attach). If we already learned our session, synthesize now; otherwise issue @@ -834,7 +834,7 @@ final class CdpRelay { if let s = jsonString(cmd) { sendToPipe(s) } } - /// H2: our scoped attachToTarget came back — record the page session and (if the + /// Our scoped attachToTarget came back — record the page session and (if the /// client that issued it is still attached) ack every queued setAutoAttach. The page's /// attachedToTarget is delivered by FORWARDING the real browser-level event (see the /// filter), not synthesized here — so the client gets the genuine targetInfo. If the @@ -855,7 +855,7 @@ final class CdpRelay { for ack in acks { synthesizeOk(ack) } } - /// H2: fabricate the page's Target.attachedToTarget for the client (flatten mode) so + /// Fabricate the page's Target.attachedToTarget for the client (flatten mode) so /// Playwright/connectOverCDP discovers our page without us forwarding the browser-wide /// auto-attach — mirrors synthesizeGetTargets' single-tile view. private func synthesizeAttachedToTarget(sessionId: String) { @@ -910,7 +910,7 @@ final class CdpRelay { } // Rewrite an outgoing command's top-level id to a globally-unique pipe id and - // record the mapping. No-op for the CEF-2a passthrough (nil scope) and for + // record the mapping. No-op for the unscoped passthrough (nil scope) and for // messages without a top-level Int id (none, in practice clients only send // commands). Called for client->pipe traffic only. Internal (not private) so the // standalone filter tests can drive the rewrite↔demux round-trip directly. diff --git a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Cdp.swift b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Cdp.swift index eebaf1d..4c655eb 100644 --- a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Cdp.swift +++ b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Cdp.swift @@ -25,7 +25,7 @@ extension CefProfileHost { cdpWriteLock.unlock() } - /// CEF-2b: deliver one CDP pipe message to EVERY live relay. Snapshot the relays + /// Deliver one CDP pipe message to EVERY live relay. Snapshot the relays /// under cdpHandlerLock, then call deliverToClient OUTSIDE the lock on each — /// deliverToClient does blocking IO and takes the relay's own locks, so holding /// cdpHandlerLock across it would invert the lock order (and could deadlock / @@ -38,7 +38,7 @@ extension CefProfileHost { for r in relays { r.deliverToClient(msg) } } - /// CEF-2b: start (lazily) a token-gated CDP relay SCOPED to `browserId`'s tile and + /// Start (lazily) a token-gated CDP relay SCOPED to `browserId`'s tile and /// return the brokered endpoint Campus hands an agent. Async: first resolves the /// browser's CDP targetId (round-trip to cef_host), then creates a relay whose /// Target-domain filter exposes only that tile, then starts it (so no client ever @@ -84,7 +84,7 @@ extension CefProfileHost { scopeTargetId: tid, pipeIds: self.cdpPipeIds) guard relay.start() else { self.cdpHandlerLock.unlock(); completion(nil); return } // Install the fan-out pipe → relays handler ONCE, when the first relay appears, - // CHAINING any prior handler (preserves the debug CEF-1 validation probe) rather + // CHAINING any prior handler (preserves the debug pipe-validation probe) rather // than clobbering it. Subsequent relays just join cdpRelays; deliverCdpToRelays // snapshots the dict per message, so it picks them up automatically. if self.cdpRelays.isEmpty { @@ -103,7 +103,7 @@ extension CefProfileHost { ("ws://127.0.0.1:\(r.port)/devtools/browser?token=\(r.token)", r.token, Int(r.port)) } - /// CEF-2b: resolve `browserId`'s CDP targetId via cef_host (Target.getTargetInfo). + /// Resolve `browserId`'s CDP targetId via cef_host (Target.getTargetInfo). /// All waiters for that browserId fire exactly once — on the response or a 5s /// timeout, whichever removes the entry first. Concurrent calls for the SAME /// browserId COALESCE onto one in-flight resolve (a second call appends its waiter @@ -171,7 +171,7 @@ extension CefProfileHost { waiters.forEach { $0(nil) } } - /// CEF-2a/b: tear down `browserId`'s relay (closes the listener + any client, + /// Tear down `browserId`'s relay (closes the listener + any client, /// invalidates the token). Idempotent — a no-op if that tile has no relay. When /// the LAST relay goes, drop the fan-out onCdpMessage too. The pipe itself stays /// up (the tile keeps running). The relay is stopped OUTSIDE the lock: stop() may @@ -223,7 +223,7 @@ extension CefProfileHost { } } - /// CEF-1 validation gate: prove the pipe round-trips end to end. Only when + /// Debug validation gate: prove the CDP pipe round-trips end to end. Only when /// agent-control AND FLUTTER_CEF_DEBUG is set, install a temporary CDP handler /// and send {"id":1,"method":"Browser.getVersion"}; the first response line is /// NSLogged. Behind the debug env so it never runs in normal flow, and it diff --git a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+CreatePacing.swift b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+CreatePacing.swift index a679756..baf0007 100644 --- a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+CreatePacing.swift +++ b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+CreatePacing.swift @@ -17,7 +17,7 @@ extension CefProfileHost { // browserIds are STRICTLY MONOTONIC and never reused: nextBrowserId only ever // increments (never reset/decremented) and a disposed id is never recycled, so // guard it — the slot we're about to hand out must be FREE (never previously - // registered). H8: a UInt32 wrap (or any bug) reusing an id would SILENTLY + // registered). A UInt32 wrap (or any bug) reusing an id would SILENTLY // overwrite a live sibling's slot in a release build (the old guard was a // debug-only `assert`, compiled out) → the reader misroutes that wire id's frames // (paint/cookies/CDP/relay) to the wrong tile. Make it a hard runtime invariant (a @@ -33,7 +33,7 @@ extension CefProfileHost { writeLock.lock() let isReady = ready if !isReady { - // Queue until kOpReady; the safety-rail (F.5) may refuse to flush these. The + // Queue until kOpReady; the ad-hoc-build safety rail may refuse to flush these. The // payload is built at FLUSH time inside sendCreate from the session's LIVE // surfaceId/geometry — a resize during the pre-ready spawn window // reallocates the IOSurface (freeing the old global id) and updates the @@ -52,7 +52,7 @@ extension CefProfileHost { /// pre-connect resizes are no longer dropped. The payload is assembled HERE /// (not at createBrowser time) so it carries the session's current surfaceId + /// geometry: {u32 w}{u32 h}{f64 dpr}{u32 iosurfaceId}{utf8 url}. allowedSchemes - /// is NOT here — it's a process arg fixed at spawn (A.4). + /// is NOT here — it's a process arg fixed at spawn. private func sendCreate(_ id: UInt32, _ session: CefWebSession, _ url: String) { writeLock.lock() // Read the session's LIVE geometry + surfaceId AND write the create frame in a @@ -62,7 +62,7 @@ extension CefProfileHost { // Any resize after this lands after the create, so cef_host has a slot and // self-heals the surface via DoResize. (writeLock→bufferLock here is safe: no // path holds bufferLock then takes writeLock.) - // H4: read (w, h, dpr, surfaceId) as ONE atomic snapshot rather than four separate + // Read (w, h, dpr, surfaceId) as ONE atomic snapshot rather than four separate // bufferLock acquisitions — otherwise a resize interleaving between the reads could // ship e.g. old width + new surfaceId, blitting the first paint into a mis-sized // surface. (create-pacing widened this window: a browser can sit queued for N× @@ -97,7 +97,7 @@ extension CefProfileHost { ok = frame.withUnsafeBytes { writeAll(connFd, $0.baseAddress!, frame.count) } } writeLock.unlock() - // H2: surface a dead pipe (unlocked first — handleHostDeath re-takes writeLock). + // Surface a dead pipe (unlocked first — handleHostDeath re-takes writeLock). if !ok { handleHostDeath() } } @@ -124,7 +124,7 @@ extension CefProfileHost { // advanceCreatePacer, which re-pumps. while true { writeLock.lock() - // H6: never pump on a dead/dying host — the queue was abandoned in + // Never pump on a dead/dying host — the queue was abandoned in // shutdown()/handleHostDeath(); pumping would sendCreate into a closed pipe and a // stuck slot could wedge a reused host. if !running || crashed || createInFlight.count >= maxCreateInFlight || @@ -149,7 +149,7 @@ extension CefProfileHost { // Arm the watchdog (insert into firstPresentPending) BEFORE sendCreate so a first // kOpPresent can never be observed before the id is registered as pending (which would // leave a healthy painting tile stuck "pending" → false perpetual paintStalled). - armFirstPresentWatchdog(next.id) // C1 + armFirstPresentWatchdog(next.id) sendCreate(next.id, next.session, next.url) // Release this slot on the browser's FIRST PAINT (firstPresentArrived, in the // reader); this timer is only the backstop if it binds but never paints in time. @@ -186,12 +186,12 @@ extension CefProfileHost { guard let s = browsers[browserId], !s.goneReported else { browsersLock.unlock(); return } s.goneReported = true browsersLock.unlock() - firstPresentArrived(browserId) // cancel the C1 watchdog for a browser that won't paint + firstPresentArrived(browserId) // cancel the first-present watchdog for a browser that won't paint onBrowserGone?(browserId, reason) advanceCreatePacer(after: browserId, timedOut: false) } - // MARK: C1 first-present watchdog + // MARK: First-present watchdog /// Arm the first-present watchdog for a freshly-sent create: after `firstPaintGrace` /// with no frame at all, run a liveness check. @@ -218,7 +218,7 @@ extension CefProfileHost { presentLock.unlock() } - /// C1: track WasHidden state (peeked from kOpSetVisible). A hidden browser produces no + /// Track WasHidden state (peeked from kOpSetVisible). A hidden browser produces no /// frames, so the watchdog suspends rather than flagging it stalled. On UNHIDE, re-arm /// the watchdog for a browser that's still blank, so a genuinely-stuck now-visible tile /// is still caught. diff --git a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Ipc.swift b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Ipc.swift index 77fd30b..41c4306 100644 --- a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Ipc.swift +++ b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Ipc.swift @@ -13,7 +13,7 @@ extension CefProfileHost { /// process arg shared by every browser in the profile — it's taken from the /// first browser that triggered this spawn. Returns false on failure. /// - /// `agentControl` (CEF-1) switches the LAUNCH MECHANISM only: when true we use + /// `agentControl` switches the LAUNCH MECHANISM only: when true we use /// posix_spawn instead of Foundation.Process so cef_host inherits two CDP pipes /// on fds 3/4 (Foundation.Process can't place arbitrary fds), and we add the /// `--cdp-pipe` flag so the native side injects the `remote-debugging-pipe` @@ -115,13 +115,13 @@ extension CefProfileHost { running = true readerStarted = true Thread.detachNewThread { [weak self] in self?.acceptAndRead() } - startLivenessSweep() // F-6: steady-state post-establishment liveness watchdog + startLivenessSweep() // steady-state post-establishment liveness watchdog // Agent-control: drain CDP off fd 3/4's parent ends on a dedicated reader, // splitting the NUL-delimited JSON stream into messages. Started only after // a successful spawn (the fds exist). Joined in shutdown() before close. // Install the (debug-only) validation handler BEFORE starting the reader so // the reader never observes a half-installed onCdpMessage (the only path that - // mutates it in CEF-1); in normal flow it's a no-op and onCdpMessage stays + // mutates it before a relay exists); in normal flow it's a no-op and onCdpMessage stays // nil. The probe-send loop it kicks off is fine to start first — the response // just buffers in the pipe until the reader drains it. if agentControl && cdpReadFd >= 0 { @@ -252,7 +252,7 @@ extension CefProfileHost { cdpReadFd = outRead _ = fcntl(cdpWriteFd, F_SETFD, FD_CLOEXEC) _ = fcntl(cdpReadFd, F_SETFD, FD_CLOEXEC) - // SIGPIPE guard on the WRITE end (H2 discipline, pipe edition): the IPC conn + // SIGPIPE guard on the WRITE end (same as the IPC socket's): the IPC conn // fd uses the SO_NOSIGPIPE socket option, but pipe fds don't take it, so a // write to a cef_host that closed its CDP read end (it died) would otherwise // raise SIGPIPE and kill the whole host APP. F_SETNOSIGPIPE is the Darwin @@ -333,15 +333,15 @@ extension CefProfileHost { // No connection and no clean shutdown in flight is a dead host too: // cef_host exited before connecting (e.g. a crash during CefInitialize, or // a FLUTTER_CEF_HOST that isn't cef_host), or accept() failed. - // handleHostDeath() no-ops on a clean shutdown (running==false). The C2 - // cache-lock loss connects first (it SendLogs "profile-locked" then exits - // 2), so it usually surfaces via the read-loop EOF below; either way + // handleHostDeath() no-ops on a clean shutdown (running==false). A + // cache-lock loss (another process holds the profile) connects first (it + // SendLogs "profile-locked" then exits 2), so it usually surfaces via the read-loop EOF below; either way // handleHostDeath() reads the real exit status. NSLog("[cef] cef_host for profile '\(profileId)' never connected") handleHostDeath() return } - // After accept(), guard the conn fd against SIGPIPE (H2): a write() to a + // After accept(), guard the conn fd against SIGPIPE: a write() to a // peer-closed socket would otherwise raise SIGPIPE and kill the whole host // APP, not just fail the write. With SO_NOSIGPIPE the write returns -1/EPIPE // and writeAll() reports failure, which we route to handleHostDeath(). @@ -370,7 +370,7 @@ extension CefProfileHost { } pendingFrames.removeAll() writeLock.unlock() - // A flush write that failed means the pipe is already dead (H2) — treat it + // A flush write that failed means the pipe is already dead — treat it // as a host death rather than spinning into the read loop on a broken fd. if !flushOk { handleHostDeath(); return } while running { @@ -378,7 +378,7 @@ extension CefProfileHost { if !readAll(fd, &hdr, 4) { break } let bodyLen = (Int(hdr[0]) << 24) | (Int(hdr[1]) << 16) | (Int(hdr[2]) << 8) | Int(hdr[3]) // Minimum valid body is 5 bytes (4 browserId + 1 op + 0 payload). - // H9: a malformed/oversized length means a wire desync and tears down EVERY + // A malformed/oversized length means a wire desync and tears down EVERY // browser on this host — log the rejected length first so it isn't a silent, // breadcrumb-less all-tiles crash (the IPC peer is trusted, so this only fires // on a genuine framing bug). @@ -394,7 +394,7 @@ extension CefProfileHost { if bid == 0 { handleProcessFrame(op, payload) } else if op == CefOp.targetId { - // CEF-2b: a targetId resolution result — route to the pending completion, + // A targetId resolution result — route to the pending completion, // not the session. handleTargetId(bid, String(bytes: payload, encoding: .utf8)) } else if op == CefOp.evalResult, @@ -427,7 +427,7 @@ extension CefProfileHost { } else { browsersLock.lock() let session = browsers[bid] - // C1: detect the FIRST present under the browsersLock we already hold, via a + // Detect the FIRST present under the browsersLock we already hold, via a // per-session flag, so the watchdog-cancel (presentLock) fires once per browser // instead of acquiring a second lock on every (up to 60fps) present frame. var firstPaint = false @@ -442,7 +442,7 @@ extension CefProfileHost { DispatchQueue.global().async { [weak self] in self?.recordGpuProcess() } } if s.presentCount == estabStableFrames { reachedStableFrames = true } - // F-6: any present clears the liveness-stall state — the browser is alive. + // Any present clears the liveness-stall state — the browser is alive. s.lastPresentNs = DispatchTime.now().uptimeNanoseconds s.livenessNudgedAt = 0 } else if op == CefOp.pageStart, let s = session { @@ -477,7 +477,7 @@ extension CefProfileHost { session?.handleFrame(op, payload) } } - // C1: the loop exited. If `running` is still true this was NOT a clean + // The loop exited. If `running` is still true this was NOT a clean // shutdown() (which clears `running` BEFORE shutting the fds down) — the // host died (EOF/ECONNRESET on the peer, or a malformed frame). Surface it. // shutdown() flips `running` false first, so its fd-close-driven read EOF @@ -485,11 +485,11 @@ extension CefProfileHost { handleHostDeath() } - /// C1/H2: the host has (apparently) died — the reader hit EOF while running, + /// The host has (apparently) died — the reader hit EOF while running, /// accept()/the pre-ready flush failed, or a send's writeAll failed. Fire /// `onHostDied` ONCE on the main thread (the plugin's maps are main-thread - /// confined — H3), passing the process exit status so the plugin can tell a - /// cache-lock loss (status 2 — C2 contract) from a generic crash. A clean + /// confined), passing the process exit status so the plugin can tell a + /// cache-lock loss (cef_host exits 2) from a generic crash. A clean /// shutdown() (running==false) is not a death and is ignored. func handleHostDeath() { writeLock.lock() @@ -499,12 +499,12 @@ extension CefProfileHost { guard running, !diedFired else { writeLock.unlock(); return } diedFired = true crashed = true - // H6: abandon paced creates — the host is gone. Sessions stay in `browsers`, so + // Abandon paced creates — the host is gone. Sessions stay in `browsers`, so // the onHostDied → plugin path still emits processGone for each queued one. createSendQueue.removeAll() createInFlight.removeAll() let p = process - // H5: TAKE the posix_spawn pid (zero it) so this reaper is the SOLE owner of its + // TAKE the posix_spawn pid (zero it) so this reaper is the SOLE owner of its // waitpid — a later terminateProcess()/shutdown() then sees 0 and won't // double-reap a pid this thread is about to harvest (which could kill an // OS-recycled pid). If it's wedged and we can't reap within the grace window @@ -541,12 +541,12 @@ extension CefProfileHost { // terminationStatus traps if read while the process is still running — so we // must not busy-wait here. Hop to a background queue, wait briefly for the // process to actually exit (EOF usually means it already has), then deliver - // on main (the plugin's maps are main-thread confined — H3). Generic-crash + // on main (the plugin's maps are main-thread confined). Generic-crash // status (-1) if it outlives the grace window. // // Two launch paths: `process` (Foundation.Process) exposes isRunning/ // terminationStatus; the posix_spawn path has only `pid`, so we poll waitpid - // (WNOHANG) and extract the exit code via WEXITSTATUS so the C2 cache-lock + // (WNOHANG) and extract the exit code via WEXITSTATUS so the cache-lock // signal (exit 2 -> "locked") matches Process.terminationStatus's semantics. DispatchQueue.global().async { [weak self] in var status: Int32 = -1 @@ -571,7 +571,7 @@ extension CefProfileHost { } usleep(50_000) } - // H5: still alive after the grace window (a wedged child that didn't exit on + // Still alive after the grace window (a wedged child that didn't exit on // EOF). Don't merely hand it back — the clean-shutdown path may never call // terminateProcess() again, leaving a zombie/orphan cef_host. SIGKILL + reap it // right here. We exclusively own this pid (spawnedPid was zeroed above) and it @@ -610,7 +610,7 @@ extension CefProfileHost { } let flags = payload.first ?? 0 let adhoc = (flags & 0x01) != 0 - // F.5 dev safety-rail: an ad-hoc (mock-keychain) host must NOT load a named + // Dev safety rail: an ad-hoc (mock-keychain) host must NOT load a named // persistent profile unless explicitly allowed, because at-rest creds // wouldn't be protected. Nothing has been written yet (no browser was // created), so refusing here leaks nothing. The plugin respawns an diff --git a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Liveness.swift b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Liveness.swift index 05e01dc..4ed54eb 100644 --- a/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Liveness.swift +++ b/packages/flutter_cef_macos/macos/Classes/CefProfileHost+Liveness.swift @@ -4,7 +4,7 @@ import Foundation extension CefProfileHost { - // ── F-6: steady-state liveness watchdog ───────────────────────────────────────────── + // ── Steady-state liveness watchdog ────────────────────────────────────────────────── // The first-paint watchdog RETIRES at first paint (firstPresentArrived), so a // browser that painted ≥1 frame then WEDGES (renderer/GPU stall inside a shared host // that keeps the pipe alive, so no processGone) had NO detector — silent blank until @@ -135,7 +135,7 @@ extension CefProfileHost { // in one session). A converged idle tile is healthy by definition; we keep serving its // last good frame. Do NOT recreate. (Never-painted tiles are owned by the separate // first-paint watchdog via firstPresentPending; genuine renderer death is caught by - // OnRenderProcessTerminated; eviction-while-hidden by the F-1 un-hide repaint.) Leave + // OnRenderProcessTerminated; eviction-while-hidden by the native un-hide repaint.) Leave // nudgedAt set so we don't re-nudge every cycle; a real future repaint clears it. // // A VISIBLE renderer that HANGS post-establishment (a deadlock that keeps the process diff --git a/packages/flutter_cef_macos/macos/Classes/CefProfileHost.swift b/packages/flutter_cef_macos/macos/Classes/CefProfileHost.swift index 8ae9545..9c0ab29 100644 --- a/packages/flutter_cef_macos/macos/Classes/CefProfileHost.swift +++ b/packages/flutter_cef_macos/macos/Classes/CefProfileHost.swift @@ -74,7 +74,7 @@ final class CefProfileHost { // (HostConfigPolicy). Set in spawn() on the main thread, read there too. var allowedSchemes = "" - // Agent-control / pipe mode (CEF-1). When true, cef_host was launched via + // Agent-control / pipe mode. When true, cef_host was launched via // posix_spawn so it inherits two CDP pipes (child reads CDP on fd 3, writes on // fd 4) and was passed --cdp-pipe; the Chromium "remote-debugging-pipe" switch // makes it speak NUL-delimited JSON over those fds instead of a TCP port. Off @@ -94,9 +94,9 @@ final class CefProfileHost { let cdpReaderDone = DispatchSemaphore(value: 0) // Invoked (off the CDP reader thread) for each complete CDP message (one // NUL-delimited UTF-8 JSON line, NUL stripped). Set by the plugin/relay; the - // CEF-1 validation hook installs a temporary one to prove the round-trip. + // debug validation hook installs a temporary one to prove the round-trip. var onCdpMessage: ((String) -> Void)? - // CEF-2a/b: the token-gated localhost CDP relays (created lazily by + // The token-gated localhost CDP relays (created lazily by // enableAgentControl()). Each bridges a CDP client's WebSocket ⇄ this host's pipe // and is scoped to ONE browser's CDP target. Keyed by the wire browserId so N // tiles in the same shared cef_host can be agent-controlled concurrently — they @@ -113,9 +113,9 @@ final class CefProfileHost { var cdpClosed = false // The CDP command ids every relay on this host's pipe uses. let cdpPipeIds = CdpPipeIds() - // Guards onCdpMessage and cdpRelays. CEF-2a/b mutates onCdpMessage LIVE (enable/ + // Guards onCdpMessage and cdpRelays. Agent control mutates onCdpMessage LIVE (enable/ // disable on the main thread) while the CDP reader thread reads it per message, - // so — unlike CEF-1, which only set it before the reader started — both must be + // so — unlike the debug validation hook, set before the reader starts — both must be // synchronized. A plain closure property is a fat (ptr+context) value; a concurrent // read during a write can tear it and call into freed context. let cdpHandlerLock = NSLock() @@ -149,7 +149,7 @@ final class CefProfileHost { let writeLock = NSLock() var pendingFrames: [[UInt8]] = [] // queued until the pipe connects var running = false - // C1: set true (under writeLock) when the host dies unexpectedly — reader EOF + // Set true (under writeLock) when the host dies unexpectedly — reader EOF // while running, or a writeAll to a dead pipe. Distinct from `running=false` // (clean shutdown()): `crashed` stops the pacer, the sweeps and agent control // on a dead host. @@ -165,7 +165,7 @@ final class CefProfileHost { var browsers: [UInt32: CefWebSession] = [:] var nextBrowserId: UInt32 = 1 var ready = false - // The host refused its named profile at kOpReady (ad-hoc build, see F.5): it + // The host refused its named profile at kOpReady (ad-hoc build): it // never becomes ready, so a create that lands before the plugin moves the // sessions off it stays queued instead of loading the profile. var refused = false @@ -205,13 +205,13 @@ final class CefProfileHost { // heavy real site that's slow to composite isn't de-serialized prematurely. }() - // C1 first-present watchdog (guarded by presentLock). browserIds awaiting their FIRST + // First-present watchdog (guarded by presentLock). browserIds awaiting their FIRST // kOpPresent: if none arrives within the deadline we re-kick via kOpInvalidate, then (if // still blank) surface paintStalled to Dart — converting a silent never-painted tile // into self-healing-or-signalled. let presentLock = NSLock() var firstPresentPending: Set = [] - // C1: browsers the host has hidden (WasHidden(true) via kOpSetVisible). A hidden CEF + // Browsers the host has hidden (WasHidden(true) via kOpSetVisible). A hidden CEF // browser stops producing frames entirely, so it legitimately never sends kOpPresent — // the watchdog must NOT treat that as a stall (work_canvas creates tiles already // off-screen as a normal lazy-spawn pattern). Guarded by presentLock. @@ -257,7 +257,7 @@ final class CefProfileHost { return 0.4 }() - // F-6 steady-state liveness sweep (CefProfileHost+Liveness.swift). + // Steady-state liveness sweep (CefProfileHost+Liveness.swift). let livenessStalenessNs: UInt64 = { if let s = ProcessInfo.processInfo.environment["FLUTTER_CEF_LIVENESS_MS"], let ms = Double(s), ms > 0 { return UInt64(ms * 1_000_000) } @@ -289,7 +289,7 @@ final class CefProfileHost { // racing a read on another thread can tear it. // // Invoked (off the reader thread) when an ad-hoc host refuses to load a named - // profile (no creds were written — see F.5). The plugin tears this host down + // profile (no creds were written). The plugin tears this host down // and moves every session on it to an ephemeral host of its own. var onInsecureProfileRefused: (() -> Void)? @@ -301,19 +301,19 @@ final class CefProfileHost { // mismatched binary and loop. var onProtocolMismatch: ((UInt8) -> Void)? - // C1: invoked ON THE MAIN THREAD when the reader loop exits UNEXPECTEDLY + // Invoked ON THE MAIN THREAD when the reader loop exits UNEXPECTEDLY // (cef_host died: EOF/ECONNRESET while running, or a writeAll to a dead pipe) // — NOT on a clean shutdown(). Carries the process exit status so the plugin - // can distinguish a cache-lock loss (status 2 — see the C2 cross-group - // contract) from a generic crash, emit `processGone` to Dart, and drop the + // can distinguish a cache-lock loss (status 2: another process holds the + // profile) from a generic crash, emit `processGone` to Dart, and drop the // host so the profile_in_use guard unblocks. Fires at most once per host. var onHostDied: ((Int32) -> Void)? var diedFired = false // guarded by writeLock; one onHostDied per host // One browser can't continue while the host is otherwise fine — its create // failed ("createFailed"), its renderer kept crashing or hung ("crashed") — so - // the plugin drops that one session and emits processGone(reason) for it. C1: a - // browser never painted its first frame despite a re-kick — the plugin surfaces + // the plugin drops that one session and emits processGone(reason) for it. If a + // browser never painted its first frame despite a re-kick, the plugin surfaces // paintStalled so the consumer can recover (e.g. recreate the view) instead of // staring at a silent blank tile. Both carry the wire browserId; invoked off the // reader / a timer thread. @@ -332,7 +332,7 @@ final class CefProfileHost { /// the current geometry, so replaying the resize could reference a since-freed /// IOSurface id. func send(_ browserId: UInt32, _ op: UInt8, _ payload: [UInt8]) { - // C1: peek visibility so the first-present watchdog doesn't flag an intentionally + // Peek visibility so the first-present watchdog doesn't flag an intentionally // hidden (WasHidden) browser as stalled — it produces no frames by design. if op == CefOp.setVisible, let v = payload.first { noteVisibility(browserId, visible: v != 0) @@ -350,7 +350,7 @@ final class CefProfileHost { } let ok = frame.withUnsafeBytes { writeAll(connFd, $0.baseAddress!, frame.count) } writeLock.unlock() - // H2: a failed write means the pipe is dead — until now the return was + // A failed write means the pipe is dead — until now the return was // discarded and a dead pipe was indistinguishable from success. Surface it // (unlocked first: handleHostDeath re-takes writeLock). if !ok { handleHostDeath() } @@ -367,7 +367,7 @@ final class CefProfileHost { /// Close ONE browser (kOpDisposeBrowser) and unregister it under lock. Returns /// the number of browsers still registered on this host afterward. func removeBrowser(_ browserId: UInt32) -> Int { - // CEF-2b: if this tile was agent-controlled, tear down ITS relay (its scoped + // If this tile was agent-controlled, tear down ITS relay (its scoped // targetId is now dead) BEFORE disposing the browser — disableAgentControl is // a no-op when there's no relay for this id. Does its own locking + stops the // relay outside cdpHandlerLock. @@ -387,7 +387,7 @@ final class CefProfileHost { writeLock.lock() createEnqueued.remove(browserId) writeLock.unlock() - // C1: drop any watchdog/visibility bookkeeping for the gone browser so the sets + // Drop any watchdog/visibility bookkeeping for the gone browser so the sets // don't grow across a long session of tile churn. presentLock.lock() firstPresentPending.remove(browserId) @@ -416,10 +416,10 @@ final class CefProfileHost { // fds): this is a CLEAN teardown, so neither the reader's read-EOF nor a // failed kOpShutdown write should be mistaken for a crash — handleHostDeath() // guards on `running`, so flipping it false here keeps onHostDied from firing - // on the shutdown path (C1). + // on the shutdown path. writeLock.lock() running = false - // H6: abandon any paced creates so a stuck pacer can't wedge a reused host and + // Abandon any paced creates so a stuck pacer can't wedge a reused host and // queued-never-sent sessions don't linger. The browsers map still holds them, so // disposeSession/onHostDied path cleans them up. createSendQueue.removeAll() @@ -428,7 +428,7 @@ final class CefProfileHost { // kOpReady tears down all THREE create-state queues symmetrically. pendingCreates.removeAll() writeLock.unlock() - // CEF-2a/b: drop ALL relays (each a listener + any client) before tearing down + // Drop ALL relays (each a listener + any client) before tearing down // the pipe, so none keeps bridging into a closing fd. Snapshot under the lock, // clear the dict + onCdpMessage, then stop each OUTSIDE the lock (stop() may // block briefly on a stuck client and takes the relay's own locks). @@ -447,7 +447,7 @@ final class CefProfileHost { // which Swift would otherwise resolve this unqualified call to. Not the // listening socket: on Darwin that fails with ENOTCONN and wakes nothing. if c >= 0 { Darwin.shutdown(c, SHUT_RDWR) } - // H1: gate the join on `readerStarted` ALONE (not the old `wasRunning`). The + // Gate the join on `readerStarted` ALONE (not the old `wasRunning`). The // semaphore is level-triggered — if the reader already exited (e.g. it drove the // crash path and signalled readerDone before this runs), wait() returns at once. // Gating on `wasRunning` could SKIP the join while the reader is still blocked in @@ -486,7 +486,7 @@ final class CefProfileHost { try? FileManager.default.removeItem(atPath: dir) } } - // H1: same discipline for the CDP reader — gate on cdpReaderStarted alone, and + // Same discipline for the CDP reader — gate on cdpReaderStarted alone, and // never close the read fd on a join timeout (the reader is still in read() on it). let cdpJoined = !cdpReaderStarted || cdpReaderDone.wait(timeout: .now() + 2) == .success if cdpReadFd >= 0 { if cdpJoined { close(cdpReadFd) }; cdpReadFd = -1 } @@ -496,7 +496,7 @@ final class CefProfileHost { /// paths: `process` (Foundation.Process, default) and `spawnedPid` (posix_spawn, /// agent-control). Idempotent — clears whichever handle it used. private func terminateProcess() { - // H5: take BOTH handles atomically under writeLock so this is the sole owner of + // Take BOTH handles atomically under writeLock so this is the sole owner of // its terminate/waitpid — handleHostDeath's reaper can't be reaping the same pid // concurrently (it took ownership the same way, or handed it back to us). writeLock.lock() diff --git a/packages/flutter_cef_macos/macos/Classes/CefWebSession.swift b/packages/flutter_cef_macos/macos/Classes/CefWebSession.swift index 1b45e54..9170821 100644 --- a/packages/flutter_cef_macos/macos/Classes/CefWebSession.swift +++ b/packages/flutter_cef_macos/macos/Classes/CefWebSession.swift @@ -97,7 +97,7 @@ final class CefWebSession: NSObject, FlutterTexture { // Without this the addChannel op would go out with browserId=0 and the host // couldn't bind it to this browser, so the window. shim was never injected. private var channels: Set = [] - // C1: set once when this browser delivers its first present frame. Owned/guarded by + // Set once when this browser delivers its first present frame. Owned/guarded by // CefProfileHost under its browsersLock (the reader flips it there) — a cheap per-frame // first-paint check that avoids a second lock on the hot paint path. var firstPresentSeen = false @@ -107,7 +107,7 @@ final class CefWebSession: NSObject, FlutterTexture { // first frame — so the next create's first-frame GPU allocation can't knock a barely- // established browser back out. var presentCount = 0 - // F-6 steady-state liveness watchdog (guarded by CefProfileHost.browsersLock, like + // Steady-state liveness watchdog (guarded by CefProfileHost.browsersLock, like // presentCount). `lastPresentNs` = the most recent present's uptime; `livenessNudgedAt` // = uptime of an outstanding discriminating kOpInvalidate (0 = none). The host's periodic // sweep reads these to catch a browser that painted ≥1 frame then WEDGED (the first-paint @@ -156,10 +156,10 @@ final class CefWebSession: NSObject, FlutterTexture { // has since gone out — so during a smoothly-advancing drag the watchdog is a no-op, and it // only acts when a resize wedges (generation stops advancing because no present came). private var resizeGen: UInt64 = 0 - // F-4: mirrors the cef_host slot's hidden state (set by setVisible). While hidden the + // Mirrors the cef_host slot's hidden state (set by setVisible). While hidden the // begin-frame pump is gated off so no present can land — the resize watchdog must NOT // force-promote a never-painted (blank) buffer; it waits for the native un-hide repaint - // (F-1) to drive a real present. Guarded by bufferLock like the rest of the buffer state. + // to drive a real present. Guarded by bufferLock like the rest of the buffer state. private var hidden = false private let bufferLock = NSLock() // The consumer's last setVisible, which a browser bound later must honor (see @@ -350,7 +350,7 @@ final class CefWebSession: NSObject, FlutterTexture { // scaled to the tile (momentarily soft if the box grew) — never blank, never frozen-wrong. guard active else { return } // While hidden the pump is gated off, so kOpInvalidate can't paint — skip the nudge but keep - // the watchdog alive; the native un-hide repaint (F-1) drives a real present that promotes. + // the watchdog alive; the native un-hide repaint drives a real present that promotes. if !isHidden { sendFrame(CefOp.invalidate, []) } DispatchQueue.main.asyncAfter(deadline: .now() + 0.08) { [weak self] in self?.resizeWatchdog(gen) @@ -707,7 +707,7 @@ final class CefWebSession: NSObject, FlutterTexture { } /// WebRTC frame export: notify any consumer that the live surface (re)allocated, so it - /// can IOSurfaceLookup the new id and re-point its capture (R2). Call OUTSIDE bufferLock + /// can IOSurfaceLookup the new id and re-point its capture. Call OUTSIDE bufferLock /// so the callback can read session accessors without self-deadlock. Reports PHYSICAL /// (Retina) pixel dims = logical * dpr. private func notifySurface(_ sid: UInt32, _ logicalW: Int, _ logicalH: Int) { @@ -796,7 +796,7 @@ final class CefWebSession: NSObject, FlutterTexture { && diagPresentCount % 120 == 0 { NSLog("[cefdiag] present bid=\(browserId) tex=\(tid) count=\(diagPresentCount)") } - // R2: a resized surface just went live — tell WebRTC consumers to re-point their + // A resized surface just went live — tell WebRTC consumers to re-point their // IOSurface capture at the new id (this is the "fires on each resize" half). if promotedSid != 0 { notifySurface(promotedSid, promotedW, promotedH) } if tid != 0 { diff --git a/packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift b/packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift index 3acfa07..a55bd05 100644 --- a/packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift +++ b/packages/flutter_cef_macos/macos/Classes/FlutterCefPlugin.swift @@ -17,7 +17,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { private var profiles: [String: CefProfileHost] = [:] // key: profile name OR "~ephemeral~"+sessionId private var sessions: [String: SessionRecord] = [:] // sessionId -> its record - /// How a session was created. C2: when a shared host turns out to be ad-hoc and + /// How a session was created. When a shared host turns out to be ad-hoc and /// refuses its named profile, EVERY session on it is re-homed onto an ephemeral /// host with its own url + schemes + agent-control transport. Also the /// freeze/thaw recipe: thaw respawns a host of the ORIGINAL kind (profile / @@ -27,7 +27,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { agentControl: Bool, profile: String?, enableCdp: Bool, hostGroup: String?) - /// Everything the plugin keeps for one session. Main-thread only (H3), like the + /// Everything the plugin keeps for one session. Main-thread only, like the /// rest of these maps. private struct SessionRecord { let session: CefWebSession @@ -40,7 +40,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { var key: String? var frozen: Bool { host == nil } } - // C2: named profiles a running ad-hoc host already refused — future creates for them + // Named profiles a running ad-hoc host already refused — future creates for them // go straight to ephemeral instead of racing onto a doomed shared host. private var adhocBlockedProfiles: Set = [] // Named-profile hosts this plugin shut down, by profile, with when: a host of the @@ -97,7 +97,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { /// Shut down EVERY live cef_host (SIGTERM+SIGKILL escalation + reap, via the host's /// own shutdown()) so app termination leaves no orphaned subprocess holding a /// profile's Chromium SingletonLock. Main-thread confined like the other map - /// accessors (H3); the willTerminate observer is queued on .main. Idempotent: clears + /// accessors; the willTerminate observer is queued on .main. Idempotent: clears /// the maps so a stray second call (or a later normal teardown) is a no-op, and /// drops the self-observer. private func shutdownAllHosts() { @@ -291,7 +291,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { } result(nil) case "enableAgentControl": - // CEF-2b: broker a token-gated CDP endpoint scoped to THIS tile's CDP target. + // Broker a token-gated CDP endpoint scoped to THIS tile's CDP target. // Async (resolves the targetId via cef_host first). Requires the session to // have been created with agentControl (pipe) mode. guard let sid = args["sessionId"] as? String, let rec = sessions[sid], @@ -311,7 +311,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { } } case "disableAgentControl": - // CEF-2b: route by this session's browserId (mirrors enableAgentControl) so + // Route by this session's browserId (mirrors enableAgentControl) so // only THIS tile's relay is torn down — siblings on the same shared host stay // agent-controlled. if let sid = args["sessionId"] as? String, let rec = sessions[sid] { @@ -349,7 +349,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { private func create(_ a: [String: Any], _ result: @escaping FlutterResult) { // The session/profile dictionaries below are unlocked and rely on being - // touched only from the main thread (H3) — the method-channel handler always + // touched only from the main thread — the method-channel handler always // runs here. Assert it so a future off-main caller fails loudly, not silently // corrupting the maps. dispatchPrecondition(condition: .onQueue(.main)) @@ -371,7 +371,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { let dpr = (a["dpr"] as? Double).map { CGFloat($0) } ?? 1.0 let allowedSchemes = a["allowedSchemes"] as? String ?? "" let enableCdp = a["enableCdp"] as? Bool ?? false - // Agent-control / pipe mode (CEF-1): CDP rides cef_host's inherited fds 3/4 + // Agent-control / pipe mode: CDP rides cef_host's inherited fds 3/4 // (a private, NUL-framed pipe) instead of a TCP port. Because there's no // listening socket, the open-port cookie-exfil rationale doesn't apply, so // (unlike TCP enableCdp) it's permitted on a named profile — see below. Omit- @@ -418,15 +418,14 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { details: nil)) return } - // P2-step1: the single-view guard is lifted — multiple views on a named - // profile now share ONE cef_host (resolveOrSpawnHost de-dups by key), so - // every web tile renders and shares one cookie jar (sign-in persists across - // tiles + relaunch). P2-step2: agent-control is now multi-tile — N tiles on - // one shared host can be agent-controlled concurrently, each via its own - // per-target CDP relay (one relay per browserId, demuxed over the shared pipe - // by the per-relay CDP-id rewrite — see CdpRelay's multiplex note). + // Multiple views on a named profile share ONE cef_host (resolveOrSpawnHost + // de-dups by key), so every web tile renders and shares one cookie jar + // (sign-in persists across tiles + relaunch). Agent control is multi-tile + // too — N tiles on one shared host can be agent-controlled concurrently, each + // via its own per-target CDP relay (one relay per browserId, demuxed over the + // shared pipe by the per-relay CDP-id rewrite — see CdpRelay's multiplex note). - // C2: if a running ad-hoc host already refused this named profile, don't race onto + // If a running ad-hoc host already refused this named profile, don't race onto // a doomed shared host — go ephemeral directly. let effectiveNamed = namedProfile && !adhocBlockedProfiles.contains(profile ?? "") let key = effectiveNamed @@ -570,8 +569,8 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { /// Resolve an existing host for `key`, or spawn a fresh one. Returns nil if the /// spawn fails. `agentControl` switches the launch to posix_spawn (CDP over /// inherited fds 3/4) — see CefProfileHost.spawn. Only meaningful when this call - /// actually spawns; an EXISTING host keeps its original transport. Since P2, - /// a named profile is MULTI-view (N tiles share one host), so an agent-control + /// actually spawns; an EXISTING host keeps its original transport. A named + /// profile is MULTI-view (N tiles share one host), so an agent-control /// create() resolving to a pre-existing host is the normal path for the 2nd+ /// tile — the host was already spawned in agent-control mode by the first, and /// each tile gets its own per-target CDP relay (see CefProfileHost.enableAgentControl). @@ -612,7 +611,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { /// Fail every session attached to `host` (emit processGone with `reason` + dispose), /// drop the host from the profile registry, and reap it. Main-thread only (the maps - /// are main-thread confined — H3). Shared by the host-death and protocol-mismatch + /// are main-thread confined). Shared by the host-death and protocol-mismatch /// paths, which differ only in the reason string. private func failHost(_ host: CefProfileHost, reason: String) { dispatchPrecondition(condition: .onQueue(.main)) @@ -621,7 +620,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { let goneSessions = sessions.compactMap { $0.value.host === host ? $0.key : nil } for sid in goneSessions { emit("processGone", ["sessionId": sid, "reason": reason]) - // F-5: dispose the session BEFORE dropping its record. dispose() is the only caller + // Dispose the session BEFORE dropping its record. dispose() is the only caller // of registry.unregisterTexture (+ frees the CVPixelBuffer / IOSurface / any pending // buffer). If we just nil sessions[sid], the later Dart controller.dispose -> // disposeSession early-returns on the now-missing session, so the texture + surfaces @@ -642,13 +641,13 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { /// Install every callback `host` makes. Called once per host, before spawn(): the /// host invokes them from its own threads, and a closure reassigned while another - /// thread reads it can tear. `namedProfile` wires the F.5 refusal, which only a + /// thread reads it can tear. `namedProfile` wires the ad-hoc-build refusal, which only a /// named-profile host can raise. private func wireHost(_ host: CefProfileHost, namedProfile: String?) { host.onHostDied = { [weak self, weak host] status in dispatchPrecondition(condition: .onQueue(.main)) guard let self = self, let host = host else { return } - // C2 cross-group contract: cef_host exits 2 (after SendLog "profile-locked") + // cef_host exits 2 (after SendLog "profile-locked") // when it loses the cache singleton lock to another process. Surface that as // a distinct reason so the widget can say "already open elsewhere" instead of // a generic crash. A host that died before kOpReady never created a browser: @@ -695,7 +694,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { self.disposeSession(sid) } } - // C1: a browser never painted its first frame despite a re-kick — surface + // A browser never painted its first frame despite a re-kick — surface // paintStalled so Dart/the consumer can recover (e.g. recreate the view) instead of // a silent, unrecoverable blank tile. The browser stays alive (it may yet paint). host.onPaintStalled = { [weak self, weak host] browserId in @@ -705,8 +704,8 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { self.emit("paintStalled", ["sessionId": sid]) } } - // F.5 dev safety-rail: an ad-hoc (mock-keychain) host refuses a named persistent - // profile at kOpReady (nothing's been written, so no creds leak). C2: re-home the + // Dev safety rail: an ad-hoc (mock-keychain) host refuses a named persistent + // profile at kOpReady (nothing's been written, so no creds leak). Re-home the // WHOLE shared host's sessions onto ephemeral hosts — not just the one that // spawned it — so a burst of tiles that all attached before kOpReady are all // rescued. @@ -767,7 +766,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { } } - /// C2/F.5: a running cef_host turned out to be an ad-hoc (mock-keychain) build and + /// A running cef_host turned out to be an ad-hoc (mock-keychain) build and /// refused its named profile (at kOpReady, BEFORE any browser was created — so nothing /// rendered or leaked). Re-home EVERY session that was on that shared host onto its /// own ephemeral host, preserving each session's url/schemes/agent-control, and @@ -775,7 +774,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { /// the old per-session respawn that shut the whole shared host down — which stranded /// every sibling tile blank-and-dead with no error. private func respawnHostEphemeral(_ oldHost: CefProfileHost, refusedProfile: String) { - // The unlocked session/profile dictionaries are confined to the main thread (H3); + // The unlocked session/profile dictionaries are confined to the main thread; // reached from onInsecureProfileRefused via DispatchQueue.main. dispatchPrecondition(condition: .onQueue(.main)) guard let cefHost = resolveCefHostPath() else { return } @@ -798,7 +797,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { // The old host is already shut down, so a bare `continue` would strand this // session bound to a dead host: blank tile, no signal, leaked session+texture. // Fail it explicitly instead — processGone lets the consumer recreate. - NSLog("[cef] C2 respawn ephemeral host failed for \(sid)") + NSLog("[cef] ephemeral respawn failed for \(sid)") emit("processGone", ["sessionId": sid, "reason": "respawnFailed"]) sessions[sid] = nil session.dispose() @@ -879,13 +878,13 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { } /// Tear down one session: close its browser on the shared host, then dispose - /// the session. ORDERING (binding, F.3): if this was the host's last browser, + /// the session. ORDERING (binding): if this was the host's last browser, /// `host.shutdown()` (which joins the reader, so no more inbound) runs BEFORE /// `session.dispose()` and the profile is dropped. Otherwise `removeBrowser` /// has already unregistered this browser under lock, so `session.dispose()` /// runs safely while the shared reader keeps serving the siblings. private func disposeSession(_ id: String) { - // Unlocked session/profile dictionaries — main-thread confined (H3). Reached + // Unlocked session/profile dictionaries — main-thread confined. Reached // from create()/destroy() (channel handler, on main) and never off-main. dispatchPrecondition(condition: .onQueue(.main)) guard let rec = sessions.removeValue(forKey: id) else { return } @@ -923,7 +922,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { } let session = rec.session let key = rec.key - // Same F.3 ordering discipline as disposeSession: removeBrowser unregisters + // Same ordering discipline as disposeSession: removeBrowser unregisters // under lock (reader stops routing to this session), and a last-browser // host is fully shut down (reader joined) BEFORE the session's unlocked // establishment counters are reset in detachForFreeze. @@ -999,7 +998,7 @@ public class FlutterCefPlugin: NSObject, FlutterPlugin { return "~ephemeral~" + sessionId } - /// Resolve the on-disk cache dir for a profile. F.4: a null/empty profile gets + /// Resolve the on-disk cache dir for a profile. A null/empty profile gets /// a unique throwaway temp dir (ephemeral, removed on host shutdown); a named /// profile gets a stable 0700 dir under Application Support that survives /// relaunch. Both go through one downstream code path: the host always receives diff --git a/packages/flutter_cef_macos/macos/Classes/LivenessProbePolicy.swift b/packages/flutter_cef_macos/macos/Classes/LivenessProbePolicy.swift index 9270830..9728127 100644 --- a/packages/flutter_cef_macos/macos/Classes/LivenessProbePolicy.swift +++ b/packages/flutter_cef_macos/macos/Classes/LivenessProbePolicy.swift @@ -1,4 +1,4 @@ -// Pure decision policy for the STEADY-STATE liveness watchdog (F-6 / audit C-3): the +// Pure decision policy for the STEADY-STATE liveness watchdog: the // backstop that catches a browser which painted at least once and then WEDGED (blank / // frozen) with no other detector — the first-present watchdog retires at first paint, so // post-establishment wedges were previously silent until relaunch. diff --git a/packages/flutter_cef_macos/macos/Classes/ResizeWatchdogPolicy.swift b/packages/flutter_cef_macos/macos/Classes/ResizeWatchdogPolicy.swift index 9643a61..c41f756 100644 --- a/packages/flutter_cef_macos/macos/Classes/ResizeWatchdogPolicy.swift +++ b/packages/flutter_cef_macos/macos/Classes/ResizeWatchdogPolicy.swift @@ -1,6 +1,6 @@ // Pure decision policy for the resize watchdog (CefWebSession.resizeWatchdog) — the // hidden/in-flight/elapsed gating, with NO dependency on Flutter, CEF, IOSurface, or the -// host IPC. Extracted so the gating that prevents the visibility/resize WEDGE (F-4: never +// host IPC. Extracted so the gating that prevents the visibility/resize WEDGE (never // force-promote a never-painted surface for a HIDDEN browser) is unit-testable standalone // — compiles + runs with `swiftc` alone, exactly like CdpRelay's filter tests: // @@ -16,16 +16,16 @@ enum ResizeWatchdogPolicy { /// but the present was dropped/mis-tagged. /// /// - `inFlight` / `gen` / `currentGen`: a newer resize (gen advanced) cancels this one. - /// - `hidden`: **the F-4 fix** — while hidden the begin-frame pump is gated off, so the + /// - `hidden`: **the wedge guard** — while hidden the begin-frame pump is gated off, so the /// pending surface is zero-filled (never painted); promoting it wedges the texture /// permanently blank. Must NOT promote while hidden — wait for the native un-hide - /// repaint (F-1) to drive a real present that promotes through the normal path. + /// repaint to drive a real present that promotes through the normal path. /// - `elapsedNs` / `thresholdNs`: only after the grace window with no present. static func shouldForcePromote(inFlight: Bool, gen: UInt64, currentGen: UInt64, hidden: Bool, elapsedNs: UInt64, thresholdNs: UInt64) -> Bool { guard inFlight, gen == currentGen else { return false } // superseded / already promoted - if hidden { return false } // F-4: never promote a hidden (blank) surface + if hidden { return false } // never promote a hidden (blank) surface return elapsedNs > thresholdNs } diff --git a/packages/flutter_cef_macos/native/build-cef-from-source.sh b/packages/flutter_cef_macos/native/build-cef-from-source.sh index 0220954..3a48c76 100755 --- a/packages/flutter_cef_macos/native/build-cef-from-source.sh +++ b/packages/flutter_cef_macos/native/build-cef-from-source.sh @@ -10,7 +10,6 @@ # prefix EMPTY (".org.chromium.Chromium.webauthn"), which no valid entitlement can # match. The prebuilt CEF exposes no API/switch for it, so the only fix is a # from-source rebuild with native/patches/campus_webauthn_keychain.patch. -# Full write-up: work_canvas specs/cef-passkey/PLAN.md. # # This is Tier-2 (build libcef from source), NOT a Chromium fork -- fold it into # the same from-source build you stand up for proprietary codecs. @@ -85,7 +84,7 @@ fi # BENIGN red herring -- present in the working build too; don't chase it. # proprietary_codecs + ffmpeg_branding="Chrome" add H.264/AAC. These are # ROYALTY-BEARING (MPEG-LA / Via-LA) -- gate *distribution* (prod release-tag) -# on legal sign-off. See work_canvas specs/cef-passkey/PLAN.md. +# on legal sign-off. echo "[cef-src] build (Release arm64, official + H.264/AAC) -- multi-hour first compile" GN_DEFINES='is_official_build=true proprietary_codecs=true ffmpeg_branding="Chrome"' \ python3 "$AUTOMATE" \ @@ -114,7 +113,7 @@ echo " FLUTTER_CEF_CACHE=/tmp/cef_cache CEF_HOST_ADHOC=OFF \\" echo " CODESIGN_ID='Developer ID Application: FlutterFlow, Inc. (KLAJ5X6PJP)' \\" echo " '$HERE/build_cef_host.sh'" echo -echo "cef_host then needs (see specs/cef-passkey/PLAN.md): the keychain-access-group" +echo "cef_host then needs: the keychain-access-group" echo "entitlement on the BROWSER process only + an embedded Developer-ID" echo "provisioning profile authorizing that group. Release Campus.app is already" echo "signed+notarized, so the validation-category condition is satisfied there." diff --git a/packages/flutter_cef_macos/native/cef_host/CMakeLists.txt b/packages/flutter_cef_macos/native/cef_host/CMakeLists.txt index 789a313..0cc5385 100644 --- a/packages/flutter_cef_macos/native/cef_host/CMakeLists.txt +++ b/packages/flutter_cef_macos/native/cef_host/CMakeLists.txt @@ -45,9 +45,9 @@ option(CEF_HOST_ADHOC "Compile in dev/ad-hoc-only Chromium security shortcuts" O # Codesign entitlements: dev/ad-hoc carries get-task-allow (local debugging); a # signed release (CEF_HOST_ADHOC=OFF) uses a stripped variant without it # (notarization hard-fails with get-task-allow, and it's a local task-port -# privilege-escalation vector). Both keep allow-jit / allow-unsigned-executable- -# memory / disable-library-validation, which the JIT renderer + dlopen'd -# framework need. +# privilege-escalation vector). The dev set also relaxes allow-unsigned- +# executable-memory / disable-library-validation; the release set keeps only +# allow-jit, which the JIT renderer needs (see entitlements.release.plist). if(CEF_HOST_ADHOC) set(ENT "${CMAKE_CURRENT_SOURCE_DIR}/entitlements.plist") else() diff --git a/packages/flutter_cef_macos/native/cef_host/browser_ops.mm b/packages/flutter_cef_macos/native/cef_host/browser_ops.mm index 4762db5..29cdb75 100644 --- a/packages/flutter_cef_macos/native/cef_host/browser_ops.mm +++ b/packages/flutter_cef_macos/native/cef_host/browser_ops.mm @@ -104,7 +104,7 @@ void DoCreateBrowser(uint32_t wire_id, int w, int h, double dpr, // build leaves shared_texture_enabled off.) All browsers in this process share // one GPU/Viz process; set per-create, it resolves to that same process (the // second+ browser attaching cleanly is the one multiplex behavior to confirm - // at runtime under a signed build — see CONTRACT H.6). + // at runtime under a signed build). window_info.shared_texture_enabled = true; #endif // Own the frame clock. Without this CEF's internal scheduler decides when to paint and can @@ -157,7 +157,7 @@ void DoCreateBrowser(uint32_t wire_id, int w, int h, double dpr, slot->trusted_pending.insert(NormalizeAuthoredUrl(url)); } CefRefPtr client = NewHostClient(slot); - // H3: ASYNC create. CreateBrowserSync BLOCKS this (the single CEF UI) thread until + // ASYNC create. CreateBrowserSync BLOCKS this (the single CEF UI) thread until // the renderer + GPU/Viz accelerated-surface handshake completes — so a burst of // creates serialized here, contended the one shared GPU process (later browsers got // no surface, never painted), and one hung create wedged input/resize/dispose for @@ -171,7 +171,7 @@ void DoCreateBrowser(uint32_t wire_id, int w, int h, double dpr, window_info, client, create_url, settings, TakeDocumentStartExtraInfo(wire_id, &slot->channels), nullptr); if (!dispatched) { - // H7: the create couldn't even be dispatched — OnAfterCreated/OnBeforeClose will + // The create couldn't even be dispatched — OnAfterCreated/OnBeforeClose will // never fire, so reclaim the slot + the looked-up IOSurface (+1 ref) here (else // they leak and the wire id is stranded) and tell the host so it drops the session // (processGone) and its create-pacer advances instead of stalling on the ack. @@ -215,7 +215,7 @@ void DoDisposeBrowser(uint32_t wire_id) { if (slot->browser) { slot->browser->GetHost()->CloseBrowser(true); } else { - // H3: the async CreateBrowser hasn't bound the browser yet — record the close so + // The async CreateBrowser hasn't bound the browser yet — record the close so // OnAfterCreated closes it the instant it lands. Without this the create completes // into a live orphan browser the Swift side has already forgotten (browsers[id] // cleared), leaking a renderer + IOSurface until whole-host shutdown. @@ -259,12 +259,12 @@ void DoResize(const std::shared_ptr& slot, int w, int h, double dpr) { // the new surface immediately; PumpBeginFrame's ongoing ticks cover the heavy-page settle. slot->browser->GetHost()->SendExternalBeginFrame(); } else { - // F-2: HIDDEN — the begin-frame pump is gated off (PumpBeginFrame skips while + // HIDDEN — the begin-frame pump is gated off (PumpBeginFrame skips while // !visible), so WasResized()+SendExternalBeginFrame() here would never paint the // freshly-swapped (blank) surface, yet the Swift resizeWatchdog would force-promote // it to the live texture → permanent blank on a static page. The surface + dims are // already swapped above (geometry is current); defer the screen-info re-assert + the - // repaint to DoSetVisible's hidden->visible edge (F-1). WasResized while hidden is + // repaint to DoSetVisible's hidden->visible edge. WasResized while hidden is // pointless (no frame can result), so it is dropped, not deferred. if (dpr_changed) slot->needs_screen_info_on_show = true; } @@ -373,10 +373,10 @@ void DoSetVisible(const std::shared_ptr& slot, bool visible) { slot->visible = visible; // PumpBeginFrame reads this to idle the begin-frame pump while hidden if (!slot->browser) return; slot->browser->GetHost()->WasHidden(!visible); - // F-1 (keystone): on the hidden->visible edge, FORCE a fresh full-viewport repaint at the + // On the hidden->visible edge, FORCE a fresh full-viewport repaint at the // current geometry. WasHidden(false) alone does NOT repaint, and three things can have left // the live texture blank/stale while hidden: (a) a resize landed while the pump was gated off - // (F-2 deferred its paint here); (b) a dpr/screen-info change was deferred; (c) Chromium's + // (DoResize deferred its paint here); (b) a dpr/screen-info change was deferred; (c) Chromium's // FrameEvictionManager reclaimed the off-screen compositor frame entirely (happens past ~5 // browsers / under memory pressure) so there is nothing to show even though geometry is // unchanged. Re-assert screen info (if a dpr change was deferred) + size, then drive a @@ -723,7 +723,7 @@ void DoShowDevTools(const std::shared_ptr& slot, int inspect_x, namespace { -// CEF-2b: resolve a browser's CDP targetId so the Swift relay can scope an agent's +// Resolve a browser's CDP targetId so the Swift relay can scope an agent's // CDP session to exactly this tile. Extract the first quoted string value for `key` // from a flat CDP result JSON (targetIds are GUIDs with no embedded quotes/escapes). std::string ExtractJsonStringField(const std::string& json, @@ -924,7 +924,7 @@ void DoKey(const std::shared_ptr& slot, int type, uint32_t modifiers, slot->browser->GetHost()->SendKeyEvent(ev); } -// C1: force a repaint. The host's first-present watchdog sends kOpInvalidate when a +// Force a repaint. The host's first-present watchdog sends kOpInvalidate when a // browser hasn't delivered its first frame within the deadline — re-requesting the // frame self-heals a dropped/raced first paint instead of a permanently blank texture. void DoInvalidate(const std::shared_ptr& slot) { diff --git a/packages/flutter_cef_macos/native/cef_host/entitlements.browser.plist b/packages/flutter_cef_macos/native/cef_host/entitlements.browser.plist deleted file mode 100644 index c8f116e..0000000 --- a/packages/flutter_cef_macos/native/cef_host/entitlements.browser.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - com.apple.security.cs.allow-jit - com.apple.security.device.bluetooth - - keychain-access-groups - - KLAJ5X6PJP.org.chromium.Chromium.webauthn - - - com.apple.application-identifier - KLAJ5X6PJP.io.flutterflow.campus.dev.mac - com.apple.developer.team-identifier - KLAJ5X6PJP - - diff --git a/packages/flutter_cef_macos/native/cef_host/entitlements.release.plist b/packages/flutter_cef_macos/native/cef_host/entitlements.release.plist index 96051b3..918cc39 100644 --- a/packages/flutter_cef_macos/native/cef_host/entitlements.release.plist +++ b/packages/flutter_cef_macos/native/cef_host/entitlements.release.plist @@ -31,10 +31,12 @@ com.apple.security.device.bluetooth - + diff --git a/packages/flutter_cef_macos/native/cef_host/host_client.mm b/packages/flutter_cef_macos/native/cef_host/host_client.mm index 186e094..650b94f 100644 --- a/packages/flutter_cef_macos/native/cef_host/host_client.mm +++ b/packages/flutter_cef_macos/native/cef_host/host_client.mm @@ -463,13 +463,13 @@ void OnLoadEnd(CefRefPtr browser, CefRefPtr frame, // Report the new page's remembered camera/mic decision so the URL bar can // show a "blocked" indicator for a site Chromium will silently refuse. SendMediaState(slot_); - // C1 + RENDER FLOOR: force a repaint when the main frame finishes. Invalidate(PET_VIEW) + // RENDER FLOOR: force a repaint when the main frame finishes. Invalidate(PET_VIEW) // ALONE is coalesce-able — the scheduler can drop it, which on a shared GPU/Viz process // under a multi-browser establishment burst is exactly when the real-content first frame // gets lost, leaving a permanently blank tile though the page loaded. Mirror the proven // DoSetVisible visibility-edge kick: re-assert size + damage + a NON-coalesce-able // SendExternalBeginFrame, which deterministically drives one renderer frame the scheduler - // cannot swallow. (slot_->visible gate: a hidden tile must stay paused — F-2.) + // cannot swallow. (slot_->visible gate: a hidden tile must stay paused.) if (browser && browser->GetHost() && slot_->visible) { auto h = browser->GetHost(); h->WasResized(); @@ -513,7 +513,7 @@ void OnLoadingProgressChange(CefRefPtr, double progress) override { SendFrame(slot_->browser_id, kOpProgress, p, 4); } - // H3: async create completes here on the CEF UI thread. Bind the browser to its slot + // Async create completes here on the CEF UI thread. Bind the browser to its slot // (DoCreateBrowser no longer does — it dropped the blocking CreateBrowserSync) and ack // the host so its create-pacer sends the NEXT create: creates serialize by COMPLETION // (each browser's render + GPU/Viz accelerated-surface handshake done before the next @@ -521,7 +521,7 @@ void OnLoadingProgressChange(CefRefPtr, double progress) override { void OnAfterCreated(CefRefPtr browser) override { slot_->browser = browser; SendFrame(slot_->browser_id, kOpCreated, nullptr, 0); - // H3: a dispose arrived during the async-create window and recorded intent — honor + // A dispose arrived during the async-create window and recorded intent — honor // it now (OnBeforeClose then does the normal map-erase + surface release + retain- // cycle break) so we don't leak a live orphan browser the Swift side already forgot. if (slot_->close_requested || g_shutting_down) { @@ -535,7 +535,7 @@ void OnAfterCreated(CefRefPtr browser) override { slot_->nav_after_create.clear(); if (auto frame = browser->GetMainFrame()) frame->LoadURL(nav); } - // F-3: reconcile a visibility intent that arrived before the browser bound. A + // Reconcile a visibility intent that arrived before the browser bound. A // setVisible(false) on a still-creating slot ran DoSetVisible with browser==null // (WasHidden skipped), so slot_->visible is already false but CEF never heard it — // the slot would establish VISIBLE and pump at 60fps off-screen until the next flip. diff --git a/packages/flutter_cef_macos/native/cef_host/host_state.h b/packages/flutter_cef_macos/native/cef_host/host_state.h index 9003746..898be11 100644 --- a/packages/flutter_cef_macos/native/cef_host/host_state.h +++ b/packages/flutter_cef_macos/native/cef_host/host_state.h @@ -87,7 +87,7 @@ bool NoteCrashBurstAndCheckHostLoop(uint32_t wire_id, struct Slot { uint32_t browser_id = 0; // Swift-assigned wire id (>=1); NOT GetIdentifier(). CefRefPtr browser; - // H3 async-create dispose-loss guard: a dispose arriving while the async + // Async-create dispose-loss guard: a dispose arriving while the async // CreateBrowser is still in flight (browser == null) can't CloseBrowser yet, so it // records intent here and OnAfterCreated honors it the instant the browser binds — // otherwise that browser is a live orphan (renderer + IOSurface) nothing reclaims @@ -178,11 +178,11 @@ struct Slot { std::map> dialogs; uint32_t dialog_next = 1; - // CEF-2b: registration for the DevTools message observer used to resolve this + // Registration for the DevTools message observer used to resolve this // 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 + // 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 @@ -203,7 +203,7 @@ struct Slot { // consumer can drop an unengaged tile to ~30fps without touching hidden // gating. UI-thread only, like `visible`. int pump_interval_ms = 16; - // F-1/F-2: a dpr/screen-info change that lands while the slot is HIDDEN is deferred — + // A dpr/screen-info change that lands while the slot is HIDDEN is deferred — // the begin-frame pump is gated off while hidden, so notifying + painting now would // composite into a surface nothing displays and mislead the Swift resize watchdog into // promoting a never-painted buffer. DoResize sets this while hidden; DoSetVisible's diff --git a/packages/flutter_cef_macos/native/cef_host/ipc.mm b/packages/flutter_cef_macos/native/cef_host/ipc.mm index 9eb80df..9b1fda4 100644 --- a/packages/flutter_cef_macos/native/cef_host/ipc.mm +++ b/packages/flutter_cef_macos/native/cef_host/ipc.mm @@ -57,7 +57,7 @@ void SendFrame(uint32_t browser_id, uint8_t opcode, const void* payload, return; } std::lock_guard lock(g_ipc_write_mutex); - // C3: SNAPSHOT the fd under the write lock and write to the snapshot, never re-loading + // SNAPSHOT the fd under the write lock and write to the snapshot, never re-loading // g_ipc_fd at write time. Teardown sets g_ipc_fd=-1 (exchange) and close()s the old fd // under this same lock, so once we hold it the fd is either still valid (write) or // already -1 (skip) — a paint thread can no longer pass the early-out and then write diff --git a/packages/flutter_cef_macos/native/cef_host/ipc_reader.mm b/packages/flutter_cef_macos/native/cef_host/ipc_reader.mm index 2b4265d..0375ee0 100644 --- a/packages/flutter_cef_macos/native/cef_host/ipc_reader.mm +++ b/packages/flutter_cef_macos/native/cef_host/ipc_reader.mm @@ -25,7 +25,7 @@ void IpcReadLoop() { if (!ReadAll(g_ipc_fd, hdr, 4)) break; uint32_t body_len = ReadU32BE(hdr); // Minimum valid body is 5 bytes (4 browserId + 1 op + 0 payload). - // H9: a malformed/oversized length is a wire desync and tears down EVERY browser in + // A malformed/oversized length is a wire desync and tears down EVERY browser in // this process — log it first so it isn't a silent, breadcrumb-less all-tiles exit // (the IPC peer is trusted, so this only fires on a genuine framing bug). if (body_len < 5 || body_len > (64u << 20)) { diff --git a/packages/flutter_cef_macos/native/cef_host/main.mm b/packages/flutter_cef_macos/native/cef_host/main.mm index 32f505d..30cd022 100644 --- a/packages/flutter_cef_macos/native/cef_host/main.mm +++ b/packages/flutter_cef_macos/native/cef_host/main.mm @@ -325,7 +325,7 @@ void OnBeforeCommandLineProcessing( // browser creation on demand via kOpCreateBrowser (one per CefWebView sharing // this profile). Nothing loads — and nothing is written to the profile cache — // until the first kOpCreateBrowser, which is the safety window the host uses to - // refuse a persistent profile under a mock-keychain (ad-hoc) build (F.5). The + // refuse a persistent profile under a mock-keychain (ad-hoc) build. The // payload is [readyFlags (bit0 = ad-hoc build), protocolVersion] — the version // byte lets the host refuse a protocol-skewed binary at the handshake instead of // silently mis-parsing every later frame. @@ -520,7 +520,7 @@ int main(int argc, char* argv[]) { #ifdef CEF_HOST_ADHOC // Ad-hoc / mock-keychain build: secrets at rest aren't really encrypted, so a // persistent (named) profile here is insecure. Swift downgrades named profiles - // to ephemeral on an ad-hoc host (F.5); this is an advisory log only, and only + // to ephemeral on an ad-hoc host; this is an advisory log only, and only // for a real persistent profile (an ephemeral throwaway dir is never at risk). if (!profile_dir.empty() && !is_ephemeral && !std::getenv("FLUTTER_CEF_ALLOW_INSECURE_PROFILE")) { @@ -528,7 +528,7 @@ int main(int argc, char* argv[]) { } #endif - // Cross-process single-writer lock on a PERSISTENT profile dir (C2). Swift's + // Cross-process single-writer lock on a PERSISTENT profile dir. Swift's // in-memory dedup only covers one plugin instance; two app instances (or two // FlutterEngines in one process) would resolve the same root_cache_path and // spawn two cef_host on it. Chromium's own profile singleton then fails the @@ -671,7 +671,7 @@ int main(int argc, char* argv[]) { // (no concurrent SendFrame) and clear the fd so any late write is a no-op. { std::lock_guard lock(g_ipc_write_mutex); - // C3: store -1 FIRST (atomic exchange), THEN close — so a SendFrame that snapshots + // Store -1 FIRST (atomic exchange), THEN close — so a SendFrame that snapshots // the fd under this lock never holds a value that's already closed/recycled. The // GPU/compositor threads that call SendFrame aren't joined until CefShutdown below, // so this ordering (not close-then-clear) is what makes a late paint write a safe diff --git a/packages/flutter_cef_macos/test/CdpRelayFilterTests.swift b/packages/flutter_cef_macos/test/CdpRelayFilterTests.swift index 83ae820..391cf19 100644 --- a/packages/flutter_cef_macos/test/CdpRelayFilterTests.swift +++ b/packages/flutter_cef_macos/test/CdpRelayFilterTests.swift @@ -1,4 +1,4 @@ -// Standalone unit tests for the CEF-2b per-tile CDP isolation filter — THE security +// Standalone unit tests for the per-tile CDP isolation filter — THE security // boundary. CdpRelay.swift depends only on system frameworks (Foundation/CryptoKit/ // Security), so this compiles + runs without Xcode or the Flutter/pod harness: // @@ -75,7 +75,7 @@ enum CdpRelayFilterTests { drop("Target.getTargets (synthesized, not forwarded)", #"{"id":1,"method":"Target.getTargets"}"#) drop("Target.setAutoAttach non-flatten", #"{"id":1,"method":"Target.setAutoAttach","params":{"flatten":false}}"#) // Browser-level setAutoAttach(flatten) is INTERCEPTED, not forwarded: the relay - // self-attaches to our target + synthesizes attachedToTarget (H2), so a client + // self-attaches to our target + synthesizes attachedToTarget, so a client // can't change a sibling tile's auto-attach. Forwarding would be a cross-tile // control leak — this is the per-tile isolation boundary, so it must return nil. drop("Target.setAutoAttach flatten (self-attached + synthesized, not forwarded)", @@ -134,7 +134,7 @@ enum CdpRelayFilterTests { tokNo("Authorization: Bearer (empty)", "/devtools/browser", ["authorization": "Bearer "]) tokOK("?token= query fallback", "/devtools/browser?token=\(tok)", [:]) tokNo("?token= query", "/devtools/browser?token=deadbeef", [:]) - // header/query precedence + parsing edges (audit-driven) + // header/query precedence + parsing edges tokOK("non-bearer header falls through to a valid query", "/devtools/browser?token=\(tok)", ["authorization": "Basic \(tok)"]) tokNo("wrong Bearer header does NOT consult the query", "/devtools/browser?token=\(tok)", ["authorization": "Bearer deadbeef"]) tokOK("empty 'Bearer ' header falls through to a valid query", "/devtools/browser?token=\(tok)", ["authorization": "Bearer "]) @@ -147,9 +147,9 @@ enum CdpRelayFilterTests { tokNo("lookalike key ?tokenx=", "/devtools/browser?tokenx=\(tok)", [:]) tokNo("tab (not SP) between scheme and token", "/devtools/browser", ["authorization": "Bearer\t\(tok)"]) - // ════ CEF-2b MULTIPLEX (P2-step2): N relays share ONE browser-wide pipe ════ - // Two scoped relays on one host share its pipe-id allocator. This is PLAN - // Test I: feed each relay traffic for both tiles and assert ZERO cross-leak. + // ════ MULTIPLEX: N relays share ONE browser-wide pipe ════ + // Two scoped relays on one host share its pipe-id allocator. Feed each relay + // traffic for both tiles and assert ZERO cross-leak. let hostIds = CdpPipeIds() let relayA = CdpRelay(sendToPipe: { _ in }, scopeTargetId: "TILE-A", pipeIds: hostIds) let relayB = CdpRelay(sendToPipe: { _ in }, scopeTargetId: "TILE-B", pipeIds: hostIds) @@ -171,7 +171,7 @@ enum CdpRelayFilterTests { check("mux: relayA demux RESTORES its own client id (42)", topId(relayA.demuxPipeToClient(aResp)) == 42) check("mux: a consumed response is not re-delivered (no double-send)", relayA.demuxPipeToClient(aResp) == nil) - // ── THE §3.2 fix: a browser-level response (NO sessionId) must not fan to siblings. + // ── The id-rewrite fix: a browser-level response (NO sessionId) must not fan to siblings. // Without the id-rewrite, filterPipeToClient forwards no-sid responses to EVERY // relay (see the single-relay "browser-level response (no sid)" PASS above) — i.e. // both clients would see both. The rewrite makes it route to exactly one. ── diff --git a/packages/flutter_cef_macos/test/LivenessProbePolicyTests.swift b/packages/flutter_cef_macos/test/LivenessProbePolicyTests.swift index 24c2afa..5cf53e3 100644 --- a/packages/flutter_cef_macos/test/LivenessProbePolicyTests.swift +++ b/packages/flutter_cef_macos/test/LivenessProbePolicyTests.swift @@ -1,4 +1,4 @@ -// Standalone unit tests for LivenessProbePolicy — the F-6 steady-state liveness +// Standalone unit tests for LivenessProbePolicy — the steady-state liveness // watchdog decision (catch a painted-then-wedged browser; discriminate a healthy idle // static page via a nudge before declaring a stall). Swift stdlib only, so it compiles + // runs with `swiftc` alone (no Xcode/pod harness/Campus): @@ -54,7 +54,7 @@ enum LivenessProbePolicyTests { check("one ns under staleness → healthy", act(sinceLastPresentNs: staleness - 1) == .healthy) - // Grace boundary (audit P3): one ns under grace still waits; at/over grace declares. + // Grace boundary: one ns under grace still waits; at/over grace declares. check("nudged, one ns UNDER grace → wait (healthy)", act(sinceLastPresentNs: 14_000_000_000, nudged: true, sinceNudgeNs: grace - 1) == .healthy) @@ -65,7 +65,7 @@ enum LivenessProbePolicyTests { act(sinceLastPresentNs: 14_000_000_000, nudged: true, sinceNudgeNs: grace + 1) == .declareStalled) - // WEDGE-vs-IDLE INDISTINGUISHABILITY (audit P3, documented as a test so the limitation is + // WEDGE-vs-IDLE INDISTINGUISHABILITY (documented as a test so the limitation is // explicit + locked): a static-idle tile and a hung-renderer tile present the SAME inputs to // this policy (stale, nudged, no present within grace) → BOTH reach .declareStalled. The // policy CANNOT tell them apart by timing alone; the consumer (CefProfileHost) deliberately diff --git a/packages/flutter_cef_macos/test/ResizeWatchdogPolicyTests.swift b/packages/flutter_cef_macos/test/ResizeWatchdogPolicyTests.swift index 9d7994c..9640874 100644 --- a/packages/flutter_cef_macos/test/ResizeWatchdogPolicyTests.swift +++ b/packages/flutter_cef_macos/test/ResizeWatchdogPolicyTests.swift @@ -1,5 +1,5 @@ // Standalone unit tests for ResizeWatchdogPolicy — the resize-watchdog force-promote -// gating, and specifically the F-4 fix: NEVER force-promote a pending surface while the +// gating, and specifically the wedge guard: NEVER force-promote a pending surface while the // browser is HIDDEN (the gated begin-frame pump never painted it, so promoting it wedges // the texture permanently blank). ResizeWatchdogPolicy depends only on the Swift stdlib, // so this compiles + runs without Xcode or the Flutter/pod harness: @@ -28,7 +28,7 @@ enum ResizeWatchdogPolicyTests { } static func main() { - // ── The F-4 fix: HIDDEN must never force-promote, no matter how long it's been ── + // ── The wedge guard: HIDDEN must never force-promote, no matter how long it's been ── check("hidden + timed-out → NO promote (the wedge guard)", promote(hidden: true, elapsedNs: pastGrace) == false) check("hidden + way past grace → still NO promote", diff --git a/packages/flutter_cef_macos/test/run_filter_tests.sh b/packages/flutter_cef_macos/test/run_filter_tests.sh index 8f8c725..7a9e1e4 100755 --- a/packages/flutter_cef_macos/test/run_filter_tests.sh +++ b/packages/flutter_cef_macos/test/run_filter_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Compile + run the standalone CdpRelay filter unit tests (CEF-2b security boundary). +# Compile + run the standalone CdpRelay filter unit tests (the per-tile security boundary). # CdpRelay.swift uses only system frameworks, so no Xcode/pod harness is needed. set -euo pipefail DIR="$(cd "$(dirname "$0")/.." && pwd)" diff --git a/packages/flutter_cef_macos/test/run_liveness_probe_tests.sh b/packages/flutter_cef_macos/test/run_liveness_probe_tests.sh index 8fc8e48..25f8728 100755 --- a/packages/flutter_cef_macos/test/run_liveness_probe_tests.sh +++ b/packages/flutter_cef_macos/test/run_liveness_probe_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Compile + run the standalone LivenessProbePolicy unit tests (F-6 steady-state liveness +# Compile + run the standalone LivenessProbePolicy unit tests (steady-state liveness # decision). Swift stdlib only — no Xcode/pod harness needed. set -euo pipefail DIR="$(cd "$(dirname "$0")/.." && pwd)" diff --git a/packages/flutter_cef_macos/test/run_resize_watchdog_tests.sh b/packages/flutter_cef_macos/test/run_resize_watchdog_tests.sh index d14f32c..5971c92 100755 --- a/packages/flutter_cef_macos/test/run_resize_watchdog_tests.sh +++ b/packages/flutter_cef_macos/test/run_resize_watchdog_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Compile + run the standalone ResizeWatchdogPolicy unit tests (the F-4 visibility-gating +# Compile + run the standalone ResizeWatchdogPolicy unit tests (the visibility gating # that prevents the resize/cull wedge). ResizeWatchdogPolicy uses only the Swift stdlib, # so no Xcode/pod harness is needed. set -euo pipefail diff --git a/tool/check_comment_tags.sh b/tool/check_comment_tags.sh new file mode 100755 index 0000000..41cb6f6 --- /dev/null +++ b/tool/check_comment_tags.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Fail if a tracked file carries an audit or port-plan tag, or a line citation. +# +# Tags like audit ids and port-plan law/slice numbers mean nothing without the +# report that defined them, and a `file.ext:` citation goes stale with +# the next edit. Say the reason in words, or name the function. The original +# plans and audits live in docs/history/, which is skipped along with the +# CHANGELOGs and binary files. +# +# Only unambiguous shapes are checked: +# - "LAW" followed by a number, "slice" in capitals, the port's spike log file +# - a line citation: a .mm/.cc/.cpp/.h/.swift/.dart file name, a colon, digits +# - a comment (after //, ///, # or *) that opens with a label of one capital +# letter, one or two digits and a colon, or two such labels joined by "/" +# - such a label in parentheses, optionally after "audit " +# +# Prints `file:line: text` for each hit and exits 1 if there are any. +# Usage: tool/check_comment_tags.sh (from anywhere in the repo) +set -eu + +cd "$(git rev-parse --show-toplevel)" + +# POSIX extended regexes, so git grep reads them the same on macOS and Linux. +# (Written so this file doesn't match itself.) +W='[^A-Za-z0-9_]' +L='[A-Z][0-9]{1,2}' +patterns=( + "(^|$W)LAW ?[0-9]+" + "(^|$W)SLIC[E]($W|\$)" + 'SPIKE[S]\.md' + '[A-Za-z0-9_]\.(mm|cc|cpp|h|swift|dart):[0-9]+' + "(//|#|\\*) ?$L(/$L)?:" + "\\((audit )?$L\\)" +) + +args=() +for p in "${patterns[@]}"; do args+=(-e "$p"); done + +set +e +hits="$(git grep -n -I -E "${args[@]}" -- . \ + ':(exclude)docs/history/' ':(exclude,glob)**/CHANGELOG*')" +rc=$? +set -e +if [ "$rc" -gt 1 ]; then + echo "check_comment_tags: git grep failed ($rc)" >&2 + exit 2 +fi +if [ -n "$hits" ]; then + printf '%s\n' "$hits" | sed 's/^\([^:]*:[0-9]*\):/\1: /' + echo >&2 + echo "Audit/port tags or line citations found (see tool/check_comment_tags.sh):" >&2 + echo "replace each with the reason in plain words." >&2 + exit 1 +fi +echo "check_comment_tags: clean"