Handbook
Edge CDP attach (generic browser)
Module: forge_lcdl.playwright.edge_cdp_attach
Updated
Rules
- Generic browser only — Skip CDP listeners owned by Edge started with
--app=(installed Outlook/Teams web apps). Usecdp_url_runs_generic_browser()before connect. - Page-level attach — pick the OWA tab from CDP
/json/list, thenconnect_over_cdp(target.webSocketDebuggerUrl). Browser-levelconnect_over_cdp(http://127.0.0.1:9222)can hang on Edge 149+ with many tabs open. - 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=Trueforces a new tab even when OWA is already open. - Teardown —
detach_generic_edge_cdp(..., close_owned=False)(Cockpit default) only stops Playwright; Edge and all tabs stay open. Whenclose_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. - Tab visibility without OS focus — Microsoft surfaces (OWA mail, Teams, Org Explorer, Dynamics) throttle virtualized lists when
document.hiddenis true. LCDL enables CDP focus emulation so the tab reports active withoutPage.bringToFrontor/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.bringToFrontwhencdp_bring_to_front_enabled()activate_cdp_page_target→PUT /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-minimizedcan 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.hiddenin 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):
- Protocol blocker cancelling anchor exports. OWA “Download as EML” saves via a synthetic click on a
blob:/data:anchor. An earlier blocker revision treateddata:/blob:as unsafe andpreventDefault()-ed the click — the download never began (Browser.downloadWillBeginnever fired). The blocker now whitelistsdata:/blob:and skips any anchor with adownloadattribute; neither can reach an OS protocol handler. allowAndNamecapture 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”.- Harness clobber. Chromium keeps one download delegate per browser context (last
setDownloadBehaviorwrite wins, page- or browser-level). The focus harness’sPage.setDownloadBehavior {default}used to race the capture set between set and trigger.restore_cdp_page_download_behaviornow skips whilecdp_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-web — cockpit_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).
Related
- MESSAGING-CONSUMER-BOUNDARY.md — session +
register_playwright_page - OUTLOOK-READ-STRATEGIES.md