631 lines
25 KiB
Python
631 lines
25 KiB
Python
# updater.py — Windows, PyInstaller onefile 권장 (로깅/진단/언인스/백업/복원 포함)
|
|
import argparse, os, shutil, subprocess, sys, time, tempfile, uuid, datetime, csv, io, traceback
|
|
import json
|
|
import threading
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
try:
|
|
import winreg
|
|
except ImportError:
|
|
winreg = None
|
|
|
|
# 기본 종료 대상 (필요시 --targets 로 덮어쓰기)
|
|
TARGETS = ["Edit_PartTimer3.exe", "main.exe"]
|
|
|
|
UNINSTALL_KEYS = [
|
|
r"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
|
|
r"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall",
|
|
]
|
|
|
|
# ── 로깅 ─────────────────────────────────────────────────────────────────────
|
|
LOG_PATH = os.path.join(tempfile.gettempdir(), "updater_debug.log")
|
|
|
|
def log(msg: str, exc_info: bool = False):
|
|
try:
|
|
ts = datetime.datetime.now().isoformat(timespec="seconds")
|
|
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
|
f.write(f"[{ts}] {msg}\n")
|
|
if exc_info:
|
|
f.write(traceback.format_exc())
|
|
except Exception:
|
|
pass
|
|
|
|
# ── GUI 진행 상황 표시 ─────────────────────────────────────────────────────
|
|
class ProgressWindow:
|
|
"""업데이트 진행 상황을 표시하는 간단한 GUI 창"""
|
|
def __init__(self):
|
|
self.root = None
|
|
self.label = None
|
|
self.progress = None
|
|
self.detail_label = None
|
|
self._thread = None
|
|
|
|
def start(self):
|
|
"""GUI 스레드 시작"""
|
|
self._thread = threading.Thread(target=self._run_gui, daemon=True)
|
|
self._thread.start()
|
|
time.sleep(0.2) # GUI 초기화 대기
|
|
|
|
def _run_gui(self):
|
|
"""GUI 메인 루프 (별도 스레드에서 실행)"""
|
|
self.root = tk.Tk()
|
|
self.root.title("편집알바생 업데이트 중")
|
|
self.root.geometry("450x180")
|
|
self.root.resizable(False, False)
|
|
|
|
# 창을 항상 위에 표시
|
|
self.root.attributes("-topmost", True)
|
|
|
|
# 중앙 배치
|
|
self.root.update_idletasks()
|
|
width = self.root.winfo_width()
|
|
height = self.root.winfo_height()
|
|
x = (self.root.winfo_screenwidth() // 2) - (width // 2)
|
|
y = (self.root.winfo_screenheight() // 2) - (height // 2)
|
|
self.root.geometry(f'{width}x{height}+{x}+{y}')
|
|
|
|
# 프레임
|
|
frame = ttk.Frame(self.root, padding="20")
|
|
frame.pack(fill=tk.BOTH, expand=True)
|
|
|
|
# 제목
|
|
title = ttk.Label(frame, text="업데이트를 진행하고 있습니다",
|
|
font=("맑은 고딕", 12, "bold"))
|
|
title.pack(pady=(0, 15))
|
|
|
|
# 상태 메시지
|
|
self.label = ttk.Label(frame, text="업데이트를 준비하는 중...",
|
|
font=("맑은 고딕", 10))
|
|
self.label.pack(pady=(0, 10))
|
|
|
|
# 프로그레스 바
|
|
self.progress = ttk.Progressbar(frame, mode='indeterminate', length=380)
|
|
self.progress.pack(pady=(0, 10))
|
|
self.progress.start(10)
|
|
|
|
# 상세 정보
|
|
self.detail_label = ttk.Label(frame, text="잠시만 기다려주세요...",
|
|
font=("맑은 고딕", 8), foreground="gray")
|
|
self.detail_label.pack()
|
|
|
|
self.root.mainloop()
|
|
|
|
def update_status(self, message: str, detail: str = ""):
|
|
"""상태 메시지 업데이트"""
|
|
if self.root and self.label:
|
|
try:
|
|
self.label.config(text=message)
|
|
if detail and self.detail_label:
|
|
self.detail_label.config(text=detail)
|
|
self.root.update()
|
|
except:
|
|
pass
|
|
|
|
def close(self):
|
|
"""창 닫기"""
|
|
if self.root:
|
|
try:
|
|
self.root.quit()
|
|
self.root.destroy()
|
|
except:
|
|
pass
|
|
|
|
# 전역 프로그레스 윈도우
|
|
progress_window = None
|
|
|
|
# ── 유틸 ─────────────────────────────────────────────────────────────────────
|
|
def user_data_path(appdir: str) -> str:
|
|
return os.path.join(appdir, "lib", "src", "user_data")
|
|
|
|
def tasklist_names() -> set[str]:
|
|
try:
|
|
out = subprocess.check_output(["tasklist", "/FO", "CSV", "/NH"], encoding="cp949", errors="replace")
|
|
names = set()
|
|
for line in out.splitlines():
|
|
parts = [p.strip('"') for p in line.split(",")]
|
|
if parts and parts[0]:
|
|
names.add(parts[0])
|
|
return {n.lower() for n in names}
|
|
except Exception as e:
|
|
log(f"tasklist read failed: {e}")
|
|
return set()
|
|
|
|
def wait_until_killed(targets: list[str], timeout_s=10) -> bool:
|
|
deadline = time.time() + timeout_s
|
|
tset = {t.lower() for t in targets}
|
|
while time.time() < deadline:
|
|
alive = tasklist_names() & tset
|
|
if not alive:
|
|
return True
|
|
time.sleep(0.4)
|
|
log(f"still alive after kill wait: {', '.join(sorted(alive))}")
|
|
return False
|
|
|
|
def pids_by_image(name: str) -> list[int]:
|
|
"""tasklist로 특정 이미지 이름의 PID 목록을 얻는다."""
|
|
try:
|
|
out = subprocess.check_output(
|
|
["tasklist", "/FI", f"IMAGENAME eq {name}", "/FO", "CSV", "/NH"],
|
|
encoding="cp949", errors="replace"
|
|
)
|
|
except Exception:
|
|
return []
|
|
pids = []
|
|
for line in out.splitlines():
|
|
if not line.strip() or "정보:" in line: # "정보: 일치하는 작업이 없습니다." 등 한글 메시지
|
|
continue
|
|
row = next(csv.reader(io.StringIO(line)))
|
|
# row = ["Image Name","PID","Session Name","Session#","Mem Usage"]
|
|
if len(row) >= 2 and row[0].lower() == name.lower():
|
|
try:
|
|
pids.append(int(row[1]))
|
|
except ValueError:
|
|
pass
|
|
return pids
|
|
|
|
def kill_targets(targets: list[str]):
|
|
log(f"kill_targets START: {targets}")
|
|
self_pid = os.getpid()
|
|
log(f"self_pid={self_pid}")
|
|
# PyInstaller onefile일 때도 이 값이 "현재 실행 중인 (temp 경로의) updater.exe" PID
|
|
for img in targets:
|
|
log(f"Processing image: {img}")
|
|
try:
|
|
# 자기 자신 이미지명이라도 PID로 걸러서 본인 제외
|
|
pids = pids_by_image(img)
|
|
log(f"Found PIDs for {img}: {pids}")
|
|
safe_pids = [pid for pid in pids if pid != self_pid]
|
|
log(f"Safe PIDs for {img}: {safe_pids}")
|
|
|
|
if not safe_pids:
|
|
log(f"no killable PIDs for {img} (found={pids}, self_pid={self_pid})")
|
|
continue
|
|
|
|
# 개별 PID로 종료하면 자기 자신을 실수로 죽일 일이 없음
|
|
cmd = ["taskkill", "/F"] # /T 옵션 제거 (자식 프로세스까지 죽이면 위험)
|
|
for pid in safe_pids:
|
|
cmd.extend(["/PID", str(pid)])
|
|
|
|
log(f"Executing taskkill command: {' '.join(cmd)}")
|
|
r = subprocess.run(cmd, capture_output=True, text=True, shell=False, timeout=10)
|
|
log(f"taskkill {img} PIDs={safe_pids} -> rc={r.returncode} out='{(r.stdout or '').strip()}' err='{(r.stderr or '').strip()}'")
|
|
except subprocess.TimeoutExpired:
|
|
log(f"taskkill {img} timeout")
|
|
except Exception as e:
|
|
log(f"taskkill {img} exception: {e}", exc_info=True)
|
|
|
|
log("kill_targets: waiting for processes to die...")
|
|
# 짧게 안정 대기
|
|
deadline = time.time() + 10
|
|
while time.time() < deadline:
|
|
alive = []
|
|
try:
|
|
for img in targets:
|
|
for pid in pids_by_image(img):
|
|
if pid != self_pid:
|
|
alive.append((img, pid))
|
|
if not alive:
|
|
break
|
|
except Exception as e:
|
|
log(f"Error checking alive processes: {e}")
|
|
break
|
|
time.sleep(0.4)
|
|
log(f"kill wait done; alive after wait: {alive if 'alive' in locals() else []}")
|
|
|
|
def run_installer(installer_path: str, silent: bool, inno_log: str, appdir: str | None = None) -> int:
|
|
args = [installer_path]
|
|
if silent:
|
|
args += ["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/CLOSEAPPLICATIONS", "/RESTARTAPPLICATIONS"]
|
|
if appdir:
|
|
args += [f'/DIR={appdir}'] # subprocess.run이 공백 자동 처리
|
|
args += [f'/LOG={inno_log}'] # 따옴표 제거
|
|
log(f"run_installer: args={args}")
|
|
try:
|
|
completed = subprocess.run(args, check=False, shell=False, close_fds=True)
|
|
log(f"installer rc={completed.returncode}")
|
|
return completed.returncode
|
|
except Exception as e:
|
|
log(f"installer exception: {e}", exc_info=True)
|
|
return 1
|
|
|
|
def relaunch(appdir: str, appexe: str):
|
|
import os, subprocess
|
|
# 실행 파일 전체 경로
|
|
full = os.path.join(appdir, appexe) if appdir else appexe
|
|
exists = os.path.exists(full)
|
|
log(f"relaunch: {full} exists={exists}")
|
|
if not exists:
|
|
log("relaunch abort: target exe not found")
|
|
return
|
|
|
|
# 작업폴더(cwd) 결정: appdir이 있으면 그걸 사용, 없으면 exe의 폴더
|
|
cwd = appdir if appdir else os.path.dirname(full)
|
|
try:
|
|
subprocess.Popen([full], shell=False, cwd=cwd, close_fds=True)
|
|
log("relaunch Popen OK")
|
|
except Exception as e:
|
|
log(f"relaunch error: {e}")
|
|
|
|
def find_uninstaller_by_appid_or_name(app_name_keywords=(), app_id=None):
|
|
if winreg is None:
|
|
return None
|
|
hives = [(winreg.HKEY_LOCAL_MACHINE, "HKLM"), (winreg.HKEY_CURRENT_USER, "HKCU")]
|
|
for hive, _ in hives:
|
|
for base in UNINSTALL_KEYS:
|
|
try:
|
|
with winreg.OpenKey(hive, base) as k:
|
|
idx = 0
|
|
while True:
|
|
try:
|
|
subkey_name = winreg.EnumKey(k, idx)
|
|
except OSError:
|
|
break
|
|
idx += 1
|
|
try:
|
|
with winreg.OpenKey(k, subkey_name) as sk:
|
|
display_name = None
|
|
uninstall_str = None
|
|
try:
|
|
display_name, _ = winreg.QueryValueEx(sk, "DisplayName")
|
|
except OSError:
|
|
pass
|
|
try:
|
|
uninstall_str, _ = winreg.QueryValueEx(sk, "UninstallString")
|
|
except OSError:
|
|
pass
|
|
if app_id:
|
|
try:
|
|
appid_reg, _ = winreg.QueryValueEx(sk, "Inno Setup: AppId")
|
|
if appid_reg and appid_reg.strip().lower() == app_id.strip().lower():
|
|
if uninstall_str:
|
|
return uninstall_str
|
|
except OSError:
|
|
pass
|
|
if display_name and app_name_keywords:
|
|
name_l = display_name.lower()
|
|
if all(kw.lower() in name_l for kw in app_name_keywords):
|
|
if uninstall_str:
|
|
return uninstall_str
|
|
except OSError:
|
|
continue
|
|
except OSError:
|
|
continue
|
|
return None
|
|
|
|
def run_uninstaller(uninstall_cmd: str, unins_log: str) -> int:
|
|
if not uninstall_cmd:
|
|
return 0
|
|
cmdline = uninstall_cmd.strip()
|
|
extra = f' /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG="{unins_log}"'
|
|
if "/LOG" not in cmdline.upper():
|
|
cmdline = f"{cmdline} {extra}"
|
|
log(f"run_uninstaller: {cmdline}")
|
|
try:
|
|
completed = subprocess.run(cmdline, check=False, shell=True, close_fds=True)
|
|
log(f"uninstaller rc={completed.returncode}")
|
|
return completed.returncode
|
|
except Exception as e:
|
|
log(f"uninstaller exception: {e}")
|
|
return 1
|
|
|
|
def find_unins_in_appdir(appdir: str):
|
|
if not appdir or not os.path.isdir(appdir):
|
|
return None
|
|
for name in os.listdir(appdir):
|
|
if name.lower().startswith("unins") and name.lower().endswith(".exe"):
|
|
return os.path.join(appdir, name)
|
|
return None
|
|
|
|
def backup_user_data(appdir: str) -> str | None:
|
|
src = user_data_path(appdir)
|
|
if not appdir or not os.path.isdir(src):
|
|
log("backup: no user_data to backup")
|
|
return None
|
|
backup_root = os.path.join(tempfile.gettempdir(), f"user_data_backup_{uuid.uuid4().hex}")
|
|
try:
|
|
shutil.copytree(src, backup_root, dirs_exist_ok=True)
|
|
log(f"backup: {src} -> {backup_root}")
|
|
return backup_root
|
|
except Exception as e:
|
|
log(f"backup error: {e}")
|
|
return None
|
|
|
|
def restore_user_data(appdir: str, backup_dir: str | None):
|
|
if not backup_dir or not os.path.isdir(backup_dir) or not appdir:
|
|
log("restore: skip (no backup)")
|
|
return
|
|
dst = user_data_path(appdir)
|
|
os.makedirs(dst, exist_ok=True)
|
|
copied = 0
|
|
for root, dirs, files in os.walk(backup_dir):
|
|
rel = os.path.relpath(root, backup_dir)
|
|
target = os.path.join(dst, rel) if rel != "." else dst
|
|
os.makedirs(target, exist_ok=True)
|
|
for d in dirs:
|
|
os.makedirs(os.path.join(target, d), exist_ok=True)
|
|
for f in files:
|
|
src_file = os.path.join(root, f)
|
|
dst_file = os.path.join(target, f)
|
|
try:
|
|
shutil.copy2(src_file, dst_file); copied += 1
|
|
except Exception as e:
|
|
log(f"restore copy error {src_file} -> {dst_file}: {e}")
|
|
log(f"restore: copied {copied} files to {dst}")
|
|
try:
|
|
shutil.rmtree(backup_dir, ignore_errors=True)
|
|
except Exception:
|
|
pass
|
|
|
|
def find_uninstaller_by_appid_fuzzy(appid: str) -> str | None:
|
|
"""
|
|
Inno 언인스키는 보통 '<AppId>_is1'. AppId가 GUID든 문자열이든 모두 시도.
|
|
"""
|
|
if not appid or winreg is None:
|
|
return None
|
|
# 시도할 서픽스 후보 만들기
|
|
cand_suffixes = []
|
|
a = appid.strip()
|
|
cand_suffixes.append((a + "_is1").lower())
|
|
if a.startswith("{") and a.endswith("}"):
|
|
naked = a[1:-1]
|
|
cand_suffixes.append((naked + "_is1").lower())
|
|
else:
|
|
cand_suffixes.append(("{"+a+"}" + "_is1").lower())
|
|
|
|
# 레지스트리 열거
|
|
hives = [(winreg.HKEY_LOCAL_MACHINE, "HKLM"), (winreg.HKEY_CURRENT_USER, "HKCU")]
|
|
for hive, _ in hives:
|
|
for base in UNINSTALL_KEYS:
|
|
try:
|
|
with winreg.OpenKey(hive, base) as k:
|
|
i = 0
|
|
while True:
|
|
try:
|
|
sub = winreg.EnumKey(k, i); i += 1
|
|
except OSError:
|
|
break
|
|
sub_l = sub.lower()
|
|
if any(sub_l.endswith(suf) for suf in cand_suffixes):
|
|
try:
|
|
with winreg.OpenKey(k, sub) as sk:
|
|
uninstall_str, _ = winreg.QueryValueEx(sk, "UninstallString")
|
|
if uninstall_str:
|
|
return uninstall_str
|
|
except OSError:
|
|
pass
|
|
except OSError:
|
|
continue
|
|
return None
|
|
|
|
def read_manifest_near(installer_path: str) -> dict:
|
|
"""
|
|
설치본(Setup_*.exe) 옆에 '{installer}.manifest.json'이 있으면 읽는다.
|
|
거기에 app_id, app_name, default_dirname 등이 들어있다고 가정.
|
|
"""
|
|
try:
|
|
base = os.path.splitext(installer_path)[0] # .exe 제거
|
|
cand = base + ".manifest.json"
|
|
if os.path.exists(cand):
|
|
with open(cand, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except Exception as e:
|
|
log(f"manifest read error: {e}")
|
|
return {}
|
|
|
|
def resolve_appdir_from_registry(appid: str, app_name_keywords: tuple[str, ...]) -> str:
|
|
"""
|
|
InstallLocation 또는 언인스 exe 경로의 상위 폴더로 appdir 추정.
|
|
"""
|
|
if winreg is None:
|
|
return ""
|
|
def _search_entry():
|
|
hives = [(winreg.HKEY_LOCAL_MACHINE, "HKLM"), (winreg.HKEY_CURRENT_USER, "HKCU")]
|
|
for hive, _ in hives:
|
|
for base in UNINSTALL_KEYS:
|
|
try:
|
|
with winreg.OpenKey(hive, base) as k:
|
|
i = 0
|
|
while True:
|
|
try:
|
|
sub = winreg.EnumKey(k, i); i += 1
|
|
except OSError:
|
|
break
|
|
try:
|
|
with winreg.OpenKey(k, sub) as sk:
|
|
# AppId 우선
|
|
if appid:
|
|
try:
|
|
v, _ = winreg.QueryValueEx(sk, "Inno Setup: AppId")
|
|
if v and v.strip().lower() == appid.strip().lower():
|
|
return sk
|
|
except OSError:
|
|
pass
|
|
# 키워드로 DisplayName 매칭
|
|
if app_name_keywords:
|
|
try:
|
|
dn, _ = winreg.QueryValueEx(sk, "DisplayName")
|
|
if dn and all(kw.lower() in dn.lower() for kw in app_name_keywords):
|
|
return sk
|
|
except OSError:
|
|
pass
|
|
except OSError:
|
|
pass
|
|
except OSError:
|
|
continue
|
|
return None
|
|
|
|
sk = _search_entry()
|
|
if not sk:
|
|
return ""
|
|
# InstallLocation → 최선
|
|
try:
|
|
il, _ = winreg.QueryValueEx(sk, "InstallLocation")
|
|
if il and os.path.isdir(il):
|
|
return il
|
|
except OSError:
|
|
pass
|
|
# UninstallString 경로에서 상위 폴더 추정
|
|
try:
|
|
us, _ = winreg.QueryValueEx(sk, "UninstallString")
|
|
if us:
|
|
# 예: "C:\Program Files\...\unins000.exe" /SILENT ...
|
|
# 첫 따옴표 블록 추출
|
|
if us.startswith('"'):
|
|
p = us.split('"')
|
|
if len(p) >= 3:
|
|
unins = p[1]
|
|
else:
|
|
unins = us.strip().split()[0]
|
|
else:
|
|
unins = us.strip().split()[0]
|
|
if os.path.isfile(unins):
|
|
return os.path.dirname(unins)
|
|
except OSError:
|
|
pass
|
|
return ""
|
|
|
|
# ── 메인 ─────────────────────────────────────────────────────────────────────
|
|
def main():
|
|
|
|
try:
|
|
os.chdir(tempfile.gettempdir()) # _MEI가 아닌 안전한 폴더로 CWD 이동
|
|
except Exception:
|
|
pass
|
|
|
|
global LOG_PATH # ← FIX: 전역 재할당 전에 선언
|
|
|
|
ap = argparse.ArgumentParser(description="Minimal updater (Windows, PyInstaller onefile)")
|
|
ap.add_argument("--installer", required=True)
|
|
ap.add_argument("--appexe", required=True)
|
|
ap.add_argument("--appdir", default="")
|
|
ap.add_argument("--nosilent", action="store_true")
|
|
ap.add_argument("--appid", default="")
|
|
ap.add_argument("--appnamekw", nargs="*", default=["Edit", "PartTimer"])
|
|
ap.add_argument("--targets", default="", help="쉼표구분 프로세스명 목록으로 TARGETS 대체 (예: a.exe,b.exe)")
|
|
ap.add_argument("--log", default=LOG_PATH, help="updater log path")
|
|
a = ap.parse_args()
|
|
|
|
# 로그 경로 설정을 먼저 적용
|
|
LOG_PATH = a.log
|
|
|
|
log("===== updater start =====")
|
|
log(f"args: installer='{a.installer}', appexe='{a.appexe}', appdir='{a.appdir}', nosilent={a.nosilent}, appid='{a.appid}', appnamekw={a.appnamekw}, targets='{a.targets}'")
|
|
|
|
# installer 변수 먼저 정의
|
|
installer = os.path.abspath(a.installer)
|
|
appdir = os.path.abspath(a.appdir) if a.appdir else ""
|
|
|
|
# ---- 자동결정 (appid/appdir) ----
|
|
mf = read_manifest_near(installer)
|
|
# 우선순위: CLI > 매니페스트 > 빈값
|
|
appid = a.appid.strip() or str(mf.get("app_id", "")).strip()
|
|
if not appdir:
|
|
# 매니페스트에 default_dirname 있으면 Program Files 하위 기본값 시도
|
|
default_dir = str(mf.get("default_dirname", "")).strip()
|
|
if default_dir:
|
|
pf = os.environ.get("ProgramFiles", r"C:\Program Files")
|
|
cand = os.path.join(pf, default_dir)
|
|
if os.path.isdir(cand):
|
|
appdir = cand
|
|
# 그래도 못 찾으면 레지스트리에서 설치경로 역추적
|
|
if not appdir:
|
|
appdir = resolve_appdir_from_registry(appid, tuple(a.appnamekw))
|
|
# 최종값 로깅
|
|
log(f"auto-decided: appid='{appid}', appdir='{appdir}'")
|
|
inno_log = os.path.join(tempfile.gettempdir(), "inno_install.log")
|
|
unins_log = os.path.join(tempfile.gettempdir(), "inno_uninstall.log")
|
|
log(f"resolved: installer='{installer}', appdir='{appdir}', inno_log='{inno_log}', unins_log='{unins_log}'")
|
|
|
|
# 런타임 대상 프로세스 조정
|
|
targets = TARGETS.copy()
|
|
if a.targets.strip():
|
|
targets = [t.strip() for t in a.targets.split(",") if t.strip()]
|
|
log(f"targets override -> {targets}")
|
|
|
|
if not os.path.exists(installer):
|
|
log("installer not found -> abort(2)")
|
|
return 2
|
|
|
|
# GUI 시작 (항상 표시)
|
|
global progress_window
|
|
progress_window = ProgressWindow()
|
|
progress_window.start()
|
|
log("Progress window started")
|
|
|
|
try:
|
|
# 1) 프로세스 종료
|
|
if progress_window:
|
|
progress_window.update_status("프로그램을 종료하는 중...",
|
|
"실행 중인 프로세스를 안전하게 종료합니다")
|
|
kill_targets(targets)
|
|
|
|
# 2) user_data 백업
|
|
if progress_window:
|
|
progress_window.update_status("데이터를 백업하는 중...",
|
|
"사용자 데이터를 안전하게 백업합니다")
|
|
backup_dir = backup_user_data(appdir)
|
|
|
|
# 3) 기존 버전 언인스톨
|
|
if progress_window:
|
|
progress_window.update_status("이전 버전을 제거하는 중...",
|
|
"기존 설치 파일을 제거합니다")
|
|
uninstall_cmd = None
|
|
|
|
# appid로 먼저 시도
|
|
if appid:
|
|
uninstall_cmd = find_uninstaller_by_appid_fuzzy(appid)
|
|
if uninstall_cmd:
|
|
log(f"uninstall_cmd (appid fuzzy) = {uninstall_cmd}")
|
|
|
|
# 키워드 검색으로 시도
|
|
if not uninstall_cmd and (a.appid or (a.appnamekw and len(a.appnamekw) > 0)):
|
|
uninstall_cmd = find_uninstaller_by_appid_or_name(tuple(a.appnamekw), a.appid)
|
|
if uninstall_cmd:
|
|
log(f"uninstall_cmd (registry) = {uninstall_cmd}")
|
|
if not uninstall_cmd and appdir:
|
|
unins_path = find_unins_in_appdir(appdir)
|
|
if unins_path:
|
|
uninstall_cmd = f'"{unins_path}"'
|
|
log(f"uninstall_cmd (appdir) = {uninstall_cmd}")
|
|
|
|
unins_rc = 0
|
|
if uninstall_cmd:
|
|
unins_rc = run_uninstaller(uninstall_cmd, unins_log)
|
|
time.sleep(0.7)
|
|
|
|
# 4) 새 버전 설치
|
|
if progress_window:
|
|
progress_window.update_status("새 버전을 설치하는 중...",
|
|
"업데이트 파일을 설치합니다 (약 30초 소요)")
|
|
rc = run_installer(installer, silent=not a.nosilent, inno_log=inno_log, appdir=appdir)
|
|
|
|
# 5) user_data 복원
|
|
if progress_window:
|
|
progress_window.update_status("데이터를 복원하는 중...",
|
|
"백업한 사용자 데이터를 복원합니다")
|
|
restore_user_data(appdir, backup_dir)
|
|
time.sleep(0.15) # 150ms 정도
|
|
|
|
# 6) 메인 앱 재실행
|
|
if progress_window:
|
|
progress_window.update_status("업데이트 완료!",
|
|
"프로그램을 다시 시작합니다...")
|
|
time.sleep(0.5) # 메시지 표시 시간
|
|
|
|
relaunch(appdir, a.appexe)
|
|
|
|
log(f"return code: install={rc}, uninstall={unins_rc}")
|
|
log("===== updater end =====")
|
|
return rc if rc != 0 else unins_rc or 0
|
|
|
|
finally:
|
|
# GUI 종료
|
|
if progress_window:
|
|
time.sleep(0.3) # 완료 메시지 표시
|
|
progress_window.close()
|
|
log("Progress window closed")
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|