142 lines
3.6 KiB
Python
142 lines
3.6 KiB
Python
"""
|
||
以“计划任务(最高权限)”方式执行静默覆盖更新。
|
||
|
||
工作流:
|
||
- 主程序(普通权限)下载新 exe 到可写目录(建议 ProgramData)并写入 update_request.json
|
||
- 计划任务以 SYSTEM/管理员身份运行本程序
|
||
- 本程序读取请求文件 -> 等待主程序退出 -> 覆盖安装目录 exe -> 重启 -> 清理请求文件
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import subprocess
|
||
import sys
|
||
import time
|
||
|
||
|
||
APP_NAME = "CleanDesktopOrganizer"
|
||
REQUEST_FILE = "update_request.json"
|
||
LOG_FILE = "update.log"
|
||
|
||
|
||
def _programdata_dir() -> str:
|
||
base = os.environ.get("PROGRAMDATA") or r"C:\ProgramData"
|
||
return os.path.join(base, APP_NAME)
|
||
|
||
|
||
def _request_path() -> str:
|
||
return os.path.join(_programdata_dir(), REQUEST_FILE)
|
||
|
||
|
||
def _log(msg: str):
|
||
try:
|
||
os.makedirs(_programdata_dir(), exist_ok=True)
|
||
p = os.path.join(_programdata_dir(), LOG_FILE)
|
||
with open(p, "a", encoding="utf-8") as f:
|
||
f.write(msg.rstrip() + "\n")
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _read_request() -> dict:
|
||
p = _request_path()
|
||
with open(p, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
|
||
|
||
def _pid_exists(pid: int) -> bool:
|
||
if pid <= 0:
|
||
return False
|
||
try:
|
||
# tasklist 不需要管理员权限,SYSTEM 下也可用
|
||
out = subprocess.run(
|
||
["tasklist", "/FI", f"PID eq {pid}"],
|
||
capture_output=True,
|
||
text=True,
|
||
encoding="utf-8",
|
||
errors="replace",
|
||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||
).stdout
|
||
return str(pid) in out
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _copy_with_retries(src: str, dst: str, retries: int = 10) -> bool:
|
||
for _ in range(max(1, retries)):
|
||
try:
|
||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||
if os.path.exists(dst):
|
||
try:
|
||
os.chmod(dst, 0o666)
|
||
except Exception:
|
||
pass
|
||
# 用二进制流复制,避免权限/锁问题时直接抛错
|
||
with open(src, "rb") as rf:
|
||
data = rf.read()
|
||
with open(dst, "wb") as wf:
|
||
wf.write(data)
|
||
return True
|
||
except Exception:
|
||
time.sleep(0.8)
|
||
return False
|
||
|
||
|
||
def main() -> int:
|
||
try:
|
||
req = _read_request()
|
||
except FileNotFoundError:
|
||
return 0
|
||
except Exception:
|
||
_log("ERROR: cannot read request file")
|
||
return 2
|
||
|
||
src = str(req.get("src") or "")
|
||
dst = str(req.get("dst") or "")
|
||
pid = int(req.get("pid") or 0)
|
||
restart = bool(req.get("restart", True))
|
||
|
||
if not src or not dst:
|
||
_log(f"ERROR: bad request src={src!r} dst={dst!r} pid={pid}")
|
||
return 3
|
||
|
||
_log(f"START: pid={pid} src={src} dst={dst}")
|
||
|
||
# 等待主程序退出,最多 60 秒
|
||
waited = 0.0
|
||
while _pid_exists(pid) and waited < 60.0:
|
||
time.sleep(0.5)
|
||
waited += 0.5
|
||
|
||
ok = _copy_with_retries(src, dst, retries=15)
|
||
_log(f"COPY: ok={ok}")
|
||
try:
|
||
os.remove(src)
|
||
except Exception:
|
||
pass
|
||
|
||
# 清理请求文件(成功/失败都尽量清掉,避免重复执行)
|
||
try:
|
||
os.remove(_request_path())
|
||
except Exception:
|
||
pass
|
||
|
||
if ok and restart:
|
||
try:
|
||
subprocess.Popen([dst], creationflags=subprocess.DETACHED_PROCESS | subprocess.CREATE_NO_WINDOW)
|
||
_log("RESTART: started")
|
||
except Exception:
|
||
_log("RESTART: failed")
|
||
pass
|
||
return 0
|
||
|
||
_log("DONE: failed")
|
||
return 4
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|
||
|