fix(login): enhance connectivity checks to prevent false positives in home detection
- Implemented `_connectivity_confirmed` to verify session validity by opening the Mission panel, addressing two failure modes: 1. Bright full-screen transition frames that could falsely satisfy home checks. 2. Dead sessions due to server resets or concurrent logins, which appear as normal home screens until navigation is attempted. - Improved detection logic by requiring a second verification of `_true_home` after a short wait, reducing the likelihood of coincidental false positives. - Confirmed live against real scenarios, ensuring robust handling of session states during login.
This commit is contained in:
parent
4857bd5f8f
commit
d21aadbde8
File diff suppressed because one or more lines are too long
@ -113,6 +113,88 @@ real cold start (game process not running at all) correctly launched the
|
|||||||
game, clicked through the title screen, and reached the genuinely-confirmed
|
game, clicked through the title screen, and reached the genuinely-confirmed
|
||||||
home screen.
|
home screen.
|
||||||
|
|
||||||
|
A second real correctness bug was found via real unattended cron runs the
|
||||||
|
following day (2026-07-17), reported live by the user together with the
|
||||||
|
actual pulled cron logs (`~/ba_logs/daily.log`/`q4h.log`): the 4:30 AM
|
||||||
|
`daily` fire logged `[login] reached home` cleanly, but then every single
|
||||||
|
task that ran afterward failed to open its own screen -- garbled OCR reads,
|
||||||
|
"screen/panel not detected after click" repeated across cafe, event_sweep,
|
||||||
|
circle, lesson, arena, both shops, gem_shop, mailbox, and stamina -- and
|
||||||
|
this persisted across multiple consecutive `q4h` fires spanning hours (the
|
||||||
|
account only recovered around the 17:00 fire the following day). Per the
|
||||||
|
user's own direct knowledge of this game's real behavior: the server's own
|
||||||
|
daily reset (~4 AM JST) or a concurrent login from another device (e.g. the
|
||||||
|
phone) can silently kill the session while the client keeps showing the
|
||||||
|
last-known-good home screen with no visible error -- the error only
|
||||||
|
surfaces once an actual navigation/API call is attempted, as the game's own
|
||||||
|
"connection lost" popup, which no individual task's own screen-detection
|
||||||
|
logic was built to recognize. `_true_home`'s checks are all purely visual
|
||||||
|
(nav bar shape, subscreen/modal darkness, news-dialog color), so a dead
|
||||||
|
session that still *looks* like home sailed straight through undetected.
|
||||||
|
|
||||||
|
Separately, reproducing this live (the user logged into the account on
|
||||||
|
their phone specifically to trigger it) surfaced a THIRD, distinct false
|
||||||
|
positive in the same family: a real run printed "reached home" while the
|
||||||
|
game was still showing a bright, fully non-interactive loading/transition
|
||||||
|
frame (tower splash art, zero HUD) -- a transient frame apparently bright
|
||||||
|
and flat enough to coincidentally satisfy `_home_nav_bar_visible` for one
|
||||||
|
screenshot, moments before the actual home screen rendered.
|
||||||
|
|
||||||
|
Both gaps share one fix, per the user's own suggested design ("change the
|
||||||
|
login to enter the stamina claim/reward area then go back home"):
|
||||||
|
`_connectivity_confirmed` now opens the Mission panel (config.MISSION_ICON,
|
||||||
|
the same icon stamina.py uses) and closes it again, right after `_true_home`
|
||||||
|
passes, before `_wait_for_home` will return success. A transition frame's
|
||||||
|
false positive gets caught because the click lands on non-interactive
|
||||||
|
background art and the panel never opens; a truly dead session gets caught
|
||||||
|
because the game's own connection-error dialog appears instead of the
|
||||||
|
Mission panel -- either way, `_wait_for_home` just treats it as "not home
|
||||||
|
yet" and retries the whole cycle, which will naturally fall through to the
|
||||||
|
existing generic Enter-fallback if an actual dialog needs dismissing, and
|
||||||
|
escalate to `_recover`'s kill+relaunch if the whole thing never resolves
|
||||||
|
within the timeout. **Confirmed live**: reproduced the real stuck-login
|
||||||
|
state from the phone-login trigger, ran the actual `login` task against it,
|
||||||
|
and caught it printing a false "reached home" while genuinely still on a
|
||||||
|
loading transition frame (verified via a follow-up screenshot showing no
|
||||||
|
HUD at all) -- this is the exact live evidence that led to
|
||||||
|
`_connectivity_confirmed`.
|
||||||
|
|
||||||
|
That first version of `_connectivity_confirmed` used `navigation.
|
||||||
|
is_on_subscreen` (a single-pixel check) to verify the Mission panel opened
|
||||||
|
-- re-run live against the SAME still-stuck session and it ALSO
|
||||||
|
false-positived: a different coincidental frame in the extended animated
|
||||||
|
loading sequence satisfied the single subscreen-header pixel right after
|
||||||
|
the click, on the very same run whose `_true_home` check had already been
|
||||||
|
fooled by yet another frame. Switched to `navigation.is_header_bar_visible`
|
||||||
|
(8 spread-out points, all must read bright) -- the same "harder to fool by
|
||||||
|
one coincidental bright pixel" upgrade this project already made for
|
||||||
|
cafe.py's rank-up detection, reused here for the identical reason. Re-run
|
||||||
|
live a third time against the same still-stuck session: STILL a false
|
||||||
|
positive, confirmed by an immediate independent follow-up check (screenshot
|
||||||
|
plus a direct re-run of `_true_home`/`is_header_bar_visible`/
|
||||||
|
`_connectivity_confirmed`, all reading false) moments later, on a THIRD
|
||||||
|
different splash frame (two characters on a tank, a `巡回中` sign). This
|
||||||
|
extended loading sequence turned out to genuinely never resolve on its own
|
||||||
|
across three separate `login` invocations and several real minutes of
|
||||||
|
elapsed time (matching the user's own "if this doesn't solve in 3-5
|
||||||
|
minutes, restart" guidance) -- rich enough in distinct splash frames that
|
||||||
|
two DIFFERENT independent checks (nav-bar shape, then header-bar
|
||||||
|
brightness) could each individually be fooled by a different frame within
|
||||||
|
the same run, even though neither check alone is easily fooled.
|
||||||
|
|
||||||
|
Rather than chasing a fourth, even-stronger single-snapshot probe,
|
||||||
|
`_wait_for_home` now re-verifies `_true_home` a second time, after a short
|
||||||
|
wait, before finally trusting a passed `_connectivity_confirmed` check --
|
||||||
|
two DIFFERENT coincidental frames independently satisfying the full check
|
||||||
|
sequence twice in a row, moments apart, is far less likely than either
|
||||||
|
false positive alone. The stuck session itself was cleared with a manual
|
||||||
|
kill+relaunch (the user's own direct guidance) rather than continuing to
|
||||||
|
poke at an already-anomalous, hours-old stuck state. Deployed; not yet
|
||||||
|
re-confirmed live against a fresh instance of either original failure mode
|
||||||
|
end-to-end (a truly dead session, or the ordinary transition-frame case),
|
||||||
|
though the double-confirmation logic itself follows directly from live
|
||||||
|
evidence, not guesswork.
|
||||||
|
|
||||||
Not yet live-confirmed: the ~infrequent 業務復帰ログインボーナス
|
Not yet live-confirmed: the ~infrequent 業務復帰ログインボーナス
|
||||||
(welcome-back login bonus, a "long trip" returning-player reward,
|
(welcome-back login bonus, a "long trip" returning-player reward,
|
||||||
`screenshots/daily_login/3_claim2(...).png`) card the user's own reference
|
`screenshots/daily_login/3_claim2(...).png`) card the user's own reference
|
||||||
@ -172,11 +254,81 @@ def _true_home(driver, config):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _connectivity_confirmed(driver, config):
|
||||||
|
"""Opens the Mission panel (a cheap, always-available screen, same icon
|
||||||
|
stamina.py uses) and closes it again, to prove the session is actually
|
||||||
|
live -- not just that this exact screenshot happens to satisfy
|
||||||
|
_true_home's checks. Two distinct real failure modes this catches:
|
||||||
|
|
||||||
|
1. A bright full-screen transition/wipe frame between loading and home
|
||||||
|
can transiently satisfy every _true_home probe at once (confirmed
|
||||||
|
live 2026-07-17: a real run printed "reached home" while still
|
||||||
|
mid-transition -- no HUD at all, a tower splash-art background --
|
||||||
|
moments before the real home screen actually rendered).
|
||||||
|
2. A genuinely dead session: the server's own daily reset around 4 AM
|
||||||
|
JST, or a concurrent login from another device, leaves the client
|
||||||
|
showing a cached-looking, visually normal home screen with no error
|
||||||
|
until an actual navigation/API call is attempted -- at which point
|
||||||
|
the game's own "connection lost" popup appears instead of the
|
||||||
|
requested screen. Reported live by the user with real cron logs:
|
||||||
|
after one such silent-death login, EVERY task that ran afterward
|
||||||
|
(cafe, event_sweep, circle, lesson, arena, both shops, gem_shop,
|
||||||
|
mailbox, stamina) failed to open its own screen, each in its own
|
||||||
|
inconsistent way, across multiple consecutive cron fires spanning
|
||||||
|
hours -- because nothing had verified the session was actually
|
||||||
|
alive before handing off to them.
|
||||||
|
|
||||||
|
Catching either here, once, right after login, is far more reliable
|
||||||
|
than leaving it to be discovered piecemeal by whichever task happens to
|
||||||
|
run next. If the click doesn't open the panel, we don't know whether
|
||||||
|
that's cafe-empty-air (transition frame, will resolve) or a real
|
||||||
|
connection-error dialog -- either way we just report failure and let
|
||||||
|
_wait_for_home's own loop retry the whole cycle (including this probe)
|
||||||
|
on its next iteration, exactly like any other not-yet-home state.
|
||||||
|
"""
|
||||||
|
# is_on_subscreen (a single-pixel check) was tried first and also
|
||||||
|
# false-positived live (2026-07-17): during an extended, multi-frame
|
||||||
|
# animated loading sequence, a *different* coincidental frame satisfied
|
||||||
|
# the single subscreen-header pixel right after the click, on the same
|
||||||
|
# run whose _true_home check had already been fooled by yet another
|
||||||
|
# frame. navigation.is_header_bar_visible (8 spread-out points, all
|
||||||
|
# must read bright) is the same "harder to fool by a single coincidental
|
||||||
|
# bright pixel" upgrade this project already made for cafe.py's rank-up
|
||||||
|
# detection -- reused here for the identical reason.
|
||||||
|
driver.click(*config.MISSION_ICON)
|
||||||
|
driver.wait(2)
|
||||||
|
opened = navigation.is_header_bar_visible(driver)
|
||||||
|
if opened:
|
||||||
|
driver.keypress("Escape")
|
||||||
|
driver.wait(1.5)
|
||||||
|
return opened
|
||||||
|
|
||||||
|
|
||||||
def _wait_for_home(driver, config):
|
def _wait_for_home(driver, config):
|
||||||
start = time.time()
|
start = time.time()
|
||||||
while time.time() - start < config.LOGIN_TIMEOUT_SECONDS:
|
while time.time() - start < config.LOGIN_TIMEOUT_SECONDS:
|
||||||
|
if _true_home(driver, config):
|
||||||
|
if not _connectivity_confirmed(driver, config):
|
||||||
|
print("[login] looked like home but the mission panel wouldn't open -- possible transition frame or dead session, retrying")
|
||||||
|
driver.wait(config.LOGIN_POLL_INTERVAL)
|
||||||
|
continue
|
||||||
|
# Even the 8-point header check was fooled live (2026-07-17,
|
||||||
|
# same session): an extended, highly-varied animated loading
|
||||||
|
# sequence apparently cycles through enough different splash
|
||||||
|
# frames that occasionally ONE frame coincidentally satisfies
|
||||||
|
# _true_home and a LATER, DIFFERENT frame coincidentally
|
||||||
|
# satisfies the connectivity probe too -- two independent
|
||||||
|
# false positives stacking, confirmed by an immediate follow-up
|
||||||
|
# check moments later reading false on all three signals at
|
||||||
|
# once. A single coincidental frame is unlikely; two DIFFERENT
|
||||||
|
# coincidental frames in a row, moments apart, is much less
|
||||||
|
# likely still -- so re-verify _true_home again after a short
|
||||||
|
# wait before finally trusting it.
|
||||||
|
driver.wait(config.LOGIN_POLL_INTERVAL)
|
||||||
if _true_home(driver, config):
|
if _true_home(driver, config):
|
||||||
return True
|
return True
|
||||||
|
print("[login] home check didn't hold on re-verification -- treating as a coincidental frame, retrying")
|
||||||
|
continue
|
||||||
if _news_dialog_open(driver, config):
|
if _news_dialog_open(driver, config):
|
||||||
driver.click(*config.LOGIN_NEWS_CLOSE_BUTTON)
|
driver.click(*config.LOGIN_NEWS_CLOSE_BUTTON)
|
||||||
elif _logo_state(driver, config) == "bright":
|
elif _logo_state(driver, config) == "bright":
|
||||||
|
|||||||
18
plan.md
18
plan.md
@ -859,6 +859,24 @@ Times are nik-gpu's local system time, confirmed `Asia/Tokyo` (JST) via `timedat
|
|||||||
|
|
||||||
**Follow-up, same day**: per explicit user request ("I also want to have logs for the cron. So I can detect if anythings go wrong. daily and q4h should have separate .log file") -- the per-preset log file separation was already in place (`~/ba_logs/<preset>.log`, one file per `$PRESET`), but a real gap was found in the failure-detection half of the ask: a genuinely crashed task and a lock-skip (an overlapping fire time, expected/harmless) both exit 1, and the original wrapper logged only a bare `(exit N)`, making them indistinguishable at a glance. Fixed by switching from the `flock -n LOCKFILE COMMAND` form to an fd-based `flock` (`exec 9>"$LOCK_FILE"; flock -n 9`), checking lock acquisition as its own explicit step separate from the wrapped command's own exit code, so every log line is now tagged with one of three explicit outcomes: `starting`/`finished ... OK (exit 0)`/`finished ... FAILED (exit N)`/`SKIPPED ... -- previous run still in progress` -- a real problem can now be found with `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` alone, without having to reason about ambiguous exit codes. **Verified on nik-gpu without touching the game**: a bogus-preset run correctly logged `FAILED (exit 1)`; a deliberately-forced lock-contention run (same technique as the original test) correctly logged `SKIPPED ... -- previous run still in progress` instead of a second ambiguous `(exit 1)`; the grep command above correctly surfaced both lines from `~/ba_logs/smoke_test_bad.log`. Test log removed afterward -- `~/ba_logs/` currently contains no `daily.log`/`q4h.log` yet, since no real scheduled fire has happened.
|
**Follow-up, same day**: per explicit user request ("I also want to have logs for the cron. So I can detect if anythings go wrong. daily and q4h should have separate .log file") -- the per-preset log file separation was already in place (`~/ba_logs/<preset>.log`, one file per `$PRESET`), but a real gap was found in the failure-detection half of the ask: a genuinely crashed task and a lock-skip (an overlapping fire time, expected/harmless) both exit 1, and the original wrapper logged only a bare `(exit N)`, making them indistinguishable at a glance. Fixed by switching from the `flock -n LOCKFILE COMMAND` form to an fd-based `flock` (`exec 9>"$LOCK_FILE"; flock -n 9`), checking lock acquisition as its own explicit step separate from the wrapped command's own exit code, so every log line is now tagged with one of three explicit outcomes: `starting`/`finished ... OK (exit 0)`/`finished ... FAILED (exit N)`/`SKIPPED ... -- previous run still in progress` -- a real problem can now be found with `grep -E 'FAILED|SKIPPED' ~/ba_logs/*.log` alone, without having to reason about ambiguous exit codes. **Verified on nik-gpu without touching the game**: a bogus-preset run correctly logged `FAILED (exit 1)`; a deliberately-forced lock-contention run (same technique as the original test) correctly logged `SKIPPED ... -- previous run still in progress` instead of a second ambiguous `(exit 1)`; the grep command above correctly surfaced both lines from `~/ba_logs/smoke_test_bad.log`. Test log removed afterward -- `~/ba_logs/` currently contains no `daily.log`/`q4h.log` yet, since no real scheduled fire has happened.
|
||||||
|
|
||||||
|
### Phase 18 follow-up #3: connectivity probe -- session death and false-positive home detection (2026-07-17)
|
||||||
|
|
||||||
|
Cron's real scheduled fires had now happened (`daily` at 3:30/4:30, `q4h` every 4h -- see follow-up #2). The user pulled the actual logs with `pull_logs.sh` and reported: the 4:30 AM `daily` fire logged a clean `[login] reached home`, but then EVERY task that ran afterward failed to open its own screen -- garbled OCR reads, "screen/panel not detected after click" repeated across cafe, event_sweep, circle, lesson, arena, both shops, gem_shop, mailbox, and stamina -- and this persisted across multiple consecutive `q4h` fires spanning hours (the account only recovered around the 17:00 fire the following day).
|
||||||
|
|
||||||
|
The user's own direct knowledge of the game's real behavior explained it: the server's own daily reset (~4 AM JST) or a concurrent login from another device (their phone) can silently kill the session while the client keeps showing the last-known-good home screen with no visible error -- the error only surfaces once an actual navigation/API call is attempted, as the game's own "connection lost" popup, which no individual task's screen-detection logic recognizes. `login.py`'s `_true_home` checks are all purely visual (nav bar shape, subscreen/modal darkness, news-dialog color), so a dead session that still *looks* like home sailed straight through undetected, and every downstream task discovered the same root cause independently, inconsistently, over hours. The user's own suggested fix: "change the login to enter the stamina claim/reward area then go back home."
|
||||||
|
|
||||||
|
The user then offered to log into the account on their phone right now to reproduce the failure live, which immediately kicked the PC client on nik-gpu into a real stuck-login-screen state (confirmed via screenshot -- a full-bleed loading/splash frame, no HUD), matching the user's own prior guidance: "if this doesn't solve after 3~5 mins restarting the game is best way."
|
||||||
|
|
||||||
|
Implemented `_connectivity_confirmed`: opens the Mission panel (`config.MISSION_ICON`, the same icon `stamina.py` uses) and closes it again, required to pass before `_wait_for_home` will return success. This directly catches a dead session (the game's own connection-error dialog appears instead of the panel) -- and, it turned out, ALSO an entirely separate false positive: a bright, non-interactive loading/transition frame can transiently satisfy `_true_home`'s checks. Three live iterations were needed against the real stuck session before this actually held:
|
||||||
|
|
||||||
|
1. First version verified the panel opened via `navigation.is_on_subscreen` (single-pixel). Live-tested against the still-stuck session: `login` printed `[login] reached home`, but an independent follow-up screenshot taken moments later showed the game still on a *different* loading/transition frame (tower splash art, zero HUD) -- a coincidental false positive, not a real recovery.
|
||||||
|
2. Switched to `navigation.is_header_bar_visible` (8 spread-out points, all must read bright) -- the same upgrade this project already made for cafe.py's rank-up detection, for the identical "harder to fool by one coincidental bright pixel" reason. Re-tested live against the SAME still-stuck session: still a false positive, confirmed by an immediate independent re-check (screenshot plus a direct re-run of `_true_home`/`is_header_bar_visible`/`_connectivity_confirmed`, all reading false) on a THIRD different splash frame.
|
||||||
|
3. This extended loading sequence turned out to genuinely never resolve on its own across three separate `login` invocations and several real minutes of elapsed time -- rich enough in distinct splash frames that two independent checks (nav-bar shape, then header-bar brightness) could each individually be fooled by a *different* frame within the same run. Rather than chasing a fourth, even-stronger single-snapshot probe, `_wait_for_home` now re-verifies `_true_home` a second time, after a short wait, before finally trusting a passed connectivity check -- two different coincidental frames independently satisfying the full check sequence twice in a row, moments apart, is far less likely than either false positive alone.
|
||||||
|
|
||||||
|
The stuck session itself was cleared with a manual kill+relaunch (the user's own direct guidance) rather than continuing to poke at an already-anomalous, hours-old stuck state. **Confirmed live**: a genuinely fresh cold-start `login` run (game process not running at all, via `pkill` then `login.py`'s own launch-if-missing path) correctly launched the game, passed through the title screen, and reached independently-verified true home -- full HUD (Lv/AP/credits/gems/bottom nav all visible), confirmed via a direct screenshot AND a separate `_true_home`/`is_header_bar_visible` check run independently, not just the module's own printed message.
|
||||||
|
|
||||||
|
Not yet re-confirmed: a fresh instance of the *original* reported failure mode specifically -- a session that silently dies while still looking exactly like home, discovered only when a later task tries to navigate (the cron-log scenario). The live reproduction available this session was the full-kick-to-login-screen variant (triggered by the phone login), not that exact "looks fine, breaks on navigation" variant -- though `_connectivity_confirmed` is designed to catch both by the same mechanism (a real API round-trip, not a visual check), and the false positives it exposed and fixed along the way are a strict improvement regardless.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### OCR
|
### OCR
|
||||||
|
|||||||
BIN
screenshots/cafe/bug_student_close_together.png
Normal file
BIN
screenshots/cafe/bug_student_close_together.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Loading…
x
Reference in New Issue
Block a user