diff --git a/2fa_bot.py b/2fa_bot.py index 4ab87dd..76645cc 100644 --- a/2fa_bot.py +++ b/2fa_bot.py @@ -305,6 +305,17 @@ def report_gateway_blocker_dialogs(): return found +def gateway_ui_blocker_present() -> bool: + """Return whether a compact, unacknowledged Gateway error is visible. + + The health-recovery scripts call this through the container instead of + reading historical logs. A cleared dialog therefore resumes normal + recovery automatically, while an active account/login error is never + mistaken for a recoverable API outage. + """ + return bool(find_gateway_blocker_dialogs()) + + def dismiss_dialog(candidate): if candidate.window_id not in dismissed_dialog_windows: log.info( @@ -480,4 +491,6 @@ def main(): if __name__ == "__main__": + if sys.argv[1:] == ["--check-gateway-ui-blocker"]: + raise SystemExit(0 if gateway_ui_blocker_present() else 1) main() diff --git a/scripts/detect_gateway_ui_blocker.sh b/scripts/detect_gateway_ui_blocker.sh new file mode 100755 index 0000000..38a7503 --- /dev/null +++ b/scripts/detect_gateway_ui_blocker.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Return zero only when an unacknowledged compact Gateway dialog is currently +# visible. The probe is intentionally live-window based: a historical error +# line must not suppress recovery after an operator has cleared the dialog. +container_name="${IB_GATEWAY_CONTAINER_NAME:-ib-gateway}" + +if ! docker inspect --format '{{.State.Running}}' "${container_name}" 2>/dev/null | grep -Fxq 'true'; then + exit 1 +fi + +docker exec "${container_name}" \ + python3 /home/ibgateway/2fa_bot.py --check-gateway-ui-blocker >/dev/null diff --git a/scripts/recover_ib_gateway_ready.sh b/scripts/recover_ib_gateway_ready.sh index 730e2ee..0c27f1b 100755 --- a/scripts/recover_ib_gateway_ready.sh +++ b/scripts/recover_ib_gateway_ready.sh @@ -78,6 +78,16 @@ gateway_recently_progressing() { gateway_recently_progressing_from_docker_logs || gateway_recently_progressing_from_file_logs } +gateway_ui_blocker_present() { + IB_GATEWAY_CONTAINER_NAME="${container_name}" \ + bash "${script_dir}/detect_gateway_ui_blocker.sh" +} + +stop_for_gateway_ui_blocker() { + echo "GATEWAY_UI_BLOCKER: Gateway dialog requires account/login review; skipping automatic restart and recreate." >&2 + exit 3 +} + wait_for_ready_with_progress() { local timeout_seconds="$1" local stage="$2" @@ -110,10 +120,18 @@ echo "Ensuring ${container_name} is running before readiness check." docker compose up -d --no-build "${compose_service_name}" ensure_2fa_bot_running +if gateway_ui_blocker_present; then + stop_for_gateway_ui_blocker +fi + if wait_for_ready_with_progress "${initial_wait_seconds}" "initial"; then exit 0 fi +if gateway_ui_blocker_present; then + stop_for_gateway_ui_blocker +fi + echo "IB gateway API was not ready; restarting ${container_name} and retrying." >&2 docker compose ps >&2 || true docker compose restart "${compose_service_name}" diff --git a/scripts/restart_ib_gateway_daily.sh b/scripts/restart_ib_gateway_daily.sh index bb3580a..de9c4e1 100755 --- a/scripts/restart_ib_gateway_daily.sh +++ b/scripts/restart_ib_gateway_daily.sh @@ -10,6 +10,12 @@ ready_wait_seconds="${IB_GATEWAY_DAILY_RESTART_READY_WAIT_SECONDS:-240}" cd "${repo_dir}" +if IB_GATEWAY_CONTAINER_NAME="${container_name}" \ + bash "${script_dir}/detect_gateway_ui_blocker.sh"; then + echo "GATEWAY_UI_BLOCKER: Gateway dialog requires account/login review; skipping scheduled restart." >&2 + exit 3 +fi + echo "Restarting ${container_name} for scheduled IB Gateway refresh (mode=${gateway_mode})." docker compose up -d --no-build "${compose_service_name}" docker compose restart "${compose_service_name}" diff --git a/tests/test_docker_compose_ports.sh b/tests/test_docker_compose_ports.sh index 6720389..5af2c77 100644 --- a/tests/test_docker_compose_ports.sh +++ b/tests/test_docker_compose_ports.sh @@ -86,4 +86,8 @@ assert module.is_gateway_blocker_dialog("IBKR Gateway", 509, 131) assert module.is_gateway_blocker_dialog("Gateway", 510, 131) assert not module.is_gateway_blocker_dialog("IBKR Gateway", 700, 550) assert not module.is_gateway_blocker_dialog("Gateway", 790, 610) +module.find_gateway_blocker_dialogs = lambda: [] +assert not module.gateway_ui_blocker_present() +module.find_gateway_blocker_dialogs = lambda: [module.WindowCandidate("1", "Gateway", 510, 131)] +assert module.gateway_ui_blocker_present() PY diff --git a/tests/test_wait_for_ib_gateway_ready.sh b/tests/test_wait_for_ib_gateway_ready.sh index f0d5d38..7916d68 100644 --- a/tests/test_wait_for_ib_gateway_ready.sh +++ b/tests/test_wait_for_ib_gateway_ready.sh @@ -3,6 +3,9 @@ set -euo pipefail repo_dir="$(cd "$(dirname "$0")/.." && pwd)" script_file="$repo_dir/scripts/wait_for_ib_gateway_ready.sh" +recovery_script="$repo_dir/scripts/recover_ib_gateway_ready.sh" +daily_restart_script="$repo_dir/scripts/restart_ib_gateway_daily.sh" +blocker_probe_script="$repo_dir/scripts/detect_gateway_ui_blocker.sh" test -f "$script_file" test -x "$script_file" || true @@ -42,6 +45,11 @@ grep -Fq 'b"API\0" + struct.pack(">I", len(b"v157..176")) + b"v157..176"' "$scri grep -Fq 'has_next_valid_id and has_managed_accounts' "$script_file" grep -Fq 'IB API handshake readiness' "$script_file" grep -Fq 'docker logs --tail 120 "${container_name}"' "$script_file" +test -x "$blocker_probe_script" +grep -Fq -- '--check-gateway-ui-blocker' "$blocker_probe_script" +grep -Fq 'gateway_ui_blocker_present()' "$recovery_script" +grep -Fq 'GATEWAY_UI_BLOCKER: Gateway dialog requires account/login review' "$recovery_script" +grep -Fq 'skipping scheduled restart' "$daily_restart_script" tmp_dir="$(mktemp -d)" trap 'rm -rf "$tmp_dir"' EXIT