Login retry after a 2FA timeout waits for the closed Chrome, then opens a new one

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ManikandanandClaude Opus 5.5 committed 2026-10-27 19:18:55 +05:30
1 parent 64efb68ea1
commit a8af44e673
3 files changed
+60 -15

No files matched your search

+18 -8
View File
@@ -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 **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 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 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 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 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) `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 - 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` 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 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, 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). 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`. 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 — office login — §2), `cc3a01a` (2026-10-27, Portal container on India time —
§10), `69c51d8` (2026-10-27, Dockerfile: libgssapi-krb5-2 for ODBC Driver §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 / 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 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 ticketed: a landing-page timeout with one retry, a 2FA retry, a week of
daily runs to measure login times on the Runs page. daily runs to measure login times on the Runs page.
- **Login retry bug (seen 2026-10-27 16:50, not fixed).** After a 2FA - **Login retry bug (seen 2026-10-27 16:50, fixed in `a89f689`, not yet
timeout the run closes the Chrome it opened and attempt 2 attaches to seen live).** After a 2FA timeout the run closed the Chrome it opened and
9222 at once, while that Chrome is still closing → `connect_over_cdp: attempt 2 attached to 9222 at once, while that Chrome was still closing →
Target page, context or browser has been closed`, run over. Fix: wait `connect_over_cdp: Target page, context or browser has been closed`, run
until 9222 stops answering, then open a fresh Chrome (test-first). 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 - **Gitea:** one overall snapshot update at the end of a day, when the
operator asks (from the clone `D:\ecw-charge-validation`; operator operator asks (from the clone `D:\ecw-charge-validation`; operator
pushes; 2026-10-26: `ba15a57` → `120fd68`). pushes; 2026-10-26: `ba15a57` → `120fd68`).
+31 -6
View File
@@ -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`, 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 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 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. 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. a clean, cookie-less browser is the point of that retry.
""" """
if cfg.BROWSER_CDP_URL and not incognito: 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) args = list(CHROME_ARGS)
if incognito: if incognito:
@@ -111,8 +117,12 @@ def launch(pw, incognito: bool = False):
PROXY_BYPASS = "claude.ai;*.claude.ai;*.anthropic.com" PROXY_BYPASS = "claude.ai;*.claude.ai;*.anthropic.com"
# Debug-port URLs of the Chromes this process opened — the only ones it # Debug-port URLs of the Chromes this process opened — the only ones it
# closes when it is done. # closes when it is done — with the extra arguments they were opened with.
_STARTED = set() _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 # 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 # 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) stderr=subprocess.DEVNULL)
for _ in range(60): for _ in range(60):
if cdp_ready(url): if cdp_ready(url):
_STARTED.add(url) _STARTED[url] = list(extra_args)
print(f"[BROWSER] Opened Chrome at {url} " print(f"[BROWSER] Opened Chrome at {url} "
f"(profile {cfg.CDP_PROFILE_DIR})") f"(profile {cfg.CDP_PROFILE_DIR})")
return True return True
@@ -225,6 +235,16 @@ def cdp_ready(url: str) -> bool:
return False 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: def ecw_sites(urls) -> set:
"""eCW's registrable domains ("ecwcloud.com"), from its configured URLs. """eCW's registrable domains ("ecwcloud.com"), from its configured URLs.
@@ -297,11 +317,16 @@ class _AttachedBrowser:
print("[BROWSER] Closed the Chrome this run opened") print("[BROWSER] Closed the Chrome this run opened")
except Exception: except Exception:
pass # the connection goes with it pass # the connection goes with it
_STARTED.discard(self._url) _CLOSED[self._url] = _STARTED.pop(self._url)
try: try:
self._browser.close() self._browser.close()
except Exception: except Exception:
pass 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 return
try: try:
mine = [p for p in self._context.pages if p not in self._theirs] mine = [p for p in self._context.pages if p not in self._theirs]
+11 -1
View File
@@ -354,7 +354,17 @@ def _run_bot_starts_chrome() -> int:
page.set_content("<p>x</p>") page.set_content("<p>x</p>")
b.close() b.close()
if close_after: 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("<p>again</p>")
b.close()
assert not browser.cdp_ready(url), \
"the reopened Chrome still answers"
else: else:
time.sleep(1) time.sleep(1)
assert browser.cdp_ready(url), "Chrome was closed" assert browser.cdp_ready(url), "Chrome was closed"