Files
server_manager/scripts/update/mini_updater.py
T
2026-05-22 00:16:08 +08:00

155 lines
5.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
独立进程外更新器 (Out-of-Process Updater)
无第三方依赖(仅标准库 + 可选 ctypes 探测 PID),
打包为 mini_updater.exe 后置于安装目录,由主程序在“确认重启”后拉起。
负责:等待主进程退出 → 重命名替换 exe → 以正确 cwd 与 DETACHED_PROCESS 启动新进程。
"""
import argparse
import os
import subprocess
import sys
import time
# 仅标准库:PID 检测用 ctypesWindows)或 os.killUnix
if sys.platform == "win32":
import ctypes
try:
_kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
except Exception:
_kernel32 = None
else:
_kernel32 = None
def _log_error(install_dir: str, message: str, exc: Exception = None) -> None:
"""将错误写入安装目录下的 logs/update_error.log,便于排查。"""
try:
log_dir = os.path.join(install_dir, "logs")
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, "update_error.log")
with open(log_path, "a", encoding="utf-8") as f:
from datetime import datetime
line = "[%s] %s" % (datetime.now().isoformat(), message)
if exc is not None:
line += " (%s)" % exc
f.write(line + "\n")
except Exception:
pass
def is_process_alive(pid: int) -> bool:
"""
跨平台判断指定 PID 是否仍存活。
Windows: ctypes 调用 OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION)
Unix: os.kill(pid, 0)。
"""
if sys.platform == "win32" and _kernel32 is not None:
# PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
handle = _kernel32.OpenProcess(0x1000, False, pid)
if handle is None or handle == 0:
return False
try:
_kernel32.CloseHandle(handle)
except Exception:
pass
return True
try:
os.kill(pid, 0)
return True
except (OSError, ProcessLookupError, PermissionError):
return False
def main() -> None:
parser = argparse.ArgumentParser(description="独立更新守护:等主进程退出后替换 exe 并重启")
parser.add_argument("--pid", type=int, required=True, help="主程序进程 ID,必须等其完全退出")
parser.add_argument("--install-dir", type=str, required=True, help="主程序安装根目录")
parser.add_argument("--new-exe-path", type=str, required=True, help="已准备好的新版本 main.exe 的完整路径(通常在 TEMP)")
parser.add_argument("--target-exe-name", type=str, default="main.exe", help="主程序文件名,如 main.exe")
args = parser.parse_args()
install_dir = os.path.abspath(args.install_dir)
new_exe_path = os.path.abspath(args.new_exe_path)
target_exe_name = args.target_exe_name.strip() or "main.exe"
target_exe_full = os.path.join(install_dir, target_exe_name)
old_exe_full = target_exe_full + ".old"
# -------------------------------------------------------------------------
# 1. 死神凝视:每隔 0.5 秒检测 --pid 是否存活,必须等到进程彻底消失
# -------------------------------------------------------------------------
while is_process_alive(args.pid):
time.sleep(0.5)
# -------------------------------------------------------------------------
# 2. 障眼法替换 (The Swap)
# -------------------------------------------------------------------------
import shutil
try:
os.chdir(install_dir)
except Exception as e:
_log_error(install_dir, "切换到安装目录失败", e)
sys.exit(1)
# 若存在上次遗留的 .old,先静默删除
if os.path.isfile(old_exe_full):
try:
os.remove(old_exe_full)
except Exception as e:
_log_error(install_dir, "删除旧 .old 文件失败: %s" % old_exe_full, e)
# 将当前主程序重命名为 .old,腾出位置
if not os.path.isfile(target_exe_full):
_log_error(install_dir, "目标 exe 不存在: %s" % target_exe_full)
sys.exit(1)
try:
os.rename(target_exe_full, old_exe_full)
except Exception as e:
_log_error(install_dir, "重命名当前 exe 为 .old 失败", e)
sys.exit(1)
# 将新 exe 移入安装目录并命名为 target-exe-name
if not os.path.isfile(new_exe_path):
_log_error(install_dir, "新 exe 不存在: %s" % new_exe_path)
sys.exit(1)
try:
shutil.move(new_exe_path, target_exe_full)
except Exception as e:
_log_error(install_dir, "移动新 exe 到安装目录失败", e)
try:
os.rename(old_exe_full, target_exe_full)
except Exception:
pass
sys.exit(1)
# -------------------------------------------------------------------------
# 3. 干净的重生:Popen 新程序,强制 cwd、close_fds、DETACHED_PROCESS
# -------------------------------------------------------------------------
try:
creationflags = 0
if sys.platform == "win32":
# DETACHED_PROCESS = 0x00000008,不继承控制台与父进程句柄
creationflags = 0x00000008
p = subprocess.Popen(
[target_exe_full],
cwd=install_dir,
close_fds=True,
creationflags=creationflags,
shell=False,
)
# 不等待子进程,更新器功成身退
except Exception as e:
_log_error(install_dir, "拉起新主程序失败", e)
sys.exit(1)
# -------------------------------------------------------------------------
# 4. 功成身退
# -------------------------------------------------------------------------
sys.exit(0)
if __name__ == "__main__":
main()