Edge CDP attach (generic browser)

Module: forge_lcdl.playwright.edge_cdp_attach

Updated

Rules

  1. Generic browser only — Skip CDP listeners owned by Edge started with --app= (installed Outlook/Teams web apps). Use cdp_url_runs_generic_browser() before connect.
  2. Page-level attach — pick the OWA tab from CDP /json/list, then connect_over_cdp(target.webSocketDebuggerUrl). Browser-level connect_over_cdp(http://127.0.0.1:9222) can hang on Edge 149+ with many tabs open.
  3. Tab pick — reuse an existing OWA mail tab when present; otherwise open a new tab for Outlook. Never attach to unrelated sites (pages[0]). open_new_tab=True forces a new tab even when OWA is already open.
  4. Teardowndetach_generic_edge_cdp(..., close_owned=False) (Cockpit default) only stops Playwright; Edge and all tabs stay open. When close_owned=True, closes only tabs/contexts Cockpit created. Ephemeral third-party tabs (e.g. linkedin.com/in/ opened during LPC harvest) must be closed explicitly — see TAB-LIFECYCLE-POLICY.md.
  5. Tab visibility without OS focus — Microsoft surfaces (OWA mail, Teams, Org Explorer, Dynamics) throttle virtualized lists when document.hidden is true. LCDL enables CDP focus emulation so the tab reports active without Page.bringToFront or /json/activate. See Tab visibility (focus emulation).

Tab visibility (focus emulation)

Problem: Background or minimized Edge tabs keep document.hidden === true. OWA search (received:M/D/YYYY) may show the query in the search box but return zero harvest_list_rows until the operator focuses the window.

Workaround: Chromium CDP Emulation.setFocusEmulationEnabled with {enabled: true}. The page sees document.visibilityState === "visible" and document.hasFocus() === true while the OS window stays in the background.

API / helper Module When
ensure_cdp_tab_focus_harness(page) edge_cdp_attach Preferred — focus emulation + Page.setWebLifecycleState(active) + visibility probe; re-applied before harvest/export
apply_cdp_focus_emulation(page) edge_cdp_attach Low-level CDP focus emulation only
cdp_focus_emulation_enabled() edge_cdp_attach Policy gate (env)
connect_owa_page_via_selective_cdp cdp_owa_page_bridge Calls ensure_cdp_tab_focus_harness after Page.enable

Environment

Variable Default Meaning
FORGE_LCDL_CDP_FOCUS_EMULATION (unset) When unset: on if bring-to-front is off
FORGE_LCDL_CDP_FOCUS_REFRESH_SEC 15 Min seconds between harness re-applies when tab still reports visible (0 = every call)
FORGE_LCDL_CDP_BRING_TO_FRONT 0 1 = allow Page.bringToFront and CDP /json/activate (steals OS focus)
COCKPIT_EDGE_FOCUS_EDGE 0 Cockpit alias for bring-to-front

Set FORGE_LCDL_CDP_FOCUS_EMULATION=0 only when debugging visibility; ingest scripts should leave the default (auto-on with no OS focus steal).

Does not steal OS focus

  • apply_cdp_focus_emulation / Emulation.setFocusEmulationEnabled
  • Selective page-target attach (connect_owa_page_via_selective_cdp)
  • detach_generic_edge_cdp(..., close_owned=False)

Does steal OS focus

  • Page.bringToFront when cdp_bring_to_front_enabled()
  • activate_cdp_page_targetPUT /json/activate/{targetId}
  • Ad-hoc page.bring_to_front() in consumer scripts

Verify before harvest

() => ({
  hidden: document.hidden,
  visibility: document.visibilityState,
  hasFocus: document.hasFocus(),
})

Expect hidden: false, visibility: "visible" after attach. Then run search-day or list harvest; viewport row count should be > 0 when mail exists for that day.

Limitations

  • Edge started with --start-minimized can still be slower; focus emulation fixes the Page Visibility API, not every compositor edge case.
  • Page.setWebLifecycleState({state: "active"}) alone is not a substitute for focus emulation on OWA.
  • Patching document.hidden in JS is fragile; prefer CDP focus emulation.

Downloads blocked on CDP-attached tabs

Symptom: After Cockpit/LCDL attach or a long OWA harvest, no file downloads work on the attached tab (OWA “Download as EML”, SharePoint save, or manual save-as on any site in that tab). Often mistaken for Microsoft throttling; it is a CDP download-behavior side effect.

Cause: An older external-protocol blocker called Page.setDownloadBehavior({behavior: "deny"}), which denies all downloads on that page — not only sip: / mailto: links. The JS capture-phase blocker alone is sufficient for protocol safety.

Fix (code): ensure_cdp_tab_focus_harness now calls restore_cdp_page_download_behavior (behavior: default) on each refresh. Upgrade forge-lcdl (and bump Cockpit/KA submodule pointers).

Fix (operator, immediate): Close and reopen the affected tab, or restart Edge. After upgrading LCDL, re-run sync or call attach again so the harness resets download behavior.

Do not use Page.setDownloadBehavior({behavior: deny}) to block external protocols — use the JS blocker in ensure_cdp_external_protocol_blocker only.

Session-wide download wedge (allowAndName capture lifecycle)

Symptom: Downloads work at first, then every download in the whole Edge session (all tabs, including manual saves) stops producing visible files, sometimes recovering later without a restart.

Causes (all confirmed live):

  1. Protocol blocker cancelling anchor exports. OWA “Download as EML” saves via a synthetic click on a blob:/data: anchor. An earlier blocker revision treated data:/blob: as unsafe and preventDefault()-ed the click — the download never began (Browser.downloadWillBegin never fired). The blocker now whitelists data:/blob: and skips any anchor with a download attribute; neither can reach an OS protocol handler.
  2. allowAndName capture held browser-wide. Browser.setDownloadBehavior {behavior: allowAndName, downloadPath: <capture dir>} applies to the default browser context — every tab. While it is active, manual downloads silently land as GUID-named files in the hidden capture dir. It auto-resets when the CDP session that set it detaches, which is why the wedge “recovers when something changes”.
  3. Harness clobber. Chromium keeps one download delegate per browser context (last setDownloadBehavior write wins, page- or browser-level). The focus harness’s Page.setDownloadBehavior {default} used to race the capture set between set and trigger. restore_cdp_page_download_behavior now skips while cdp_download_capture_inflight() is true.

Capture lifecycle (current): outlook_download_eml serializes set/restore behind a lock, re-creates the capture dir before every set (behavior never points at a missing path), counts in-flight attempts, restores default (never deny) only when the last attempt finishes, and self-heals: if a triggered export produces no file within FORGE_LCDL_OUTLOOK_EML_SELFHEAL_WAIT_MS (default 12 s), it re-issues setDownloadBehavior against a verified dir and re-triggers once (odl_download_selfheal log step).

Resolve CDP URL (KA-compatible)

resolve_operator_cdp_url(explicit_cdp_url=…, user_data_dir=…, configured_port=9222):

Order Source
1 Explicit COCKPIT_CDP_URL / KA_CDP_URL when generic
2 DevToolsActivePort under the Edge user-data dir
3 Configured port (Cockpit 9222, KA 9223)
4 Scan nearby ports for a generic listener

Consumer usage

from forge_lcdl.playwright.edge_cdp_attach import (
    apply_cdp_focus_emulation,
    attach_generic_edge_cdp,
    cdp_focus_emulation_enabled,
    detach_generic_edge_cdp,
    resolve_operator_cdp_url,
)

cdp, hint = resolve_operator_cdp_url(
    explicit_cdp_url=settings.cdp_url,
    user_data_dir=profile_dir,
    configured_port=9222,
)
handle = attach_generic_edge_cdp(pw, cdp, prefer_url="https://outlook.office.com/mail/")
register_playwright_page(session_ref, handle.page)
# … LCDL tasks …
detach_generic_edge_cdp(handle, playwright=pw, close_owned=False)

forge-cockpit-webcockpit_server.lcdl_edge_attach.start_lcdl_browser_session wraps this for sync.

forge-knowledge-assistant — mirrors the same helpers in edge_cdp_generic / edge_job_window (port 9223).