fix(submit): harden atcoder and codeforces submit flow

Problem: AtCoder file upload always used a `.cpp` temp file regardless
of language. CF submit used `solve_cloudflare=True` causing a spurious
"No Cloudflare challenge found" error, and `_wait_for_gate_reload` in
`login_action` was dead code. Stale or expired cookies caused silent
auth failures with no recovery. The `uv.spawn` ndjson path for submit
had no overall timeout, so a hung browser process would live forever.

Solution: Replace AtCoder's temp file with `page.set_input_files` using
an in-memory buffer and correct extension via `_LANGUAGE_ID_EXTENSION`.
Replace CF's temp-file/fallback dance with a direct
`textarea[name="source"]` fill and set `solve_cloudflare=False` on the
submit fetch. Add a fast-path that skips the homepage login check when
cookies exist, with automatic stale-cookie recovery via `_retried` flag
on redirect-to-login detection. Remove `_wait_for_gate_reload`. Fix
`_ensure_browser` to propagate install errors instead of swallowing
them. Add a 120s kill timer to the ndjson `uv.spawn` submit path.
This commit is contained in:
Barrett Ruth 2026-03-05 11:09:43 -05:00
parent 6fcb5d1bbc
commit 6923301562
3 changed files with 105 additions and 99 deletions

View file

@ -2,9 +2,7 @@
import asyncio
import json
import os
import re
import tempfile
from typing import Any
import requests
@ -21,10 +19,8 @@ from .models import (
TestCase,
)
from .timeouts import (
BROWSER_ELEMENT_WAIT,
BROWSER_NAV_TIMEOUT,
BROWSER_SESSION_TIMEOUT,
BROWSER_SETTLE_DELAY,
HTTP_TIMEOUT,
)
@ -307,24 +303,13 @@ class CodeforcesScraper(BaseScraper):
)
def _wait_for_gate_reload(page, wait_selector: str) -> None:
from .atcoder import _solve_turnstile
if "Verification" not in page.title():
return
_solve_turnstile(page)
page.wait_for_function(
f"() => !!document.querySelector('{wait_selector}')",
timeout=BROWSER_ELEMENT_WAIT,
)
def _submit_headless(
contest_id: str,
problem_id: str,
source_code: str,
language_id: str,
credentials: dict[str, str],
_retried: bool = False,
) -> SubmitResult:
from pathlib import Path
@ -349,9 +334,10 @@ def _submit_headless(
except Exception:
pass
logged_in = False
logged_in = cookie_cache.exists() and not _retried
login_error: str | None = None
submit_error: str | None = None
needs_relogin = False
def check_login(page):
nonlocal logged_in
@ -362,10 +348,6 @@ def _submit_headless(
def login_action(page):
nonlocal login_error
try:
_wait_for_gate_reload(page, "#enterForm")
except Exception:
pass
try:
page.fill(
'input[name="handleOrEmail"]',
@ -383,28 +365,21 @@ def _submit_headless(
login_error = str(e)
def submit_action(page):
nonlocal submit_error
nonlocal submit_error, needs_relogin
if "/enter" in page.url or "/login" in page.url:
needs_relogin = True
return
try:
_solve_turnstile(page)
except Exception:
pass
tmp_path: str | None = None
try:
page.select_option(
'select[name="submittedProblemIndex"]',
problem_id.upper(),
)
page.select_option('select[name="programTypeId"]', language_id)
with tempfile.NamedTemporaryFile(
mode="w", suffix=".cpp", delete=False, prefix="cf_"
) as tf:
tf.write(source_code)
tmp_path = tf.name
try:
page.set_input_files('input[name="sourceFile"]', tmp_path)
page.wait_for_timeout(BROWSER_SETTLE_DELAY)
except Exception:
page.fill('textarea[name="source"]', source_code)
page.fill('textarea[name="source"]', source_code)
page.locator("form.submit-form input.submit").click(no_wait_after=True)
try:
page.wait_for_url(
@ -419,26 +394,21 @@ def _submit_headless(
submit_error = "Submit failed: page did not navigate"
except Exception as e:
submit_error = str(e)
finally:
if tmp_path:
try:
os.unlink(tmp_path)
except OSError:
pass
try:
with StealthySession(
headless=True,
timeout=BROWSER_SESSION_TIMEOUT,
google_search=False,
cookies=saved_cookies,
cookies=saved_cookies if (cookie_cache.exists() and not _retried) else [],
) as session:
print(json.dumps({"status": "checking_login"}), flush=True)
session.fetch(
f"{BASE_URL}/",
page_action=check_login,
network_idle=True,
)
if not (cookie_cache.exists() and not _retried):
print(json.dumps({"status": "checking_login"}), flush=True)
session.fetch(
f"{BASE_URL}/",
page_action=check_login,
network_idle=True,
)
if not logged_in:
print(json.dumps({"status": "logging_in"}), flush=True)
@ -456,7 +426,7 @@ def _submit_headless(
session.fetch(
f"{BASE_URL}/contest/{contest_id}/submit",
page_action=submit_action,
solve_cloudflare=True,
solve_cloudflare=False,
)
try:
@ -466,15 +436,21 @@ def _submit_headless(
except Exception:
pass
if submit_error:
return SubmitResult(success=False, error=submit_error)
return SubmitResult(
success=True,
error="",
submission_id="",
verdict="submitted",
if needs_relogin and not _retried:
cookie_cache.unlink(missing_ok=True)
return _submit_headless(
contest_id, problem_id, source_code, language_id, credentials, _retried=True
)
if submit_error:
return SubmitResult(success=False, error=submit_error)
return SubmitResult(
success=True,
error="",
submission_id="",
verdict="submitted",
)
except Exception as e:
return SubmitResult(success=False, error=str(e))