fix: exe 점검이 본 화면 대신 자막 오버레이를 보고 통과하던 것
Some checks failed
Windows / verify (push) Has been cancelled

"아무 창이나 뜨면 통과" 로 둔 탓에, 자막 오버레이 창(제목이 앱 이름뿐이다)이
잡혀서 본 화면이 안 떠도 통과할 수 있었다. 지난 두 번의 오보가 전부 같은
형태였다 — 느슨한 통과 조건. 어떤 창을 기다리는지 호출하는 쪽이 지정하게
바꾼다. 첫 실행은 저장 위치 화면, 두 번째 실행은 본 화면 제목에만 있는
표어를 기다린다. 찾은 창 목록을 전부 기록해 나중에 되짚을 수 있게 한다.
This commit is contained in:
EJClaw
2026-09-28 18:17:46 +09:00
parent 7f678fba7b
commit bec5e285bd

View File

@@ -473,13 +473,19 @@ def _terminate_tree(proc: subprocess.Popen) -> None:
child.kill() child.kill()
def _launch_and_wait_for_window(exe: Path, appdata: Path, timeout_s: int = 90) -> str: def _launch_and_wait_for_window(
"""exe 를 띄우고 **실제 창이 뜰 때까지** 기다린다. exe: Path, appdata: Path, require: str, timeout_s: int = 90
) -> str:
"""exe 를 띄우고 **기다리던 창이 뜰 때까지** 기다린다.
예전에는 "8초 뒤에도 프로세스가 살아 있으면 통과" 였다. 그런데 진입 예전에는 "8초 뒤에도 프로세스가 살아 있으면 통과" 였다. 그런데 진입
스크립트가 터지면 PyInstaller 가 오류창을 띄우고 그대로 서 있는다 — 스크립트가 터지면 PyInstaller 가 오류창을 띄우고 그대로 서 있는다 —
프로세스는 멀쩡히 살아 있으므로 통과로 보고됐다. 실제로 그 상태의 exe 가 프로세스는 멀쩡히 살아 있으므로 통과로 보고됐다. 실제로 그 상태의 exe 가
릴리스까지 나갔다. 살아 있다는 것은 떴다는 뜻이 아니다. 창을 봐야 한다. 릴리스까지 나갔다. 살아 있다는 것은 떴다는 뜻이 아니다.
"아무 창이나 뜨면 통과" 도 부족하다. 자막 오버레이는 제목이 앱 이름
그대로라, 본 화면이 안 떠도 그게 잡혀 통과해 버린다. 그래서 어떤 창을
기다리는지(`require`)를 호출하는 쪽이 지정한다.
""" """
env = dict(os.environ) env = dict(os.environ)
env["APPDATA"] = str(appdata) env["APPDATA"] = str(appdata)
@@ -493,14 +499,14 @@ def _launch_and_wait_for_window(exe: Path, appdata: Path, timeout_s: int = 90) -
err = (proc.stderr.read() or b"").decode("utf-8", "replace")[-800:] err = (proc.stderr.read() or b"").decode("utf-8", "replace")[-800:]
raise RuntimeError(f"창이 뜨기 전에 종료 (코드 {proc.returncode}): {err}") raise RuntimeError(f"창이 뜨기 전에 종료 (코드 {proc.returncode}): {err}")
for title in _visible_windows(_process_tree(proc.pid)): titles = _visible_windows(_process_tree(proc.pid))
low = title.lower() for title in titles:
if any(marker in low for marker in _CRASH_MARKERS): if any(marker in title.lower() for marker in _CRASH_MARKERS):
raise RuntimeError(f"오류창이 떴습니다: {title!r}") raise RuntimeError(f"오류창이 떴습니다: {title!r}")
if "livesub" in low: if any(require in title for title in titles):
return title return " / ".join(titles)
time.sleep(1) time.sleep(1)
raise RuntimeError(f"{timeout_s}초 안에 창이 뜨지 않았습니다") raise RuntimeError(f"{timeout_s}초 안에 {require!r} 창이 뜨지 않았습니다")
finally: finally:
_terminate_tree(proc) _terminate_tree(proc)
@@ -536,8 +542,8 @@ def run_exe_check(exe: Path) -> None:
def _first_run(): def _first_run():
# APPDATA 를 빈 폴더로 돌려 "처음 켜는 사람" 을 그대로 재현한다. # APPDATA 를 빈 폴더로 돌려 "처음 켜는 사람" 을 그대로 재현한다.
with _scratch_appdata() as tmp: with _scratch_appdata() as tmp:
title = _launch_and_wait_for_window(exe, appdata=tmp) titles = _launch_and_wait_for_window(exe, appdata=tmp, require="저장 위치")
return f"창 제목 {title!r}" return f"창 {titles!r}"
@check("exe 두 번째 실행 — 본 화면") @check("exe 두 번째 실행 — 본 화면")
def _second_run(): def _second_run():
@@ -555,10 +561,14 @@ 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=tmp) # 자막 오버레이는 제목이 앱 이름뿐이라 그것만으로는 본 화면이 떴다는
if "저장 위치" in title: # 증거가 못 된다. 본 화면 제목에만 있는 표어를 기다린다.
raise RuntimeError(f"위치를 이미 골랐는데 또 물어봤습니다: {title!r}") from livesub.constants import APP_SLOGAN
return f"창 제목 {title!r}"
titles = _launch_and_wait_for_window(exe, appdata=tmp, require=APP_SLOGAN)
if any("저장 위치" in t for t in titles.split(" / ")):
raise RuntimeError(f"위치를 이미 골랐는데 또 물어봤습니다: {titles!r}")
return f"창 {titles!r}"
def main() -> int: def main() -> int: