fix(collect): handle login target close race
Tests / Python 3.11 / Windows (push) Has been cancelled

This commit is contained in:
chengma
2026-07-14 12:08:12 +08:00
parent cbfd36af56
commit f99cde8e05
11 changed files with 588 additions and 88 deletions
+215 -60
View File
@@ -6,7 +6,15 @@ import time
from urllib.parse import urlparse
from . import appconfig, image_paths
from .cdp import CDP, close_tab, create_tab, create_tab_info, find_product_tab, http_get
from .cdp import (
CDP,
close_tab,
close_tab_and_wait,
create_tab,
create_tab_info,
find_product_tab,
http_get,
)
DEFAULT_REGION_HOST = "seller.shopee.tw"
@@ -669,71 +677,206 @@ def _current_url(cdp):
return ""
def _wait_for_login_probe(cdp, timeout=8):
end = time.time() + timeout
last_url = ""
def _wait_for_login_probe(cdp, timeout=8, initial_url=""):
deadline = time.monotonic() + max(0.0, float(timeout))
last_url = str(initial_url or "")
cookie_names = set()
while time.time() < end:
last_url = _current_url(cdp) or last_url
cookie_read_succeeded = False
probe_error = None
first_probe = True
while first_probe or time.monotonic() < deadline:
first_probe = False
try:
last_url = cdp.val("location.href") or last_url
except Exception as exc:
return {
"url": last_url,
"cookie_names": cookie_names,
"cookie_read_succeeded": cookie_read_succeeded,
"target_available": False,
"probe_error": exc.__class__.__name__,
}
if _is_login_url(last_url):
return {
"url": last_url,
"cookie_names": cookie_names,
"cookie_read_succeeded": cookie_read_succeeded,
"target_available": True,
"probe_error": probe_error,
}
try:
cookies = cdp.send("Network.getAllCookies").get("cookies", [])
cookie_names = {
c.get("name")
for c in cookies
if "shopee" in (c.get("domain") or "")
except Exception as exc:
return {
"url": last_url,
"cookie_names": cookie_names,
"cookie_read_succeeded": cookie_read_succeeded,
"target_available": False,
"probe_error": exc.__class__.__name__,
}
except Exception:
cookie_names = set()
if _is_login_url(last_url) or "SPC_ST" in cookie_names or "SPC_U" in cookie_names:
cookie_read_succeeded = True
cookie_names = {
cookie.get("name")
for cookie in cookies
if "shopee" in (cookie.get("domain") or "")
}
if "SPC_ST" in cookie_names or "SPC_U" in cookie_names:
break
time.sleep(0.5)
return last_url, cookie_names
remaining = deadline - time.monotonic()
if remaining <= 0:
break
time.sleep(min(0.5, remaining))
return {
"url": last_url,
"cookie_names": cookie_names,
"cookie_read_succeeded": cookie_read_succeeded,
"target_available": cookie_read_succeeded,
"probe_error": probe_error,
}
def _login_target_priority(target):
url = str((target or {}).get("url") or "").lower()
if _is_login_url(url):
return 3
if "/portal/product/" in url:
return 2
if "/portal/" in url:
return 0
return 1
def _login_target_key(target):
target = target or {}
return str(target.get("webSocketDebuggerUrl") or target.get("id") or target.get("url") or "")
def _shopee_page_targets(host):
pages = [
target
for target in http_get("/json", host=host)
if target.get("type") == "page"
and "shopee" in str(target.get("url") or "").lower()
and target.get("webSocketDebuggerUrl")
]
return sorted(pages, key=_login_target_priority)
def login_status(account, timeout=8) -> dict:
"""Return detailed Shopee login status for an account's CDP session."""
host = _cdp_host(account)
pages = [t for t in http_get("/json", host=host) if t.get("type") == "page"]
shopee_page = next((p for p in pages if "shopee" in (p.get("url") or "")), None)
if not shopee_page:
ws = create_tab(_seller_home_url(account), host=host)
initial_url = _seller_home_url(account)
cdp = CDP(ws)
else:
initial_url = shopee_page.get("url") or ""
if _is_login_url(initial_url):
deadline = time.monotonic() + max(0.0, float(timeout))
attempted_targets = set()
opened_home = False
probe_attempts = 0
last_probe = {
"url": "",
"cookie_names": set(),
"cookie_read_succeeded": False,
"target_available": False,
"probe_error": None,
}
while True:
candidates = [
target
for target in _shopee_page_targets(host)
if _login_target_key(target) not in attempted_targets
]
if not candidates and not opened_home:
home_url = _seller_home_url(account)
ws = create_tab(home_url, host=host)
candidates = [
{
"type": "page",
"url": home_url,
"webSocketDebuggerUrl": ws,
}
]
opened_home = True
if not candidates:
break
for target in candidates:
key = _login_target_key(target)
if key:
attempted_targets.add(key)
probe_attempts += 1
initial_url = str(target.get("url") or "")
if _is_login_url(initial_url):
return {
"logged_in": False,
"reason": "LOGIN_PAGE",
"url": initial_url,
"host": host,
"cookie_names": [],
"cookie_read_succeeded": False,
"probe_error": None,
"probe_attempts": probe_attempts,
}
cdp = None
try:
cdp = CDP(target["webSocketDebuggerUrl"])
_ensure_page_domains(cdp)
remaining = max(0.0, deadline - time.monotonic())
probe = _wait_for_login_probe(
cdp,
timeout=remaining,
initial_url=initial_url,
)
except Exception as exc:
probe = {
"url": initial_url,
"cookie_names": set(),
"cookie_read_succeeded": False,
"target_available": False,
"probe_error": exc.__class__.__name__,
}
finally:
if cdp is not None:
cdp.close()
last_probe = probe
url = str(probe.get("url") or initial_url)
names = set(probe.get("cookie_names") or [])
if _is_login_url(url):
return {
"logged_in": False,
"reason": "LOGIN_PAGE",
"url": url,
"host": host,
"cookie_names": sorted(name for name in names if name),
"cookie_read_succeeded": bool(probe.get("cookie_read_succeeded")),
"probe_error": probe.get("probe_error"),
"probe_attempts": probe_attempts,
}
if not probe.get("cookie_read_succeeded") or not probe.get(
"target_available"
):
continue
logged_in = "SPC_ST" in names or "SPC_U" in names
return {
"logged_in": False,
"reason": "LOGIN_PAGE",
"url": initial_url,
"logged_in": logged_in,
"reason": None if logged_in else "NO_SESSION_COOKIE",
"url": url,
"host": host,
"cookie_names": [],
"cookie_names": sorted(name for name in names if name),
"cookie_read_succeeded": True,
"probe_error": probe.get("probe_error"),
"probe_attempts": probe_attempts,
}
cdp = CDP(shopee_page["webSocketDebuggerUrl"])
try:
_ensure_page_domains(cdp)
url, names = _wait_for_login_probe(cdp, timeout=timeout)
url = url or initial_url
if _is_login_url(url):
reason = "LOGIN_PAGE"
logged_in = False
elif "SPC_ST" in names or "SPC_U" in names:
reason = None
logged_in = True
else:
reason = "NO_SESSION_COOKIE"
logged_in = False
return {
"logged_in": logged_in,
"reason": reason,
"url": url,
"host": host,
"cookie_names": sorted(name for name in names if name),
}
finally:
cdp.close()
if time.monotonic() >= deadline:
break
return {
"logged_in": False,
"reason": "LOGIN_CHECK_TARGET_UNAVAILABLE",
"url": str(last_probe.get("url") or ""),
"host": host,
"cookie_names": sorted(
name for name in (last_probe.get("cookie_names") or []) if name
),
"cookie_read_succeeded": False,
"probe_error": last_probe.get("probe_error"),
"probe_attempts": probe_attempts,
}
def is_logged_in(account) -> bool:
@@ -742,7 +885,7 @@ def is_logged_in(account) -> bool:
return bool(login_status(account).get("logged_in"))
def _close_open_product_failure(cdp):
def _close_open_product_failure(cdp, confirm_target_closed=False):
target_id = getattr(cdp, "target_id", None)
created_by_app = bool(getattr(cdp, "created_by_app", False))
host = getattr(cdp, "cdp_host", None)
@@ -751,7 +894,10 @@ def _close_open_product_failure(cdp):
finally:
if created_by_app and target_id:
try:
close_tab(target_id, host=host)
if confirm_target_closed:
close_tab_and_wait(target_id, host=host, timeout=2.0)
else:
close_tab(target_id, host=host)
except Exception:
pass
@@ -786,7 +932,7 @@ def open_product(account, item_id, on_step=None, bring_to_front=True) -> CDP:
_wait_ready(cdp)
return cdp
except Exception:
_close_open_product_failure(cdp)
_close_open_product_failure(cdp, confirm_target_closed=not bring_to_front)
raise
@@ -847,6 +993,7 @@ def collect(account, task, on_step=None) -> dict:
item_id = _item_id(task)
cdp = open_product(account, item_id, on_step=on_step, bring_to_front=False)
result = None
try:
_notify_collect_step(on_step, "read_title")
old_title = read_title(cdp)
@@ -862,13 +1009,15 @@ def collect(account, task, on_step=None) -> dict:
)
_notify_collect_step(on_step, "download_cover")
old_cover_path = download_cover(old_cover_src, out_path)
return {
result = {
"old_title": old_title,
"old_cover_src": old_cover_src,
"old_cover_path": old_cover_path,
}
finally:
_close_collected_product(cdp)
close_target_confirmed = _close_collected_product(cdp)
result["close_target_confirmed"] = close_target_confirmed
return result
@@ -897,20 +1046,26 @@ def _close_collected_product(cdp):
target_id = getattr(cdp, "target_id", None)
created_by_app = bool(getattr(cdp, "created_by_app", False))
host = getattr(cdp, "cdp_host", None)
close_target_confirmed = None
try:
cdp.close()
finally:
if created_by_app and target_id:
try:
close_tab(target_id, host=host)
close_target_confirmed = close_tab_and_wait(
target_id,
host=host,
timeout=2.0,
)
except Exception:
pass
close_target_confirmed = False
return close_target_confirmed
def close_readonly_product(cdp):
"""Close a read-only product CDP session using the collection tab cleanup rules."""
_close_collected_product(cdp)
return _close_collected_product(cdp)
def change_title(cdp, new_title) -> dict: