134 lines
4.5 KiB
Python
134 lines
4.5 KiB
Python
"""甄 AI 客服独立更新器;仅依赖 Python 标准库,打包为单文件 EXE。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ctypes
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as source:
|
|
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _process_running(pid: int) -> bool:
|
|
if pid <= 0:
|
|
return False
|
|
if os.name == "nt":
|
|
process_query_limited_information = 0x1000
|
|
handle = ctypes.windll.kernel32.OpenProcess(
|
|
process_query_limited_information, False, pid
|
|
)
|
|
if not handle:
|
|
return False
|
|
ctypes.windll.kernel32.CloseHandle(handle)
|
|
return True
|
|
try:
|
|
os.kill(pid, 0)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def _write_result(path: Path, payload: dict) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
temporary = path.with_suffix(path.suffix + ".tmp")
|
|
temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
os.replace(temporary, path)
|
|
|
|
|
|
def _wait_for_parent(pid: int, timeout: int = 120) -> None:
|
|
deadline = time.monotonic() + timeout
|
|
while _process_running(pid):
|
|
if time.monotonic() >= deadline:
|
|
raise RuntimeError("主程序在 120 秒内没有退出")
|
|
time.sleep(0.25)
|
|
|
|
|
|
def _start_available_version(install_dir: Path, preferred: Path | None = None) -> bool:
|
|
"""启动指定新版本;失败时回退到安装目录中最近的可执行旧版本。"""
|
|
|
|
candidates = []
|
|
if preferred is not None:
|
|
candidates.append(preferred)
|
|
try:
|
|
discovered = sorted(
|
|
install_dir.glob("ZhenAI-WeCom-Assistant-v*.exe"),
|
|
key=lambda item: item.stat().st_mtime,
|
|
reverse=True,
|
|
)
|
|
except OSError:
|
|
discovered = []
|
|
candidates.extend(item for item in discovered if item not in candidates)
|
|
for candidate in candidates:
|
|
if not candidate.is_file():
|
|
continue
|
|
try:
|
|
subprocess.Popen([str(candidate)], cwd=install_dir, close_fds=True)
|
|
return True
|
|
except OSError:
|
|
continue
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="甄 AI 客服独立更新器")
|
|
parser.add_argument("--installer", required=True)
|
|
parser.add_argument("--parent-pid", type=int, required=True)
|
|
parser.add_argument("--install-dir", required=True)
|
|
parser.add_argument("--target-version", required=True)
|
|
parser.add_argument("--result-file", required=True)
|
|
parser.add_argument("--expected-sha256", default="")
|
|
parser.add_argument("--from-version", default="")
|
|
args = parser.parse_args()
|
|
|
|
installer = Path(args.installer).resolve()
|
|
install_dir = Path(args.install_dir).resolve()
|
|
result_file = Path(args.result_file).resolve()
|
|
result = {
|
|
"target_version": args.target_version,
|
|
"from_version": args.from_version,
|
|
"status": "failed",
|
|
"error": "",
|
|
"updated_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
}
|
|
try:
|
|
_wait_for_parent(args.parent_pid)
|
|
if not installer.is_file():
|
|
raise RuntimeError("升级安装包不存在")
|
|
if args.expected_sha256 and _sha256(installer) != args.expected_sha256.lower():
|
|
raise RuntimeError("安装前 SHA256 复核失败")
|
|
completed = subprocess.run(
|
|
[str(installer), "/S"],
|
|
timeout=600,
|
|
creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
|
)
|
|
if completed.returncode != 0:
|
|
raise RuntimeError(f"静默安装失败,退出码 {completed.returncode}")
|
|
target = install_dir / f"ZhenAI-WeCom-Assistant-v{args.target_version}.exe"
|
|
if not target.is_file():
|
|
raise RuntimeError("安装完成但没有找到新版本主程序")
|
|
result["status"] = "installed"
|
|
_write_result(result_file, result)
|
|
if not _start_available_version(install_dir, target):
|
|
raise RuntimeError("安装成功,但无法启动新版本主程序")
|
|
return 0
|
|
except Exception as exc:
|
|
result["error"] = str(exc)[:1000]
|
|
_write_result(result_file, result)
|
|
_start_available_version(install_dir)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|