fix(collect): handle login target close race
Tests / Python 3.11 / Windows (push) Has been cancelled
Tests / Python 3.11 / Windows (push) Has been cancelled
This commit is contained in:
+215
-60
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user