Files
PYAS/PYAS_Startup.py
2026-10-09 12:12:42 +08:00

202 lines
6.9 KiB
Python

from PYAS_Diagnostics import log_exception
import ctypes
import importlib.metadata
import logging
import os
import platform
import subprocess
import sys
import tempfile
import threading
import uuid
from PYAS_Diagnostics import configure_report_logging
def configure_startup_logging():
return configure_report_logging()
def bootstrap_startup():
log_path = configure_startup_logging()
logger = logging.getLogger("PYAS.Startup")
for stream in ("stdout", "stderr"):
if getattr(sys, stream) is None:
setattr(sys, stream, open(os.devnull, "w"))
def exception_hook(exc_type, exc_value, traceback):
logger.critical("Unhandled startup exception", exc_info=(exc_type, exc_value, traceback))
if not any(
arg in sys.argv
for arg in ("-hide", "-h", "-quit", "-driver-unload", "-driver-uninstall")
):
try:
ctypes.windll.user32.MessageBoxW(
None,
f"PYAS failed to start.\n\n{exc_value}\n\nDiagnostic log: {log_path or 'unavailable'}",
"PYAS Security",
0x10,
)
except Exception:
logger.exception("Could not display startup error")
sys.excepthook = exception_hook
threading.excepthook = lambda args: logger.error(
"Unhandled thread exception: %s",
args.thread.name,
exc_info=(args.exc_type, args.exc_value, args.exc_traceback),
)
logger.info(
"Starting PYAS; Python=%s; platform=%s; frozen=%s; recovery=%s",
sys.version.split()[0],
platform.platform(),
getattr(sys, "frozen", False),
os.environ.get("PYAS_WEBVIEW_RECOVERY", "0"),
)
for package in ("pywebview", "pythonnet", "pyinstaller"):
try:
logger.info("Dependency %s=%s", package, importlib.metadata.version(package))
except importlib.metadata.PackageNotFoundError:
logger.info("Dependency metadata unavailable: %s", package)
return log_path
def prepare_webview_profile(preferred_path):
logger = logging.getLogger("PYAS.Startup")
candidates = [preferred_path, os.path.join(tempfile.gettempdir(), "PYAS", "WebView2")]
for path in dict.fromkeys(candidates):
try:
os.makedirs(path, exist_ok=True)
with tempfile.TemporaryFile(dir=path) as probe:
probe.write(b"PYAS")
probe.flush()
probe.seek(0)
if probe.read() != b"PYAS":
raise OSError("Profile readback failed")
logger.info("WebView2 profile ready: %s", path)
return path
except OSError:
logger.exception("WebView2 profile is not writable: %s", path)
raise OSError("No writable WebView2 user data folder is available")
def check_webview_dependencies(webview):
from webview.util import interop_dll_path
import sysconfig
logger = logging.getLogger("PYAS.Startup")
architecture = (
"win-x86"
if ctypes.sizeof(ctypes.c_void_p) == 4
else ("win-arm64" if "arm64" in sysconfig.get_platform().lower() else "win-x64")
)
loader_directory = interop_dll_path(architecture)
loader_path = os.path.join(loader_directory, "WebView2Loader.dll")
loader = ctypes.WinDLL(loader_path)
get_version = loader.GetAvailableCoreWebView2BrowserVersionString
get_version.argtypes = [ctypes.c_wchar_p, ctypes.POINTER(ctypes.c_void_p)]
get_version.restype = ctypes.c_long
version_pointer = ctypes.c_void_p()
runtime_path = webview.settings.get("WEBVIEW2_RUNTIME_PATH") or None
result = get_version(runtime_path, ctypes.byref(version_pointer))
try:
if result < 0 or not version_pointer.value:
raise RuntimeError(
f"WebView2 Runtime detection failed: HRESULT 0x{result & 0xFFFFFFFF:08X}"
)
version = ctypes.wstring_at(version_pointer)
logger.info("WebView2 Runtime=%s; loader=%s", version, loader_path)
finally:
if version_pointer.value:
ole32 = ctypes.WinDLL("ole32")
ole32.CoTaskMemFree.argtypes = [ctypes.c_void_p]
ole32.CoTaskMemFree.restype = None
ole32.CoTaskMemFree(version_pointer)
os.environ["PATH"] = loader_directory + os.pathsep + os.environ.get("PATH", "")
import clr
clr.AddReference("System.Windows.Forms")
for name in ("Microsoft.Web.WebView2.Core.dll", "Microsoft.Web.WebView2.WinForms.dll"):
dll_path = interop_dll_path(name)
clr.AddReference(dll_path)
logger.info("WebView2 managed dependency loaded: %s", dll_path)
return version
def wait_for_recovery_parent():
parent = os.environ.pop("PYAS_WEBVIEW_RECOVERY_PARENT", None)
if not parent:
return
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
kernel32.OpenProcess.argtypes = [ctypes.c_ulong, ctypes.c_int, ctypes.c_ulong]
kernel32.OpenProcess.restype = ctypes.c_void_p
kernel32.WaitForSingleObject.argtypes = [ctypes.c_void_p, ctypes.c_ulong]
kernel32.WaitForSingleObject.restype = ctypes.c_ulong
kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
kernel32.CloseHandle.restype = ctypes.c_int
handle = kernel32.OpenProcess(0x00100000, False, int(parent))
if handle:
try:
if kernel32.WaitForSingleObject(handle, 10000) == 0x102:
raise RuntimeError("Previous PYAS instance did not exit for WebView2 recovery")
finally:
kernel32.CloseHandle(handle)
def restart_with_new_webview_profile(profile_path, script_path):
logger = logging.getLogger("PYAS.Startup")
if os.environ.get("PYAS_WEBVIEW_RECOVERY") == "1":
return False
try:
recovery_path = prepare_webview_profile(profile_path + "-Recovery-" + uuid.uuid4().hex)
environment = os.environ.copy()
environment.update(
{
"PYAS_WEBVIEW_RECOVERY": "1",
"PYAS_WEBVIEW_PROFILE": recovery_path,
"PYAS_WEBVIEW_RECOVERY_PARENT": str(os.getpid()),
"PYINSTALLER_RESET_ENVIRONMENT": "1",
}
)
command = [sys.executable]
if not getattr(sys, "frozen", False):
command.append(os.path.abspath(script_path))
command.extend(sys.argv[1:])
working_directory = (
os.path.dirname(sys.executable)
if getattr(sys, "frozen", False)
else os.path.dirname(os.path.abspath(script_path))
)
subprocess.Popen(
command,
env=environment,
cwd=working_directory,
creationflags=0x08000000,
close_fds=True,
)
logger.warning("Restarting once with a new WebView2 profile: %s", recovery_path)
return True
except Exception:
logger.exception("Could not restart for WebView2 profile recovery")
return False