From a8af44e67394a13c7ba990630614e85c4b23941c Mon Sep 17 00:00:00 2001 From: ManikandanC Date: Tue, 27 Oct 2026 19:18:55 +0530 Subject: [PATCH] Login retry after a 2FA timeout waits for the closed Chrome, then opens a new one Co-Authored-By: Claude Opus 5.5 --- Context.md | 26 ++++++++++++++++++-------- src/ecw_bot/core/browser.py | 37 +++++++++++++++++++++++++++++++------ tests/test_cdp_browser.py | 12 +++++++++++- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/Context.md b/Context.md index cd18bbb..c458562 100644 --- a/Context.md +++ b/Context.md @@ -97,7 +97,8 @@ of 23–26/10 changed no other result. **2026-10-27 terminal runs:** 16:40 — 2FA mail never came in 10 min (eCW timed the approval out), then attempt 2 crashed: the run closed the Chrome it opened and re-attached to 9222 while that Chrome was still closing -(`connect_over_cdp: Target page ... has been closed`; §10). 16:53 (after a +(`connect_over_cdp: Target page ... has been closed`; fixed in `a89f689`, +§10). 16:53 (after a 120 s guard cooldown) — login 220 s, 2FA mail 42 s, **50/50, 38 Pass / 12 Fail / 0 Review** (the 50-claim sheet; same as 26/10 17:40). 17:30 with `Mecor Ready 2 - 10262026.xlsx` (all claims, QUIET_BOXES, ECW_KEEP_OPEN=0) @@ -213,7 +214,7 @@ Get-ChildItem tests\test_*.py | ForEach-Object { "$($_.Name): $((python $_.FullN - Tests run offline against fake HTML pages in `tests/fixtures/` and fake DB connections; they never touch eCW or SQL Server (`test_cdp_browser.py` starts a local Chromium on a debug port). As of 2026-10-27: 71 test - files, **1239 tests** (full run 2026-10-27: 1239/1239) (earlier flaky, each + files, **1239 tests** (full run 2026-10-27 after `a89f689`: 1239/1239) (earlier flaky, each failed once and passed on rerun: test_verify_no_live_writes, depression_dx_pointer, progress_note_wait — watch them) (about 5 minutes for all of them). @@ -649,7 +650,9 @@ the Portal (restarted 2026-10-26, [portal] terminal tab). History: `git log --oneline`. -Last code commits: `1da9e7c` (2026-10-27, Portal sign-in with the Ecare +Last code commits: `a89f689` (2026-10-27, login retry after a 2FA timeout +waits for the closed Chrome and opens a new one — §10), `1da9e7c` +(2026-10-27, Portal sign-in with the Ecare office login — §2), `cc3a01a` (2026-10-27, Portal container on India time — §10), `69c51d8` (2026-10-27, Dockerfile: libgssapi-krb5-2 for ODBC Driver 18 — §10), `70ba709` (2026-10-27, Portal server mode for Dokploy — §10), `f99dc17` (2026-10-27, Portal roles Super Admin / @@ -787,11 +790,18 @@ test claim ids — new posts must not (§7). watchdog came after them and has not fired live yet. Suggested, not ticketed: a landing-page timeout with one retry, a 2FA retry, a week of daily runs to measure login times on the Runs page. -- **Login retry bug (seen 2026-10-27 16:50, not fixed).** After a 2FA - timeout the run closes the Chrome it opened and attempt 2 attaches to - 9222 at once, while that Chrome is still closing → `connect_over_cdp: - Target page, context or browser has been closed`, run over. Fix: wait - until 9222 stops answering, then open a fresh Chrome (test-first). +- **Login retry bug (seen 2026-10-27 16:50, fixed in `a89f689`, not yet + seen live).** After a 2FA timeout the run closed the Chrome it opened and + attempt 2 attached to 9222 at once, while that Chrome was still closing → + `connect_over_cdp: Target page, context or browser has been closed`, run + over. Two causes: `Browser.close` only starts Chrome's shutdown, and the + "no Chrome → open it" step ran once, before the login loop. Now the + attached browser's `close()` waits (up to 30 s) until the port stops + answering, and `launch()` opens a new Chrome when this run opened one and + closed it (same extra args; the eCW proxy comes from `chrome_command`). + A Chrome the run did not open is still never opened or closed. The + "Chrome open" box is not shown again on the retry. Test: + `test_cdp_browser.py` (close → port quiet → launch again → close). - **Gitea:** one overall snapshot update at the end of a day, when the operator asks (from the clone `D:\ecw-charge-validation`; operator pushes; 2026-10-26: `ba15a57` → `120fd68`). diff --git a/src/ecw_bot/core/browser.py b/src/ecw_bot/core/browser.py index 0129b69..bed2b56 100644 --- a/src/ecw_bot/core/browser.py +++ b/src/ecw_bot/core/browser.py @@ -30,7 +30,8 @@ only), so the run can be watched with the Claude extension in that Chrome. No Chrome there when the run starts → the bot opens it (`start_chrome`, operator 2026-10-22: Start on the Portal is all it should take), and closes it again at the end (`CDP_CLOSE_AFTER_RUN`) — never a Chrome it did not -open. The bot opens its own tab, clears eCW's cookies and storage when it +open. A login retry after a 2FA timeout waits for that Chrome to be gone and +opens a new one. The bot opens its own tab, clears eCW's cookies and storage when it attaches and again when it is done, and closes only its own tabs. """ @@ -72,7 +73,12 @@ def launch(pw, incognito: bool = False): a clean, cookie-less browser is the point of that retry. """ if cfg.BROWSER_CDP_URL and not incognito: - return _attach(pw, cfg.BROWSER_CDP_URL) + url = cfg.BROWSER_CDP_URL + # This run opened Chrome and closed it (a 2FA timeout): open a new + # one. Never one it did not open — that is the operator's to start. + if url in _CLOSED and not start_chrome(url, _CLOSED.pop(url)): + raise RuntimeError(f"Could not open Chrome again at {url}") + return _attach(pw, url) args = list(CHROME_ARGS) if incognito: @@ -111,8 +117,12 @@ def launch(pw, incognito: bool = False): PROXY_BYPASS = "claude.ai;*.claude.ai;*.anthropic.com" # Debug-port URLs of the Chromes this process opened — the only ones it -# closes when it is done. -_STARTED = set() +# closes when it is done — with the extra arguments they were opened with. +_STARTED = {} + +# The ones it opened and has closed again: a login retry (2FA timed out) +# opens them afresh instead of attaching to nothing (2026-10-27 16:50). +_CLOSED = {} # Started through a Python that exits at once: Chrome's parent is then # gone, so a Stop on the Portal (taskkill /T on the bot) never takes Chrome @@ -204,7 +214,7 @@ def start_chrome(url: str, extra_args=()) -> bool: stderr=subprocess.DEVNULL) for _ in range(60): if cdp_ready(url): - _STARTED.add(url) + _STARTED[url] = list(extra_args) print(f"[BROWSER] Opened Chrome at {url} " f"(profile {cfg.CDP_PROFILE_DIR})") return True @@ -225,6 +235,16 @@ def cdp_ready(url: str) -> bool: return False +def _gone(url: str, seconds: float = 30) -> bool: + """Wait until nothing answers at `url`. False if something still does.""" + end = time.monotonic() + seconds + while cdp_ready(url): + if time.monotonic() > end: + return False + time.sleep(0.25) + return True + + def ecw_sites(urls) -> set: """eCW's registrable domains ("ecwcloud.com"), from its configured URLs. @@ -297,11 +317,16 @@ class _AttachedBrowser: print("[BROWSER] Closed the Chrome this run opened") except Exception: pass # the connection goes with it - _STARTED.discard(self._url) + _CLOSED[self._url] = _STARTED.pop(self._url) try: self._browser.close() except Exception: pass + # Browser.close only starts the shutdown; the port answers for + # a while yet. Attaching then gets a Chrome that dies under it. + if not _gone(self._url): + print(f"[BROWSER] The Chrome this run opened still answers " + f"at {self._url}") return try: mine = [p for p in self._context.pages if p not in self._theirs] diff --git a/tests/test_cdp_browser.py b/tests/test_cdp_browser.py index 8736114..f07f0db 100644 --- a/tests/test_cdp_browser.py +++ b/tests/test_cdp_browser.py @@ -354,7 +354,17 @@ def _run_bot_starts_chrome() -> int: page.set_content("

x

") b.close() if close_after: - assert _gone(url), "Chrome still answers" + # close() returns once Chrome is gone, not while it + # is still shutting down. + assert not browser.cdp_ready(url), \ + "Chrome still answers after close()" + # A login retry (2FA timed out) launches again: a + # fresh Chrome, not the dying one (2026-10-27 16:50). + b, ctx, page = browser.launch(pw) + page.set_content("

again

") + b.close() + assert not browser.cdp_ready(url), \ + "the reopened Chrome still answers" else: time.sleep(1) assert browser.cdp_ready(url), "Chrome was closed"