fix: exe 점검이 자식 프로세스를 안 죽여 임시폴더 정리에서 실패하던 것
Some checks failed
Windows / verify (push) Has been cancelled
Some checks failed
Windows / verify (push) Has been cancelled
onefile 의 부모는 압축을 푸는 부트로더일 뿐이고 창과 로그 파일을 쥐고 있는 것은 자식이다. 부모만 terminate 하니 자식이 livesub.log 를 계속 잡고 있어 임시 APPDATA 를 지울 때 PermissionError 가 났다. exe 의 문제가 아니라 점검이 뒷정리를 못 한 것인데 exe 기동 실패로 보고됐다. - 프로세스 트리를 통째로 정리한다 - 임시폴더 삭제는 실패해도 점검을 실패시키지 않는다 - 두 번째 실행에서 저장 위치 화면이 또 뜨면 실패로 본다. "기억한다"는 약속이 깨진 것이므로 본 화면이 아닌 창은 통과가 아니다
This commit is contained in:
@@ -14,8 +14,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import contextlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -440,6 +442,37 @@ def _visible_windows(pids: set[int]) -> list[str]:
|
|||||||
return titles
|
return titles
|
||||||
|
|
||||||
|
|
||||||
|
def _terminate_tree(proc: subprocess.Popen) -> None:
|
||||||
|
"""부모만 죽이면 안 된다.
|
||||||
|
|
||||||
|
onefile 의 부모는 압축을 푸는 부트로더일 뿐이고, 창과 로그 파일을 쥐고 있는
|
||||||
|
것은 자식이다. 부모만 terminate 하면 자식이 살아남아 로그 파일을 계속
|
||||||
|
잡고 있고, 임시 폴더를 지울 수 없어 점검이 엉뚱한 곳에서 실패한다.
|
||||||
|
"""
|
||||||
|
children = []
|
||||||
|
try:
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
children = psutil.Process(proc.pid).children(recursive=True)
|
||||||
|
except Exception: # noqa: BLE001 - psutil 이 없으면 부모만이라도 정리한다
|
||||||
|
pass
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
child.terminate()
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=15)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
for child in children:
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
child.wait(timeout=10)
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
if child.is_running():
|
||||||
|
child.kill()
|
||||||
|
|
||||||
|
|
||||||
def _launch_and_wait_for_window(exe: Path, appdata: Path, timeout_s: int = 90) -> str:
|
def _launch_and_wait_for_window(exe: Path, appdata: Path, timeout_s: int = 90) -> str:
|
||||||
"""exe 를 띄우고 **실제 창이 뜰 때까지** 기다린다.
|
"""exe 를 띄우고 **실제 창이 뜰 때까지** 기다린다.
|
||||||
|
|
||||||
@@ -469,11 +502,22 @@ def _launch_and_wait_for_window(exe: Path, appdata: Path, timeout_s: int = 90) -
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
raise RuntimeError(f"{timeout_s}초 안에 창이 뜨지 않았습니다")
|
raise RuntimeError(f"{timeout_s}초 안에 창이 뜨지 않았습니다")
|
||||||
finally:
|
finally:
|
||||||
proc.terminate()
|
_terminate_tree(proc)
|
||||||
try:
|
|
||||||
proc.wait(timeout=15)
|
|
||||||
except subprocess.TimeoutExpired:
|
@contextlib.contextmanager
|
||||||
proc.kill()
|
def _scratch_appdata():
|
||||||
|
"""점검용 %APPDATA%.
|
||||||
|
|
||||||
|
치울 때 실패해도 점검을 실패시키지 않는다. Windows 는 방금 죽인 프로세스가
|
||||||
|
파일 핸들을 놓는 데 잠깐 시간이 걸려서, 임시 폴더 삭제가 PermissionError 로
|
||||||
|
튀는 일이 있다. 그건 exe 의 문제가 아니라 치우는 쪽 사정이다.
|
||||||
|
"""
|
||||||
|
tmp = Path(tempfile.mkdtemp(prefix="livesub-smoke-"))
|
||||||
|
try:
|
||||||
|
yield tmp
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
def run_exe_check(exe: Path) -> None:
|
def run_exe_check(exe: Path) -> None:
|
||||||
@@ -491,17 +535,17 @@ def run_exe_check(exe: Path) -> None:
|
|||||||
@check("exe 첫 실행 — 저장 위치 화면")
|
@check("exe 첫 실행 — 저장 위치 화면")
|
||||||
def _first_run():
|
def _first_run():
|
||||||
# APPDATA 를 빈 폴더로 돌려 "처음 켜는 사람" 을 그대로 재현한다.
|
# APPDATA 를 빈 폴더로 돌려 "처음 켜는 사람" 을 그대로 재현한다.
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with _scratch_appdata() as tmp:
|
||||||
title = _launch_and_wait_for_window(exe, appdata=Path(tmp))
|
title = _launch_and_wait_for_window(exe, appdata=tmp)
|
||||||
return f"창 제목 {title!r}"
|
return f"창 제목 {title!r}"
|
||||||
|
|
||||||
@check("exe 두 번째 실행 — 본 화면")
|
@check("exe 두 번째 실행 — 본 화면")
|
||||||
def _second_run():
|
def _second_run():
|
||||||
# 위치를 이미 고른 사람. 묻지 않고 곧바로 본 화면이 떠야 한다.
|
# 위치를 이미 고른 사람. 묻지 않고 곧바로 본 화면이 떠야 한다.
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with _scratch_appdata() as tmp:
|
||||||
data = Path(tmp) / "data"
|
data = tmp / "data"
|
||||||
data.mkdir()
|
data.mkdir()
|
||||||
pointer = Path(tmp) / "LiveSub"
|
pointer = tmp / "LiveSub"
|
||||||
pointer.mkdir()
|
pointer.mkdir()
|
||||||
(pointer / "location.json").write_text(
|
(pointer / "location.json").write_text(
|
||||||
json.dumps({"data_dir": str(data)}), encoding="utf-8"
|
json.dumps({"data_dir": str(data)}), encoding="utf-8"
|
||||||
@@ -511,7 +555,9 @@ def run_exe_check(exe: Path) -> None:
|
|||||||
(data / "config.json").write_text(
|
(data / "config.json").write_text(
|
||||||
json.dumps({"models": {"preload_on_start": False}}), encoding="utf-8"
|
json.dumps({"models": {"preload_on_start": False}}), encoding="utf-8"
|
||||||
)
|
)
|
||||||
title = _launch_and_wait_for_window(exe, appdata=Path(tmp))
|
title = _launch_and_wait_for_window(exe, appdata=tmp)
|
||||||
|
if "저장 위치" in title:
|
||||||
|
raise RuntimeError(f"위치를 이미 골랐는데 또 물어봤습니다: {title!r}")
|
||||||
return f"창 제목 {title!r}"
|
return f"창 제목 {title!r}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user