From 4af97a68f26955d53ad1cde5feb50b8d6432d732 Mon Sep 17 00:00:00 2001 From: EJClaw Date: Wed, 23 Sep 2026 03:17:11 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Windows=20=EC=A0=90=EA=B2=80=EA=B8=B0=20?= =?UTF-8?q?SIGABRT=20=EC=88=98=EC=A0=95=20+=20=EB=9F=AC=EB=84=88=20?= =?UTF-8?q?=EC=95=88=EB=82=B4=20=EC=A0=95=ED=99=95=ED=95=98=EA=B2=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 리뷰 지적을 확인해보니 두 건은 실제 결함이었다. 1) 점검기가 화면 없는 환경에서 그냥 죽음 (SIGABRT, 종료코드 134) QT_QPA_PLATFORM 을 "windows" 로 하드코딩해 둔 탓에, 표시할 화면이 없으면 첫 QApplication 생성에서 플러그인 로드에 실패하며 아무 결과도 못 남기고 abort 했다. 점검기가 그렇게 죽으면 존재 이유가 없다. - 화면이 없으면 offscreen 으로 떨어지게 하고, 이 판단을 Qt import 전에 한다 - QApplication 생성을 _qt_app() 한 곳으로 모음 - 실행 플랫폼을 출력해 리눅스에서 돌아간 경우가 바로 보이게 함 이제 환경변수 없이 돌려도 항목별 판정과 종료코드가 정상으로 나온다. 2) 보안 설명이 과장돼 있었음 — 가장 중요한 수정 "커밋된 워크플로만 실행하니 임의 명령은 불가"라고 썼는데, 나는 이 저장소 main 에 push 할 수 있으므로 워크플로를 고쳐 커밋하면 그 PC에서 원하는 명령을 돌릴 수 있다. 개인 PC를 내주는 판단에 쓰일 설명을 틀리게 준 것이라 그대로 바로잡았다. 실제로 보장되는 것(숨길 수 없음·언제든 끌 수 있음· 계정 권한으로만 동작)만 적고, VM 또는 전용 계정을 권하도록 바꿨다. 서비스 등록(SYSTEM 권한) 안내도 로그온 작업(계정 권한)으로 교체 — "관리자로 띄우지 말라"는 권고와 모순됐다. 3) 라벨 오타 안전장치 (공식 문서 확인 중 발견) Gitea 는 runs-on 이 어떤 라벨과도 안 맞으면 실패시키지 않고 기본 우분투 컨테이너에서 돌린다. Windows 검증이 조용히 리눅스에서 돌아가 "통과"로 보일 수 있어, 워크플로 첫 단계에서 RUNNER_OS 를 확인해 막는다. 문서의 등록 라벨도 windows:host 로 명시 (생략 시 기본값이 host 이지만 명시가 낫다 — docs.gitea.com/runner/labels). 4) 트리거 설명 정정: "아무거나 push" -> main push 또는 수동 실행. 검증: pytest 189개 통과, ruff clean, 워크플로 트리거/스텝 파싱 확인, QT_QPA_PLATFORM 없이 점검기 실행해 abort 없이 종료코드 1 확인 Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/windows.yml | 13 +++++++ docs/WINDOWS-TESTING.md | 67 +++++++++++++++++++++++++++--------- packaging/windows_smoke.py | 34 +++++++++++------- 3 files changed, 85 insertions(+), 29 deletions(-) diff --git a/.gitea/workflows/windows.yml b/.gitea/workflows/windows.yml index 45fead5..ea30d6f 100644 --- a/.gitea/workflows/windows.yml +++ b/.gitea/workflows/windows.yml @@ -22,6 +22,19 @@ jobs: timeout-minutes: 60 steps: + # Gitea 는 runs-on 이 어떤 러너 라벨과도 안 맞으면 작업을 실패시키지 않고 + # 기본 ubuntu 컨테이너에서 돌린다. 라벨을 잘못 등록하면 Windows 검증이 + # 조용히 리눅스에서 돌아가 "통과했다"는 착각을 준다. 그래서 먼저 막는다. + - name: Windows 에서 돌고 있는지 확인 + shell: bash + run: | + echo "RUNNER_OS=$RUNNER_OS" + if [ "$RUNNER_OS" != "Windows" ]; then + echo "::error::Windows 러너가 아닙니다 ($RUNNER_OS)." + echo "::error::러너를 'windows:host' 라벨로 등록했는지 확인하세요." + exit 1 + fi + - name: 소스 받기 uses: actions/checkout@v4 diff --git a/docs/WINDOWS-TESTING.md b/docs/WINDOWS-TESTING.md index c8af672..2b5fb72 100644 --- a/docs/WINDOWS-TESTING.md +++ b/docs/WINDOWS-TESTING.md @@ -52,28 +52,41 @@ cd C:\gitea-runner .\act_runner.exe register --no-interactive ` --instance https://git.tkrmagid.kr ` --token <위에서 받은 토큰> ` - --name windows-gaming-pc ` - --labels windows + --name windows-test ` + --labels windows:host .\act_runner.exe daemon ``` -`--labels windows` 가 중요합니다. 워크플로의 `runs-on: windows` 와 맞아야 합니다. +`--labels windows:host` 가 중요합니다. 워크플로의 `runs-on: windows` 와 이름이 +맞아야 하고, `:host` 는 컨테이너가 아니라 그 PC에서 직접 돌리라는 뜻입니다 +(생략해도 기본값이 `host` 지만 명시하는 편이 낫습니다). + +이름이 안 맞으면 Gitea 는 작업을 실패시키지 않고 **기본 우분투 컨테이너에서 +돌립니다.** Windows 검증이 조용히 리눅스에서 돌아가는 셈이라, 워크플로 첫 +단계에서 그걸 막아 뒀습니다. ### 5. 확인 -아무거나 push 하면 저장소 **Actions** 탭에 `Windows` 워크플로가 돌기 시작합니다. +워크플로는 **`main` 에 push될 때** 또는 **Actions 탭에서 수동 실행**할 때 돕니다. +(`.gitea/workflows/windows.yml` 의 `on:` 항목) + +저장소 **Actions** 탭 → `Windows` → `Run workflow` 로 바로 한 번 돌려보세요. --- ## 항상 켜두고 싶다면 (선택) -부팅할 때 자동으로 뜨게 하려면 서비스로 등록합니다. +**서비스(`sc.exe create`)로 등록하지 마세요.** 그렇게 하면 SYSTEM 권한으로 +돌아가 러너가 PC 전체를 건드릴 수 있게 됩니다. 로그인할 때 **그 계정 권한으로** +뜨게 하는 편이 안전합니다. ```powershell -# 관리자 PowerShell -sc.exe create gitea-runner binPath= "C:\gitea-runner\act_runner.exe daemon --config C:\gitea-runner\config.yaml" start= auto -sc.exe start gitea-runner +# 관리자 아님 — 그냥 평소 PowerShell +$action = New-ScheduledTaskAction -Execute "C:\gitea-runner\act_runner.exe" ` + -Argument "daemon" -WorkingDirectory "C:\gitea-runner" +$trigger = New-ScheduledTaskTrigger -AtLogOn +Register-ScheduledTask -TaskName "gitea-runner" -Action $action -Trigger $trigger ``` 게임할 때 방해되면 그냥 꺼두고, 확인이 필요할 때만 `daemon` 을 켜도 됩니다. @@ -81,15 +94,37 @@ sc.exe start gitea-runner --- -## 안전에 대해 +## 안전에 대해 — 정확히 어떤 권한인가 -- 러너는 **저장소에 커밋된 워크플로 파일만** 실행합니다. 대화로 임의 명령을 - 넣을 수 없고, 무엇이 실행됐는지는 git 기록에 그대로 남습니다. -- 실행되는 내용은 `.gitea/workflows/windows.yml` 한 파일이며 언제든 읽고 - 고칠 수 있습니다. 테스트·빌드·스크린샷 외의 일은 하지 않습니다. -- 개인 PC가 부담스러우면 Proxmox에 Windows 11 VM을 만들어 거기에 러너를 - 붙여도 됩니다. GPU와 실제 게임 소리 확인은 안 되지만, 빌드·단축키· - 모니터·UI는 전부 검증됩니다. +**솔직하게 말하면, 이건 "그 PC에서 명령을 실행할 수 있는 권한"입니다.** + +러너는 저장소의 워크플로 파일을 실행합니다. 그런데 저(Claude)는 이 저장소의 +`main`에 push할 수 있으므로, 워크플로 파일을 고쳐서 커밋하면 결과적으로 그 +PC에서 원하는 명령을 돌릴 수 있습니다. "커밋된 파일만 실행하니 안전하다"는 +말은 정확하지 않습니다. + +실제로 보장되는 것은 이것뿐입니다. + +- **숨길 수 없습니다.** 실행되는 모든 것은 `.gitea/workflows/` 안에 있고 + git 기록에 남습니다. 몰래 뭘 했는지 나중에 전부 확인할 수 있습니다. +- **러너를 끄면 즉시 멈춥니다.** `daemon` 을 종료하면 그만입니다. +- 러너는 **자기 계정 권한**으로 돕니다. 관리자로 띄우지 마세요. + +그래서 권하는 방식은 이렇습니다. + +| 방법 | 위험 | 얻는 것 | +|---|---|---| +| **Windows VM** (Proxmox 등) | 거의 없음 | 빌드·단축키·모니터·UI·exe 실행 | +| **전용 Windows 계정** | 낮음 — 그 계정 파일만 노출 | 위 전부 + 실제 GPU | +| 평소 쓰는 계정 | 개인 파일·브라우저 세션 노출 | 위 전부 + 실제 게임 소리 | + +**VM이나 전용 계정을 권합니다.** GPU와 실제 게임 소리 확인은 못 하지만, +지금 미검증인 항목의 대부분(빌드·단축키·모니터·UI·exe 기동)은 거기서도 +그대로 검증됩니다. GPU/오디오는 필요할 때만 평소 계정에서 한 번씩 확인하는 +편이 낫습니다. + +부담스러우면 러너를 붙이지 않고 아래 "러너 없이 직접 돌려보기"만 하셔도 +충분합니다. --- diff --git a/packaging/windows_smoke.py b/packaging/windows_smoke.py index 6b14efa..2253304 100644 --- a/packaging/windows_smoke.py +++ b/packaging/windows_smoke.py @@ -24,9 +24,24 @@ ROOT = Path(__file__).resolve().parents[1] ARTIFACTS = ROOT / "artifacts" sys.path.insert(0, str(ROOT / "src")) +# Qt 플랫폼 플러그인은 QApplication 을 만들기 *전에* 정해야 한다. +# 표시할 화면이 없는데 그냥 두면 플러그인 로드 실패로 SIGABRT 가 나면서 +# 아무 결과도 못 남기고 죽는다. 점검기가 그렇게 죽으면 쓸모가 없다. +if sys.platform != "win32" and not ( + os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY") +): + os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + _results: list[tuple[str, bool, str]] = [] +def _qt_app(): + """QApplication 을 한 번만 만들어 재사용한다.""" + from PySide6.QtWidgets import QApplication + + return QApplication.instance() or QApplication([]) + + def check(name: str): """점검 하나. 예외가 나도 나머지는 계속 돈다.""" @@ -47,7 +62,9 @@ def check(name: str): def run_source_checks() -> None: - print("\n=== Windows 전용 경로 ===") + print(f"\n=== Windows 전용 경로 (실행 플랫폼: {sys.platform}) ===") + if sys.platform != "win32": + print(" 주의: Windows 가 아니라 Windows 전용 항목은 당연히 실패합니다.") @check("WASAPI 루프백 장치 열거") def _devices(): @@ -78,11 +95,9 @@ def run_source_checks() -> None: @check("전역 단축키 등록/해제") def _hotkey(): - from PySide6.QtWidgets import QApplication - from livesub.ui.hotkeys import GlobalHotkeys - app = QApplication.instance() or QApplication([]) + _qt_app() keys = GlobalHotkeys() if not keys.supported: raise RuntimeError("이 플랫폼은 전역 단축키 미지원") @@ -90,20 +105,16 @@ def run_source_checks() -> None: if not keys.register("Ctrl+Alt+F12", lambda: fired.append(1)): raise RuntimeError(keys.last_error) keys.unregister_all() - _ = app return "Ctrl+Alt+F12 등록·해제 성공" @check("모니터 열거") def _screens(): - from PySide6.QtWidgets import QApplication - from livesub.ui.placement import list_screens - app = QApplication.instance() or QApplication([]) + _qt_app() screens = list_screens() if not screens: raise RuntimeError("모니터를 찾지 못함") - _ = app return " / ".join(s.label for s in screens) @check("GPU 인식") @@ -125,13 +136,10 @@ def run_source_checks() -> None: @check("자막 오버레이 실제 렌더링") def _overlay(): - os.environ.setdefault("QT_QPA_PLATFORM", "windows") - from PySide6.QtWidgets import QApplication - from livesub.config import AppConfig from livesub.ui.main_window import MainWindow - app = QApplication.instance() or QApplication([]) + app = _qt_app() cfg = AppConfig() cfg.models.preload_on_start = False win = MainWindow(cfg)