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:
1 parent
64efb68ea1
commit
a8af44e673
3 files changed
+60
-15
No files matched your search
+18
-8
@@ -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`).
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in new issue
Block a user