logging, timeout warning

This commit is contained in:
lda committed 2026-11-01 06:12:56 +07:00 Verified
1 parent 8b8793a51e
commit f3e43ede92
1 file changed
+143 -21
+143 -21
View File
@@ -16,11 +16,16 @@ const fn =
# autopep8: off # autopep8: off
import asyncio import asyncio
import logging
import os import os
import random import random
import sys import sys
from collections.abc import Awaitable, Callable
from inspect import isawaitable
from itertools import count from itertools import count
from math import ceil
from pathlib import Path from pathlib import Path
from typing import Any
os.environ["PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD"] = "1" os.environ["PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD"] = "1"
from desktop_notifier import DesktopNotifier, Icon from desktop_notifier import DesktopNotifier, Icon
@@ -51,6 +56,13 @@ URL = "https://game.skport.com/endfield/sign-in"
DESKTOP_NOTIFIER = DesktopNotifier(app_name=APP_NAME, app_icon=Icon(EF_ICON_PATH)) DESKTOP_NOTIFIER = DesktopNotifier(app_name=APP_NAME, app_icon=Icon(EF_ICON_PATH))
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s.%(msecs)03d %(levelname)-8s %(name)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger(__name__)
async def launch_context(p: Playwright, *, headless: bool): async def launch_context(p: Playwright, *, headless: bool):
return await p.chromium.launch_persistent_context( return await p.chromium.launch_persistent_context(
@@ -59,19 +71,26 @@ async def launch_context(p: Playwright, *, headless: bool):
) )
async def notify(msg: str, title: str = "Login", *, notifier=DESKTOP_NOTIFIER): async def notify(
msg: str,
title: str = "Login",
*,
notifier=DESKTOP_NOTIFIER,
on_clicked: Callable[[], Any] | None = None,
):
"""Send a desktop notification. """Send a desktop notification.
desktop_notifier can't make callbacks that rerun the script... im crine. desktop_notifier can't make callbacks that rerun the script... im crine.
""" """
print(f"{title}: {msg}") # print(f"{title}: {msg}")
logger.info("%s: %s", title, msg)
await notifier.send(title=title, message=msg, timeout=5) await notifier.send(title=title, message=msg, timeout=5, on_clicked=on_clicked)
async def wait_for_manual_login(page: Page, *, timeout: int = LOGIN_TIMEOUT_MS): async def wait_for_manual_login(page: Page, *, timeout: int = LOGIN_TIMEOUT_MS):
"suitable as a task, does raise PlaywrightTimeoutError if timeout reached. Set to 0 for no timeout." "suitable as a task, does raise PlaywrightTimeoutError if timeout reached. Set to 0 for no timeout."
return await page.locator("form").first.wait_for(timeout=timeout) await page.locator("form").first.wait_for(timeout=timeout)
logger.info("Manual login detected, form is visible.")
async def already_claimed(container: Locator): async def already_claimed(container: Locator):
@@ -109,17 +128,21 @@ async def click_loop(page: Page):
May run indefinitely if the claim button is not visible or the page is not ready.""" May run indefinitely if the claim button is not visible or the page is not ready."""
container: Locator = page.locator("#lottie-container").first container: Locator = page.locator("#lottie-container").first
logger.debug("Waiting for claim button to be visible.")
await container.wait_for(timeout=0) # wait for visible await container.wait_for(timeout=0) # wait for visible
# at this point, the page is ready. # at this point, the page is ready.
logger.debug("Claim button is visible, starting click loop.")
if await already_claimed(container): if await already_claimed(container):
await notify( await notify(
"Work already completed elsewhere.", "Work already completed elsewhere.",
title="Already Claimed", title="Already Claimed",
) )
logger.info("Already claimed, exiting click loop.")
return return
for t in count(1): # can be interrupted by timeout at any await point for t in count(1): # can be interrupted by timeout at any await point
logger.debug("Attempt %d: Clicking claim button.", t)
try: try:
await container.click( await container.click(
timeout=3000, timeout=3000,
@@ -127,11 +150,12 @@ async def click_loop(page: Page):
# we checked for visibility manually. This will probs work. # we checked for visibility manually. This will probs work.
force=True, force=True,
) )
except ( except PlaywrightTimeoutError:
PlaywrightTimeoutError, logger.debug("Attempt %d: Click timed out.", t)
PlaywrightError, except PlaywrightError:
): logger.debug("Attempt %d: Playwright error during click.", t, exc_info=True)
pass
logger.debug("Attempt %d: Checking claim state.", t)
if await already_claimed(container): # what a fucking mess if await already_claimed(container): # what a fucking mess
if t == 1: if t == 1:
@@ -144,6 +168,8 @@ async def click_loop(page: Page):
f"Claimed successfully after {t} attempts.", f"Claimed successfully after {t} attempts.",
title="Success", title="Success",
) )
logger.info("Claimed successfully after %d attempts.", t)
return return
await asyncio.sleep(2) await asyncio.sleep(2)
@@ -165,15 +191,16 @@ async def claim_loop(p: Playwright) -> bool:
async with await launch_context(p, headless=True) as context: async with await launch_context(p, headless=True) as context:
page = await context.new_page() page = await context.new_page()
await page.goto(URL) await page.goto(URL)
# the following code is BUNS logger.debug("Page loaded.")
# the following code is BUNS
tasks: list[asyncio.Task] = [ tasks: list[asyncio.Task] = [
asyncio.create_task(wait_for_manual_login(page, timeout=0)), asyncio.create_task(wait_for_manual_login(page, timeout=0)),
asyncio.create_task(click_loop(page)), asyncio.create_task(click_loop(page)),
asyncio.create_task(page.wait_for_event("close")), asyncio.create_task(page.wait_for_event("close")),
] ]
done, _pending = await asyncio.wait( done, _ = await asyncio.wait(
tasks, tasks,
# important line below, no tasks cancels normally, as in they run forever. # important line below, no tasks cancels normally, as in they run forever.
timeout=CLAIM_TIMEOUT_MS / 1000, # deadline in seconds timeout=CLAIM_TIMEOUT_MS / 1000, # deadline in seconds
@@ -184,20 +211,66 @@ async def claim_loop(p: Playwright) -> bool:
await asyncio.gather(*tasks, return_exceptions=True) # we dont care await asyncio.gather(*tasks, return_exceptions=True) # we dont care
if not done: if not done:
logger.warning("Claim attempt timed out after %ds.", CLAIM_TIMEOUT_MS // 1000)
return False return False
if tasks[0] in done: if tasks[0] in done:
tasks[0].result() tasks[0].result()
logger.info("Manual login required.")
raise LoginNeededError("Manual login required.") raise LoginNeededError("Manual login required.")
if tasks[1] in done: if tasks[1] in done:
tasks[1].result() tasks[1].result()
logger.debug("Claim worker finished.")
return True return True
if tasks[2] in done: if tasks[2] in done:
logger.warning("Browser page closed before claim completed.")
return False # page closed return False # page closed
return False # should never happen, but just in case return False # should never happen, but just in case
async def focus_page(page: Page):
"""Focus the page by bringing it to the front."""
if page.is_closed():
return
try:
await page.bring_to_front()
logger.debug("Brought page to front.")
except PlaywrightError:
pass # Page/context closed in the meantime.
async def notify_schedule(
duration: float,
callback: Callable[[float], None | Awaitable[None]] | None = None,
*,
checkpoints: tuple[float, ...] = (1800, 900, 600, 300, 180, 120, 60),
):
"""Run callback at selected remaining-time checkpoints.
Args:
duration (float): Total duration in seconds.
callback (Callable[[float], None | Awaitable[None]] | None): Callback function to be called at each checkpoint. It receives the remaining time in seconds as an argument.
checkpoints (tuple[float, ...]): Checkpoints in seconds at which the callback will be called. Default is (1800, 900, 600, 300, 180, 120, 60).
"""
loop = asyncio.get_running_loop()
deadline = loop.time() + duration
for target_remaining in sorted(checkpoints, reverse=True):
if duration - target_remaining < 1: # give it a second
continue
await asyncio.sleep(max(0, deadline - target_remaining - loop.time()))
if callback is not None:
remaining = max(0, deadline - loop.time())
result = callback(remaining)
if isawaitable(result):
await result
await asyncio.sleep(max(0, deadline - loop.time()))
async def manual_login(p: Playwright) -> bool: async def manual_login(p: Playwright) -> bool:
"""Handle manual login process. """Handle manual login process.
@@ -208,27 +281,75 @@ async def manual_login(p: Playwright) -> bool:
page = context.pages[0] if context.pages else await context.new_page() page = context.pages[0] if context.pages else await context.new_page()
await page.goto(URL) await page.goto(URL)
logger.info(
"Manual login required. Waiting for user to log in and close the browser."
)
await notify( await notify(
f"Please log in manually and close the browser. You have {LOGIN_TIMEOUT_MS // (60 * 1000)} minute(s).", f"Please log in manually and close the browser. You have {LOGIN_TIMEOUT_MS // (60 * 1000)} minute(s).",
title="Manual Login Required", title="Manual Login Required",
on_clicked=lambda: asyncio.create_task(focus_page(page)),
) )
try: close_task = asyncio.create_task(
await context.wait_for_event( context.wait_for_event(
"close", "close",
timeout=LOGIN_TIMEOUT_MS, timeout=LOGIN_TIMEOUT_MS,
) )
except PlaywrightTimeoutError, TimeoutError: )
return False
async def notify_schedule_callback(remaining: float):
minutes, seconds = divmod(ceil(remaining), 60)
logger.info("Manual login timeout in %dm %ds.", minutes, seconds)
await notify(
f"Manual login timeout in {minutes}m {seconds}s. Click to return.",
title="Manual Login Timeout",
on_clicked=lambda: asyncio.create_task(focus_page(page)),
)
wake_task = asyncio.create_task(
notify_schedule(LOGIN_TIMEOUT_MS / 1000, callback=notify_schedule_callback)
)
done, _ = await asyncio.wait(
{close_task, wake_task},
return_when=asyncio.FIRST_COMPLETED,
)
close_task_finished_first = close_task in done
if close_task_finished_first and not wake_task.done():
wake_task.cancel()
# Context has now closed. Drain both tasks.
await asyncio.gather(
close_task,
wake_task,
return_exceptions=True,
)
try:
close_task.result()
except PlaywrightTimeoutError:
logger.warning("Manual login timed out.")
return False
if close_task_finished_first:
logger.info("Manual login browser closed by user.")
return True return True
wake_task.result()
logger.warning("Manual login timed out.")
return False
async def program2(): async def program2():
logger.info("Starting sign-in automation.")
async with async_playwright() as p: async with async_playwright() as p:
for i in range(2): for i in range(2):
try: try:
logger.info("Attempting to claim reward (%d)", i + 1)
if await claim_loop(p): if await claim_loop(p):
logger.info("Sign-in reward workflow completed.")
return return
await notify( await notify(
@@ -238,21 +359,22 @@ async def program2():
raise SystemExit(1) raise SystemExit(1)
except LoginNeededError: except LoginNeededError:
if i > 0: if i > 0:
logger.warning(
"Manual authentication failed; login is still required."
)
await notify( await notify(
"Still not logged in. Please check your session.", "Still not logged in. Please check your session.",
title="Login Failed", title="Login Failed",
) )
raise SystemExit(1) raise SystemExit(1)
await notify( logger.info("Opening browser for manual authentication.")
"Manual login required. A new browser window will open.",
title="Manual Login Required",
)
if not await manual_login(p): if not await manual_login(p):
await notify( await notify(
"Login timeout reached. Please rerun the script.", "Login timeout reached. Please rerun the script.",
title="Login Timeout", title="Login Timeout",
) )
raise SystemExit(1) raise SystemExit(1)
logger.info("Manual authentication finished. Retrying claim.")
def main(): # uv no main fix def main(): # uv no main fix
@@ -264,7 +386,7 @@ def main(): # uv no main fix
else: else:
await program2() await program2()
except Exception as e: except Exception as e:
# pondering using BaseException logger.exception("Unhandled exception in sign-in helper.")
await notify( await notify(
f"An error occurred\n{e!s}", f"An error occurred\n{e!s}",
title="Error", title="Error",