T-585 Chrome 路径自动检测

This commit is contained in:
chengma
2026-07-10 16:19:01 +08:00
parent 8daaf7f4af
commit 7e491ed10e
11 changed files with 441 additions and 7 deletions
+155
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import json
import os
import re
import shutil
import subprocess
import time
import urllib.error
@@ -15,12 +16,166 @@ from . import config as account_config
CDP_HOST = "127.0.0.1"
CHROME_APP_PATHS_KEY = r"SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe"
class ChromeLaunchError(RuntimeError):
"""Raised when Chrome launch configuration is invalid."""
def _normalize_chrome_path(value) -> str:
"""Return a filesystem-ready Chrome path, or an empty string."""
text = str(value or "").strip()
if len(text) >= 2 and text[0] == text[-1] and text[0] in {"\"", "'"}:
text = text[1:-1].strip()
if not text:
return ""
text = os.path.expandvars(os.path.expanduser(text))
return os.path.abspath(text)
def _load_winreg():
"""Return winreg only on Windows so non-Windows callers stay harmless."""
if os.name != "nt":
return None
try:
import winreg
except ImportError:
return None
return winreg
def _registry_view_flags(winreg):
flags = [0]
for name in ("KEY_WOW64_64KEY", "KEY_WOW64_32KEY"):
value = getattr(winreg, name, 0)
if value and value not in flags:
flags.append(value)
return flags
def _close_registry_key(key):
close = getattr(key, "Close", None) or getattr(key, "close", None)
if callable(close):
try:
close()
except OSError:
pass
def _registry_chrome_candidates():
"""Yield Chrome App Paths values without exposing registry failures."""
winreg = _load_winreg()
if winreg is None:
return
key_read = getattr(winreg, "KEY_READ", 0)
hives = (
getattr(winreg, "HKEY_CURRENT_USER", None),
getattr(winreg, "HKEY_LOCAL_MACHINE", None),
)
seen = set()
for hive in hives:
if hive is None:
continue
for view_flag in _registry_view_flags(winreg):
marker = (hive, view_flag)
if marker in seen:
continue
seen.add(marker)
key = None
try:
key = winreg.OpenKey(
hive,
CHROME_APP_PATHS_KEY,
0,
key_read | view_flag,
)
value, _value_type = winreg.QueryValueEx(key, "")
except (AttributeError, OSError):
continue
finally:
if key is not None:
_close_registry_key(key)
if value:
yield value
def _standard_chrome_candidates():
"""Yield the supported Windows Chrome install locations."""
suffix = ("Google", "Chrome", "Application", "chrome.exe")
for variable in ("ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"):
base = os.environ.get(variable, "").strip()
if base:
yield os.path.join(base, *suffix)
def is_configured_chrome_path_valid(value) -> bool:
"""Return whether a configured Chrome executable can be launched safely."""
normalized = _normalize_chrome_path(value)
if normalized and os.path.isfile(normalized):
return True
text = str(value or "").strip().strip("\"")
return text.lower() == "chrome.exe" and bool(shutil.which(text))
def detect_chrome_path() -> str:
"""Find an installed Google Chrome executable without probing other browsers."""
for candidate in _registry_chrome_candidates():
normalized = _normalize_chrome_path(candidate)
if normalized and os.path.isfile(normalized):
return normalized
for candidate in _standard_chrome_candidates():
normalized = _normalize_chrome_path(candidate)
if normalized and os.path.isfile(normalized):
return normalized
path_candidate = shutil.which("chrome.exe")
normalized = _normalize_chrome_path(path_candidate)
if normalized and os.path.isfile(normalized):
return normalized
return ""
def ensure_configured_chrome_path(config=None, config_path=None) -> dict:
"""Persist an auto-detected path only when the configured path is unusable."""
path = config_path or (config or {}).get("config_path") or appconfig.CONFIG_PATH
current = appconfig.load_config(path) if config is None else config
configured_path = appconfig.chrome_path(current)
if is_configured_chrome_path_valid(configured_path):
return {
"changed": False,
"config": current,
"path": configured_path,
"message": "",
}
detected_path = detect_chrome_path()
if not detected_path:
return {
"changed": False,
"config": current,
"path": configured_path,
"message": "",
}
updated = dict(current)
updated["chrome_path"] = detected_path
saved = appconfig.save_config(updated, path=path)
return {
"changed": True,
"config": saved,
"path": detected_path,
"message": f"已自动定位 Chrome:{detected_path}",
}
def _account_value(account, field, default=None):
if isinstance(account, dict):
return account.get(field, default)