# 변경 이력 [English](../CHANGELOG.md) | **한국어** 이 프로젝트의 주요 변경 사항은 이 문서에 기록됩니다. 형식은 [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)를 기반으로 하며, 버전 정책은 [Semantic Versioning](https://semver.org/lang/ko/)을 지향합니다. ## [Unreleased] ### Fixed - **non-purge `uninstall.sh`가 Windows VM 디스크를 삭제하지 않도록 수정** (#716, @munir-abbasi 데이터 손실 리포트 감사). 기본 VM 저장소가 앱 데이터 디렉토리 하위에 있는데(`~/.local/share/winpodx/storage/data.img`), "앱 정의 제거" 단계가 `rm -rf ~/.local/share/winpodx`를 그냥 실행해서 — VM 데이터를 *보존*한다고 문서화된 non-purge 언인스톨이, 특히 무인 실행되는 패키지 매니저 `postrm` 재진입 경로에서 `storage/` 아래 Windows VM을 통째로 지웠습니다. 이제 두 경로를 정규화해서, 저장소 디렉토리가 `DATA_DIR` 자신이거나 그 하위이고 `--purge`가 **없으면** 저장소 서브트리를 보존하고 나머지 앱 데이터 항목만 삭제합니다. 앱 데이터 디렉토리 밖의 커스텀 `WINPODX_STORAGE_PATH`는 기존대로 손대지 않으며, `--purge`는 여전히 전부 삭제합니다. 언인스톨 스모크 하네스로 회귀 테스트(중첩 저장소·외부 저장소·purge 케이스). ### Added - **Windows의 "Linux Apps" 바로가기에서 Linux 앱을 파일 없이 직접 실행 가능** (#616, @notnotno 기여 감사). Windows 시작 메뉴/바탕화면 "Linux Apps" 폴더의 reverse-open shim은 주로 "연결 프로그램" 선택 항목인데, 파일 없이 직접 클릭하면 게스트 shim이 파일 인자를 요구해서 조용히 종료돼 아무 일도 안 일어났습니다. 이제 launch-only 요청(`origin: "launch"`, 빈 경로)을 보내 호스트가 파일 없이 Linux 앱을 실행하므로(`%f`/`%u` placeholder를 채우지 않고 제거), 그 바로가기가 일반 앱 런처로도 동작합니다. 갱신된 게스트 shim 필요(다음 `winpodx guest sync` / `apply-fixes` 때 재프로비저닝). ### Fixed - **최소 데스크톱에서 GUI의 `libxcb-cursor0` 의존성을 자동 설치** (#712, @numericOverflow 기여 감사). Qt 6.5+ (PySide6)는 `libxcb-cursor.so.0` 없이는 `xcb` 플랫폼 플러그인을 시작하지 못하는데("could not load the Qt platform plugin 'xcb'"), 새 최소 설치(예: Linux Mint 22)에선 전이 의존성으로 딸려오지 않습니다. 이제 `install.sh`가 GUI가 켜져 있고 런타임 lib가 없을 때 이를 설치합니다(배포판 매핑: Debian/Ubuntu/openSUSE는 `libxcb-cursor0`, Fedora/Arch는 `xcb-util-cursor`). 존재 여부는 `ldconfig -p`가 아니라 표준 lib 디렉토리의 실제 `.so` 파일로 감지합니다 — openSUSE에선 라이브러리가 `/usr/lib64`에 있지만 ld.so 캐시엔 없어서, `ldconfig` 프로브가 패키지 설치 후에도 매번 다시 프롬프트를 띄웠습니다. ## [0.9.0] - 2026-07-11 ### Added - **Windows 앱을 Linux에서 URL-scheme 핸들러로 등록 가능** (#421, #694 — 호스트측 기반). 앱이 URL 스킴(`mailto`, `https`, `slack`, `vnc`, …)을 가지면 `.desktop`에 `x-scheme-handler/` 항목이 붙고 `Exec`가 URL을 받아, 호스트에서 그런 링크를 클릭하면 Windows 앱에서 열립니다 — URL은 `$HOME` 파일 경로로 매핑하지 않고 앱에 그대로 전달됩니다. 공유 정책 모듈(denylist + 엄격한 스킴 정규식 + `/app`-cmd sanitizer)이 위험 스킴(`file:`, `javascript:`, `data:`, …)을 차단하고 등록·실행 양쪽에서 커맨드라인 인젝션을 무력화합니다. discovery가 이제 게스트에서 각 앱의 스킴을 자동 수확하므로(스킴별 `UrlAssociations` 기본값, 앱별 `Capabilities\URLAssociations`, UWP 매니페스트의 `windows.protocol`), `mailto`/`https` 등을 처리하는 앱이 수동 `app.toml` 편집 없이 자동 등록됩니다. ### Changed - **선택적 Windows 디블로트가 더 많은 광고/텔레메트리를 끕니다** (#669, #684, @GameSoul7Eugene 기여 감사). 광고·제안 옵트아웃을 넓히고(광고 ID, 잠금화면 "팁", 시작/설정/검색 제안, 소비자 앱 무음 자동설치), SQM/CEIP 텔레메트리 예약 작업 비활성화를 추가했으며, RDP 친화적 시각 효과 조정을 적용했습니다 — 모두 보수적·되돌릴 수 있는 범위로 한정. ### Fixed - **discovery 로 발견된 Windows 앱이 호스트의 `http`/`https` 기본 핸들러를 조용히 탈취할 수 없게 함** (보안 강화). URL 스킴 핸들러 등록이 선언된 모든 스킴에 대해 `xdg-mime default` 를 실행해서, (반신뢰) 게스트에서 발견된 앱이 `app install --mime` 나 `doctor --fix` 를 통해 `http`/`https` 의 시스템 기본이 되어 사용자가 클릭하는 모든 웹 링크(세션 토큰 포함)를 받을 수 있었습니다. 이제 이 두 범용 웹 스킴은 자동 기본 획득에서 제외됩니다: Windows 브라우저는 여전히 URL 대상(`app run https://…`)으로 동작하고 "Open with" 목록에도 남지만, 웹 기본값으로 삼는 것은 명시적 옵트인이 됐습니다. `mailto` 와 벤더 스킴은 영향 없음. - **세션 창-reaper 가 방금 재실행된 앱을 오살하거나, 일시적 스캔 실패에 전 세션을 몰살하지 않도록 수정** (#680 후속). 문서를 닫고 바로 다시 열면 새 세션을 SIGTERM 할 수 있었고(리퍼가 앱을 이름으로 추적하는데 재실행이 프로세스 마커를 덮어씀), 이제 각 reap 을 무장 시점의 정확한 PID 에 묶습니다. 또 `wmctrl` 비정상 종료(윈도우 매니저 일시 사용중)를 "창 없음"으로 읽어 두 번 연속 실패 시 실행 중인 앱을 전부 죽였는데, 실패한 스캔을 이제 창 0개가 아니라 실패로 취급합니다. - **설치 원라이너가 `git` 이 없으면 중단되지 않고 자동 설치하도록 수정** (#705, @numericOverflow 기여 감사). `git` 이 없는 새 시스템(예: Linux Mint 22)에서 `curl … | bash` 설치가 clone 단계에서 "git is required for remote install" 로 실패했습니다 — 다른 의존성은 다 자동 설치하면서요. 이제 나머지와 같은 패키지 매니저 경로로 `git` 을 설치하고, 패키지 매니저가 제공하지 못할 때만 (수동 설치 안내와 함께) 오류를 냅니다. - **kio-fuse 가 실제로 설치돼 있는데도 "미설치"로 표시되던 문제 수정** (#697, @twkirk161 기여 감사). Info/헬스 체크가 `kio-fuse` 바이너리를 고정된 경로 몇 개에서만 찾아서, 다른 곳에 두는 배포판 — Fedora Kinoite / KF6 버전 libexec, 또는 Debian multiarch `/usr/lib//libexec/kio-fuse`(3단계 깊이라 기존 2단계 glob 이 놓침) — 에선 reverse-open 게스트 디스크 마운트(#616)가 잘 되는데도 미설치로 읽혔습니다. 이제 multiarch 경로와, 마운트가 실제로 호출하는 authoritative 신호인 `org.kde.KIOFuse` D-Bus 활성화 서비스까지 확인합니다. - **reverse-open listener 가 이제 트레이와 함께 자동 시작되고, 트레이 Quit 이 명시적으로 중지함** (#691, @notnotno 기여 감사). listener 는 `pod start` 와 앱 실행 경로에서만 (재)시작돼서, `winpodx gui` 만 켜거나 로그인 autostart 로 트레이만 뜨면 기능을 켜놔도 Windows→Linux "Open with" 가 조용히 죽어 있었습니다. 그리고 Quit 은 *우연히만* listener 를 죽였습니다: listener 는 double-fork 데몬이라 부모의 커맨드라인을 상속하는데, GUI 설정 패널에서 시작한 경우에만 Quit 의 `pkill …gui` 패턴에 걸렸습니다. 이제 트레이가 시작 시 idempotent listener self-heal 을 (UI 스레드 밖에서) 실행하고, Quit 은 `stop_listener()` 를 명시적으로 호출합니다. - **빠른 절전/복구 감지가 이제 실제로 systemd-logind 를 구독함** (#690, @notnotno 기여 감사). 트레이의 `PrepareForSleep` D-Bus 구독이 `QDBusConnection.connect`에 맨 파이썬 함수를 넘겼는데 PySide6가 런타임에 거부(`called with wrong argument types`) — 그래서 복구-후 빠른 pod 새로고침(#225)이 한 번도 작동하지 않고 모든 설치가 조용히 30초 폴링으로 폴백하며 `winpodx gui`/트레이 시작마다 경고를 남겼습니다. 이제 QObject 리시버 + `SLOT("onPrepareForSleep(bool)")` 시그니처로 구독하며(가비지 컬렉션에 연결이 끊기지 않게 트레이 아이콘에 보관), `org.freedesktop.login1` 대상 라이브 검증 완료. - **창을 닫아도 남아있는 Windows 앱이 더 이상 영원히 "RUNNING"으로 표시되지 않음** (#680, @mdshahalam3 기여 감사). FreeRDP RemoteApp 클라이언트는 게스트가 연결을 끊어야 종료되는데, Office는 문서 창을 닫아도 프로세스(와 RemoteApp 세션)를 남겨둬서 세션이 끝나지 않고 앱이 영원히 RUNNING으로 뜨며 다음 열기를 깨뜨리는 반쯤-멈춘 `\\tsclient\home` 리다이렉트를 남겼습니다. 이제 시스템 트레이 헬퍼가 모든 세션의 RAIL 창을 감시해, 앱의 창이 전부 닫히면(짧은 디바운스 후) 멈춘 세션을 종료해 running 목록에서 사라지게 합니다. 즉시 종료되는 `winpodx app run` 프로세스가 아니라 **장수하는 트레이**에서 돌기 때문에 메뉴/CLI 실행에서도 작동합니다. best-effort(`wmctrl` 있는 X11/XWayland); 창이 실제로 보인 세션만 reap하므로 실행 중이거나 창 없는 앱을 조기에 죽이지 않습니다. - **RemoteApp 실행마다 뜨던 FreeRDP의 무해한 `[get_next_comma]: Invalid quoted argument` 경고 침묵** (#680, @mdshahalam3 기여 감사). 공백 포함 파일 경로가 쪼개지지 않게 유지하는 `cmd:""` 따옴표 형식에서 나온 경고입니다; 이제 FreeRDP 서브프로세스가 `WLOG_FILTER=com.winpr.commandline:FATAL`로 실행돼 전달 커맨드라인은 그대로 둔 채 그 WLog 태그만 음소거합니다(`/log-filters` 플래그는 파서보다 늦게 파싱돼 자기 경고를 못 막음). - **Office 2016(MSI) 앱들이 더 이상 "Microsoft Office 2016 component" 하나의 깨진 엔트리로 뭉치지 않음** (#680 regression, @mdshahalam3 기여 감사). 0.8.0의 Start-Menu-only discovery가 각 바로가기의 raw `.TargetPath`를 읽었는데, MSI 설치는 *advertised shortcut*(Windows Installer "Darwin descriptor")를 써서 그 타깃이 `C:\WINDOWS\Installer\{ProductCode}\` 아래 제품별 아이콘 스텁(예: `xlicons.exe`)으로 해석됩니다. 그 스텁들은 파일 설명 "Microsoft Office 2016 component"를 공유해 Word/Excel/PowerPoint/Outlook이 전부 동일해 보였고 dedup으로 실행 불가 엔트리 하나로 뭉쳐 `winpodx app list`에서 사라졌습니다. 이제 advertised shortcut은 타깃 기록 전에 `MsiGetShortcutTarget` + `MsiGetComponentPath`로 실제 설치 실행파일로 해석됩니다; 일반(Click-to-Run/직접) 바로가기는 영향 없음. - **이미 실행 중인 Windows 앱에 "연결 프로그램으로 열기"가 이제 새 창으로 안정적으로 파일을 엶** (#675 @ajeshchrist, #680 @mdshahalam3 기여 감사). 예전엔 조용히 아무것도 안 하거나(실행-중 경로가 모든 에러를 삼킴), 게스트 agent로 전달한 경우 "Access denied", 또는 ~30초 멈춘 뒤 crash처럼 재실행됐습니다. 원인: 게스트가 멀티세션 RDP라 *이미 보이는* 앱에 파일을 넘기려는 시도가 **다른 세션**에 착지합니다(agent는 아예 autologon 콘솔 세션에서 돌아 세션별 `\\tsclient\home` 리다이렉트가 없음). 그래서 winpodx는 더 이상 "warm" 전달을 시도하지 않고 — 실행 중 앱에 파일 열기는 **파일을 실은 새 RemoteApp 창**으로 곧장 가며(자격증명 접속이 세션 잠금도 해제/재로그온). 공유 `$HOME`/미디어 밖의 파일은 사라지지 않고 눈에 보이는 에러를 냅니다. 조용한-실패 표면도 강화: `.desktop`의 `Exec`가 `%f`, `notify-send`·`file://` URI를 절대경로로 해석해 stripped-PATH 실행에서도 살아남습니다. - **게스트 잠금화면 체크가 세션-스코프로 바뀌고, cold 실행이 느린 게스트를 더 오래 대기** (#680/#675, @mdshahalam3·@ajeshchrist 기여 감사). winpodx는 RemoteApp 창을 만들기 전 게스트 데스크톱이 interactive인지 기다리는데, 체크가 머신전역 `Get-Process LogonUI`라 *다른* 세션(콘솔이나 멈춘 disconnected RAIL 세션)의 잠금화면이 앱 세션을 잠긴 걸로 오탐해 full timeout까지 대기시켰습니다. 이제 LogonUI/explorer를 세션 ID로 매칭합니다. cold 실행도 느린 게스트의 자동로그온 완료를 위해 최대 45초(기존 20초) 대기해 앱 위에 stale 로그온 화면을 그리지 않습니다. ### Contributors 이번 릴리스를 이끈 리포트와 기여에 감사드립니다 — @notnotno (#694, #691, #690), @twkirk161 (#697), @mdshahalam3 (#680), @ajeshchrist (#675), @numericOverflow (#705), @GameSoul7Eugene (#669, #684). ## [0.8.0] - 2026-06-30 ### Added - **Windows 앱이 Linux 메뉴에서 시작 메뉴 폴더별로 그룹화됩니다** (#581, @Milliw 기여 감사). 각 앱이 속한 시작 메뉴 하위 폴더(예: `Microsoft Office\Tools`)를 "winpodx" 메뉴 폴더 아래 중첩 하위 그룹으로 미러 — Windows에서 보이는 그대로. KDE Plasma·XFCE·Cinnamon·MATE·LXQt에서 렌더(freedesktop `.menu` 메커니즘); GNOME은 앱이 보이긴 하나 그룹화 안 됨(오버뷰가 평면 그리드). 최상위 앱·폴더 없는 앱은 "winpodx" 바로 아래에 위치. - **`[pod] keyboard` 설정이 이제 FreeRDP 세션 키보드 레이아웃에 반영됨** (#660, @lsvab 기여 감사). Windows 설치용으로 고른 로케일(예: `keyboard = "hu-HU"`)이 대응하는 Windows 레이아웃으로 매핑되어 FreeRDP에 `/kbd:layout:0x…`로 전달됩니다. 비-US 키보드가 `rdp.extra_flags`를 손으로 안 써도 RemoteApp 창에서 동작합니다. 기본값 `en-US`는 그대로 둬서(FreeRDP가 호스트 XKB 레이아웃 자동감지 유지 — 설정을 안 건드린 유저가 US로 강제되지 않음), `rdp.extra_flags`에 명시한 `/kbd`가 항상 우선하고, 매핑에 없는 로케일은 자동감지로 폴백합니다. (`rdp.extra_flags`로 `/kbd`를 직접 넘기는 건 0.7.4에서 이미 허용됨.) ### Changed - **앱 검출이 기본적으로 Windows 시작 메뉴에 실제로 뜨는 앱만 노출합니다** (#581, @Milliw 기여 감사). 기존에는 등록된 모든 실행 파일(레지스트리 App Paths, Chocolatey/Scoop shim, 모든 UWP 패키지)을 긁어 Linux 메뉴에 쏟아부어 언인스톨러·헬퍼·백그라운드 프로세스로 범람했습니다. 이제 기본값은 시작 메뉴 전용입니다: 시작 메뉴 바로가기 + 시작 메뉴에 보이는 UWP 앱(`Get-StartApps`와 교차) + OS 필수앱(파일 탐색기/계산기/설정). 옛 동작은 **`winpodx config set desktop.full_app_scan true`** 또는 설정 → "설치된 모든 앱 검출(시작 메뉴 외 포함)" 체크박스로 되돌립니다 — 시작 메뉴 항목이 없는 포터블 앱에 유용. 다음 `winpodx app refresh`부터 적용. - **번들 rdprrap 0.1.3 → 0.3.0.** rdprrap(각 RemoteApp 창에 독립 세션을 주는 멀티세션 RDP wrapper)이 `termsrv.dll` 패치 지점을 **동적으로** 도출합니다 — 하드코딩된 struct 오프셋/레지스터/바이트 템플릿 대신 런타임에 각 타겟 함수를 디스어셈블해 패치 바이트를 인코딩. Windows 빌드별 `termsrv.dll` 구조 변화에도 멀티세션이 유지됩니다. OEM 버전 27 → 28이라 기존 설치는 다음 `winpodx guest sync` / `apply-fixes` 때 반영. ### Fixed - **앱 이름이 길거나 CJK일 때, 또는 앱 목록이 비었을 때 GUI가 레이아웃 재귀로 죽던(SIGSEGV) 문제 수정** (#553, @hermitguo 기여 감사). 리사이즈되는 앱 목록 스크롤 영역 안의 word-wrap `QLabel`(빈 상태 패널, 런처 타일)이 wrap 폭을 viewport에 맞춰 따라가서, 높이가 폭으로 되먹임돼 Qt 6.11에서 `QBoxLayout::setGeometry` → `heightForWidth`를 무한 재진입했습니다 — 시작 시(빈 목록) 또는 CJK 이름 앱 추가 직후 하드 세그폴트. 해당 라벨들에 고정 wrap 폭을 줘서 되먹임 루프를 끊었습니다. - **Debloat / 유지보수 작업 창이 작업 완료 시 스스로 닫힘** (#550, @ismikes 기여 감사). 진행 `BusyDialog`를 워커 내부에서 `QTimer.singleShot(0, dlg.finish)`로 닫았는데, 맨 `threading.Thread`엔 Qt 이벤트 루프가 없어 그 타이머가 영영 안 떠서 창이 손으로 닫을 때까지 떠 있었습니다(v0.7.2 변경은 *워커 시작 시점*만 고쳤고 닫기는 아님). `BusyDialog.finish()`가 이제 시그널 기반이라 스레드 간 닫기가 `accept()`를 GUI 스레드에 큐잉해서 완료 시 창이 닫힙니다(Debloat, Grow Disk, Sync Guest, Apply Fixes). - **GUI "Refresh Apps"가 완료 시점에 죽던(SIGSEGV) 문제 수정.** 디스커버리 워커(parent 없는 파이썬 소유 `QObject`)에 삭제 경로가 2개였습니다 — 워커 스레드의 `deleteLater` *와* 메인 스레드의 파이썬 ref-drop. Qt6는 워커의 `deleteLater` flush *전에* `QThread.finished`를 emit하므로, cleanup 슬롯이 그 삭제가 진행 중인데 마지막 파이썬 ref를 떨궈 워커를 스레드 간 double-free(`QObject::~QObject` 세그폴트)했습니다. 이제 삭제 경로가 정확히 하나(ref-drop, `thread.wait()`로 워커 스레드 종료 확인 후)이고, 시작 가드가 살아있는 스레드 ref에도 bail하며(빠른 재클릭이 끝나가는 워커를 떨구지 못하게), 윈도우 close 시 실행 중 워커 스레드를 join합니다. - **백그라운드 워커가 디스플레이 배율을 읽을 때 GUI가 죽던(SIGABRT) 문제 수정.** `QGuiApplication.screens()`는 GUI 메인 스레드 전용인데, 워커 스레드(Info 패널의 `gather_info` 등)에서 닿을 수 있어 `QObject::setParent: ... different thread` 경고를 쏟아내고 워커 스레드 종료 시 `__cxa_pure_virtual`로 앱 전체를 abort시켰습니다 — refresh 시 GUI가 죽는 증상으로 나타남. Qt 배율 프로브가 이제 메인 스레드가 아니면 `None`으로 단락되어 서브프로세스/환경변수 감지로 폴백합니다. - **`winpodx app refresh`가 이제 더 이상 검출 안 되는 앱을 추가만 하지 않고 제거도 함** (#581). 기존 refresh는 항상 *추가만* 해서, 사라진 검출 앱(Windows에서 언인스톨된 앱, 또는 새 시작메뉴-전용 기본값으로 빠진 모든 앱)이 수동 삭제 전까지 Linux 메뉴에 남았습니다. 이제 사라진 discovered 프로필(과 런처)을 prune해서 메뉴가 실제로 현재 집합으로 마이그레이션됩니다. 수동 추가 앱(`~/.local/share/winpodx/apps/`)은 절대 안 건드리고, 실패/빈 스캔은 메뉴를 비우지 않습니다. - **`--win-iso`가 이제 다운로드 대신 실제로 그 ISO에서 설치함** (#647, @ismikes 기여 감사). 로컬 ISO가 `winpodx setup`이 이미 `compose up`을 돌린 *뒤*에 `/custom.iso`로 스테이징돼서, 컨테이너가 이미 부팅되고 dockur가 Microsoft 다운로드를 시작한 다음에야 파일이 생겼습니다(dockur는 부팅 순간 `custom.iso`를 찾음). 이제 스테이징이 **`winpodx setup` 내부**에서 storage 경로 확정 후 **컨테이너 (재)생성 전**에 일어나, dockur가 ISO를 찾아 설치합니다. `winpodx setup --win-iso `로도 노출. - **reverse-open 리스너가 다음 `pod start`까지 죽어있지 않고 앱 실행 시 자가복구됨.** `winpodx pod stop` / 트레이 Quit이 리스너를 멈추는데(`stop_listener()`), pod는 계속 돌고 있으면 watcher를 다시 띄우는 게 없어 Windows의 "연결 프로그램 → Linux 앱"이 조용히 무반응이었습니다. 이제 `ensure_ready`(모든 `winpodx app run` / GUI 실행)가 `reverse_open` 활성 시 리스너를 idempotent하게 보장합니다. v0.7.4 스모크 중 발견. ### Contributors 이번 릴리즈에서 고친 이슈를 제보해 주신 분들께 감사드립니다: @Milliw (#581), @lsvab (#660), @ismikes (#647, #550), @hermitguo (#553), @liveifsh (#659). ## [0.7.4] - 2026-06-23 ### Added - **`install.sh --storage-dir ` (및 `winpodx setup --storage-path`)로 Windows VM 위치 선택** (#646, @realahmed7777 기여 감사). VM 디스크 + ISO를 `~/.local/share/winpodx/storage` 대신 더 넉넉한 파티션에 둡니다 — 디렉터리는 기본값과 동일한 처리로 생성(btrfs면 `chattr +C`로 raw 디스크 단편화 방지, 대상이 비회전 디스크면 SSD 에뮬레이션). 신규 설치 전용; 기존 설치 이전은 `winpodx setup --migrate-storage --migrate-storage-target` 유지. - **24GB+ 호스트의 신규 설치는 VM RAM 기본값이 6 대신 8GB** (#630, @ismikes 기여 감사). Windows 11은 8GB에서 눈에 띄게 부드럽고, ≥24GB 호스트는 이를 감당할 수 있습니다 — `winpodx setup`이 `pod.ram_gb`를 미리 채우는 auto-tier가 해당 호스트에서 mid 티어를 6→8GB로 올립니다(CPU 사이징 불변; ≥32GB/≥12스레드는 여전히 12GB high 티어). 기존 설치는 설정값 유지 — Settings 또는 `winpodx config set pod.ram_gb 8`로 언제든 변경. - **`install.sh --win-iso `로 다운로드 대신 로컬 Windows ISO에서 설치** (#647, @ismikes 기여 감사). 이미 가진 Windows ISO 경로를 넘기면 storage 디렉터리에 dockur의 `custom.iso`로 스테이징돼 ~5-8 GB Microsoft 다운로드를 건너뜁니다 — 반복 purge/reinstall 사이클에 유용. 파일시스템이 지원하면(btrfs/xfs) reflink 복사라 추가 디스크 비용 없음. (직접 storage에 `custom.iso`를 둘 수도 있었지만, 이걸 플래그로 연결하고 `--help`에 문서화함.) ### Fixed - **Windows 비밀번호가 더 이상 로그에 평문으로 기록되지 않음.** "Launching RDP" / "Relaunching RDP" 로그 줄이 `/p:`를 포함한 전체 `xfreerdp` argv를 출력했습니다. 이제 비밀번호 토큰(`/p:`, `/gp:`)을 로깅 전 `/p:***`로 마스킹합니다. (로컬 로그 한정; 기존 문제 — 0.7.4 보안 검토 중 강화.) - **Logs 탭의 Status / Pod logs / Inspect 버튼이 항상 `podman`을 실행하는 대신 선택된 컨테이너 백엔드를 사용함** (#658). Docker 백엔드 설치에서 Terminal 탭 진단 버튼이 `pod.backend`와 무관하게 `podman ps` / `podman logs` / `podman inspect`를 실행해, podman이 활성 백엔드가 아닐 때 실패하거나 엉뚱한 런타임을 조회했습니다. 이제 `cfg.pod.backend`를 따릅니다 — Docker 선택 시 `docker`(`manual`은 `podman`으로 폴백하며, 거기선 컨테이너 명령이 무의미함). 화면의 pod 로그 tail `$ …` 에코도 하드코딩된 `podman` 대신 실제 백엔드를 출력하도록 수정. - **이미 실행 중인 Windows 앱에서 두 번째 문서를 여는 게 이제 동작함(조용히 무시되지 않음)** (#657). 앱이 이미 열려 있을 때(예: Word) 두 번째 파일을 그 앱으로 실행하면 — 두 번째 `gio launch`, 또는 우클릭 → *연결 프로그램* → 해당 앱 — 파일이 버려졌습니다: 라이브 세션은 재사용했지만 경로를 폐기했습니다. 이제 파일이 실행 중인 세션으로 전달됩니다 — `\\tsclient` UNC 경로로 매핑돼 `Start-Process`로 열리며, 게스트 에이전트로 보내되 에이전트가 닿지 않으면 FreeRDP RemoteApp으로 폴백합니다. Best-effort: 실패는 로그만 남기고 실행을 막지 않습니다. - **우클릭 → *연결 프로그램*으로 Windows 앱 실행이 PATH가 잘리는 데스크탑(예: Deepin)에서도 동작** (#657). 생성된 `.desktop` 파일이 bare `Exec=winpodx …`를 써서, 데스크탑 환경이 런처를 `~/.local/bin`을 PATH에서 떨어뜨리는 systemd transient 유닛으로 실행하면(Deepin의 `dde-application-manager`) `exec: winpodx: not found`로 실패했습니다. 터미널에서 `gio launch`는 셸 PATH를 상속받아 동작했기에 버그가 가려졌습니다. 이제 엔트리가 설치 시점에 `shutil.which()`로 해석한 winpodx 절대 경로를 박아 넣습니다(해석 실패 시 bare 이름으로 폴백). - **RDP 비밀번호 누락 시 답할 수 없는 프롬프트 대신 명확한 에러로 실패** (#569, @biskasarchaniotakis 기여 감사). `rdp.password`와 `rdp.askpass`가 둘 다 미설정이면 xfreerdp가 GUI 실행에서 답할 수 없는 대화형 비밀번호 프롬프트로 빠져, 실행이 불투명한 `Inappropriate ioctl for device` / `ERRCONNECT_CONNECT_CANCELLED`로만 실패했습니다. 이제 `build_rdp_command`가 누락된 설정을 짚어 사전에 예외를 던집니다. - **비밀번호 로테이션이 거부된 `net user`를 성공으로 보고하지 않음** (#569). 게스트측 PowerShell이 `net user` 종료 코드와 무관하게 `password set`를 출력해서, 거부된 변경이 저장된 비밀번호와 실제 Windows 비밀번호를 어긋나게 둔 채 rc=0으로 보고됐습니다 — 이는 이후 RDP 인증 실패 / 앱이 안 열림으로 드러납니다. 이제 `$LASTEXITCODE`를 확인하고 실패 시 non-zero로 종료합니다. - **`/kbd`가 허용된 FreeRDP extra 플래그가 됨** (#657). `rdp.extra_flags`로 키보드 레이아웃 오버라이드(예: `/kbd:layout:0x409`)를 넘겨 일부 레이아웃의 FreeRDP keycode 스캐닝 경고를 우회할 수 있습니다. - **Debian/Ubuntu `.deb`가 이제 `podman-compose`를 끌어오고, compose provider 누락 시 cryptic 대신 명확히 실패** (#644, @paolodongilli 기여 감사). Debian 13에서 `.deb`가 winpodx + podman은 깔아도 `podman-compose`는 안 깔아 `winpodx setup`이 컨테이너를 못 만들고 나중에 `no such container "winpodx-windows"`로 죽었습니다. 이제 `podman-compose`가 패키지 `Recommends`(apt 기본 설치)이고, setup 시점에 compose provider가 없으면 배포판별 설치 패키지명을 알려주는 실행가능 에러를 출력합니다(조용히 건너뛰지 않음). (`curl … install.sh` 경로는 이미 설치했음 — `.deb` 빈틈을 메움.) - **유지보수 작업 다이얼로그(Debloat, Grow Disk, Sync Guest 등)가 더 이상 비좁게 열리지 않음** (#550, @ismikes 기여 감사). `BusyDialog` 진행 창이 최소 너비 380px에 높이는 내용에 맞춰져 약 392×139로 떠 읽기에 너무 작았습니다(특히 Debloat *Speed* 실행). 480×168 하한 부여. (picker 창 크기와 빠른 작업 자동 닫힘은 0.7.2에서 이미 처리됨.) ### Contributors 코드 기여 @cxgreat2014 (PR #657, #658), 그리고 이번 릴리스를 이끈 리포트·제안에 감사드립니다 — @ismikes (#630, #550), @realahmed7777 (#646), @paolodongilli (#644), @biskasarchaniotakis (#569). ## [0.7.3] - 2026-06-20 ### Added - **`+multitouch`가 허용된 FreeRDP 플래그가 됨 — 터치스크린 / 스타일러스 / 펜 패스스루** (#623, @Scratch2xs 기여 감사). extra FreeRDP 플래그(`winpodx.toml`의 `rdp.extra_flags`, 또는 Settings의 extra-args 필드)에 `+multitouch`를 추가하면 터치스크린·드로잉 태블릿·펜이 Windows 앱 안에서 동작합니다 — Linux 빌드가 없는 드로잉 소프트웨어에 유용. 입력 전용 토글이라 보안 트레이드오프 없이 FreeRDP 플래그 허용목록에 들어갑니다. - **VM RAM 해제를 위한 idle 자동 정지 옵션** (#622, @hermitguo 기여 감사). Settings → **Idle Action**에서 *Idle Timeout* 경과 시 동작 선택: **Pause**(기본 — VM 동결, CPU 해제, RAM 유지, 즉시 재개) 또는 **Stop**(VM의 RAM 해제; 다음 실행은 풀 부팅). 기본 off — Idle Timeout 자체가 기본 off이고, Stop을 직접 켜지 않으면 Pause 유지. (인터페이스 다국어도 이 요청의 일부인데 이미 지원 — Settings → **UI Language**에서 中文/日本語/한국어 포함 8개 선택.) - **Reverse-open이 공유 Home뿐 아니라 Windows VM 자체의 파일도 엽니다** (#616, @notnotno 기여 감사). Windows 파일의 *연결 프로그램* 메뉴에서 Linux 앱을 고르는 기능이 기존엔 공유 Home(`\\tsclient\home`) 아래 파일만 됐고, Windows 데스크탑이나 `C:` 어디든 있는 파일은 "파일 없음" 오류로 실패했습니다. 이제 게스트 `C:`를 호스트에 SMB(루프백 전용)로 공유·온디맨드 마운트해서, 호스트 앱이 실제 게스트 파일을 열고 **편집은 원본으로 바로 저장**됩니다. 전부 런타임 전달(에이전트로 게스트 공유 + 호스트 마운트)이라 `install.bat`은 안 건드립니다. 호스트에 **kio-fuse**(KDE) 필요 — 없으면 `winpodx doctor`가 경고합니다. - **Debloat가 더 많은 텔레메트리·광고 항목을 비활성화** (#590, @GameSoul7Eugene 기여 감사). *Ads & suggestions* 항목이 추가 `ContentDeliveryManager` 추천 키와 회전 잠금화면 광고를 제거하고, *불필요한 예약 작업* 항목이 순수 텔레메트리 작업(`AitAgent`, `ProgramInventoryUpdater`, CEIP `BthSQM`, `Feedback\Siuf`, `WindowsAI` Copilot/Insights 데이터 수집, Office 텔레메트리 에이전트)을 추가로 비활성화합니다. 보안·시스템 핵심 작업(Windows Defender, 라이선스/활성화, 인증서 서비스, Windows Update 복구, 언어 팩, Windows Hello)은 활성화·업데이트·IME가 깨지지 않도록 의도적으로 건드리지 않습니다. ### Fixed - **대시보드 RAM + Disk C: 게이지가 "n/a"에 멈추지 않음** (#634, @ismikes 기여 감사). 둘 다 게스트에서 에이전트로 읽는데(CPU는 호스트측이라 멀쩡했음), 그 조용한 poll의 타임아웃이 빡센 4초여서 — 느리거나 방금 재실행된 게스트에선 그 안에 못 끝내고 둘 다 "n/a"로 비었습니다. 넉넉한 12초로 상향(poll은 GUI 스레드 밖 + 재진입 가드로 돌아서, 긴 budget이 대시보드를 멈추거나 중첩시키지 않습니다). #619와 같은 "타임아웃 너무 빡셈" 부류. - **`winpodx app refresh`가 느린 게스트에서 30초 만에 타임아웃되지 않음** (#619, @KyleSanderson 기여 감사). 디스커버리는 게스트의 시작 메뉴 + AppX 패키지를 열거하는데, 콜드/저사양 게스트에선 1분을 넘는 게 정상입니다 — 그런데 CLI가 30초로 캡(`/exec timed out after 29.0s`)한 반면 새 설치는 라이브러리 자체 기본 180초로 잘 됐습니다. 이제 모든 경로(provision, `app refresh`, GUI Refresh)가 단일 넉넉한 기본값(`DEFAULT_DISCOVERY_TIMEOUT` = 300초)을 씁니다. 타임아웃은 정상 완료를 자르려는 게 아니라 진짜 멈춘(wedged) 게스트만 한정하기 위한 것이며, 극단적으로 느린 게스트는 `winpodx app refresh --timeout`으로 올릴 수 있습니다. - **`install.sh --ref ` 재실행 시 이제 브랜치 최신 커밋을 설치** (#616). 원격을 fetch만 하고 stale한 *로컬* 브랜치 ref를 체크아웃했고(`reset --hard`는 `main`만), 그래서 피처 브랜치로 업데이트를 재실행하면 처음 clone된 커밋이 다시 깔리는 **조용한 no-op**이었습니다. 이제 `origin/`를 체크아웃합니다. - **업데이트가 compose 변경을 적용하고 멈춘 pod를 기동** (#616). `install.sh`는 업그레이드마다 `compose.yaml`을 재생성하지만 컨테이너 recreate는 pod가 떠 있을 때만 했고, 멈춰 있으면 Windows측 적용을 통째로 건너뛰어, 업데이트가 가져온 게스트측 변경이 다음 수동 실행 전까지 조용히 적용 안 됐습니다. 이제 compose가 바뀌면 멈춰 있어도 recreate하고, 멈춘 pod를 기동해 적용합니다(기존 설치 — ISO 재다운로드 없음). - **설치를 계속 불안정하게 만들던 USB 드라이브 문자 자동매핑 기능 제거** (#613, #638, @zephir2008·@ismikes 기여 감사). `media_monitor.ps1`은 각 USB 볼륨을 드라이브 문자(E:, F:…)로 띄우려 했지만, RemoteApp(RAIL) 세션에서 안정적으로 띄우지 못했고, OEM 번들에 포함되자 간헐적 Windows Defender/rdprrap first-boot 설치 데드락을 재발시켰습니다(새 설치가 그 파일 복사 직후 멈춤). 제거했습니다. USB 미디어는 여전히 **모든** 세션에서 `\\tsclient\media` 리다이렉션과 바탕화면 **USB** 바로가기로 접근 가능하며, 진짜 드라이브 문자/raw 블록 장치가 필요하면 USB passthrough를 쓰면 됩니다. 기존 pod는 다음 `apply-fixes` 때 잔존 `WinpodxMedia` 자동시작 항목이 정리됩니다. - **`usbredirect not found` 안내가 배포판별로 올바른 패키지를 안내** (#593, @techabsol 기여 감사). Debian/Ubuntu는 `usbredirect`, Fedora는 `usbredir-tools`(Atomic Fedora는 `rpm-ostree` 형태 추가), openSUSE는 `usbredir` 유지 — 기존 메시지는 바이너리가 들어있지 않은 패키지를 안내했습니다. - **`install.bat`가 에이전트 URL 예약 시 더 이상 구문 오류를 내지 않음** (#614, @zephir2008 기여 감사). `netsh http add urlacl … sddl=D:(A;;GX;;;WD)` 줄에서 SDDL 값을 따옴표로 감싸지 않아 `cmd.exe`가 `(`, `;`, `)`를 메타문자로 해석해 예약이 실패할 수 있었습니다. 이제 SDDL 값을 따옴표로 감쌉니다(`sddl="D:(A;;GX;;;WD)"`). - **게스트 에이전트가 드리프트된 bearer 토큰에서 복구됨 — 더 이상 영구 401에 머무르지 않음** (#615, @zephir2008 기여 감사). 에이전트는 부팅 시 `C:\OEM\agent_token.txt`의 bake본에서 토큰을 한 번만 읽는데, 이게 호스트 토큰과 어긋나면 `guest_exec`/`guest_summary`(및 모든 authed `/exec`)가 HTTP 401을 반환하고 에이전트가 스스로 고칠 방법이 없습니다. 새 `winpodx guest resync-token`이 현재 토큰을 FreeRDP 채널(Windows 암호로 인증하므로 에이전트가 401이어도 동작)로 다시 밀어넣고 에이전트를 재시작해 재독하게 합니다. `winpodx doctor`는 `guest_exec` 프로브가 401을 만나면 이를 자동 실행한 뒤 재확인합니다. ### Contributors 이번 릴리스를 이끈 리포트와 기여에 감사드립니다 — @notnotno (#616), @hermitguo (#622), @Scratch2xs (#623), @KyleSanderson (#619), @ismikes (#634, #638), @GameSoul7Eugene (#590), @techabsol (#593), @zephir2008 (#613, #614, #615). ## [0.7.2] - 2026-06-15 ### Fixed - **"Refresh Apps" 클릭 시 GUI 크래시** (KDE/Plasma Wayland) 수정 (#567). 앱 목록이 `widgetResizable` 스크롤 영역이라 as-needed 수직 스크롤바가 `updateScrollBars`→word-wrap 라벨 `heightForWidth`를 무한 재귀시켜 스택 오버플로 SIGSEGV. 스크롤바를 고정해 뷰포트 폭이 진동하지 않게 함. - **트레이 "Terminate Session" · "USB Devices" 서브메뉴가 다시 열림** (KDE/Plasma) (#573). 메뉴 항목을 헬퍼 안에서 부모 없는 `QAction`으로 만들어 헬퍼 반환 후 PySide6 GC가 수거 → 서브메뉴가 children 0개로 export(화살표만 뜨고 안 열림). 각 액션에 서브메뉴를 부모로 지정해 생존시킴. - **중국어/일본어/한국어 앱 이름이 디스커버리에서 누락되지 않음** (#553). 순수 비ASCII 이름이 빈 슬러그가 되어 앱이 사라졌음; 이제 원본 문자를 표시 이름으로 유지하고 안정적 내부 id를 부여해 다른 앱처럼 표시됨. - **`winpodx pod stop`이 컨테이너를 보존** (제거하지 않음). `compose down`(컨테이너 삭제)을 써서 정지 상태로 업데이트하면 매번 컨테이너를 재생성("Container 'winpodx-windows' is missing — creating it …")하고 불필요한 Windows 재부팅 발생. 이제 `compose stop` 사용 — `start`가 보존된 컨테이너를 재시작. 디스크는 영향 없음. - **Windows 사용자명이 비면 정체불명의 FreeRDP `Inappropriate ioctl for device` 대신 명확한 에러로 빠르게 실패** (#569). 자격증명이 없으면 xfreerdp가 대화형 프롬프트로 빠지는데 GUI 실행에선 동작 불가 — 이제 `winpodx setup` 실행을 안내. - **추가 RDP 플래그가 현재 FreeRDP 3 철자를 수용** (#380): 캐시 토글은 `/cache:bitmap:on|off` 등 — xfreerdp 3가 거부하는 구 FreeRDP-2 `+/-bitmap-cache` 형식을 allow-list에서 제거. - **`-gfx`로 GFX 파이프라인 비활성화 가능** (#393) — 일부 XWayland/Plasma에서 RAIL 렌더 깨짐(파란/뭉개진 창) 워크어라운드; bare `+gfx`/`-gfx` 토글이 이전엔 allow-list에 막혀 있었음. - **Debloat 피커가 적절한 크기로 열리고**, "debloat 실행 중" 창이 작업 완료 시 자동으로 닫힘(빠른 프리셋에서 안 닫히던 문제) (#550). ### Contributors 이번 릴리즈를 이끈 버그 제보를 해주신 @ismikes (#393, #550, #573), @notnotno (#380), @hermitguo (#553), @bangetto (#567), @biskasarchaniotakis (#569) 님께 감사드립니다. ## [0.7.1] - 2026-06-13 ### Added - **빠른 앱 런처 (`winpodx launch`).** Windows 앱을 위한 시작 메뉴 스타일 선택기 — 검색, 카테고리 칩, reveal-highlight 타일, compact 모드 — 메인 GUI 와 동일한 앱 레지스트리에서 가져오며 `winpodx app run` 으로 실행. 데스크톱 환경 커스텀 단축키(KDE/GNOME)에 바인딩하면 시스템 전역 핫키로 사용 가능; 추가 의존성 없음, Wayland 안전 (#561). - **자동 파일 연결 — 탐지된 Windows 앱이 파일 관리자의 "다른 프로그램으로 열기" 메뉴에 표시 (#545).** 기본 활성화; `winpodx config set desktop.mime_associations false` 또는 GUI 설정 토글로 비활성화. 앱은 "열기" / 추천 목록에 *추가* 만 됩니다 — winpodx 는 Windows 앱을 특정 형식의 기본 핸들러로 설정하지 않습니다. 연결은 Windows 에서 실시간으로 읽으며 앱이 열 수 있다고 선언한 모든 위치를 합칩니다 (확장자별 `UserChoice`, 앱별 `Capabilities\FileAssociations`, `Applications\\SupportedTypes`, UWP AppxManifest `windows.fileTypeAssociation`) — 예: `.png` 는 Paint/Photos/Snipping Tool, `.pdf` 는 Edge, `.txt` 는 Notepad. 확장자→MIME 매핑은 시스템 `shared-mime-info` DB 사용 (하드코딩 테이블 없음), 다음 `install.sh` 업데이트 시 재탐지로 자동 적용 — 수동 `app refresh` 불필요. - **GUI 앱 관리 — 재설정, 커스텀 아이콘, 다중 선택 (#530).** 앱별 **탐지값으로 재설정** 이 자동 탐지된 이름/아이콘 복원; **커스텀 아이콘 선택기** 로 아이콘 덮어쓰기; **다중 선택** 으로 **선택 숨기기** / **선택 삭제** 일괄 처리; 삭제된 앱은 **복원 목록** 에 들어가 삭제 취소 가능 (전체 재탐지 버튼 포함). 체크박스는 이제 보이는 대비 색상 사용. - **`winpodx doctor` 가 RemoteApp 창이 깨지는 구버전 FreeRDP 를 경고 (#546).** 네이티브 `xfreerdp` 3.x 가 3.6.0 미만 (예: Ubuntu/Budgie 24.04 apt 기본인 3.5.1) 이면 RAIL 창 매핑 버그로 RemoteApp 이 연결돼도 창이 안 뜸. doctor 가 이를 경고(실패 아님)로 표시하고 업그레이드 또는 winpodx AppImage 를 안내. - **체크섬 게이트 주기적 아이콘 갱신.** 탐지 앱 아이콘은 소스 실행 파일의 체크섬이 바뀔 때만 게스트에서 재동기화 — 일상 갱신이 변경되지 않은 아이콘을 다시 쓰지 않음 (#539). ### Fixed - **`winpodx gui` 가 더 이상 터미널을 막지 않음 (#549).** 셸에서 실행하면 자체 세션으로 분리되어 트레이 + 대시보드를 띄우고 즉시 프롬프트를 반환. `.desktop` 자동시작과 빠른 런처는 이미 분리 실행 중이었음; `winpodx gui --foreground` 로 디버깅 시 연결 유지 가능. - **`install.sh --main` (및 `--ref` / `--source` / `--image-tar`) 이 Atomic Fedora 에서 존중됨 (#548).** rpm-ostree 시스템 (Silverblue/Kinoite/Bazzite/...) 이 OBS RPM 으로 기본 동작하며 명시적 소스 오버라이드를 무시했음; RPM 은 태그 릴리스만 담아 `--main` 이 도달 불가였음. 이제 rpm-ostree 경로가 해당 플래그가 모두 비었을 때만 동작 — 커스텀 이미지 빌더가 소스에서 레이어링 가능. 기본 Atomic 설치는 불변. - **`install.sh` 가 `/dev/kvm` 이 없지만 CPU 가 가상화를 지원할 때 `kvm` 모듈을 자동 로드 (#541).** - **베어메탈 위장이 더 이상 컨테이너를 크래시 루프시키지 않음 (#246 후속).** winpodx 가 합성 SMBIOS blob 을 재작성한 뒤 컨테이너가 다시 읽을 수 있도록 파일 레이블을 재지정 — 이전엔 오래된 레이블이 다음 부팅을 깨뜨림. - **`winpodx doctor` 가 의도적으로 숨긴 앱을 누락된 desktop entry 로 표시하지 않음 (#535).** - **`uninstall.sh` 가 winpodx 가 만든 것을 모두 제거** — 통합 "WinPodX" 메뉴 폴더 (`.directory` / `.menu` 조각, 안 지우면 빈 서브메뉴가 남음), 오래된 `mimeapps.list` 핸들러 항목, 그리고 (`--purge` 시) `/etc/modules-load.d/winpodx-kvm.conf` 드롭인. - **탐지된 앱 이름이 더 이상 `.desktop` 런처에 추가 키를 주입할 수 없음.** 게스트가 준 이름의 제어 문자를 `Name=` 필드에서 제거 — 기존 `Comment=` 처리와 일치 (하드닝; 악의적 게스트가 있어야 발동). ### Changed - **`winpodx gui` 가 인터랙티브 실행 시 기본 분리**, `--foreground` 로 opt-out (#549). ### Contributors 앱 런처 (#561) 기여 및 #530 · #535 · #545 제보해주신 @MirzaAyBaig12, #541 · #546 제보해주신 @hermitguo, #548 제보해주신 @notnotno, #549 제보해주신 @ismikes 님께 감사드립니다. ## [0.7.0] - 2026-06-11 ### Added - **베어메탈 위장 — VM 탐지 소프트웨어에게 Windows 게스트를 물리 머신처럼 보이게 (#246).** Opt-in, 기본 꺼짐. 하이퍼바이저 *감지* 시 실행을 거부하는 소프트웨어 — Nvidia GPU 패스스루 "code 43", launch-gate VM 체크, VM 비적합 설치 프로그램 — 가 QEMU/KVM 대신 실물 베어메탈 머신으로 인식. **베어메탈 레벨** 로 동작 계층을 선택합니다 (`winpodx config set pod.disguise_level off | balanced | max`, 또는 GUI Settings 셀렉터): - **재빌드 불필요, VM 별 적용 (`balanced`+):** CPUID 하이퍼바이저 present 비트와 KVM 시그니처 제거 (`-cpu -hypervisor,kvm=off,-kvm-pv-*`), 호스트의 실제 SMBIOS/DMI (시스템 / 보드 / BIOS 벤더 + 제품, CPU 벤더 + 모델) 를 게스트에 미러링, 합성 SMBIOS 센서/디스크립터 blob (전압 / 온도 프로브, 냉각 장치, 캐시, 메모리 어레이 + DIMM, 40+ 구조체) 을 주입하여 `Win32_*` / `CIM_*` WMI 센서 클래스가 실제 하드웨어처럼 보고하도록. - **패치된 QEMU 이미지 (`max` / "Hardened"):** `winpodx disguise build-image` 가 QEMU 를 **로컬에서** 컴파일 (~20–40 분; 바이너리는 절대 shipped 안 됨) 하되, 커맨드 라인으로 건드릴 수 없는 VM 식별 *문자열* 을 호스트 실제 값으로 재작성 — ACPI OEM ID (`BOCHS`/`BXPC` → 호스트), FADT 하이퍼바이저 벤더 + PM-profile 바이트, 디바이스 `_HID`, `WAET` 테이블 시그니처, 디스크 / 광학 모델 **및 INQUIRY 벤더** — 에 더해 thermal-zone SSDT 와 WSMT 테이블 주입. `max` 에서 winpodx 는 SATA 시스템 디스크, e1000 NIC, std VGA, `nec-usb-xhci` USB-3 컨트롤러 (USB3 유지하면서 Red Hat `VEN_1B36` 특징 제거) 를 사용하고, virtio-rng 디바이스 (`VEN_1AF4`) 를 제거하며, 게스트에서 미사용 virtio 드라이버 서비스 키를 정리. GUI 에서 Hardened 선택 시 이미지 빌드 + 연결이 자동으로 이루어짐. - **개인정보:** 호스트에서 파생된 문자열은 *비식별* (벤더 / 모델 코드 등 디스크 모델) 이며 Docker build-arg 를 거쳐 **로컬 이미지 레이어에만 — git 에 절대 커밋되지 않고 push 도 없음**. 시리얼 / UUID / 자산 태그는 읽지 않습니다. 소스는 제네릭 폴백 (`ALASKA` / `Samsung SSD` / `ATA`) 을 ship. - 실제 Windows 에서 **al-khaser 0.82** 검증 완료: 디스크, 센서, SMBIOS, ACPI, CPUID, virtio 서비스, 사용자명 탐지 계열 clean. 남은 특징들은 컨테이너-QEMU 구조적 바닥 (RDTSC 타이밍, 실제 하드웨어에서도 비어있는 레거시 `Win32_MemoryDevice` 클래스, RDP 디스플레이 드라이버를 공유하는 Windows 내재 Hyper-V 통합 객체). - **기본 Windows 게스트 사용자명이 이제 `WPX-User`** (기존 `User`) — al-khaser 의 정확한 일치 샌드박스 사용자명 목록을 회피합니다. 첫 설치 전에 `winpodx config set rdp.user ` 으로 변경하세요. ### Fixed - **`$HOME` 의 심볼릭 링크 서브디렉토리 하위 파일 열기** — 예: `~/Documents` 가 `/mnt/store/Documents` 로 심링크된 경우 — 가 "Path is outside shared locations" 오류 없이 동작 (#547). 호스트→게스트 경로가 resolve 대신 lexical 정규화되어, 심링크 서브디렉토리를 폴더처럼 traverse (FreeRDP 가 따라가도록); `..` 는 여전히 차단되며 Fedora Atomic `/home → /var/home` 처리는 유지 (#418). - **새 dockur 이미지에서 USB 핫플러그 복구.** dockur v5.16 이 QEMU 모니터를 `telnet:7100` 에서 unix socket 으로 이전하면서 라이브 USB 연결 경로가 "QEMU monitor unreachable" 로 깨졌습니다; winpodx 가 이제 소켓과 통신하며 (구 dockur 는 telnet 폴백 유지) (#286). - **GUI Devices 패널이 USB 디바이스가 탭 열린 상태에서 뽑힐 때 더 이상 깜빡이지 않음**, 그리고 열거 도중 뽑힌 디바이스가 Qt 슬롯에서 예외를 올리지 않음. ### Changed - **dockur/windows 핀을 v5.16** (x86_64) 과 dockur/windows-arm `:latest` 보안 재빌드 (aarch64) 로 업 (#554, #555). ## [0.6.0] - 2026-06-05 ### Removed - **libvirt 백엔드 제거 (breaking).** `backend = "libvirt"` 는 더 이상 유효하지 않습니다 — 기본 dockur 백엔드가 컨테이너 안 QEMU/KVM이고 이제 디바이스 패스스루까지 커버하므로(#286), 얇은 "bring-your-own libvirt 도메인" 래퍼는 존재 이유가 없어졌습니다. `--backend` 는 `podman | docker | manual` 만 받고, `backend = "libvirt"` 인 기존 config는 로드 시 `podman` 으로 fallback(경고와 함께). `libvirt` pip extra(`libvirt-python`) + install.sh/AUR/RPM/DEB 의 libvirt 참조 제거; `winpodx[all]` 이 이제 libvirt-free(`libvirt-dev` 빌드 의존성 없음). 자체 libvirt 도메인에 Windows 운영 중이라면 WinPodX ≤ 0.5.x 유지하거나 `manual` 백엔드를 그 RDP 엔드포인트에 연결하세요. ### Added - **재설계된 Start 메뉴 스타일 GUI — 리소스 Dashboard 홈 + 좌측 내비게이션 사이드바 (#460–#471).** 창이 이제 Start 메뉴 셸입니다: 좌측 세로 내비게이션 사이드바(페이지당 한 줄, 활성 페이지 강조)와 새 **Dashboard** 홈 — 실시간 Pod / RAM / CPU 링 게이지 + 디스크 사용량, 자동 복구 상태 카드, 고정/최근 워크스페이스 타일, reverse-open 토글; 앱 런처는 "All apps" 페이지로 이동. 또한 통합 디자인 시스템, 자체 SVG 아이콘 세트(기존 유니코드 글리프 "아이콘" 대체), 좁은/분수 배율 창에서 reflow 하는(컬럼 쌓기, 그리드 타일 드롭) 반응형 레이아웃, fit-to-screen 창 크기, 그리고 명령 바를 겸하는 hero 검색을 도입합니다. 표시 이름이 UI / 사이트 / 문서 전반에서 이제 **WinPodX** 입니다(소문자 `winpodx` 명령 / 패키지 식별자는 그대로). - **프로젝트 웹사이트 — [winpodx.org](https://winpodx.org) (#436–#451).** 랜딩 페이지 + Features / Get-started / FAQ 페이지, GitHub Pages 로 배포되며 앱과 동일한 언어로 현지화됨. - **Linux 메뉴에서 개별 앱 숨기기 / 표시 (#319, #415).** GUI 앱 라이브러리(그리드 + 리스트)의 **Hide** 액션이 Windows 앱을 Linux 애플리케이션 메뉴에서 제거합니다; 숨긴 앱은 프로필에 영속되고 **Hidden** 토글로 다시 되돌립니다. CLI: `winpodx app hide ` / `winpodx app show `. - **`winpodx pod recreate --keep-iso` — ISO 재다운로드 없이 Windows 재설치 (#416).** Windows 디스크 + 설치 마커를 지우되 캐시된 설치 ISO 는 유지하므로, dockur 가 Microsoft 에서 ~5–8 GB 를 다시 받지 않고 그 ISO 로 재빌드합니다(캐시된 ISO 가 없으면 일반 다운로드로 fallback). - **호스트 USB / PCI 디바이스를 Windows 게스트로 패스스루 — CLI + GUI (#286).** 호스트 디바이스(USB 보안 동글, 캡처 카드, GPU 아닌 PCI 카드 등)를 Windows 게스트에 넘길 수 있습니다. 기본 백엔드가 dockur(컨테이너 안 QEMU/KVM)라 패스스루를 QEMU 레벨에 배선 — libvirt 불필요. `winpodx device list`로 호스트 USB/PCI 디바이스 + 할당 여부 확인, `device attach ` / `detach `로 할당/해제(`cfg.pod.devices`에 영속), GUI에는 **Devices 탭**(호스트↔게스트 2컬럼 mover). **USB는 실행 중 게스트에 라이브 핫플러그** (`cfg.pod.usb_live`, 기본 ON): `device attach `가 재시작 없이 추가 — dockur 내장 QEMU `-monitor`를 (` exec ... bash /dev/tcp`로) 구동. 커스텀 `-qmp` 소켓도 `device_cgroup_rules`도 안 씀 (둘 다 rootless Podman서 Windows 부팅 crash-loop 시켰음). usb_live는 호스트 USB 버스(`/dev/bus/usb`)만 컨테이너에 bind(rootless서 정상 부팅 검증됨); QEMU(dockur 통해 root)가 기존 `qemu-xhci` 컨트롤러에 디바이스 붙임. `usb_live = false`면 USB 버스 안 넣음. (USB **대용량 저장장치**는 `\\tsclient\media` 공유로도 동작.) **PCI**는 `vfio-pci` 바인딩이라 컨테이너 QEMU에 핫플러그 불가 → 부팅 시 추가 + 게스트 재시작 필요 + 안전 검사 게이트 — 위험 디바이스(주 GPU, 부팅 디스크 컨트롤러, 활성 NIC)는 명시적 `--force`(CLI) 또는 확인 다이얼로그(GUI) 필요하고, IOMMU 그룹은 통째로 움직이므로 함께 표시됨. 디바이스 id는 hex 검증. - **시스템 트레이 USB 스위처 (#300).** 트레이 메뉴에 호스트 USB 디바이스마다 체크 항목이 있는 **USB Devices** 서브메뉴가 생겼습니다 — 체크하면 실행 중인 게스트로 디바이스 리다이렉트, 해제하면 호스트로 반환. CLI(`device attach`/`detach`) + GUI Devices 탭과 나란한 빠른 접근 표면입니다: 토글은 영속 + 라이브 usbredir attach/detach 를 UI 스레드 밖에서 실행(트레이가 멈추지 않고 `pkexec` 프롬프트도 정상 노출)하며, 서브메뉴는 열 때마다 재구성되어 핫플러그된 디바이스 + 현재 할당을 반영합니다. 영속 절반은 이제 세 표면 뒤의 단일 공유 헬퍼(`core.devices.assign_device` / `unassign_device`)입니다. - **멀티모니터 RAIL 이 이제 기본으로 동작합니다 — RemoteApp 창을 두 번째 모니터로 끌어도 입력이 계속 먹힙니다 (`cfg.rdp.multimon`, 기본값 `span`).** 이게 없으면 RAIL 앱 실행이 세션 데스크탑을 모니터 1개 크기로 잡아서, 두 번째 모니터로 끈 창이 그 데스크탑 *밖*의 호스트 가상스크린 좌표에 놓임 — 클릭이 빗나가다가 아예 안 먹힘. 이제 WinPodX 가 RAIL 앱 실행에 `/span` 을 추가해 세션 데스크탑을 전체 호스트 모니터의 bounding box(모니터별 `MonitorDefArray` 없는 하나의 넓은 직사각형)로 잡습니다. `/multimon` 을 먼저 시도했지만 전체 모니터 레이아웃을 전송해서 게스트의 `rdprrap` RAIL 헬퍼가 처리 못 하고 입력을 완전히 죽임 — 그래서 기본값은 `multimon` 이 아니라 `span`. 스팬된 bounding box 에 빈 공간이 생기는 비직사각형 배치에선 `cfg.rdp.multimon = "off"`(또는 `winpodx setup --multimon off`) 설정; `multimon` 은 진단 전용 값으로 남겨둠. - **`--extra-args` / `cfg.rdp.extra_flags` 가 이제 멀티모니터 + 리페인트 knob 을 허용합니다** (`/multimon`, `/multimon:force`, `/span`, `/gdi:sw|hw`, `/smart-sizing[:WxH]`, `/monitors:0,1`), 각각 값 검증됨. 일부 멀티디스플레이 환경의 RAIL 창-이동 깨짐(해상도/DPI 다른 모니터 사이로 창을 끌면 뭉개지거나 깨짐) 의 수동 레버. 이제 `cfg.rdp.multimon` 이 기본 `span`(위 항목)이라 입력 손실 케이스는 기본으로 처리됨; 이 플래그들은 리페인트 / 스케일링 knob 의 실행별 실험용으로 남음. - **`winpodx doctor --fix` — 흔한 진단 항목에 대한 멱등 자동 복구 (0.6.0 항목 K).** `winpodx doctor` 는 기본적으로 여전히 읽기 전용(권장 명령만 출력, 상태 변경 없음). 새 `--fix` 플래그는 알려진 복구기가 있는 각 진단 항목을 자동·멱등 복구로 전환합니다: doctor 가 진단을 수집하고, 모든 warn/fail 항목에 대해 등록된 복구기를 실행한 뒤, 해당 검사만 다시 수행해 `[fixed]` / `[still failing]` 을 보고합니다(복구기가 없는 항목은 `[skip] no auto-fix available` 출력). 모든 복구기는 이미 정상인 상태에서는 아무 동작도 안 하므로 `--fix` 는 반복 실행해도 안전합니다. 네 가지 복구 제공: (1) **죽은 에이전트** — 포드는 RUNNING 인데 게스트 내부 에이전트 `/health` 가 응답하지 않으면, 게스트 내부 `WinpodxAgentKeepAlive` 예약 작업을 즉시 실행(에이전트 전송 채널 경유; 에이전트 자체가 닿지 않으면 FreeRDP 로 폴백)하고 에이전트가 돌아올 때까지 `/health` 폴링; (2) **오래된 잠금 파일** — `~/.local/share/winpodx/run/` 의 `.cproc` 마커 중 소유 PID 가 더 이상 살아있는 FreeRDP 프로세스가 아닌 것을 정리(실행 중인 세션은 안 건드림); (3) **누락된 데스크톱 항목** — 인덱스에는 있으나 `.desktop` 파일이 설치되지 않은 앱을 기존 데스크톱 항목 설치 경로(항목 + 아이콘 + MIME + 캐시 새로고침)로 재등록; (4) **OEM 버전 드리프트** — 호스트 `oem_bundle` 스탬프가 게스트 기록 버전보다 새로우면 `guest_sync.maybe_autosync` 실행으로 갱신된 게스트 스크립트 푸시. `--fix` 는 게스트를 건드리는 두 복구기가 동작하도록 느린 컨테이너 헬스/게스트 exec 프로브를 자동 포함합니다. 오래된 잠금·누락된 데스크톱 항목 복구기는 호스트 전용, 죽은 에이전트·OEM 드리프트 복구기는 Windows 게스트를 건드립니다. `--fix` 없이는 `winpodx doctor` 동작 그대로. `docs/design/ROADMAP-0.6.0.md` 항목 K 참조. - **깔끔한 인터랙티브 설치 진행 표시 (raw 전체는 `install.sh --verbose`).** Windows 첫부팅 대기가 dockur/wget 원시 라인 수백 줄(`…K …… 78% 4.55M 21m22s`) + UEFI `BdsDxe:` 부트로더 노이즈를 쏟아냈음. 이제 기본적으로 `pod wait-ready --logs` 가 ISO 다운로드를 **제자리에서 갱신되며 지워지는 한 줄**로 표시 — `Downloading Windows ISO [#########-----] 78% 4.55 MB/s ETA 21m22s` — 비-다운로드 단계엔 `Windows is booting…` heartbeat, UEFI/`mknod` 노이즈 숨김; 실제 dockur 마일스톤만 화면에 남김. 라이브 라인은 `/dev/tty` 에 직접 써서 터미널에서 애니메이션되면서도 `install.sh` 의 `tee` 캡처 로그를 안 더럽힘(비-TTY 는 가끔 퍼센트 라인 폴백). `install.sh --verbose` / `-v`(또는 `winpodx pod wait-ready --logs --verbose`)로 전체 raw 스트리밍. 설치 끝에 요약 박스(버전/백엔드/GUI/venv) + 다음-단계 명령도 표시. 느린 다운로드 시 deadline 자동 연장은 그대로. - **`install.sh` v2 — sudo 이전 시스템 점검, 설치 모드, 필수 private venv, 실패 시 롤백 (#271 해결).** `curl | bash` 설치가 이제 `sudo` 건드리기 *전에* 호스트를 스캔: distro + 버전, podman / docker / libvirt / freerdp / python3 의 존재/버전, `/dev/kvm` 존재 여부, 그리고 `python3 -m venv` 가 실제로 동작하는지(Debian/Ubuntu 는 `python3-venv`/ensurepip 를 분리해서 없을 수 있음)를 출력. **podman major 4 미만은 too old 로 표시** — dockur/winpodx 는 rootless `group_add: keep-groups` + 모던 compose 가 필요한데 Ubuntu 22.04 는 podman 3.4 (#271) — podman 업그레이드(Kubic / `devel:kubic:libcontainers`) 또는 Docker 백엔드 사용을 안내. 이후 네 가지 모드 제공(`--mode r|a|c|n` / `WINPODX_MODE` 로 piped/비대화형 시 사전 선택): **[R]ecommended**(기존 동작 — Podman 백엔드, 누락 deps 전부 설치), **[A]utomatic**(이미 설치된 것 재사용, 동작 중인 docker/podman/libvirt 백엔드 선택, 최소 sudo), **[C]ustom**(백엔드 + GUI 여부 선택), **[N]o**(변경 없이 깨끗하게 취소). `winpodx setup` 으로 전달되는 신규 플래그 둘: `--backend podman|docker|libvirt|manual`(`WINPODX_BACKEND`) + `--no-gui`(`WINPODX_NO_GUI=1`, headless — PySide6 생략). **이제 Python 은 항상 private venv 에서 실행** — `~/.local/bin/winpodx-app/.venv` 아래; `winpodx-run` 런처가 시스템 `python3` + `PYTHONPATH` 대신 venv 인터프리터를 exec → `--user`/`--break-system-packages` 시스템 파이썬 오염 없음, reverse-open 아이콘 deps(`cairosvg` + `pyxdg`)가 venv 에 깔끔히 설치(더 이상 distro 패키지 best-effort 아님). 그리고 **fresh install 실패 시 롤백** — 그 실행에서 만든 winpodx 자체 아티팩트만 제거(venv, `winpodx-run` + `winpodx` 런처/심링크, desktop 엔트리 + 아이콘, in-progress 마커), 시스템 패키지나 기존 `~/.config/winpodx` config 는 절대 안 건드림; *업그레이드* 실패 시 동작 중인 설치를 그대로 둠. `--manual`, `--skip-deps`, `--source`, `--ref`/`--main`, `--win-version`, `--image-tar` 는 전부 기존대로 동작. - **`winpodx-git` AUR 패키지 — `main` 최신을 소스에서 설치 (#482, #483, #484).** 안정판 `winpodx` AUR 패키지(태그 릴리스 tarball)와 별개로, GitHub `main` 브랜치에서 빌드하는 `winpodx-git` VCS 패키지 추가: `yay -S winpodx-git`, 버전은 git 에서 도출되어 `yay -Syu --devel` 이 `main` 이 움직일 때마다 리빌드. `winpodx` 를 `provides`/`conflicts` 하므로 둘 중 하나만 설치. 레시피 + 메인테이너 노트는 `packaging/aur-git/`, 문서는 `docs/INSTALL.md`. ### Changed - **GUI UX 전면 정비 — 더 명확한 피드백, 더 안전한 동작, 더 적은 전문 용어.** 데스크탑 앱 전반에 걸친 폭넓은 패스(새 텔레메트리 없음, 네트워크 없음, bloat 없음): 공유 토스트/알림 + "busy" 다이얼로그 + 인라인 경고 콜아웃 + 액션 가능 에러 레이어(`_widget_helpers`)가 이제 어디서나 일관된 피드백을 받칩니다. 하이라이트: 앱 실행과 디바이스 attach/detach 가 조용한 RDP spawn 대신 비차단 토스트("Launching …" → 성공/실패) 표시; 앱 라이브러리가 **상황별 빈 상태**(Windows 미실행 / 검색 미일치 / 전부 숨김 / 발견 없음) + 통합 그리드-vs-리스트 카드 레이아웃(라벨 Launch, 일관된 Show/Hide, 확인되는 delete), 라벨된 검색 박스, 첫 8개 넘는 카테고리의 "+N more" 오버플로, 상태 바와 화해하는 "X of Y" 카운트 표시; refresh 실패는 액션 가능 다이얼로그(Start pod / Retry / View logs); 기동 다이얼로그가 중단 불가 단계 동안 Cancel 비활성(툴팁 포함) + 정직한 단계별 ETA 힌트 + 명확한 "✓ Ready" 마무리; 상태 배너가 "실행 중이나 transport 저하"(RDP/agent 도달 불가)와 "중지"를 구분하고, FreeRDP fallback 이 동작하는 down 된 agent 는 빨강이 아니라 amber; pod 상태가 더 이상 chip/배너/info-bar 에 삼중 표시 안 됨; Settings 가 어느 컨트롤이 즉시 적용되는지 표시하고, 파괴적 저장 전 wipe/recreate **경고 콜아웃** + 소요 시간 추정 노출, 상시 RAM-예산 줄, power-user knob 의 인라인 도움말 추가; 긴 유지보수 작업(grow-disk / sync-guest / debloat / apply-fixes)은 ETA 있는 busy 다이얼로그 표시; PCI 패스스루는 IOMMU 그룹이 내주는 호스트 디바이스를 평이한 말로 설명; Add-app 다이얼로그가 필드를 설명하고 Windows 실행파일 경로를 가볍게 검증; 키보드 단축키(Alt+N 페이지 전환, Ctrl+F 검색) 추가; License 페이지가 각 third-party 프로젝트를 링크. (Funding 은 repo `FUNDING.yml` / README 에만 — 앱에는 절대 노출 안 됨.) - **Windows 앱이 이제 네이티브 카테고리에 흩어지는 대신 단일 "winpodx" 메뉴 폴더 아래로 묶입니다.** 이전엔 설치된 각 앱이 발견된 카테고리(Office, Graphics, …)를 상속해 네이티브 Linux 앱들 사이에 흩어졌고; 50개 앱 게스트에선 메뉴를 파묻었습니다. 이제 winpodx 가 전용 **winpodx (Windows Apps)** 서브메뉴를 만듭니다 — Wine 이 쓰는 것과 같은 메커니즘: freedesktop `.directory` 파일이 폴더 이름을 지정하고, `applications-merged/winpodx.menu` 조각이 커스텀 `X-winpodx` 카테고리를 거기로 매핑하며, 생성된 모든 `.desktop` 이 그 한 카테고리를 답니다. 폴더는 첫 앱 설치 시 생성되고 마지막 Windows 앱이 제거되면 철거됩니다(둘 다 멱등 + `winpodx app refresh` 에서 자가 치유). KDE Plasma, XFCE, Cinnamon, MATE, LXQt 가 존중; GNOME 오버뷰는 메뉴 폴더를 무시하는 평면 그리드라 거기선 앱이 나타나긴 하지만 묶이진 않습니다. 메뉴 검색은 여전히 이름으로 앱을 찾습니다(`Keywords=windows;winpodx;`). 기존 설치 마이그레이션은 `winpodx app refresh` 재실행. - **`install.sh` 가 시스템을 건드리기 전에 설치 플랜을 출력합니다.** 모드(R/A/C/N) 와 모든 의존성 소스가 결정되면, 인스톨러가 **주요 컴포넌트를 균등하게** 나열한 짧은 플랜 — `python3`, `venv` 프로브, 컨테이너 백엔드, FreeRDP, `/dev/kvm`, GUI — 을 각각 감지 상태 + 이번 실행의 동작(기존 사용 / 설치 / 호스트 요구사항)과 함께, 그리고 배포판 패키지 매니저로 설치할 정확한 패키지 목록 + Windows VM 프로비저닝 단계까지 — *어떤 패키지 설치/`sudo` 전에* 보여줍니다. 모드 프롬프트에서 바로 설치로 직행하지 않고 진행 내용이 투명합니다. - **FreeRDP 클라이언트 소스를 선택 가능하게 했고, 런처는 Flatpak 클라이언트를 우선하며 네이티브는 fallback / opt-in 입니다 (#269, #366, #393).** 기존엔 `install.sh` 가 Flatpak `com.freerdp.FreeRDP` 이 이미 있어도 네이티브 `freerdp3` 패키지를 항상 설치했습니다(중복 — #269). 이제: (1) **런처가 Flatpak `com.freerdp.FreeRDP` 우선** (`core/rdp.find_freerdp` auto 순서가 flatpak-first) — 호스트 패키지 편차 없는 자체완결 FreeRDP 3+. 이전의 RAIL 멀티디스플레이 문제(RemoteApp 창을 두 번째 모니터로 끌면 입력 손실)는 이제 `cfg.rdp.multimon = "span"` 으로 처리되므로 Flatpak 이 우선 클라이언트로 viable; 네이티브 `xfreerdp` 는 Flatpak 이 없거나 `--freerdp-source native` 로 명시 고정했을 때 fallback. (2) **`install.sh` 가 중복 클라이언트를 설치하지 않음** — FreeRDP(네이티브든 Flatpak 이든) 가 하나라도 있으면 아무것도 안 깖; 둘 다 없으면 `auto` 는 **네이티브** 패키지 설치(경량, Flatpak 런타임 안 받음), 런처 auto 순서는 실제로 Flatpak 이 있을 때만 그걸 우선. (3) **Custom 설치 모드에서 주요 의존성마다 소스를 선택** — 컨테이너 백엔드(podman/docker/libvirt), **FreeRDP 클라이언트(auto / native / flatpak)**, GUI — 그리고 `winpodx setup --freerdp-source ` 가 선택을 `cfg.rdp.freerdp_source` 에 저장(Flatpak 샌드박스가 문제인 호스트에선 `native` 로 네이티브 고정). - **Reverse-open refresh 출력이 방향을 명시하고, deprecation / 요약 plumbing 이 중복 제거됩니다.** `winpodx host-open refresh` 가 `Reverse-open (host apps → Windows "Open with")` 헤더를 찍어 `provision` 중 바로 위에 도는 반대 방향 Windows-앱 discovery 의 `Discovered/staged/skipped` 카운트와 안 헷갈리게 하고, staged 셋은 `winpodx host-open list` 로 안내합니다. 별도로, `winpodx provision` 요약이 raw `apply_fixes` dict repr 를 더 이상 안 찍고(`N/N fixes OK` / `k: v` 한 줄), byte 단위로 동일하던 두 `pod`-deprecation 헬퍼(`_deprecate_pod` / `_emit_deprecation`)를 하나로 합쳤습니다. - **`install.sh --verbose` 가 이제 업그레이드 경로에도 전달됩니다.** `install.sh --verbose` (또는 `WINPODX_VERBOSE=1`) 가 신규 설치에선 `winpodx provision --verbose` 로 전달됐는데, 업그레이드 분기에선 `install.sh` 가 `winpodx migrate --non-interactive` 만 호출하고 verbose 플래그를 안 넘겨서 — 그리고 `migrate` 의 `_rich_wait` 내부에서 `verbose=False` 가 하드코딩돼 있어서 — 조용히 누락됐습니다. 이제 `winpodx migrate` 가 `--verbose` / `-v` 를 받고, `install.sh` 는 `$WINPODX_VERBOSE` 를 `provision` 과 `migrate` 양쪽에 전달하며, migrate 의 wait-ready 단계가 플래그를 존중해서 업그레이드 실행도 신규 설치와 동일하게 raw 컨테이너 firehose 를 스트림합니다. `--verbose` off 일 때 동작 변화는 없습니다(양쪽 경로 모두 기본은 깨끗한 self-erasing 라인). - **`agent_keepalive` 가 컨테이너 재시작 직후 agent 깜빡임에 migrate apply 단계를 더 이상 실패시키지 않습니다.** 업그레이드 `winpodx migrate` 중 컨테이너가 재시작하고(OEM reboot pass) 게스트 내부 agent 는 rdprrap (재)활성화가 트리거하는 TermService 사이클을 거칩니다. 그 구간의 *단발* `/health` OK 는 agent 가 잠깐 떠 있는 것일 수 있고 — 직후 다시 죽어서 apply 단계가 닫힌 소켓을 침(0.6.0 업그레이드 스모크에서 본 `agent_keepalive: channel failure: /exec socket error: Remote end closed connection without response`). 이제 두 겹 방어: **(A)** `require_agent` settle 단계가 진행 전 `/health` OK **3회 연속**(최대 ~30초)을 기다려, 첫 깜빡임이 아니라 agent 가 실제로 안정된 뒤에만 체인을 시작; **(B)** 모든 `_apply_*` 런타임 픽스(`agent_keepalive`, `oem_runtime_fixes`, `multi_session`, `vbs_launchers`, `max_sessions`, `rdp_timeouts`)가 공유하는 채널 `_apply_via_transport` 가 `TransportError`(닫힌 소켓 / `/health` 타임아웃)를 **5초 백오프로 2회 재시도**하고, 매 시도마다 re-dispatch 해서 복구된 agent 를 다시 고르거나(여전히 죽었으면 FreeRDP 로 폴백). 실제로 실행된 payload 의 진짜 `rc != 0` 은 재시도 없이 첫 시도에 그대로 보고. healthy 한 첫 시도엔 동작 변화 없음. - **AppImage 가 더 이상 호스트와 싸우는 번들 컨테이너 스택을 싣지 않습니다 — Thin AppImage 재설계 (#357, #363).** 0.6.0 이전 팻 AppImage 는 전체 podman 스택(podman + podman-compose + conmon/crun/netavark/aardvark-dns/pasta/passt/slirp4netns + 그 `.so` 의존성)을 `${APPDIR}/usr/bin` 와 `${APPDIR}/usr/lib` 에 번들했고, 엔트리포인트가 두 경로를 모두 `PATH` 와 `LD_LIBRARY_PATH` 앞에 prepend 했습니다. 이미 동작하는 호스트 podman 이 있는 모든 환경에서 이것이 깨졌습니다: **#357, Ubuntu 26.04** — `podman-compose` 가 번들 사본으로 해석되고, 그것이 번들 `podman` 을 탐색하는데(번들 podman 은 호스트의 `/etc/containers` 설정 · subuid/subgid · systemd 통합이 없어 단독 실행 불가) `it seems that you do not have podman installed` 로 죽으며 호스트의 정상 podman 5.7 + podman-compose 1.5 를 가렸습니다; **#363, Fedora Bluefin** — podman 이 rootless aardvark-dns 용으로 *호스트* `systemd-run` 을 실행했는데, 앞에 붙은 `${APPDIR}/usr/lib` 때문에 그 호스트 바이너리가 AppImage 번들 `libcrypto.so.3` 을 로드하게 되어 `OPENSSL_3.4.0 not found (required by host libsystemd-shared)` 가 발생, aardvark-dns 가 실패하고 컨테이너 시작이 죽었습니다. PR #365 의 `_hostenv` 헬퍼는 증상을 우회한 것이고, 0.6.0 항목 A 는 **근본 원인을 제거**합니다 — AppImage 에서 전체 컨테이너 스택을 드롭. AppImage 는 이제 번들이 안전한 것만 번들합니다(FreeRDP 3 · Python · Qt · winpodx — 호스트 헬퍼를 띄우지 않는 리프 바이너리들). 호스트 컨테이너 런타임(`podman` 권장, `docker` / `libvirt` 지원) 은 사용자가 배포판 패키지 매니저로 설치해야 합니다(`install.sh` 와 동일 모델). 다만 컨테이너 스택 제거만으론 ~20 MB 만 감소했습니다(≈296 MB fat → ≈274 MB): 진짜 용량은 PySide6 가 Qt6 전체를 번들하는 것(QtWebEngine 만 ~195 MB)이고 winpodx 는 QtCore/QtGui/QtWidgets/QtSvg/QtDBus 만 링크하므로, 동반 패스(`packaging/appimage/slim-pyside6.sh`)가 안 쓰는 Qt6 모듈 + 그 플러그인/리소스 + 고아가 된 FFmpeg 라이브러리를 strip 해서 AppImage 를 **~110 MB** 로 줄입니다. `_hostenv` 는 단일 책임으로 축소됩니다 — 모든 컨테이너 백엔드 서브프로세스에 대해 `LD_LIBRARY_PATH` 에서 `${APPDIR}` 제거. 호스트 런타임과 그것이 띄우는 호스트 헬퍼들이 번들 libcrypto / libssl 이 아니라 HOST libcrypto / libssl 을 로드하도록 보장 (번들 FreeRDP / Python / Qt 가 여전히 AppImage 자체 `LD_LIBRARY_PATH` 에 있는 번들 라이브러리들을 필요로 하기 때문에 #363 완화 조치는 유지). `host_path()` 와 `resolve_backend_bin()` 은 제거됨 — `${APPDIR}/usr/bin` 에서 호스트 컨테이너 런타임을 가리는 것이 더 이상 없으므로, 표준 `subprocess` PATH 해석이 호스트 `podman` / `docker` 를 바로 찾습니다. 회귀 테스트(`tests/test_appimage_recipe.py`) 가 recipe 가 `podman` / `podman-compose` / `conmon` / `crun` / `netavark` / `aardvark-dns` / `passt` / `pasta` / `slirp4netns` / `fuse-overlayfs` 중 어느 하나라도 재도입하는 순간 CI 를 실패시킵니다. #357 과 #363 은 근본 원인이 수정되었지만, 원래 reporter 들이 다음 0.6.0 릴리스의 재빌드된 AppImage 를 스모크 테스트할 때까지 OPEN 상태로 둡니다. AppImage 가 아닌 환경에서는 엄격한 no-op(동일 바이너리, 환경 그대로 상속)이므로 ~99% 설치는 바이트 단위로 영향이 없습니다. 자세한 내용은 `docs/design/ROADMAP-0.6.0.md` 항목 A 참조. - **게스트 에이전트가 이제 세션 교체에도 죽지 않고 살아남습니다 (다음 재부팅까지 죽어 있던 문제 해결).** 게스트 내부 HTTP 에이전트(`agent.ps1`, 호스트가 창 없는 `/exec` 에 쓰는 `:8765` 리스너)의 자동 시작 경로는 단 하나, 대화형 로그온당 한 번만 실행되는 `HKCU\Run` 항목뿐이었고, 에이전트는 자동 로그온 세션의 자식 프로세스로 실행되었습니다. 그 세션이 무너지면 — rdprrap 멀티세션이 활성화되기 전에 FreeRDP 연결이 들어와 RDP 단일 세션 강제가 세션을 걷어차거나, rdprrap (재)활성화 중 TermService 사이클이 돌 때 — 에이전트가 세션과 함께 죽고 `HKCU\Run` 은 **다시 실행되지 않아**, 파드를 재부팅할 때까지 에이전트가 죽은 채로 남았습니다(파드가 몇 시간째 떠 있는데도 `/health` 타임아웃, `pod restart` 로만 살아남). 이제 **`WinpodxAgentKeepAlive` 예약 작업**이 `HKCU\Run` 이 못 해 주던 지속 워치독 역할을 합니다: 멱등한 `agent-keepalive.ps1` 을 **로그온 시 + 1분마다** 실행하여, `agent.ps1` 프로세스가 하나도 없을 때만 기존 `hidden-launcher.vbs` 래퍼(콘솔 깜빡임 없음)로 에이전트를 (재)기동합니다 — 정상 동작 중인 에이전트는 절대 죽이지 않습니다. 이 작업은 **대화형 자동 로그온 사용자**(SYSTEM/S4U 아님)로 실행되어, 앱 디스커버리와 사용자별 reverse-open 등록이 의존하는 사용자 HKCU + 시작 메뉴 컨텍스트를 에이전트 `/exec` 가 그대로 유지합니다. World-SID `:8765` urlacl 예약과 World-읽기 가능 `C:\OEM\agent_token.txt` 도 변경 없이 그대로 접근됩니다. OEM 시점(install.bat, OEM 번들 25 → 26)과 apply 체인(`winpodx pod apply-fixes` / migrate / guest-sync) 양쪽에서 등록되므로, 이미 프로비저닝된 파드도 컨테이너 재생성 없이 받습니다. 크래시했지만 세션은 살아 있는 에이전트는 약 1분 내 복귀하며, 재로그온 없는 세션 강제 종료는 이미 멱등한 rdprrap 활성화로 애초에 발생을 막습니다(1분 반복은 TermService 사이클이 가라앉은 뒤의 백스톱이기도 합니다). - **커맨드 체계 재편 — `guest`, `install`, `doctor` 가 새 정식 위치 (0.6.0 항목 G).** `pod` 서랍에 3개 도메인의 서브커맨드 14개가 섞여 있었고, 진단 표면에는 부분적으로 겹치는 커맨드 3개(`info`, `check`, `doctor`)가 있었습니다. 이번 릴리스부터: **`winpodx pod`** 는 라이프사이클 서브커맨드만 유지(`start`, `stop`, `status`, `restart`, `recreate`, `wait-ready`). **`winpodx guest`** 는 게스트 측 작업의 새 정식 위치: `apply-fixes`, `sync`(`sync-guest` 에서 이름 변경), `sync-password`, `multi-session`, `recover-oem`. **`winpodx install`** 는 설치 진행/스토리지 작업의 새 정식 위치: `status`(`pod install-status`), `resume`(`pod install-resume`), `grow-disk`(`pod grow-disk`), `disk-usage`(`pod disk-usage`). **`winpodx doctor`** 가 정식 진단 커맨드이며 새 플래그 두 개 추가: `--json`(Finding 목록을 JSON 배열로 출력) 및 `--quick`(느린 컨테이너 헬스/게스트 exec 프로브 생략, 로컬 저비용 검사만 실행 < 1초). **`winpodx info`** 와 **`winpodx check`** 는 기존 출력/동작을 완전히 유지하되, 이제 stderr 에 한 줄 deprecation 알림 출력: `[deprecated] 'winpodx info' will be removed in 0.7.0; use 'winpodx doctor'`(check 도 동일 패턴). 기존 `pod ` 서브커맨드는 0.6.x 동안 모두 등록 상태 유지 — 공유 핸들러에 위임하기 전 동일한 deprecation 줄 출력; 별칭은 0.7.0 에서 제거. 완전한 구 → 신 커맨드 매핑: `pod apply-fixes` → `guest apply-fixes`; `pod sync-guest` → `guest sync`; `pod sync-password` → `guest sync-password`; `pod multi-session` → `guest multi-session`; `pod recover-oem` → `guest recover-oem`; `pod install-status` → `install status`; `pod install-resume` → `install resume`; `pod grow-disk` → `install grow-disk`; `pod disk-usage` → `install disk-usage`; `info` → `doctor`; `check` → `doctor`. 0.7.0 에서 deprecated 별칭 제거. `docs/design/ROADMAP-0.6.0.md` 항목 G 참조. - **백엔드 자동 선택이 `backend/select.choose_backend` 로 일원화됨.** 어느 컨테이너 백엔드 쓸지 결정하는 곳이 세 군데였고 Ubuntu 22.04 에서 서로 의견이 갈렸음 — `install.sh` Automatic-mode picker 는 `podman → docker → libvirt` 순회 + podman <4 unusable 게이트(#271), 그런데 `cli/setup_cmd.py` non-interactive 분기는 한 줄 `"podman" if which("podman") else "docker"` 로 버전 게이트 없음 → 설치 path 가 거부한 깨진 3.4 podman 을 setup wizard 가 그냥 선택. `choose_backend(prefer, deps, podman_min_major=4)` 가 이제 단일 Python SoT: 명시적 `--backend` 우선, 그 외엔 `AUTO_PRIORITY = ("podman", "docker", "libvirt")` 순회 + 첫 usable 선택 (podman 은 `podman_major_version() >= 4` 게이트); 아무것도 없으면 `"podman"` fallback (recommended install path 가 설치 가능). `install.sh` bash picker = *하나의 의도된* 셸 미러 (pre-venv deps 프로브와 동일 패턴 — Python 설치 전 실행) + Python SoT 가리키는 주석, 테스트가 두 사본의 priority 순서 + 최소 major 를 잠금 → drift 불가능. 0.6.0 정리 작업 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 E). - **호스트 의존성 감지가 `utils/deps.py:check_all` 로 일원화됨.** 0.6.0 이전엔 각 consumer (`utils/deps.py`·`core/deps_quickcheck.py`·`cli/doctor.py`·`cli/setup_cmd.py`) 가 자체 `shutil.which()` 리스트로 FreeRDP 바이너리 검사했고 drift 발생 — `deps_quickcheck` 와 `doctor` 가 `sdl-freerdp3` / Flatpak fallback 누락 → 그 둘만 있는 호스트가 GUI Quick Start + `winpodx doctor` 에서 MISSING 으로 잘못 보고됨. `check_all()` 에 `/dev/kvm` (`check_kvm()`) 도 추가 → `setup_cmd` 의 인라인 KVM 프로브 제거, `doctor` 의 `_check_kvm` 도 위임. 모든 Python consumer 가 이제 `check_all()` / `check_freerdp()` / `check_kvm()` 거침; `install.sh` 셸 측 사전 venv 프로브만 단일 의도된 중복 (Python 설치 전 실행되어야 해서 셸 unique). regression test 가 잠금 — `deps_quickcheck.py` + `doctor.py` 에 대한 AST 스캔으로 hardcoded `shutil.which("xfreerdp...")` 가 돌아오면 CI fail. 0.6.0 정리 작업 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 D). - **버전 + Windows edition 리스트가 각각 단일 소스로 통합됨.** `pyproject.toml` 이 이제 프로젝트 버전 선언의 유일한 곳; `src/winpodx/__init__.py` 는 `importlib.metadata.version("winpodx")` 로 `__version__` derive (비-설치 source checkout 용 fallback `0.0.0+source` 명시) → 릴리즈 prep 때 한 쪽 bump 누락 불가능. 큐레이션된 Windows edition 리스트는 `core/config.py` 의 `WIN_VERSION_LABELS` dict 로 이동; CLI 도움말 (`winpodx setup --win-version`) · 인터랙티브 `setup` 프롬프트 · GUI Settings 드롭다운 모두 여기서 derive → edition 추가 = 한 줄. 패키징 버전 CI guard (`scripts/ci/verify_versions.py`) 도 더 이상 `__init__.py` 리터럴 검사 안 함, `packaging/rpm/winpodx.spec` 추가, `importlib.metadata` round-trip 검증 추가; spec 의 stale `Version: 0.1.5` 리터럴 = 현재 버전으로 bump + 새 guard 포인터 주석 추가. 0.6.0 정리 작업 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 F + N). - **`winpodx.toml` 에 `schema_version` 마커 추가 (앞으로의 config 마이그레이션 기반).** on-disk config 가 버전마다 모양이 바뀌어 왔는데 (키 rename, 섹션 이동) 지금까진 `_apply()` 의 "모르는 키 무시" 로 버텨왔음. 그건 *추가* 엔 OK 지만 *rename* / *이동* 시 사용자 데이터 손실. 저장 파일 맨 위에 `schema_version = 1` + `core/config.py` 에 `_migrate_config(data, from_version)` hook 추가 → 0.7.0+ 의 rename 이 사용자 설정 안 잃고 깨끗하게 업그레이드 가능. hook 자체는 오늘은 의도적 no-op (0.6.0 은 레이아웃 안 바꿈) — 마커만 먼저 박아둬서 기존 0.5.x 파일이 첫 load+save 때 태깅되고, 나중에 *실제로* 구조 바꾸는 릴리즈가 안전하게 변환 가능. 마커 없는 hand-edit 파일은 schema 0 (pre-0.6.0) 으로 취급. 0.6.0 정리 작업 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 J). - **agent 포트 8765 가 이제 단일 Python 상수.** `core/agent.AGENT_PORT` = 호스트 측 단일 SoT — compose 템플릿 / guest 에 푸시하는 urlacl 문자열 / `AgentClient` URL 모두 여기서 derive. 게스트 측 (`agent.ps1`, `install.bat`, `agent-keepalive.ps1`, `agent-respawn.ps1`) = PowerShell 이 Python 상수 import 못해서 자체 리터럴 유지 — 짝 관계는 문서화 + 테스트로 잠금, `install.sh` 의 `/health` curl 도 상수 가리키는 주석 포함. 순수 내부 정리; 동작 변화 0. 0.6.0 정리 작업 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 C). - **레거시 FreeRDP host→guest fallback 이 이제 로그에 남음 (퇴역 준비용 계측).** 게스트 agent 가 생기기 전엔 host→guest 명령을 FreeRDP RemoteApp 위 PowerShell 로 보냈음; 그 경로가 agent 도달 불가 시 조용한 fallback 으로 아직 남아있음. `debug` 레벨이라 실제로 얼마나 자주 발동하는지 알 수 없었음. 이제 매 fallback 이 `winpodx.log` 에 `FreeRDP-fallback` 태그 `WARNING` 으로 남음 — 이유(agent `/health` detail) + 어느 작업이 fallback 했는지 포함. `grep -c FreeRDP-fallback ~/.local/state/winpodx/winpodx.log` 로 빈도 카운트. 동작 변화 0 — 무작정 빼서 깨뜨리지 않고, 실사용 데이터 보고 agent 복구 강화 + fallback 축소 수위를 결정하기 위한 측정. - **로깅 위생 정리 — 모듈 전반에서 로거 변수명 통일 (0.6.0 항목 L).** `reverse_open/` 패키지와 `gui/reverse_open_panel.py` 는 모듈 로거를 `logger` 로 정의했고, 나머지 ~50개 모듈은 `log` 를 씀 (둘 다 `logging.getLogger(__name__)`); 호출부를 모두 `log` 로 표준화. 로그 레벨 일괄 점검 결과 이미 일관됨 — 실패 / fallback / 거부는 `WARNING`(콘솔 임계값), 정상 마일스톤은 `INFO`, 진단은 `DEBUG` — 라 레벨 변경은 불필요. 순수 내부 정리, 동작 변화 0: 로거 이름은 Python 변수와 무관하게 모듈 경로라 로그 출력은 byte 단위로 동일. 0.6.0 통합 작업의 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 L). - **첫 실행 discovery 재시도를 6→2 로 축소 (0.6.0 항목 M).** 프로비저닝 체인(`winpodx provision` — 신규 설치 시 `install.sh` 가, 그리고 `setup` 자동 프로비저닝 경로가 실행)이 게스트에서 설치된 앱을 스캔할 때, 게스트 agent 가 잠깐 준비 안 됐으면 지수 백오프로 discovery 를 재시도함. `install.sh` 가 쓰던 고정 6× 루프는 `WinpodxAgentKeepAlive` 워치독(#359) 이전 시절 것 — 이제 keep-alive 가 agent 를 안정적으로 살려두므로 6회는 과하고 깨끗한 첫 실행만 느리게 함. `winpodx provision --retries` 기본값과 `finish_provisioning(retries=...)` 기본값을 모두 **2** 로 낮춰, discovery 는 보고 전 최대 1회(2 s 백오프) 재시도. `migrate` / `pending.resume` 업그레이드 경로는 기존 `retries=3` 유지(항목 B 에서 의도적으로 설정, 여기 범위 밖). 느린 게스트엔 `winpodx provision --retries N` 으로 재정의. 0.6.0 통합 작업의 일부 (`docs/design/ROADMAP-0.6.0.md` 항목 M). - **0.6.0 용 문서 갱신 (0.6.0 항목 H).** `README.md`, `docs/USAGE.md`, `docs/ARCHITECTURE.md`, `docs/FEATURES.md`, `docs/INSTALL.md` 와 각 `docs/*.ko.md` 미러가 0.6.0 형태 반영: 새 `winpodx guest` / `winpodx install` / `winpodx doctor` 명령 표면 (기존 `pod ` 는 deprecated alias 로 명시, 0.7.0 에서 제거), Thin AppImage 재설계 (FreeRDP + Python + Qt + winpodx 만; 호스트 컨테이너 런타임 필요; #357 / #363 근본 해소), 그리고 `winpodx provision` 을 pod 기동 후 단일 체인으로 명시. `USAGE.md` cheat-sheet 의 기존 "Pod 관리" 블록은 세 섹션 (`pod` 라이프사이클 / `guest` 작업 / `install` 작업) 으로 분리, System 섹션에 새 `doctor` 플래그(`--json`, `--quick`, `--fix`) + `provision` + `migrate` 추가. 양쪽 `README.md` 의 버전 블러브를 0.6.0 으로 재작성. ### Fixed - **UWP / 스토어 앱이 이제 Linux 작업 표시줄에 나타납니다 (#472).** UWP 앱의 보이는 프레임은 `ApplicationFrameHost` 소유이며 FreeRDP RAIL 로 `_NET_WM_STATE_SKIP_TASKBAR` / `SKIP_PAGER` 표시가 붙어 도착해서, 창 클래스가 이미 런처와 매칭됐는데도 패널에서 빠졌습니다. 이제 winpodx 가 실행 후 `wmctrl` 로 창을 다시 리스트합니다(best-effort, X11 / XWayland; 그 외엔 no-op). - **경로에 공백이 든 호스트 파일 열기가 이제 동작합니다 (#473).** FreeRDP RemoteApp 명령에 넘기는 UNC 경로(`\\tsclient\home\…`)를 이제 큰따옴표로 감싸므로, 게스트가 더 이상 공백에서 분리하지 않습니다 — 이전엔 앱이 첫 토큰만 받고 "path not found" 라고 보고했습니다. - **배율이 다른 듀얼 모니터 호스트에서 앱 실행 실패 / 창 멈춤 문제 (#474).** KDE Plasma 6 Wayland(XWayland) + 두 모니터가 *서로 다른* fractional 배율일 때, 기본 `/span` 다중 모니터 데스크탑이 이중으로 깨졌습니다: 컴포지터의 소수점 반올림이 두 모니터의 논리 사각형을 타일링 불가로 만들어(1 px 틈, 높이 불일치, `desktopScale: 0`) FreeRDP 가 `/span` 을 `pre_connect` 에서 거부(`ERRCONNECT_PRE_CONNECT_FAILED`) → 아무것도 실행 안 됨; 그리고 세션이 떠도 RAIL 창을 다른 배율 모니터로 드래그하면 **멈추고 입력을 안 받았습니다** — FreeRDP RAIL + XWayland 가 모니터별 fractional 배율 간 창 재매핑을 못 함(어떤 클라이언트 플래그로도 못 고치는 상류 한계). 이제 winpodx 가 **모니터별 배율을 감지**해서(KDE `kscreen-doctor`, wlroots `swaymsg`/`hyprctl`) 다르면 **RemoteApp 을 주 모니터에 고정** — 거기서 앱이 뜨고 정상 반응 — 하고, 두 모니터를 같이 쓰는 유일한 실제 해법을 로그로 안내합니다: **배율을 같게** (또는 KDE 디스플레이 → 레거시 애플리케이션 → *애플리케이션이 직접 배율*, XWayland 가 xfreerdp 에 균일한 픽셀 그리드를 줌). 배율이 **균일**하면 `/span` 을 예전 그대로 사용(창이 모니터 간 자유 이동). 단일 모니터 / 감지 불가는 영향 없음; `cfg.rdp.multimon = "off"` 로 무조건 단일 모니터 강제. 연결 시점 백스톱으로, 그래도 그 pre_connect 모니터 에러로 죽는 실행은 단일 모니터 데스크탑으로 재시도합니다. - **실행 중인 RDP 세션을 GUI 와 트레이에서 종료 (#450, #452, #453).** GUI Dashboard 의 **Running sessions** strip 이 각 라이브 앱 세션을 나열하고 클릭하면 종료하며; 트레이에 **Terminate Session** 서브메뉴가 생겼습니다. 종료는 이제 리더만이 아니라 FreeRDP 프로세스 그룹 전체에 시그널을 보내고(#458) 멈춘 세션은 강제 종료할 수 있습니다(#459); 트레이는 또한 데스크탑의 StatusNotifier 호스트가 올라올 때까지 표시를 재시도하고 하트비트마다 다시 단언하므로 호스트 재시작을 견딥니다(#455, #465). - **Dashboard 리소스 센터가 이제 CPU / RAM / 디스크를 실제로 표시하고 빠르게 갱신됩니다.** 실제 환경에서 세 문제가 겹쳤습니다: (1) `podman/docker stats` 를 설정된 컨테이너 이름으로 질의했지만 podman-compose 가 이름에 prefix 를 붙여(`winpodx_winpodx-windows_1`) 프로브가 "no such container" 에 걸려 모든 게이지가 비었음 — 이제 ` ps --filter name=` 로 이름을 먼저 해석; (2) CPU/RAM 프로브와 게스트 디스크 프로브가 순차 실행돼 느린 디스크 읽기가 스냅샷 전체를 막았음 — 이제 하드 캡과 함께 동시 실행하고, 디스크는 느린 주기로 샘플링하며, 읽기 사이에 비우지 않고 마지막 정상값을 캐시; (3) 디스크 프로브가 매 폴링마다 게스트에 보이는 PowerShell 창을 띄우는 FreeRDP RemoteApp 으로 폴백했음 — 이제 **agent 전용**(창 없는 `/exec`)이라 수동 대시보드가 콘솔을 띄우지 않음. 마지막으로 rootless podman 은 `stats` 에서 CPU%(때로는 MEM)를 `--` 로 보고하는 경우가 흔해 정상 pod 에서도 게이지가 비었는데, 이제 컨테이너의 **cgroup v2** 파일을 직접 읽어(`memory.current` 로 RAM, `cpu.stat` 의 `usage_usec` 델타로 CPU%) 채우므로 `stats` 가 비우는 rootless 환경에서도 CPU 와 RAM 이 표시됩니다. 많은 rootless 슬라이스는 사용자에게 `cpu`/`pids` 만 위임하고 `memory` 는 위임하지 않아 `cpu.stat` 은 읽히는데 `memory.current` 가 없는데 — 이때 RAM 은 컨테이너 cgroup 의 모든 프로세스 `VmRSS` 합산(커널 집계, 컨트롤러 무관; dockur 의 QEMU 프로세스가 대부분 차지)으로 폴백하므로 거기서도 RAM 게이지가 채워집니다. - **Dashboard RAM 게이지가 이제 게스트 실제 사용량을 표시하고, 패널이 더 빨리 그려집니다.** 호스트 측 메모리 읽기(cgroup `memory.current` 든 프로세스 `VmRSS` 합산이든)는 전부 *VM* 의 메모리를 보고했는데 — dockur(QEMU) 백엔드는 호스트가 게스트 RAM 거의 전부를 resident 로 봐서 게이지가 100% 근처에 박혀 있었습니다. 이제 RAM 은 **Windows 내부**에서 agent 를 통해(`Win32_OperatingSystem` 의 total/free 물리 메모리) 가져옵니다 — 게스트가 실제로 쓰는 양을 반영하는 유일한 값. 디스크 프로브의 단일 agent 왕복(둘 다 한 `/exec`)과 느린 주기를 공유하고, 마지막 값을 캐시해 폴링 사이에 비지 않습니다. CPU 는 호스트 측이지만 이제 cgroup `cpu.stat` 델타(즉시 파일 읽기)를 *먼저* 읽고 수 초 걸리는 `podman stats` 샘플링은 폴백으로만 써서, 매 틱마다 멈칫하지 않고 패널이 채워집니다. - **Dashboard 가 이제 창이 열리는 순간부터 자동 갱신됩니다.** 라이브 갱신 타이머가 nav 의 *페이지 전환* 핸들러에서만 시작됐는데, Dashboard 는 시작 시 보이는 기본 페이지라 전환이 안 일어나서 — 패널이 한 번 그려진 뒤 다른 페이지로 갔다 와야 갱신됐습니다. 이제 Dashboard 빌드 시 타이머를 시작하므로, 실행 직후부터 리소스와 pod 상태가 스스로 갱신됩니다. - **좁거나 fractional 배율 창에서 GUI 페이지가 잘리거나 가로 스크롤바가 생기지 않습니다.** "All apps" 의 Pinned/Recent 선반이 wrap 안 되는 행이라, 선반이 길면 페이지 전체가 viewport 보다 넓어져서 앱 그리드 오른쪽이 잘렸습니다(hi-DPI fractional 배율서 가장 심함). 이제 모든 페이지의 스크롤 영역이 가로 스크롤바를 끄고, 타일 그리드는 타일당 폭을 조금 더 잡아 마지막 열이 넘치지 않게 하며, Dashboard/All-apps 선반은 실제로 들어가는 타일 수만큼만 표시(나머지는 아래 그리드에서 접근 가능)하고 리사이즈 시 재배치합니다. - **Settings 콤보박스가 *지나가며* 스크롤할 때 값이 바뀌지 않습니다.** Settings 페이지를 스크롤하다 드롭다운 위를 지나가면 값(배율, 백엔드, 에디션 등)이 조용히 바뀌었습니다. 이제 콤보박스/스핀박스는 포커스되지 않으면 휠을 무시하고(값 조정하려면 먼저 클릭) 휠은 페이지 스크롤로 넘어갑니다. - **업그레이드가 실행 중인 GUI / 트레이를 재시작해 새 버전이 즉시 적용됩니다 (#467).** 인스톨러가 업그레이드 끝에 실행 중인 `winpodx tray` / GUI 를 우아하게 재시작합니다(`setsid` 경유라 인스톨러보다 오래 살아남음); pod 는 계속 실행 중. fresh 설치에선 no-op. - **파일 공유가 이제 Fedora Atomic / Silverblue / Kinoite 에서 동작합니다 (#418, #420).** 거기선 `/home` 이 `/var/home` 의 symlink 라서, UNC 경로 변환이 홈 폴더 파일을 "공유 위치 밖" 으로 보고했습니다. 이제 비교 전에 home 과 media 베이스 경로를 실제 대상으로 resolve 합니다. - **Setup 이 컨테이너 백엔드 데몬이 실제로 도달 가능한지 검증합니다 — PATH 의 CLI 설치 여부만이 아니라 (#395, #419).** 죽은 데몬(예: 오래된 `DOCKER_HOST`)을 뒤에 둔 podman / docker CLI 가 `PATH` 에 있으면 의존성 체크를 통과한 뒤 pod 시작에서 실패하곤 했습니다; 이제 체크가 도달성을 프로브하고 명확한 DAEMON DOWN 상태를 보고합니다. - **`manual` 백엔드가 이제 loopback 대신 VM 주소에서 게스트 에이전트에 도달합니다 (#426).** 에이전트 클라이언트가 Windows 머신이 다른 곳에 있어도 항상 `127.0.0.1:8765` 를 노렸습니다 — 그래서 VM 을 가리키는 `manual` 백엔드(예: `LTSC11P.local` 의 VMware 게스트)에서 RDP 는 됐지만 모든 에이전트 기반 기능이 FreeRDP-only 로 떨어지고, 에이전트가 VM 주소에서 멀쩡히 응답하는데도 `winpodx check` 가 `agent_health` 도달 불가로 보고했습니다. 이제 `AgentClient` 가 `cfg.rdp.ip` — RDP 도달성 체크가 쓰는 바로 그 주소 — 에서 호스트를 derive 하므로, `manual` 에선 VM 을 따라가고 podman/docker 에선 `127.0.0.1` 로 남습니다(컨테이너가 8765 를 호스트 loopback 에 publish 하고 `cfg.rdp.ip` 가 이미 loopback). 호출부 변경 없음; 클라이언트 생성자 한 곳 수정. - **Reverse-open 의 Windows shim 이 더 이상 Microsoft Defender 에 격리되지 않습니다 (#425).** `winpodx-reverse-open-shim.exe`(작고 stripped 된 서명 없는 Rust 바이너리)가 Defender 의 ML 휴리스틱에 `Trojan:Win32/Rafvartar!rfn` — false positive — 로 걸립니다. `install.bat` 은 이미 `C:\OEM` 과 `C:\winpodx` 를 제외했지만, `register-apps.ps1` 은 shim 과 슬러그별 `winpodx-.exe` 사본을 **`C:\Users\Public\winpodx`** 아래 staging 했는데 거기는 제외 안 돼서 — Defender 가 격리하고 reverse-open 이 조용히 깨졌습니다. 첫 부팅 Defender-제외 단계가 이제 `C:\Users\Public\winpodx` 와 `winpodx-reverse-open-shim.exe` 프로세스도 커버합니다. shim 은 또한 임베디드 Windows VERSIONINFO 리소스(CompanyName / ProductName / FileDescription / 버전 — `winresource` 빌드 스크립트 경유)와 함께 빌드됩니다 — 메타데이터 없는 stripped PE 가 AV 휴리스틱에서 더 나쁜 점수를 받으므로, 실제 publisher 정보를 박는 것이 슬러그별 사본도 상속하는 값싼 정당성 신호입니다. (장기적으론 shim 을 Authenticode 서명 / MS 에 false positive 제출해야 하지만, 제외가 winpodx 자체 게스트의 turnkey 픽스입니다.) - **Setup 탭의 "Enable reverse-open" 체크박스가 이제 리스너를 라이브로 시작/중지합니다 (#425).** 박스 체크는 `cfg.reverse_open.enabled` 만 뒤집고 상태 라벨만 새로고침했음 — 데몬은 다음 pod 기동까지 떠 있지 않아 사용자가 "Start daemon" 도 눌러야 했습니다. 이제 체크박스가 활성화 시 즉시 리스너를 시작하고 비활성화 시 중지합니다(플래그 영속). best-effort + 조용함: 게스트가 아직 안 떠 있으면 플래그는 그대로 영속되고 리스너는 다음 pod 시작 때 올라오며, 그 예상된 경우엔 에러 모달이 없습니다. - **Fresh 설치가 dockur 가 bridge-NAT 네트워킹을 고르는 호스트에서 `wait-ready` 에 영원히 멈추지 않습니다 (#269, #387).** 게스트 포트 8765 의 에이전트가 Windows 부팅 후 호스트에서 도달 불가였음 — noVNC 가 완료된 데스크탑을 보여주는데 `wait-ready` 가 무한 폴링. 근본 원인: compose 가 `USER_PORTS: "8765"` 를 설정했지만 dockur 의 네트워크 모드는 auto-select 로 뒀음. dockur 의 기본 **bridge-NAT** 경로가 성공하는 호스트(주로 rootful 백엔드)에선 그 경로가 *`USER_PORTS` 를 조용히 무시* 해서, QEMU 가 8765 를 게스트로 포워딩하지 않았습니다(Podman 호스트↔컨테이너 publish 는 정상; 컨테이너↔게스트 hop 이 갭). dockur 는 user-mode(passt) / slirp 경로에서만 `USER_PORTS` 를 참조 — 그래서 rootless 호스트는 영향 없었던 것(rootless 는 bridge 를 못 만들어 dockur 가 이미 passt 로 fallback 해 포트를 포워딩). compose 가 이제 `NETWORK: "user"` 를 핀해서 dockur 가 호스트 무관하게 항상 포트 포워딩 user-mode 경로를 씁니다. winpodx 는 게스트에 포워딩 포트(RDP 3389, 웹 뷰어 8006, 에이전트 8765)로만 도달하고 게스트를 호스트 LAN 에 둘 필요가 전혀 없으므로 user-mode 가 여기선 올바른 모드입니다; rootless 호스트엔 no-op(이미 passt 였음). - **`install.sh` 가 Recommended 모드에서 너무 오래된 podman 으로 무작정 진행하지 않습니다 (#271).** Automatic 모드는 이미 `podman < 4` 호스트(Ubuntu 22.04 = 3.4)를 docker / libvirt 로 넘겼지만, Recommended 모드와 명시적 `--backend podman` 은 그 폴백을 건너뛰어서 — 설치가 끝까지 진행된 뒤 *패키지를 깐 다음에야* 프로비저닝에서 실패했음. 이제 백엔드 확정 직후(어떤 패키지 설치 전에) 게이트가 동작: 선택된 백엔드가 podman 인데 너무 오래됐으면, 대화형 실행은 설치된 docker / libvirt 로 전환(또는 위험 감수 진행 / 중단)을 묻고, 비대화형 실행은 **시스템을 변경하지 않고** 깨끗이 종료하며 `--backend docker|libvirt`, podman 업그레이드, 또는 강제용 `--allow-old-podman` / `WINPODX_ALLOW_OLD_PODMAN=1` 을 안내합니다. #271 의 "graceful exit" 부분을 완성합니다(distro/버전 체크 + 런타임 선택기는 `install.sh` v2 작업서 이미 제공됨). - **업그레이드 시 더 이상 설치 모드를 묻거나 옛 첫부팅 로그를 재생하지 않습니다.** 업그레이드/재실행 UX 두 가지: (1) `install.sh` 가 winpodx config 가 이미 있어도 `[R]ecommended / [A]utomatic / [C]ustom / [N]o` 모드 메뉴를 띄웠음 — 업그레이드는 기존 backend/config 를 재사용하고 `migrate` 를 돌리므로 무의미. 이제 기존 설치를 감지해 프롬프트 없이 Automatic 으로 해결. (2) `pod wait-ready --logs`(업그레이드의 migrate 경로가 사용)가 `podman logs --tail 100` 으로 tail 했는데, 이미 떠있는 컨테이너에선 *원래* 첫부팅 ISO 다운로드 + 이미지 빌드 출력을 재생 — `--verbose` 에선 매 업데이트마다 Windows 를 다시 받는 것처럼 보였음. 이제 pod 가 이미 떠있으면(RDP 도달 가능) 기록을 재생하지 않고(`--tail 0`), 진행 중 다운로드를 보여줄 가치가 있는 진짜 첫부팅에만 `--tail 100` 유지. - **Flatpak FreeRDP 가 이제 풀데스크탑이 아니라 앱별 RemoteApp 을 띄웁니다.** `flatpak run com.freerdp.FreeRDP` 는 앱의 *기본* 명령 — RAIL 없는 **SDL** 클라이언트(FreeRDP #9078) — 를 실행해서, Flatpak 으로 앱 하나를 띄우면 단일 앱 창 대신 Windows 전체 데스크탑/로그인 화면이 떴습니다. 이제 WinPodX 가 Flatpak 을 `flatpak run --command=xfreerdp … com.freerdp.FreeRDP` 로 호출해 RAIL 되는 유일한 클라이언트인 X11 `xfreerdp` 바이너리를 강제하고, WinPodX 의 모든 RDP 플래그가 필요로 하는 샌드박스 권한을 부여해서 Flatpak 이 네이티브 클라이언트처럼 동작합니다: `--share=network`(로컬 RDP), `--socket=x11`/`--socket=wayland`(RAIL + 클립보드), `--socket=pulseaudio`(사운드), `--socket=cups`(프린터), `--device=dri`(디스플레이), `--filesystem=home` + 이동식 미디어 마운트 루트(`\\tsclient\home` + `\\tsclient\media` 드라이브 리다이렉션). 0.6.0 이 Flatpak 우선 사용을 시작한 뒤 드러났습니다. - **설치 모드 메뉴(R/A/C/N)가 이제 `curl … | bash` 에서도 동작합니다.** 대화형 모드 프롬프트(+ Custom 모드의 백엔드/GUI 서브프롬프트, 의존성 설치 확인)가 `[ -t 0 ]` 으로 게이트되고 stdin 에서 읽어서, 표준 `curl … | bash` 설치(stdin 이 스크립트 파이프지 터미널이 아님)에선 조용히 스킵되고 항상 Recommended 로 기본 처리됐습니다. 이제 install.sh 가 제어 터미널 `/dev/tty` 도 감지하고 모든 프롬프트를 거기서 읽으므로, 터미널에서 실행한 `curl … | bash` 는 모드 메뉴를 띄우고 선택할 수 있습니다(라이브 진행 라인이 출력에 이미 쓰는 `/dev/tty` 트릭과 동일). 완전 비대화형 실행(CI / cron / stdin 과 `/dev/tty` 둘 다 없음)은 비대화형으로 남아 Recommended 기본, `--mode` / `WINPODX_MODE` 는 여전히 프롬프트 없이 미리 선택합니다. - **Fresh 설치 프로비저닝이 flaky 게스트 agent 를 타이머로 포기하지 않고, 실제로 회복될 때까지 기다립니다.** agent-first settle 게이트와 discovery 단계가 고정 윈도(settle ~30초 상한; discovery 2회 재시도, 수 초)로 포기해서, 첫 부팅 reboot 직후 게스트 agent 가 잠깐 죽으면 — keepalive 워치독 사이클(~60초)만큼 늦을 수 있음 — 프로비저닝이 deferred 되고 설치는 나중에 `winpodx app refresh` 로만 끝났습니다. 이제 두 단계 모두 **시간 캡 없이 pod liveness 신호 기반**으로 대기합니다: pod 가 `RUNNING` 인 한 `WinpodxAgentKeepAlive` 워치독이 agent 를 되살리므로, `/health` 를 폴링하며 안정적으로 올라오면(settle 은 3회 연속 OK) 진행 — agent 가 얼마가 걸리든 discovery 를 **inline 으로** 완료합니다. 대기는 pod 자체가 멈출 때만 끝납니다(워치독 없음 → 진짜 회복 불가). `pod wait-ready` 의 wget-ETA 동적 deadline(#126) 과 같은 "실제 신호로 bound, 임의 시간 아님" 철학. bounded 재시도 budget(`--retries`, 0.6.0 항목 M)은 이제 비-agent 일시적 discovery 에러에만 적용됩니다. `winpodx migrate`(업그레이드 경로)도 동일한 인내 회복을 받습니다. - **deferred 된 fresh 설치가 더 이상 스스로 롤백되지 않습니다.** Windows 다운로드+부팅은 잘 됐는데 agent-first discovery 가 deferred 된 fresh `install.sh` 실행에서(`winpodx provision` exit 5 — 예: 첫 부팅 reboot 후 게스트 agent 가 1분 늦게 올라옴), install.sh 가 설치 전체를 롤백해서 — 스스로 끝날 상태인데 ~15분짜리 ISO 다운로드+부팅을 날렸습니다. 근본 원인: bash 는 `set +e` 중에도 실패한 *파이프라인* 에 `ERR` trap 을 발동시켜서, rc 처리(이미 non-zero provision 을 "pending 기록, 설치 유지" 로 다룸)가 돌기 전에 롤백 trap 이 provision 파이프에서 발동했습니다. 이제 provision 호출이 `set +e` 에 의존하지 않고 `ERR` trap 을 명시적으로 비활성(`trap - ERR` / 이후 재무장)하며, exit 4(wait-ready 가 오래 걸림) / exit 5(discovery deferred)를 명시적 **deferred-실패아님** 케이스로 처리합니다: 남은 단계를 pending 으로 기록하고(다음 `winpodx` 실행 또는 `winpodx app refresh` 시 자동 재개) 설치를 유지합니다. 진짜 초기 실패(deps / venv / 컨테이너 생성)는 이전처럼 trap 으로 롤백됩니다. - **설치 / provision 이 이제 등록한 앱을 카운트만이 아니라 목록으로 보여줍니다.** discovery 단계가 공유하는 `_register_desktop_entries` 는 `Registered N app(s) in your desktop menu.` 카운트만 찍어서 — `winpodx provision` / `install.sh` / `winpodx migrate` 중 사용자가 *어떤* 앱이 들어왔는지 `--verbose` 로도 못 봤습니다. 이제 등록된 앱 이름을 하나씩 나열하고(`winpodx app refresh` 와 동일), discovery 카운트와 등록 카운트가 다르면(번들 프로필이 없는 discovered 앱은 등록 불가) `Note: N discovered app(s) had no bundled profile and were not added to the menu: …` 한 줄로 그 차이를 설명합니다 — 더 이상 조용히 사라지지 않습니다. - **`migrate --verbose` 가 두 apply 경로 중 하나에만 전달됐습니다 (방금 친 verbose 픽스의 회귀).** `run_migrate` 의 cross-version 업그레이드 분기가 `_apply_runtime_fixes_to_existing_guest(non_interactive)` 를 `verbose=verbose` 없이 호출해서 — `install.sh` 가 `migrate --non-interactive --verbose` 로 모는 바로 그 경로에서 — 실제 버전 교차 업그레이드 시 raw 컨테이너 firehose 가 여전히 누락됐습니다(그 분기에서 `--verbose` 로컬이 dead). 두 호출부 모두 이제 `verbose=verbose` 를 전달합니다. - **Windows-exec 래퍼 스크립트가 더 이상 world-readable 로 쓰이지 않습니다.** `run_in_windows()` 가 `~/.local/share/winpodx/windows-exec/.ps1` (와 그 디렉터리)를 프로세스 umask(보통 `0644`/`0755`)로 썼습니다. FreeRDP-폴백 비밀번호 회전 경로에선 그 스크립트가 갓 회전된 평문 Windows 비밀번호를 담으므로, 다른 로컬 사용자가 잠깐 읽을 수 있었습니다. 이제 디렉터리는 `0700`, 스크립트는 `0600` — agent 토큰 / `compose.yaml` / config / 회전 마커가 이미 쓰는 방식과 동일. - **세션 lock `.cproc` 가 launch 중 잠깐 빈 파일로 truncate 되지 않습니다.** `launch_app` 이 FreeRDP 자식 PID 를 알기 전에 PID lock 파일을 `"w"` 모드(0바이트로 truncate)로 열어서, 동시 reader(`list_active_sessions` / idle 모니터)가 빈 파일을 보고 살아있는 세션 lock 을 corrupt 로 판단해 unlink 할 수 있는 창이 있었습니다. 이제 lock fd 를 `O_TRUNC` 없이 열고, 자식이 생긴 뒤 `flock` 하에 `ftruncate`+`write`+`fsync` 한 번으로 PID 를 씁니다 — reader 는 이전 PID 또는 새 PID 만 보고 빈 파일은 절대 못 봅니다. - **경로에 콤마가 든 discovered Windows 실행파일이 더 이상 FreeRDP `/app:` 서브키를 주입할 수 없습니다.** FreeRDP-3 `/app:program:,name:` 인자가 `app_executable` 을 raw 로 보간했는데, 인접한 `default_args` 와 UWP AUMID 는 바로 이것 때문에 이미 sanitize/검증 중이었습니다. 이제 `app_executable` 도 결합 인자 생성 전 동일한 콤마→스페이스 처리를 받습니다. - **pod resume 실패가 이제 긴 불투명 RDP 타임아웃 대신 명확한 메시지로 즉시 실패합니다.** `ensure_pod_awake` 가 `resume_pod` 결과를 무시해서, 실제로 unpause 안 된 pod 가 그대로 `launch_app` 에 넘어갔습니다. 이제 resume 시도 후 `is_pod_paused` 재확인하고, 여전히 paused 면 `ProvisionError("failed to resume paused pod …")` 를 raise 합니다. - **첫 부팅 discovery 가 총 앱 수가 그대로여도 UWP 갭을 메운 retry 를 유지합니다.** `discover_apps` 의 retry-on-empty 가 총 개수가 엄격히 더 많을 때만 retry 결과를 유지해서, 누락된 UWP(스토어) 앱을 회복했지만 총 개수가 같은 retry — retry 를 촉발한 바로 그 신호 — 가 버려졌습니다. 이제 UWP 항목이 더 많은 retry 도 유지합니다. - **프로비저닝 통합(0.6.0 항목 B)의 첫 컷이 획일화하며 날린 4개 동작 복구.** 4개 post-create 경로를 `finish_provisioning` 으로 합치면서 조용한 wait + 단일 agent 게이트로 획일화 → 각 경로가 이슈별로 쌓아둔 동작을 잃었음: (1) **동적 대기** — 자기지움 라이브 진행줄 + 느린 링크용 wget-ETA deadline 자동확장(#126) — 이제 fresh 설치가 한 줄 뒤 침묵하는 멈춤처럼 안 보임; wait-ready 단계를 rich `pod wait-ready` 로 라우팅하는 주입식 `wait_fn` 으로 복구. (2) **agent-first 설치 보호**(#271): `require_agent=True` 가 이제 apply+discovery 단계 내내 `WINPODX_REQUIRE_AGENT=1` 을 export (one-shot settle 재프로브뿐 아니라) → discovery/apply 가 first-boot 중 install.bat autologon 세션을 kick 할 수 있는 FreeRDP fallback 대신 defer; discovery 의 지속적 agent-unavailable 은 generic 실패 기록 말고 pending(exit 5)으로 깨끗이 defer. (3) **업그레이드 → `winpodx migrate`**: `install.sh` 가 이제 fresh/upgrade 분기 — fresh 는 `provision --require-agent`, upgrade 는 `migrate`(기존 guest 에 갱신된 스크립트/`agent.ps1` 동기화 + 이미지 핀 + 동일 apply→discovery→reverse-open 체인); 첫 컷은 둘 다 `provision` 돌려 업그레이드 guest 가 stale 스크립트로 남았음. (4) **`pending.resume` "migrate" step** 도 이제 `guest_sync` 실행 → 나중에 resume 된 deferred 업그레이드도 guest 스크립트 갱신. `migrate` 는 reverse-open 단계도 획득(이제 업그레이드 경로의 유일 driver). `docs/design/PROVISION_UNIFY_FIDELITY_AUDIT.md` 참조. - **`install.sh` 가 이제 reverse-open 아이콘 의존성을 설치 → placeholder 아이콘 대폭 감소.** `curl | bash` 설치는 winpodx 를 venv 없이 시스템 `python3` 로 돌려서, winpodx 의 정식 아이콘 의존성 — `cairosvg`(SVG→PNG) + `pyxdg`(전체 freedesktop 아이콘 테마 해석) — 이 없었음. 그래서 아이콘이 SVG 이거나 비-Hicolor 테마(Papirus, breeze, …)에 있는 Linux 앱은 Windows "연결 프로그램" 메뉴에서 일반 placeholder 로 떨어짐 (한 smoke 의 placeholder 18개 중 13개가 단지 `cairosvg` 누락). 이제 설치 시 없으면 `python3-cairosvg` + `python3-pyxdg` 를 distro 패키지 매니저로 설치 — best-effort + 비차단(앱은 정상 실행; 패키지나 sudo 가 없으면 placeholder 로 graceful degrade). AppImage 는 이미 둘 다 번들이라 영향 없었음. - **Guest sync 가 더 이상 콘솔 창을 깜빡이거나 업그레이드 후속 체인을 망치지 않음.** guest sync 가 실제로 처음 발화할 때(예: 0.5.8 → 0.5.9 업그레이드 — 스탬프가 호스트보다 오래됨), OEM-pull / urlacl / agent-재시작 단계가 FreeRDP RemoteApp(`run_in_windows`)으로 갔는데 — 호출마다 보이는 PowerShell/콘솔 창이 뜸(콘솔 깜빡임 회귀). 이제 guest sync 의 나머지처럼 windowless agent `/exec` 채널(`run_via_transport`)로 감 (세 단계 모두 agent 가 살아있을 때 실행 — 재시작의 `/exec` 는 scheduled task 가 agent 를 죽이기 전에 리턴). 별도로 agent 재시작이 fire-and-forget 이라 설치 후속 작업(migrate apply 체인, 앱 discovery, reverse-open)이 재기동을 레이스해서 agent 도달 불가 → pending-resume 로 떨어짐. 이제 `sync_guest` 가 재시작 후 agent `/health` 가 다시 응답할 때까지(여유롭게, bounded) 기다린 뒤 리턴 → 후속 단계가 살아있는 agent 를 봄. 재기동이 비정상적으로 느리면 pending-resume 가 backstop. - **다색 XPM 으로만 배포되는 reverse-open 아이콘(예: veracrypt)이 더 이상 빈 placeholder 로 떨어지지 않음.** Pillow 내장 XPM 디코더는 픽셀당 1글자(≤256색)만 처리 — veracrypt 아이콘(*유일한* 아이콘이 `/usr/share/pixmaps/veracrypt.xpm`, 1770색/2글자per픽셀)은 `KeyError` 나서 Windows "연결 프로그램" 메뉴에 일반 placeholder. 이제 winpodx 가 그런 XPM 을 작은 **순수 파이썬** 리더로 디코드(컬러테이블+픽셀행 파싱 → RGBA 이미지 직접 생성) → 실제 아이콘 표시. 신규 의존성 0, 외부 도구 0 — AppImage 포함 모든 설치 방식에서 동일하게 동작. 남는 placeholder 는 진짜 불가피한 경우뿐(앱이 아이콘을 아예 안 배포). ### 기여자 이번 릴리즈에서 다룬 이슈를 제보해 주신 분들께 감사드립니다: @ismikes (#269, #357, #387, #393, #450), @vlombardino (#271), @vkkindia (#286), @vw72 (#319), @jmayniac (#363), @MirzaAyBaig12 (#366), @urbantigerau (#395), @notnotno (#418, #425), @sundaysfantasy (#426), @mhmdzaien (#473), @nemonein (#474) — 그리고 코드 기여 @ismikes (#423). ## [0.5.9] - 2026-05-27 ### Highlights **다국어 UI, 스스로 커지는 디스크, 재설치 없이 갱신되는 게스트.** - 트레이 / GUI / CLI 전체가 번역 가능해졌고 **7개 언어로 완전 번역** (en/ko/zh/ja/de/fr/it) — 시스템 로케일에서 자동 선택, `winpodx language` 또는 GUI 드롭다운으로 변경 (한국어 잔존 텍스트 #335 수정). - Windows C: 드라이브가 가득 차면 **스스로 커지고** — `winpodx pod grow-disk` 로 수동 확장도 가능 — dockur 가 남기는 WinRE 복구 파티션 너머까지 확장 (#318). - **게스트 동기화**: 호스트에서 winpodx 업그레이드 시 새 agent / fixes / 바이너리를 실행 중인 게스트에 재설치 없이 적용 (`winpodx pod sync-guest`, pod 시작 시 자동). - **로그인 시 pod 자동시작** 이 실제 opt-in 기능으로 (`winpodx autostart on`). - 수정: 미디어 미장착 시 USB 바로가기 오류, RemoteApp 이 로그인 화면을 렌더 (#332), atomic distro 에서 AppImage `podman-compose ModuleNotFoundError` (#322), 너무 빡빡해 일찍 포기하던 설치 agent-wait. ### Added - **선택 가능한 완전 번역 UI — 7개 언어 (en/ko/zh/ja/de/fr/it).** WinPodX 자체 트레이 / GUI / CLI 텍스트가 번역 가능해졌고, ~814개 사용자 가시 문자열 전부 래핑 + **6개 비영어 언어로 완전 번역** (placeholder, 백틱 명령 스니펫, 제품명은 verbatim 보존). `[ui] language` (기본 `auto`) 가 호스트 로케일(`$LANG`)에서 언어를 고르고 영어로 폴백; CLI(`winpodx language ko`, 인자 없으면 현재 표시) 또는 **GUI 설정 → winpodx UI 언어** 드롭다운(다음 실행 시 적용)으로 변경. 영어가 **소스/기준** — 항상 100% — 미번역 문자열은 영어로 폴백(공백 없음). 카탈로그는 `winpodx/locale/.json` 의 평면 `{english: translation}` 맵 — 언어 추가는 코드 변경 없이 파일 드롭. 한국어 잔존 보고(#335) 대응. - **로그인 시 pod 자동시작 (opt-in).** `cfg.pod.auto_start` 는 저장만 되고 안 쓰이던 플래그였는데 이제 동작 — 단 **기본 off, 명시적 opt-in** (로그인마다 Windows 부팅은 무거움). `winpodx autostart on` (또는 GUI 설정 체크박스)으로 켬: 트레이 자동시작 항목 설치 + `auto_start` 설정 → 로그인 시 뜨는 트레이가 pod 시작(중단 상태면 재개). `winpodx autostart off` / `status` 로 관리. 백그라운드 + best-effort (실패해도 pod 정지로 두고 로그만, 트레이 안 죽음); 이미 실행 중이면 no-op. - **게스트 동기화 — 재설치 없이 호스트 업데이트를 실행 중 게스트에 적용.** 호스트에서 winpodx 업그레이드해도 게스트의 `agent.ps1`, urlacl 예약(#269), rdprrap/shim 바이너리, 레지스트리 fixes 가 wipe-reinstall 전까지 stale 했음. `/oem` 은 호스트 `config/oem` 의 live bind mount 라 호스트 업그레이드 후 컨테이너엔 이미 새 파일이 있음; WinPodX 가 이를 실행 중 게스트에 전달(`pod recover-oem` 와 같은 채널이지만 agent `/exec` 로 자동화)하고 idempotent fixes 재적용, agent 재시작(서비스 중인 `/exec` 를 안 죽이게 일회성 scheduled task 로), 게스트 버전 스탬프(`C:\winpodx\install-state\guest_version.json`). 게스트 스탬프가 호스트보다 오래되면 pod 시작당 1회 자동 실행(`guest_autosync`, 기본 on); `winpodx migrate` 도 트리거(패키지 / AppImage / flatpak 업그레이드는 `install.sh` 를 안 거치므로 한 커맨드로 전체 게스트 갱신), 수동은 `winpodx pod sync-guest [--force]`, GUI **도구 → 게스트 동기화**. agent 변경 없음. `docs/design/GUEST_SYNC_DESIGN.md` 참고. - **Windows 디스크 자동/수동 확장 (#318).** Windows C: 드라이브가 스스로 커짐: 시스템 볼륨이 `disk_autogrow_threshold_pct`(기본 80%) 넘게 차고 pod 가 idle 이면 WinPodX 가 가상 디스크를 *`disk_autogrow_target_free_pct` 여유 공간 복원에 필요한 만큼*(기본 30%, `disk_autogrow_increment` 단위 — flat bump 아님) 키우고, 컨테이너 재생성으로 dockur 가 이미지 확장, `Resize-Partition` 으로 C: 확장 — 수동 디스크 관리 불필요. 확장은 호스트 여유 공간(안전 reserve 제외)으로 제한; `disk_max_size` 는 이제 고정 cap 이 아닌 *선택적* 명시 상한(기본 빈값). 동일 작업 수동 노출: `winpodx pod grow-disk`(증분 1단계), `winpodx pod grow-disk 128G`(절대 목표), `winpodx pod grow-disk --extend-only`(기존 미할당 공간으로 C: 확장만), `winpodx pod disk-usage`(크기/여유/사용%). GUI 도구 페이지에 **디스크 확장** 액션. 자동 확장은 idle 일 때만 실행해 live RemoteApp 세션을 안 끊고, 게스트측 작업은 agent `/exec` 재사용(agent 변경 없음). 기본 on; `disk_autogrow = false` 로 수동 관리. 주의: dockur 는 온라인 디스크 resize 가 없어 매 확장마다 컨테이너 재생성(짧은 게스트 재부팅) — WinPodX 가 이래서 idle 에 자동 확장 스케줄. dockur Windows 레이아웃은 C: 바로 뒤에 WinRE **복구 파티션**을 둬서 커진 공간이 그 뒤로 가 평범한 `Resize-Partition` 으로 못 닿음; extend 단계가 이를 감지해 WinRE 분리, 막는 복구 파티션 제거, C: 확장, WinRE 재활성화(전용 파티션 없으면 `C:\Windows` 내). @drjwhitty(Linux Mint 22.2) 보고. ### Fixed - **업그레이드 재설치가 `[4/4]` OEM reboot pass 대기에서 멈춤.** 이미 프로비저닝된 게스트에 `install.sh` 재실행 시 `pod wait-ready --logs` 가 phase 4 에서 멈춤. 원인 둘: (1) `--logs --tail 100` 이 *최초* 첫부팅 다운로드 로그를 재생 → 스테일 wget ETA 라인이 대기 deadline 을 수십 분으로 부풀림; (2) phase 4(`_wait_for_oem_reboot`) 가 agent 전환 중일 때 그 부풀린 시간 내내 블록 — "마커 안 뜸 → 업그레이드" 탈출이 clean probe 에서만 발화하고 agent 가 연결 거부 중엔 안 됐음. 수정: 컨테이너 RUNNING 후엔 deadline 연장 안 함(이후 ETA 라인은 재생된 과거); phase 4 를 180s 로 하드 캡(다운로드 아니라 마커 폴링); 업그레이드 탈출이 probe 결과 무관하게 appear-grace 시계로 발화. - **미디어 미장착 시 USB 바로가기 오류.** install.bat 은 항상 `\\tsclient\media` 를 가리키는 `USB` 바로가기를 만드는데, WinPodX 는 removable-media base(`/run/media/$USER` 등)가 있을 때만 그 드라이브를 redirect 해서 — USB 안 꽂은 채 클릭하면 "`\\tsclient\media` 에 접근할 수 없습니다 … 잘못된 주소 접근" 오류. 이제 WinPodX 가 항상 `media` 드라이브를 redirect: 장착 시 실제 base, 아니면 빈 placeholder 디렉터리 — 바로가기가 오류 대신 빈 폴더를 엶. - **UI 잔존 한국어 텍스트 (#335).** 트레이 "종료" 확인 대화상자와 CPU/RAM 티어 프리셋 라벨(`Low`/`Mid`/`High`)이 한국어로 하드코딩돼 영어 로케일 사용자가 종료 시 한국어 프롬프트를 받음. 둘 다 영어로 (이번 릴리스의 완전 i18n 레이어가 별도 처리). @camegone(CachyOS) 보고. - **설치: agent-readiness 대기가 이제 동적 + 여유롭게, 오해 소지 오류 없음.** 느린 첫 부팅(cold cache / 느린 디스크 / 긴 ISO 다운로드로 설치가 늦어짐)에서 post-RDP agent `/health` 대기가 하드 180s 로 잘려 일찍 포기 — 무서운 `agent didn't answer within 180s` WARN 누출 후 agent 가 몇 분 내 뜰 것임에도 apply-fixes / discovery 스킵. 이제 caller 의 전체(동적, ISO-ETA 연장) deadline 을 따름 — `pod wait-ready` phase 3 가 자동 연장 deadline 을 넘김. 별도로 게스트 버전 스탬프 read/write 가 이제 FreeRDP 대신 windowless agent `/exec` 채널(`run_via_transport`)로 감 (#346) — 신규 첫 부팅에서 전환 중인 agent 면 clean 하게 실패하고 다음 pod 시작에 스탬프 재시도, 설치 중간에 무서운 `FreeRDP timed out after 30s` / `ERRCONNECT_ACTIVATION_TIMEOUT` 경고 누출 안 함. 스탬프는 best-effort 라 info 레벨로 표시(*"deferred; will retry next start"*), 경고 아님. 설치 후 앱 discovery 재시도 예산도 3회 → 6회(~50s)로 늘려 apply 체인의 TermService/rdprrap 사이클을 버티게 함 — 다음 실행의 pending-resume 로 안 떨어지게. - **첫 실행 setup 프롬프트가 매번 뜨던 문제 (#341).** "winpodx has not been set up yet" 는 config 파일 존재가 아니라 내부 `initialized` 플래그로 게이트됨. non-interactive 기존-config 경로가 그 플래그 플립 *전에* "skipping setup" 출력 후 return → 플래그가 `false` 인 config(install.sh 자체 skip 경로로 생성됐거나 플래그 도입 전 작성)는 프롬프트가 영원히 반복 — "Auto" 눌러도 setup 이 short-circuit, 다음에 또 뜸. 이제 skip 경로도 initialized 표시. @ntruhan(Fedora 44) 보고. - **RemoteApp 이 로그인/잠금 화면으로 실행 → 깨진 렌더 (#332).** 앱이 가끔 자기 대신 Windows 로그인 배경을 띄움 (`xf_Pointer: Invalid appWindow` 스팸) — 앱은 *실행됐지만* 게스트 세션이 로그인/잠금 화면을 거치는 도중(dockur autologon 세션이 잠깐 재생성) FreeRDP RAIL 창이 생성돼 stale 로그인 framebuffer 를 그리고 앱을 다시 안 그림. 버전 무관(현재 FreeRDP 에서도 재현). 이제 winpodx 가 RemoteApp 연결 전 게스트 콘솔이 interactive(`explorer.exe` 떴고 `LogonUI.exe` 없음)할 때까지 대기 — best-effort, **agent-only** 라 FreeRDP probe 창이 안 깜빡임; agent 도달 불가/타임아웃이면 기존대로 진행. @tolistim(Mint 22.3) 보고. - **AppImage: `podman-compose` `ModuleNotFoundError` (#322).** fat AppImage 가 Fedora 의 `/usr/bin/podman-compose` *런처 스크립트*는 번들했지만 그게 import 하는 `podman_compose` Python 모듈은 안 함(순수 Python 이라 `ldd` 가 못 끌어옴), 스크립트 shebang 이 호스트 `python3` 로 떨어짐 — atomic distro 엔 모듈 없음. Pod 생성이 `ModuleNotFoundError: No module named 'podman_compose'` 로 실패. 이제 빌드가 `podman-compose` 를 AppImage 번들 인터프리터에 pip 설치하고 `python3 -m podman_compose` 로 실행하는 `usr/bin/podman-compose` wrapper 를 함께 제공(pip console script 는 shebang 이 죽은 빌드타임 경로라 직접 못 씀 — AppRun 이 `python3 -m winpodx` 쓰는 이유와 동일). @jmayniac(Bluefin / Fedora atomic) 보고. ## [0.5.8] - 2026-05-24 신뢰성 + 도달성 release: 사용자가 보고한 두 fresh-install 실패 (#269 agent 8765 bind 불가, #287 install.bat 미실행) 수정 + distro 무관 fat AppImage + standalone `winpodx setup` 의 end-to-end provision 완성. ### Highlights **Fresh-install 신뢰성 + self-contained AppImage.** fresh-install 막힘 2개 수정, 느린 연결/호스트도 timeout 안 남, immutable distro 용 단일 파일 AppImage 에 FreeRDP + Podman 번들. - **#269 수정** — agent 의 `http://+:8765/` urlacl 예약을 World SID 로 생성 → non-admin guest User 가 bind 가능. agent.ps1 가 retry + 실패시 urlacl 상태 로깅 (@ismikes 협업). - **Fat AppImage (#227)** — `winpodx-fat-x86_64.AppImage` 가 Python + Qt + FreeRDP + Podman + podman-compose 번들; `winpodx setup-host` 가 호스트 측 kvm-group / subuid 를 pkexec 한 번으로 처리 (@leandromqrs 요청). - **Dynamic + generous timeout (#126)** — `pod wait-ready` 가 dockur 다운로드 ETA 로 자동 연장, compose-up activity-based, 모든 fixed timeout 확대 (@xiyeming 보고). - **`winpodx pod recover-oem` (#287)** — dockur 첫 부팅 OEM 복사 실패 시 C:\OEM 재stage + install.bat 실행 (driver @ankranidiotis). - **Setup wizard 완성 (#255)** — `--customize` 가 edition / language / region / keyboard / tuning 까지 prompt, standalone `winpodx setup` 이 full provision (wait-ready + discovery + reverse-open) 실행. - **`podman-compose` 강제 (#288)** — `podman compose` 가 docker-compose 로 delegate 해서 keep-groups silently fail 하던 문제 해소 (@magicdiablo 보고). ### Added - **`winpodx pod recover-oem` — dockur 첫 부팅 OEM 복사 실패 수동 복구 (#287).** dockur 의 `$OEM$ → C:\OEM` 복사가 silently fail 하면 (install.bat 미실행 → agent 미설치 → 8765 RST) 컨테이너서 `/oem` tar → 컨테이너 8766 포트 임시 HTTP server → noVNC PowerShell 로 download + extract + install.bat 실행 안내. podman/docker 만. Driver: @ankranidiotis (Linux Mint 22, Podman 4.9.3). - **`pod wait-ready` dynamic deadline + `winpodx setup-host` (#126, #227).** `pod wait-ready` 가 dockur wget ETA 파싱해서 deadline 연장 (상한 없음 — stall 시 ETA 안 나와서 자연 만료, +60min slack). `winpodx setup-host` (= `python -m winpodx.setup_wizard`) 가 AppImage 가 user space 에서 못 하는 호스트 작업 (kvm 그룹, `/etc/subuid`+`/etc/subgid`, kvm 모듈 persistent) pkexec 로 처리. @xiyeming 보고. - **Fat AppImage (FreeRDP + Podman 번들 + pkexec 위저드) (#227).** #302 의 lean AppImage (Python + winpodx + Qt 만) 를 fat 번들로 확장: `packaging/appimage/` 레시피가 python-build-standalone 의 portable Python 3.11 다운로드 → winpodx wheel + `gui` + `reverse-open` extras 설치 → Fedora 41 binary overlay (xfreerdp / wlfreerdp / sdl-freerdp, podman, podman-compose, conmon, crun, netavark, slirp4netns, passt, pasta) + host-critical 제외 transitive `.so` 의존성 모두 번들 (glibc / libX11 / libGL / libwayland / libxkbcommon 은 호스트 사용 — 번들하면 desktop integration 깨지거나 glibc mismatch crash). 단일 `winpodx-fat-x86_64.AppImage` (~290 MB squashfs / ~920 MB AppDir) user space 가능한 모든 self-contained. 새 `winpodx setup-host` subcommand (+ `python -m winpodx.setup_wizard`) 가 AppImage 가 user space 에서 못 하는 호스트 측 작업 — `kvm` 그룹 멤버십, rootless podman 용 `/etc/subuid` + `/etc/subgid`, kvm 모듈 persistent load — 단 한 번의 `pkexec` polkit 프롬프트로 처리. 타겟 사용자: immutable distro (Fedora Silverblue / Kinoite / Aeon, Steam Deck), `curl install.sh | bash` 불가 환경. CI 가 python-appimage 우회 (bundled appimagetool 가 PATH wrapper 무시 + FUSE 없는 GitHub runner 에서 silently fail) → 새 pipeline 이 AppDir 처음부터 빌드 + Fedora 41 docker 컨테이너로 binary overlay + 미리 설치된 extract-and-wrap `appimagetool` 로 pack. 번들 third-party license 텍스트 (FreeRDP / Podman / podman-compose 등) 는 `packaging/appimage/licenses/` 에 vendor 해서 AppImage 안에 동행 (CI 의 `dnf` nodocs 정책 무관, required dir 없으면 build fail-closed). README + INSTALL.md (en + ko) 업데이트. @leandromqrs 제안. - **Setup wizard 완성 — `--customize` prompt + standalone full provision (#255).** wizard 가 edition, language, region, keyboard, tuning profile prompt (이전엔 CLI skip, GUI 전용). standalone `winpodx setup` 이 install.sh 처럼 post-create flow (wait-ready, apply-fixes, discovery, reverse-open) 실행. `--create-only` flag 로 install.sh 경로 유지. - **CLI + GUI first-run setup prompt (#255 PR 1).** `cfg.pod.initialized` 가 False (또는 config 없음) 일때 첫 `winpodx ` 호출시 3지선다 prompt: `[Y]es` (auto — 호스트 감지 default, no prompts), `[C]ustom` (wizard — 모든 knob 선택), `[n]o` (skip). Skip-list 가 introspection / config / uninstall / gui / tray 명령 + non-TTY stdin bypass. GUI 도 첫 launch 시 동일 모달. setup 성공 후 `initialized` True 로 flip 되어 prompt 재발 안함. - **`winpodx setup --customize` 플래그 (#255 PR 1).** Wizard 모드 opt-in (기존 인터랙티브 prompts; debloat / tuning / anti-detection knobs 포함한 full multi-step wizard 는 PR 7). 기본 `winpodx setup` 은 이제 non-interactive (호스트 감지 default). - **`install.sh --manual` 플래그 (#255 PR 2).** `winpodx setup` + `pod wait-ready` + 앱 디스커버리 + reverse-open setup skip. 바이너리 + desktop entry + 아이콘은 정상 설치; provisioning 은 다음 `winpodx` 실행시 발화하는 first-run prompt (CLI Y/C/n 또는 GUI 모달) 로 defer. Auto path 거치지 않고 커스텀 knob (edition / 언어 / debloat / tuning / anti-detection) 선택하고 싶을 때 사용. `WINPODX_MANUAL=1` env var 동등. `docs/INSTALL.md` + 한국어 미러에 "수동 설치" 섹션 추가. - **`winpodx doctor` 서브커맨드 (#255 PR 6).** 읽기 전용 진단. ~9 개 체크 (install source 감지, freerdp 존재, /dev/kvm 존재, 백엔드 PATH, config + 바이너리 상태, 컨테이너 health, pending setup 마커, autostart entry 무결성, `cfg.pod.initialized` 플래그) 후 항목별 `[OK]` / `[WARN]` / `[FAIL]` 결과 + 다음 명령 제안 출력. 읽기 전용 — state 변경 안함, 사용자가 제안 복붙. FAIL 시 exit 1; CI / install 스크립트의 빠른 health gate 로 유용. 빠르게 동작 (< 2s healthy install) — 모든 subprocess probe 짧은 timeout, network 호출 없음. - **`winpodx setup` 가 GUI launcher `.desktop` 등록 (#255 G7).** standalone `winpodx setup` 호출 (pip install, dev checkout, 또는 install.sh / 패키징이 GUI entry 안 떨군 경로) 이 이제 `data/winpodx.desktop` 를 `~/.local/share/applications/winpodx.desktop` 로 복사 — install.sh 가 curl install 에 하던 것 mirror. `/usr/share/applications/winpodx.desktop` 이미 있으면 skip (패키지 install 이 그 경로 소유, user-level 복사는 upgrade 시 패키지 버전 shadow). `install_gui_launcher_desktop()` 가 `winpodx.desktop.icons` 의 `install_winpodx_icon()` 옆에 위치. - **`winpodx uninstall` 단일 bash 스크립트로 consolidation (#255 follow-up).** 이전 #255 PR 3 가 기존 ~300줄 `uninstall.sh` 옆에 ~470줄 Python 구현을 `winpodx.cli.uninstall` 에 추가 — 두 구현이 평행으로 drift (MIME cache / gtk-update-icon-cache / 게스트 레지스트리 scrub / 최종 pkill sweep / 볼륨 glob / storage bind-mount wipe / GUI launcher .desktop 제거 등 10+ 항목 갈림), bin/winpodx 심볼릭 링크가 purge 후에도 살아남는 버그 (#278 / #279) 반복 발생. Consolidation: `uninstall.sh` 가 이제 canonical 구현. `winpodx uninstall` 은 ~50줄 Python wrapper — `/usr/share/winpodx/`, `~/.local/bin/winpodx-app/`, `~/.local/share/winpodx/`, `sys.prefix/share/winpodx/`, 또는 dev checkout 에서 `uninstall.sh` 찾아 `os.execvp` — Python install 이 반쯤 깨져도 uninstall 동작. 동일한 `uninstall.sh` 가 이제 모든 채널 (debian/winpodx.install, rpm %files, AUR PKGBUILD package(), pip wheel 용 pyproject shared-data, curl install bundle dir) 에 well-known 경로 `/usr/share/winpodx/uninstall.sh` 로 ship. Install-source 감지 (dpkg / rpm / pacman) 가 bash 로 이동; 바이너리를 패키지 매니저가 소유하면 *먼저* `sudo apt remove` / `sudo dnf remove` / `sudo pacman -Rns` 실행 권장 후, 패키지의 postrm hook 이 `--from-postrm` 으로 같은 스크립트 재진입해서 user-side cleanup — dpkg/rpm db 와 디스크 상태 동기화 유지하는 유일한 순서. `postrm-common.sh` 단순화: Python CLI 호출 대신 `/usr/share/winpodx/uninstall.sh --from-postrm --yes [--purge]` 로 delegate. 플래그 정규화: `--yes` (이전 `--confirm` — silent alias 로 유지), `--purge`, `--from-postrm` (internal, postrm hook 이 set). PR 3 이 도입한 `--no-package-prompt` 플래그 제거 (동작이 `--from-postrm` 으로 흡수). 테스트: `tests/test_uninstall_consolidation.py` 삭제 (Python 헬퍼들 더 이상 없음), `tests/test_uninstall_wrapper.py` (10 tests: 후보 경로 커버리지, execvp argv forwarding, 스크립트 못 찾을시 SystemExit) + `tests/uninstall_smoke.sh` (샌드박스 $HOME 스모크 — run 후 모든 예상 경로 사라짐 assert, shellcheck-clean) 추가. - **`uninstall.sh` 가 pip / source install 감지 (#255 G6).** 이전엔 `unknown` 으로 fall through — pip 사용자에게 hint 없었음. 이제 site-packages 또는 dev-checkout 경로 식별 후 user-side cleanup 뒤 `pip uninstall winpodx` 를 그 install kind 의 canonical 제거 명령으로 출력. 감지는 휴리스틱 (스크립트가 venv prefix 몰라서 `pip uninstall` 직접 구동 불가) → hint 는 정보용, auto-exec 아님. - **debian / rpm / aur 패키지 매니저 post-install / post-remove 후크 (#255 PR 4).** 모든 패키지 install 경로가 이제 `winpodx setup` (또는 winpodx 만 실행해도 first-run prompt 가 처리) 안내 banner 로 끝남. 모든 패키지 remove 경로가 tray / GUI / helper 프로세스 pkill + reverse-open listener stop (`winpodx host-open stop-listener`) 하는 작은 cleanup 실행 — `apt remove` / `dnf remove` / `yay -Rns` 후 orphan 프로세스 안 남음. debian 의 `apt purge` 는 추가로 winpodx config dir 가진 사용자별로 `winpodx uninstall --purge --yes --no-package-prompt` 실행 (`apt purge` 시맨틱 match). rpm 과 aur 은 purge 개념 없음 — post-remove 후크가 full wipe 방법 안내 (`winpodx uninstall --purge --yes`). 공통 로직은 `packaging/scripts/postrm-common.sh` 에 살고, 모든 distro 가 `/usr/share/winpodx/packaging/postrm-common.sh` 로 설치. 신규 파일: `debian/postinst`, `debian/postrm`, `debian/winpodx.install`, `packaging/aur/winpodx.install`. 수정: `packaging/rpm/winpodx.spec` (%post + %postun 추가), `packaging/aur/PKGBUILD` (install=winpodx.install + 스크립트 설치), `.github/workflows/aur-publish.yml` (PKGBUILD 옆에 winpodx.install push). - **`winpodx --version` install source 접미사.** 출력이 이제 `winpodx 0.5.8 (installed via apt)` / `(curl install)` / `(pip install)` / `(install source not detected)` — 사용자가 provenance 즉시 확인. - **`winpodx info [System]` install source 라인.** 동일 감지를 info 명령의 System 블록에 winpodx / OEM bundle / rdprrap / distro / kernel 와 함께 노출. - **`winpodx.utils.install_source`** — 신규 helper. `dpkg -S` / `rpm -qf` / `pacman -Qo` (각 3s timeout) 로 install provenance 감지, `~/.local/bin/winpodx-app` curl-install 휴리스틱 + `site-packages` source-checkout fallback. 후속 uninstall / version / info / doctor consumer 용 `InstallSource(kind, label, package_name, removal_command)` 반환. - **Windows-on-KVM 튜닝 프로파일 확장 (#245).** `auto` 와 `safe` 가 이제 Hyper-V enlightenments (`hv-relaxed`, `hv-vapic`, `hv-vpindex`, `hv-runtime`, `hv-synic`, `hv-reset`, `hv-frequencies`, `hv-reenlightenment`, `hv-tlbflush`, `hv-ipi`, `hv-spinlocks=0x1fff`, `hv-stimer`, `hv-stimer-direct`) + `virtio-rng-pci` (`/dev/urandom` backed) 적용. multi-vCPU 게스트에서 스케줄링 / VM-exit 큰 개선; 엔트로피 풀 빠르게 채워져 첫 부팅 CryptoAPI / TLS handshake stall 방지. `auto` 추가로 nested-virt CPU feature (`+vmx` Intel / `+svm` AMD) + `hv-evmcs` (Intel) — `/sys/module/kvm_intel/parameters/nested` (또는 `kvm_amd`) 가 `Y` 일때 자동 노출 — Windows 게스트 안에서 Hyper-V / WSL2 / Docker Desktop 활성화. 호스트 nested KVM opt-in 안 됐으면 no-op. `safe` 는 호스트 측 명시 opt-in 필요한 nested-virt + `hv-evmcs` 제외. 새 CLI override: `winpodx pod start --tuning {auto,safe,off,manual}` 으로 one-shot per-run 프로파일 선택 (winpodx.toml 영구 저장 안 됨). GUI Settings 에 Tuning Profile 드롭다운 + 호스트에서 resolved 프로파일이 무엇을 적용하는지 보여주는 live `format_tuning_summary()` 패널 추가. 새 `TuningCapability.nested_kvm` probe + `TuningProfile.apply_hv_enlightenments` / `apply_virtio_rng` / `apply_evmcs` / `apply_nested_virt` flag. - **`performance` 튜닝 프로파일 + Settings 카드 재배치 (#245 follow-up).** `auto` 와 동일하나 `dedicated_host` gate 우회 — 호스트 현재 idle CPU / free RAM 무관하게 CPU pinning + no-balloon 강제 on. 박스가 winpodx 에 거의 dedicated 이고 게스트 latency 우선시 다른 호스트 워크로드 희생 OK 일때 사용. Hard-gated 항목 (`+invtsc`, `io_uring`) 은 capability 감지 유지 — `performance` 가 QEMU 거부할 CPU flag 강제 불가. CLI: `winpodx pod start --tuning performance`. GUI: Settings 에 전용 "Performance Tuning" 카드 신설 — dropdown 상단, `format_tuning_summary()` 감지 패널 같은 카드 frame 내부 하단. - **Settings UI polish + 디자인 토큰.** 기존 "Container / VM" 카드 (10 form rows, 옆 7-row RDP 카드보다 큼) 를 "Hardware" 카드 (Backend / Edition / CPU / RAM / Idle / Max Sessions = 6 rows) + 풀-width "Localization" 카드 (Language / Region / Keyboard / Timezone = 4 rows) 로 split — 상단 row 가 height-balanced 2 column 레이아웃으로 읽힘. `winpodx.gui.theme` 에 디자인 토큰 레이어 추가: `SPACE_XS/S/M/L/XL/XXL`, `RADIUS_XS/S/M/L/XL/XXL`, `FONT_CAPTION/BODY/SUBHEAD/HEADER/TITLE/HERO` — 카드/헤더/요약 패널/form 라벨에 흩어진 magic number (`24, 22, 14, 12, 11, ...`) 가 named scale 하나로 수렴. Settings 페이지 + tuning 카드는 새 토큰으로 변환; 나머지 페이지는 follow-up 에서. ### Changed - **`winpodx setup` 기본이 non-interactive auto 로 flip (#255 PR 1).** 이전엔 호스트 감지 default + 일부 prompt 있는 인터랙티브. 이제 완전 auto (= 기존 `--non-interactive` 동작). 위자드 원하면 `winpodx setup --customize`. `--non-interactive` 플래그는 install.sh + 스크립트 호출자용 deprecated alias 로 유지. - **Timeout 전반 확대 + compose-up activity-based (#298, #126).** 느린 호스트/연결이 진행 중인 작업을 fail 안 시키게: `compose up` 이 activity-based (120s 고정 대신 출력 5min 침묵 시에만 kill, 4h hard cap); `wait_for_windows_responsive` 90→300s 기본 / 호출부 180→600s; agent `/health` + FreeRDP RDP-port probe 2→5s; `transport.exec`/`run_in_windows` guest 호출 2-3x (sync-password 30→90s 등); `compose down` 60→180s; daemon suspend/resume/inspect 30/10→90/30s. poll-loop 내 짧은 TCP probe (`pod_status` 의 `check_rdp_port`) 는 의도적으로 짧게 유지 (poll interval 역할). - **CPU sub-flag 들을 `ARGUMENTS` 에서 dockur 의 `CPU_FLAGS` + `VMX` env 로 이동; hv-* 는 dockur 에 위임.** 세 문제 한 번에 해결: (1) PR #289 의 `-msg timestamp=on` 워크어라운드 — proc.sh:137 의 strip 후 빈 ARGUMENTS bash 슬라이스 회피용. (2) PR #281 의 hv-* enlightenments 가 dockur 의 `hv_passthrough` (default `HV=Y`) 와 중복 → QEMU 10 의 `Ambiguous CPU model string. Don't mix both "-hv-evmcs" and "hv-evmcs=on"` warning. (3) PR #281 의 명시적 `+vmx` / `+svm` nested-virt 주입이 dockur 의 `VMX` env 와 중복. winpodx 가 이제 dockur 의 문서화된 env-var interface 사용: x86 에선 `CPU_FLAGS: "arch_capabilities=off,+invtsc"` (dockur 가 안 추가하는 두 sub-flag), nested-virt 원할 때 `VMX: "Y"` (dockur 가 호스트 CPU 보고 `+vmx` / `+svm` 선택). ARGUMENTS 는 이제 virtio-rng device 쌍만 carry. proc.sh:137 strip 코드 path 가 절대 trigger 안 됨 (ARGUMENTS 에 `-cpu host,` 없음) → `-msg timestamp=on` marker 제거. ambiguous-string warning 사라짐 (우리가 `hv-evmcs` 안 추가 — dockur 가 hv-* set 전체 소유). winpodx 가 QEMU sub-flag deprecation 에서 decouple: QEMU 11/12 가 hv-* 문법 바꿔도 dockur 가 처리, 우리는 무관. ### Fixed - **Agent 8765 bind 실패 — urlacl 잘못된 소유자 (#269).** install.bat 완전 실행됐는데 (RDP listener up, HKCU\\Run 등록, agent spawn) agent 가 매 부팅 FATAL: `HttpListener.Start() failed: ... 'http://+:8765/' ... conflicts with an existing registration`. agent 가 non-admin User 라 strong-wildcard prefix bind 에 urlacl 예약 필요. 기존 `netsh http add urlacl ... user=Everyone listen=yes >nul 2>&1` 가 wrong-owner 예약 생성 or `ERROR_ALREADY_EXISTS`(183) — 둘 다 `>nul` 가 숨김. 이제: 8765 중복 예약 모두 delete 후 `sddl=D:(A;;GX;;;WD)` (World SID, locale-proof) 로 add, 결과 setup.log 로깅 + `netsh http show urlacl` dump. agent.ps1 가 bind 5회 retry + 실패시 urlacl 상태 agent.log 에 dump. OEM bundle 24 → 25. @ismikes (Kubuntu 26.04) 협업으로 진단 — install.bat 변경 3개 test branch 로 배제 후 그의 agent.log 스크린샷이 urlacl 충돌 특정. - **dockur `proc.sh:137` ARGUMENTS-strip crash, 에이전트 절대 설치 안 됨 (#287, #269).** dockur (qemus/qemu 베이스 이미지 경유) 가 compose 의 `ARGUMENTS:` env 를 파싱, `-cpu host,` 를 자체 CPU_FLAGS 파이프라인용으로 strip 한 후 남은 부분에 `ARGUMENTS="${args::-1}"` bash 슬라이스. 사용자 tuning profile 이 다른 거 아무것도 생성 안 하면 (예: `tuning_profile = off`) 남은 부분이 빈 문자열 → bash 슬라이스 `proc.sh: line 137: -1: substring expression < 0` 에러로 fail. proc.sh 가 그 라인에서 죽음 → `/oem` 이 Windows 게스트로 복사 안 됨 → `install.bat` 실행 안 됨 → winpodx 에이전트 서비스 미설치 → 8765 포트가 TCP accept 후 handshake 마다 RST → `winpodx pod wait-ready` 60 분 timeout. @ankranidiotis (v0.5.7, Linux Mint 22) + @ismikes (Kubuntu 26.04) 둘 다 fresh install + default tuning 에서 보고. `_qemu_arguments_for_host()` 워크어라운드: extra QEMU arg 가 emit 되지 않을 때 `-msg timestamp=on` (QEMU 로그 라인 timestamp, 그 외 no-op) append → proc.sh 의 strip 후 ARGUMENTS 에 최소 한 개 공백-separated 토큰 남음 → bash 슬라이스 가 빈 문자열 보지 않음. `auto` / `safe` / `performance` tuning profile 은 이미 hv-* / virtio-rng extras emit (#245) → 영향 없음. 워크어라운드는 `off` + 향후 순수 `-cpu host,...` 라인 만드는 모든 프로파일에 적용. - **`podman-compose` 강제, `podman compose` delegation 불신 (#288).** docker-compose CLI plugin 설치된 Fedora 계열에서 `podman compose` 가 docker-compose 로 delegate → `group_add: [keep-groups]` (rootless `/dev/kvm` passthrough 용) 인식 못함 → `Unable to find group keep-groups` 로 컨테이너 시작 실패. install.sh 가 이제 standalone `podman-compose` 바이너리 요구, `backend.podman._compose_cmd` 가 distro 별 설치 안내 raise. @magicdiablo (Nobara) 보고. - **`-no-hpet` 제거 — QEMU 10 (dockur v5.15+) 가 invalid option 으로 거부.** PR #281 의 hv-enlightenments 작업이 auto / safe / performance 프로파일이 Hyper-V 머신 타이머 선택할 때 QEMU ARGUMENTS 에 `-no-hpet` append. dockur v5.15 가 qemus/qemu v7.30 베이스 이미지 경유 QEMU 10.x ship; QEMU 10 이 `-no-hpet` 플래그 완전히 drop (대체는 `-machine ...,hpet=off` — dockur 의 machine type 을 override 하게 됨, risky). 컨테이너 시작이 `qemu-system-x86_64: -no-hpet: invalid option` 으로 폭망. 플래그 제거의 functional 영향 zero — `hv-stimer` + `hv-stimer-direct` enlightenments 가 이미 Windows 를 HPET clock source 에서 빠지게 함. PR #289 머지 후 main HEAD 에서 @ismikes (Kubuntu 26.04) 보고. - **install.bat 의 `[agent-install]` 구조화 마커 (#269 진단).** agent-install 각 step (`hkcu-run-register`, `spawn`, `post-spawn-probe`, `urlacl`) 를 `[agent-install] step= status=` 로 fence, HKCU\\Run write + Start-Process try/catch 로 예외 로깅, 5s post-spawn probe 가 8765 listener 확인 + agent.log tail → agent 설치 실패가 setup.log 에서 self-localize. - **RPM spec `License:` tag 를 `MIT AND Apache-2.0` 로 정정 (#301).** `packaging/rpm/winpodx.spec` 가 `License: MIT` 였는데 `config/oem/rdprrap-*-windows-x64.zip` 가 stascorp/rdpwrap (Apache-2.0) 번들. Fedora packaging guidelines 는 SPDX expression 이 redistributed binary content 의 모든 license 명시해야 함. `debian/copyright` 와 `THIRD_PARTY_LICENSES.md` 는 이미 정확. RPM spec 가 이제 매칭. ## [0.5.7] - 2026-05-21 #247 (항목별 debloat picker, 4 phase) + #254 (locale + edition save flow, 3 phase) 마무리 + 동시 발견된 #266/#267 OEM 권한 regression fix 묶음 feature 릴리즈. ### Highlights **CLI + Qt + TTY picker 로 항목별 debloat, dockur TZ env var 통한 Windows 게스트 타임존 wiring, GUI Settings → Container/VM Localization dropdown + 저장시 자동 recreate, `winpodx pod recreate [--wipe-storage]`.** - `winpodx debloat` — `data/debloat/items.toml` 항목별 카탈로그 (11개 항목, 4개 프리셋). CLI `--list` / `--preset` / `--items` / `--undo` / `--menu` + 체크박스 + 리스크 뱃지 Qt picker 다이얼로그 (#247). - Windows 게스트 타임존이 이제 호스트 자동감지되고 dockur 의 `TZ` env var 로 전달; `cfg.pod.timezone` GUI dropdown + `winpodx config set pod.timezone --auto` + setup wizard prompt 모두 wired (#254). - Settings → Container/VM 가 기존 Edition picker 옆에 Language / Region / Keyboard / Timezone dropdown 추가; dirty 필드시 통합 save flow 가 `_recreate_container` (또는 language/edition 변경시 `_wipe_pod_storage` 선행) 자동 실행 (#254 P3). - `winpodx pod recreate [--wipe-storage]` — `compose.yaml` 재생성 + 컨테이너 destroy/재생성 (기본은 Windows 디스크 보존; `--wipe-storage` 가 language/edition 변경용 Windows 재설치 트리거) (#254 P1). - `Config.save()` 가 이제 `pod.language` / `pod.region` / `pod.keyboard` / `pod.timezone` / `pod.tuning_profile` 도 영구 저장 — 본 fix 이전엔 이 5개 필드가 load 만 되고 write 안돼서 `winpodx config set` 가 다음 save 에 사라졌음. ### Added - **`winpodx debloat --menu` 텍스트 모드 picker (#247, phase 4).** SSH / TTY 전용 환경 사용자를 위한 Qt 다이얼로그 대안 (headless-friendly). 순수 `input()` + `print()` — curses 의존 없음, display server 없음, 터미널 capability 스니핑 없음. 루프가 카탈로그를 번호 매긴 리스트 + 현재 선택 상태 + 프리셋명 + 리스크 뱃지 + `(one-way)` 태그로 렌더, 그 후 명령 prompt: `` 항목 N 토글, `` 이름으로 토글, `p` 프리셋 목록, `p ` 프리셋 전환, `a` apply, `q` quit, `h` help. `run_menu` 가 `input_fn` / `print_fn` injectable 수용해서 동일 코드 경로가 인터랙티브 셸과 CI mocked-stdin 통합 테스트 둘다 처리. - **GUI debloat picker 다이얼로그 (#247, phase 3).** Tools → Debloat 가 이제 normal 프리셋 blind 실행 대신 모달 다이얼로그 (`gui/debloat_picker.py`) 를 띄움. 카탈로그 각 항목이 체크박스 + 색상 리스크 뱃지 (녹/황/적) + description 툴팁 한 행으로 노출; 프리셋 라디오 (Normal / Full / Performance / Speed / Custom) 가 체크박스 상태 seed, 항목 토글시 라디오가 Custom 으로 전환. Apply 가 기존 `run_via_transport` 채널로 선택 실행; Cancel 은 게스트 무영향. one-way 항목 (onedrive, startup_programs) 은 행에 "(one-way)" 태그 표시해서 #247 P2 의 `--undo` 경로로 되돌릴 수 없는 선택을 사용자가 한눈에 인지. - **`winpodx debloat --undo` + 항목별 undo 스크립트 + state JSON 추적 (#247, phase 2).** 카탈로그 각 entry 가 선택적 `undo_script` 필드 보유 가능; reversible 9개 (11개 중) 가 `undo/.ps1` sibling 으로 ship — apply 를 되돌림. `winpodx debloat --undo --items ` (또는 `--undo --preset `) 가 undo 경로 실행; one-way 항목 (`onedrive`, `startup_programs`) 포함된 mixed 선택은 명확한 "items have no undo path" 에러로 중단해서 부분 undo 가 일어나지 않음. Apply orchestrator 가 매 성공 apply 후 `%ProgramData%\winpodx\debloat-applied.json` 에 항목별 entry 기록, undo orchestrator 가 제거 — GUI picker (phase 3) 가 각 체크박스 옆에 "currently applied" 상태 표시 위한 기반. - **`winpodx debloat --list` / `--preset` / `--items` (#247, phase 1).** 기존 모놀리식 `winpodx debloat` (항상 동일한 70줄 `debloat.ps1` 실행) 을 항목별 카탈로그 + 선택 resolver 로 리팩터. 카탈로그는 `data/debloat/items.toml` 에 11개 항목 (telemetry, ads, onedrive, sysmain, web_search, widgets, scheduled_tasks, startup_programs, visual_effects, search_indexing, transparency) 와 함께 ship, 각 항목은 `scripts/windows/debloat/` 의 작은 per-item `.ps1` 가 백킹. 4개 프리셋 (`normal`, `full`, `performance`, `speed`) 이 일반적 선택을 seed; 인자 없는 `winpodx debloat` 는 `normal` (telemetry + ads) 으로 기본 처리해서 기존 동작 유지. `--list` 는 게스트 접촉 없이 전체 카탈로그 + 프리셋 출력. `--items` 는 쉼표 구분 명시 이름 리스트 수용, `--preset` 와 동시 지정시 우선. 본 phase 범위 밖 (#247 별도 추적): 항목별 undo, GUI picker 다이얼로그, TTY-menu (`--menu`). - **GUI Settings → Container / VM 카드에 Localization dropdown 4개 추가 (#254, phase 3).** 기존 Edition / CPU / RAM 옆에 4개 신규 행: Language, Region, Keyboard, Timezone. 각 행의 첫 옵션은 `Auto (...)` 이고 빈 TOML 문자열에 매핑 (= compose 시점 호스트 자동감지); timezone 행의 auto label 은 실시간 `detect_timezone()` 결과를 노출해서 게스트에 들어갈 값 미리보기 가능. Save 핸들러는 recreate prompt 를 두 경로로 분기: timezone / CPU / RAM / port / user 만 dirty 면 "Restart now?" 평이한 프롬프트 (Windows 디스크 보존, OEM tzutil 이 매 recreate 마다 재적용), Edition 또는 language / region / keyboard 중 하나라도 dirty 면 "Wipe and reinstall now?" 강한 프롬프트 (dockur 가 그 env 들을 초기 설치에만 적용하므로 일반 recreate 는 silent no-op). Wipe 수락 시 `winpodx pod recreate --wipe-storage` 와 동일한 `_wipe_pod_storage` 헬퍼 호출. - **`winpodx config set pod. --auto` shorthand (#254, phase 2).** 명명된 키의 호스트 감지값을 positional `` 대신 사용. 현재 wired: `pod.timezone` (`utils.locale.detect_timezone` 의 IANA 이름). 다른 locale 키 (language / region / keyboard) 는 명확한 "not yet supported" 에러를 #254 링크와 함께 출력하고 non-zero exit — 해당 detector 는 follow-up. `--auto` 와 positional `` 는 상호 배타. - **`winpodx setup` 인터랙티브 timezone prompt (#254, phase 2).** wizard 가 CPU / RAM row 뒤에 `cfg.pod.timezone` 을 prompt; 기본값은 호스트 감지 IANA zone. non-interactive setup (install.sh 의 call path) 는 `cfg.pod.timezone` 을 기본 빈 문자열로 두어 compose 생성시 호스트 자동감지가 첫 부팅에 작동. - **`cfg.pod.timezone` — Windows 게스트 타임존 wiring (#254, phase 1).** 신규 config knob. 빈 문자열 (기본) → compose 시점에 호스트 자동감지 (`timedatectl show --property=Timezone --value` → `readlink /etc/localtime` → `/etc/timezone` → `UTC` fallback chain). 감지된 IANA 이름은 CLDR 파생 `data/locale/windows_zones.toml` 테이블 (`001` wildcard subset 약 150개 entry) 로 Windows TZ ID 로 번역. 번역값이 `/timezone.txt` 에 기록되고 OEM `install.bat` 가 첫 부팅 시 이 파일을 읽어 기존의 무조건 `tzutil /s "UTC"` 대신 `tzutil /s ""` 실행. TOML 에 명시된 IANA 이름이나 bare Windows TZ ID 도 그대로 수용; 감지 실패시 UTC 는 파일을 쓰지 않아 게스트가 강제 UTC 로 전환되지 않고 현재 zone 유지. 호스트 자동감지는 신규 설치 시 Windows 게스트가 호스트 locale 과 무관하게 UTC 기본값으로 떨어지던 @ismikes 보고 (#204) 를 해소. - **`winpodx pod recreate [--wipe-storage]` 서브커맨드 (#254, phase 1).** 현재 config 로부터 `compose.yaml` 재생성 후 컨테이너 destroy + 재생성 — first-boot env knob 변경 (timezone, edition, backend) 적용에 `winpodx pod restart` 보다 명확한 primitive. `--wipe-storage` 는 Windows 디스크 볼륨 / 바인드 마운트까지 파괴해서 dockur 가 전체 설치 재실행하게 함 — language / region / keyboard / edition 변경은 dockur 가 그 env vars 를 Windows 초기 설치 시점에만 적용하기 때문에 게스트에 실제 반영되려면 wipe 필요. wipe 경로는 명시 `WIPE` 입력 confirmation 요구. - **`config/oem/install.bat` 가 `C:\OEM\timezone.txt` 존재시 읽음 (#254, phase 1).** 파일 부재시 기존 `tzutil /s "UTC"` 동작 유지. - **`data/locale/windows_zones.toml` — IANA → Windows TZ ID 매핑 테이블 (#254, phase 1).** pyproject shared-data 레이아웃을 통해 `share/winpodx/data/locale/` 에 배포. Refresh 헬퍼 (`scripts/ci/refresh_windows_zones.py`) 는 follow-up. ### Changed - **타임존 wiring 을 dockur 의 native `TZ` env var 중심으로 재설계 (#267, #254 P1 refactor).** 원래 P1 설계는 OEM bind-mount dir 에 `timezone.txt` 파일 drop + `install.bat` 에 `tzutil` 블록으로 처리 — per-config 파일을 source bundle 오염 없이 두려고 OEM dir 항상-복사 강제. 그게 PR #95 가 원래 회피했던 parent-dir traversal 문제 재도입: dockur 의 in-container OEM cp 가 non-root sub-UID 로 돌아서 0700 인 `~/.config` 또는 `~/.local` 조상 traversal 불가 → 모든 OEM 파일에 `cp: cannot stat /oem/./: Permission denied`. dockur 의 `TZ` env var (이미 Sysprep unattend.xml 의 `` 처리) 로 전환해서 OEM 파일 요구 자체 제거, `_find_oem_dir` 가 #254 이전 two-regime 레이아웃 (user-writable 시 bundle-direct / read-only 시 copy) 으로 복원, umask 077 perm 트랩 제거. `install.bat` 의 옛 `tzutil` 블록 삭제; `utils/locale.py` (호스트 감지 + IANA→Win 매핑 테이블) 는 GUI 표시 및 wizard auto-default 위해 유지. ### Fixed - **umask 077 호스트에서 fresh install 시 OEM cp `Permission denied` 캐스케이드 (#267).** 증상: dockur 첫 부팅 OEM 복사가 모든 OEM 파일에 `cp: cannot stat '/oem/./': Permission denied` 실패 — #254 P1 의 always-copy OEM dir 경로가 도입한 0700 `~/.config/` 조상을 컨테이너의 non-root sub-UID 가 traverse 불가. 사용자가 bundle 소유시 bundle-direct OEM mount 로 복원해서 수정 (위 Changed entry 의 깊은 refactor 참조). - **`Config.save()` 가 이제 `pod.language` / `pod.region` / `pod.keyboard` / `pod.timezone` / `pod.tuning_profile` 도 영구 저장 (#254, phase 1).** 본 fix 이전엔 이 5개 필드가 `winpodx.toml` 에서 로드만 되고 다시 write 되지 않음 — 프로그램적 변경 (`winpodx config set pod.language ...`) 이 다음 save 에서 조용히 버려졌음. 직접 편집한 값은 덮어쓰는 게 없어서 살아남고 있었음. 이제 5개 필드 모두 save/load roundtrip. ## [0.5.6] - 2026-05-21 @ismikes (#214) 의 보고로 시작된 핫픽스 릴리스. 모던 rootless podman + pasta (Ubuntu/Kubuntu 26.04 기본) 에서 "Launching... 떴는데 RDP 창 안 뜸" 증상 수정, `winpodx info` 의 VNC reachability 거짓 부정 수정, `uninstall.sh` 종료 전 belt-and-braces tray sweep 추가. ### Highlights **Kubuntu 26.04 에서 "Launching..." 에 걸리던 RDP launch 재동작, `winpodx info` 의 VNC reachability 거짓 보고 종결 (#214).** - `core/rdp.py` 가 더 이상 FreeRDP 를 `podman unshare --rootless-netns` 로 감싸지 않음 — wrap 이 xfreerdp3 를 컨테이너 net ns 안에 넣어서 호스트 측 publish 가 보이지 않게 만들고 있었음. @ismikes 보고, @smoore100 진단, 양쪽 모두 fix 확인. - `winpodx info` 의 VNC reachability probe 가 RDP X.224 handshake → plain TCP accept 로 변경 (VNC 는 RFB, RDP 아님). - `uninstall.sh` 가 종료 전 final tray/GUI sweep 추가 — uninstall 윈도우 **중간에** 올라온 프로세스 (다른 터미널 GUI launch, XDG-autostart race, dbus activation) 도 종료. ### Fixed - **모던 rootless podman + pasta 에서 앱 launch 가 "Launching..." 에서 멈추던 문제 수정 (#214).** Kubuntu 26.04 (Wayland + Plasma 6 + FreeRDP 3.24.2) 에서 @ismikes 제보, @smoore100 진단. `core/rdp.py` 가 모든 FreeRDP 호출을 `podman unshare --rootless-netns` 로 감싸서 xfreerdp3 를 컨테이너의 network namespace **안에서** 실행 → 호스트 측 publish 포트가 보이지 않음 → "Launching..." 만 찍히고 RDP 창은 안 뜸. 이 wrap 제거; FreeRDP 가 이제 호스트 loopback 에서 직접 실행, `127.0.0.1:` 가 기존 podman publish 로 정상 도달. 부수 이득: FreeRDP launch path 의 `podman` 바이너리 의존 해소 (flatpak-FreeRDP-only 셋업도 동작), #214 의 Plasma 6 / Wayland / FreeRDP 3.24 엣지케이스 unblock. - **`winpodx info` 가 NoVNC 가 정상 서빙중인데 "VNC unreachable" 로 거짓 보고하던 문제 수정 (#214).** VNC 포트 (기본 8007) 가 `check_rdp_port` 로 probe 되고 있었음 — X.224 Connection Request 보내고 TPKT 응답 대기. RFB 말하는 VNC 서버 상대로는 무의미 → 항상 False 반환. VNC 는 이제 `check_tcp_port` (plain TCP accept) 사용 — "NoVNC 엔드포인트 연결 수락중인가" 와 일치하는 probe. - **`uninstall.sh` 종료 전 final tray/GUI sweep.** `WINPODX_NO_TRAY_SPAWN=1` + section-0a `pkill` 은 uninstall.sh 가 tray 를 건드리는 유일한 행위자인 경우만 cover. uninstall 윈도우 **중간에** 올라온 tray/GUI 프로세스는 놓침 — 다른 터미널에서 GUI 창 열어서 이미 tray spawn 한 경우, KDE/GNOME XDG-autostart race, `cli/main.py` 의 spawn 체크를 우회하는 dbus-activated launch. 요약 직전 quiet final `pkill -f 'python.*winpodx'` + `pkill -f winpodx-app` pass 추가로 잡음. ### Internal - **`packaging/obs/obs-publish.yml` 의 download grep 을 현재 태그 버전에 앵커.** Rolling-release repo (Tumbleweed / Slowroll 의 lazy GC) 가 OBS publish 인덱스에 남긴 stale 옛 RPM 이 release asset 으로 재업로드되지 않게 됨. v0.5.5 가 처음 표면화 — stale 0.5.4 RPM 2개가 Release 페이지에 들어가서 수동 삭제 필요했음. ## [0.5.5] - 2026-05-21 @tolistim (#216) 과 @ismikes (#215) 의 사용자 리포트로 시작된 follow-up 릴리스. 호스트 config 와 Windows guest 패스워드를 desync 시키던 setup 재실행 lockout 수정, 사용자 설정 없이 자동으로 켜지는 호스트 적응형 Windows-on-KVM 튜닝 프로파일, 그리고 오래 idle 된 pod 이 tray 에서 `starting` 으로 고정되던 증상 종결. ### Highlights **`winpodx setup` 재실행 안전 + dockur compose 호스트 자동 튜닝 + idle pod 가 tray 를 `starting` 으로 얼리지 않음 (#215, #216).** - 기존 설치에서 `winpodx setup` 재실행이 `cfg.rdp.password` 를 조용히 덮어쓰던 문제 수정; `LOGON_FAILED_BAD_PASSWORD` lockout 사라짐. @tolistim 제보 (#216). - 새 `cfg.pod.tuning_profile` (기본 `"auto"`): WinPodX 가 compose 시점에 호스트 probe (invariant TSC, kernel ≥ 5.6 = io_uring, `vm.nr_hugepages`, idle CPU + RAM headroom) 후 호스트가 지원하는 모든 안전 Windows-on-KVM 튜닝 활성화. 지원 CPU 에서 `+invtsc` 가 x86_64 QEMU args 에 들어감. `winpodx info` / `winpodx setup` 가 새 `[Tuning]` 블록 출력해서 자동 적용 set 항상 가시적. @ismikes 제안 (#215). - `safe` / `off` / `manual` 프로파일 escape hatch; 호스트 사전 설정 필요한 항목 (hugepages, CPU pinning, VFIO) 은 자동 처리 대신 운영자 작업으로 문서화. - **새 `PodState.UNRESPONSIVE` + 자동 회복.** `pod_status()` 가 부팅 중 컨테이너 (`STARTING`, 가동시간 600초 미만) 와 오래 돌아간 컨테이너의 RDP 응답 정지 (`UNRESPONSIVE`, 가동시간 600초 이상) 를 구분. tray 가 `RUNNING → UNRESPONSIVE` 전이 감지 시 desktop notification 발사 + 백그라운드 worker 가 agent transport 로 in-guest `TermService` 재시작. 성공 시 "pod recovered" notification, 실패 시 `winpodx pod restart` 안내 notification. OEM install.bat 도 `powercfg` 모든 timeout 을 `0` 으로 + `PlatformAoAcOverride` 클리어해서 Modern Standby 가 idle 세션 NIC 를 끊지 못하도록 차단. ### Added - **`PodState.UNRESPONSIVE` + 자동 회복 흐름.** 컨테이너가 10분 이상 가동되었는데 RDP 포트가 응답 안 하면 `pod_status()` 가 새 `UNRESPONSIVE` 반환 — 이전에는 영원히 `STARTING` 으로 잘못 보고됨. tray 가 이전 상태를 캐시하고 `RUNNING → UNRESPONSIVE` 전이 시 `notify_pod_unresponsive` + 백그라운드 worker (`try_recover_rdp`, 신규 `core/pod/recovery.py`) 발사. 회복 절차: agent 에게 `Restart-Service -Force TermService` 요청 + RDP 포트 30초 동안 재 probe. 성공 → `notify_pod_recovered`. 실패 → `notify_pod_needs_manual_restart` 에 실패 원인 (`agent unreachable` / `rdp still down`) 표시해서 `winpodx pod restart` 안내. `Backend.uptime_secs()` 가 새 ABC 메서드; Podman + Docker 는 `inspect -f '{{.State.StartedAt}}'` 로 구현; libvirt + manual 은 `None` 반환해서 legacy `STARTING` 으로 fallback. - **`config/oem/install.bat` 가 RDP 를 죽일 수 있는 Windows idle timeout 전부 비활성화.** Ultimate Performance plan 만으로는 부족 — Modern Standby (S0 low-power idle) 가 여전히 virtio NIC 를 끊고 TermService 를 stall 시킬 수 있음. AC + DC 양쪽의 `standby` / `hibernate` / `monitor` / `disk` timeout 을 `0` 으로 설정, `HKLM\SYSTEM\CurrentControlSet\Control\Power\PlatformAoAcOverride` 클리어해서 Desktop class power 동작 강제, 그리고 현재 scheme 의 `STANDBYIDLE 0` 까지 보강. idle pod 의 `RUNNING → UNRESPONSIVE` 전이 근본 원인 제거. - **호스트 절전 / 복귀 시 tray 가 `starting` 으로 얼리는 문제 해결.** 두 갈래: 게스트에는 SYSTEM-level 스케줄 태스크가 `Win32_PowerManagementEvent` 구독 (`config/oem/power-monitor.ps1`), EventType 7 / 18 (suspend / modern-standby 복귀) 5초 후 `TermService` 재시작해서 in-guest RDP listener 를 다음 probe 전에 새로 만듦. 호스트 tray 는 QtDBus 로 `org.freedesktop.login1` 의 `PrepareForSleep(active=False)` signal 구독해서 resume 5초 후 pod state 갱신 → 기존 UNRESPONSIVE 자동회복 경로 즉시 트리거 (30초 poll 기다리지 않음). 양쪽 모두 graceful degrade — D-Bus 구독 best-effort, 스케줄 태스크는 fresh OEM apply 시에만 설치 (기존 pod 는 follow-up 의 `winpodx pod apply-fixes` 로 적용). - **OEM 설치 끝에 스케줄된 재부팅으로 레지스트리 변경 실제 적용.** `PlatformAoAcOverride = 0` (Modern Standby off) 과 NIC binding 같은 reg 키가 `HKLM` 에 들어가지만 실행 중 세션은 다음 부팅까지 옛 값 유지. install.bat 가 이제 `C:\winpodx\oem_reboot_pending.txt` marker 작성 + 두번째 부팅 시 marker 삭제하는 `RunOnce` 키 등록 + 마지막 동작으로 `shutdown /r /t 15` 발사. `winpodx pod wait-ready` 에 `[4/4] Waiting for OEM reboot pass...` 단계 추가; agent transport 로 marker 폴링. upgrade-path 설치 (marker 없음) 는 30초 appear-grace window 후 short-circuit 으로 기존 flow 변화 없음. - **Tray UX 개편: 항상 떠있는 tray + Dashboard 단축키 + 확인 후 Quit.** tray 가 이제 GUI 창과 pod 를 건드리는 모든 CLI 서브커맨드 (`setup` / `gui` / `tray` 제외) 에서 자동 spawn → `winpodx app run` 만 쓰는 사용자도 UNRESPONSIVE 자동 회복 드라이버 활용 가능. tray 아이콘이 제대로 표시 (번들 `winpodx-icon.svg` 가 `tray.show()` 전에 설정 — 이전에는 KDE Plasma / GNOME 이 아이콘 없다고 표시 자체 안 함). 새 "Open Dashboard" 메뉴 항목이 최상단. Quit 가 이제 QMessageBox 로 확인 ("winpodx 를 완전히 종료할까요?") 후 `stop_pod` + `pkill -f 'winpodx gui'` + `app.quit` 실행 — 실수 클릭으로 pod 30초 재시작 비용 발생 방지. `$XDG_RUNTIME_DIR/winpodx/tray.lock` flock 으로 중복 인스턴스 차단. - **`ensure_ready` CLI launch 시 2단계 RDP 회복.** CLI 런치 (`winpodx app run`, GUI 앱 클릭 등) 가 FreeRDP spawn 전에 stall 된 RDP 게스트를 자가 치유: 1단계 = `try_recover_rdp` (agent 로 TermService 재시작, ~5-30초, Windows uptime 유지), 2단계 = `recover_rdp_if_needed` (컨테이너 전체 재시작, ~30초, Windows uptime 리셋). 이전 동작은 connection-refused 로 launch 실패; 이제는 cheap 경로 먼저, agent 도달 불가 시에만 escalate. - **GUI Settings: "Launch winpodx tray at login" 체크박스.** Settings 페이지의 새 토글이 XDG autostart 스펙 통해 `~/.config/autostart/winpodx-tray.desktop` 작성/삭제 — KDE / GNOME / XFCE / Cinnamon 모두 portable. 파일 존재가 source of truth (cfg.toml 필드 / 데몬 불필요), 사용자가 손으로 .desktop 떼서 opt out 가능. `X-GNOME-Autostart-enabled=true` 포함 → GNOME 세션 매니저가 DE 기본값 리셋 후에도 인식. 토글 즉시 적용 — Save Settings 클릭 불필요. - **`uninstall.sh` 가 새 tray + autostart 표면 처리.** 두 신규 섹션: (0a) 파일 삭제 전 `winpodx gui` + `winpodx tray` 프로세스 `pkill -f` + 1초 grace — 두 프로세스가 install / runtime / config 디렉토리에 FD 열고 있음 + tray 가 곧 사라질 컨테이너에 대해 자동 회복 알림 계속 발사하는 걸 차단; (6b) `--purge` 여부 무관 `~/.config/autostart/winpodx-tray.desktop` 항상 제거 — .desktop 은 winpodx 전용이라 binary 없으면 의미 없고, 두면 다음 로그인에 `winpodx: command not found` 발생. - **설치 중 tray spawn 과 UNRESPONSIVE 자동 회복 억제.** install.sh 가 `$XDG_CONFIG_HOME/winpodx/.install_in_progress` marker 작성 (EXIT/INT/TERM trap 으로 삭제). 세 호출 지점이 marker 확인: (1) `maybe_spawn_tray` 가 marker 있으면 skip — `[3/4]` / `[4/4]` 단계 중 GUI/CLI auto-spawn 안 함; (2) tray 의 RUNNING → UNRESPONSIVE 전이 핸들러가 marker 있으면 알림 + 회복 worker skip — 이전 세션에서 떠있던 tray 도 조용히; (3) `ensure_ready` 의 2단계 RDP 회복도 marker 있으면 skip — install 중 사용자가 `winpodx app run` 해도 진행 중인 Sysprep 을 컨테이너 재시작으로 날려먹지 않음. install 진행 중 false-positive "Pod stopped responding" 알림 제거. - **호스트 적응형 Windows-on-KVM 튜닝 프로파일 (#215).** `cfg.pod.tuning_profile` 이 dockur compose 에 대한 WinPodX 의 튜닝 적극성을 제어. 기본값 `"auto"` 는 compose 생성 시점에 호스트를 한 번 probe (constant_tsc + nonstop_tsc, kernel ≥ 5.6 = io_uring, `vm.nr_hugepages > 0`, idle CPU + RAM headroom) 하고 표준 튜닝 중 호스트가 지원하는 것을 적용 — 현재는 invariant TSC 노출하는 x86_64 호스트에 `+invtsc`. 다른 프로파일: `"safe"` (Tier-1 만 — `+invtsc` + Windows `platform_tick`, 호스트 사전설정 불필요), `"off"` (dockur 기본만), `"manual"` (`safe` shape; 향후 개별 knob override). `winpodx info` + `winpodx setup` 둘 다 감지된 capability + resolved profile 을 출력해서 사용자가 무엇이 자동 적용됐는지 확인 가능. @ismikes 제안 (#215). sysguides.com Windows-on-KVM 튜닝 중 호스트 사전설정 필요한 항목 (hugepages, CPU pinning) 은 WinPodX 가 자동 처리하지 않고 `docs/USAGE.md` 에 사용자 호스트 작업으로 문서화. ### Fixed - **`winpodx setup` 재실행이 Windows 패스워드를 조용히 덮어써서 사용자가 잠기는 문제 수정 (#216).** 기존 설치에서 cores/RAM 만 변경하려고 setup 을 다시 돌리면 Windows 패스워드 프롬프트가 또 떠서, 입력한 값이 `winpodx.toml` 에 저장됨. dockur 는 `USERNAME` / `PASSWORD` env var 를 첫 부팅에만 적용하기 때문에 호스트 config 와 Windows guest 계정이 desync 되고 다음 RDP launch 가 `LOGON_FAILED_BAD_PASSWORD` 로 실패. interactive 프롬프트가 이제 기존 config + `cfg.rdp.user` / `cfg.rdp.password` 채워진 상태를 감지해서 보존하고, `winpodx rotate-password` (Windows-side 패스워드 변경 메커니즘 사용) 사용을 안내. non-interactive `install.sh` setup 과 신규 설치는 변경 없음. Linux Mint 22.3 / v0.5.4 에서 @tolistim 제보. - **문서: Fedora 42 / 43 / 44 설치 snippet 이 dnf5 syntax 사용 (#228).** 옛 `sudo dnf config-manager --add-repo ` 는 Fedora 41+ 의 dnf5 에서 subcommand 모양 변경으로 실패. snippet 이 이제 `sudo dnf config-manager addrepo --from-repofile=`; dnf4 형식은 Fedora ≤40 (EOL) 용으로 아래 노트. `README.md` / `docs/README.ko.md` / `docs/INSTALL.md` / `docs/INSTALL.ko.md` / `packaging/obs/README.md` 갱신. @payayas 제보. - **`install.sh` 가 하드웨어 가상화 꺼졌을 때 거짓으로 success 보고하던 문제 수정 (#220).** Linux Mint LMDE 7 에서 @pnogaret2019-code 제보. install loop 가 `qemu-system-x86` 이 이미 깔린 상태를 "all dependencies installed successfully" 로 처리했지만 BIOS 에서 VT-x 가 꺼져있어 `/dev/kvm` 은 여전히 없었음. install.sh 가 이제 (a) 설치 전 사전 경고를 강하게 표시 — 진짜 원인 3개 (BIOS, kernel module, kvm 그룹) 를 진단 명령과 함께 안내, (b) install loop 이후 `/dev/kvm` 재확인하고 없으면 live `lscpu` / `lsmod` / `id` 출력 + 중단. README + 한국어 미러에 "최소 요구사항" 섹션 추가 — 동일 3행 체크 표 — 사용자가 curl-install 라인 복사하기 전에 게이팅 요구사항을 먼저 봄. "설치는 됐는데 Windows 가 안 떠요" 류 버그 리포트 가장 큰 단일 원인 차단. - **`install.sh` Ubuntu 24.04+ / Debian 13 에서 가상 패키지 `qemu-kvm` 대신 실제 `qemu-system-*` 선택 (#200).** VirtualBox 안 xubuntu 26.04 에서 @n-osennij 제보. apt 가 `E: Package 'qemu-kvm' has no installation candidate` 에러 — Ubuntu / Debian 신규 버전이 `qemu-kvm` 을 가상 패키지로 만들고 실제 provider 는 `qemu-system-x86` (또는 `qemu-system-x86-hwe`). `pkg_name()` 가 이제 PR #198 의 freerdp2 → freerdp3 selector 와 동일한 `apt-cache show` probe 패턴 사용: `qemu-system-x86` 우선 (aarch64 면 `qemu-system-arm`), `qemu-system-x86-hwe` fallback, 그 다음 옛 distro 용 `qemu-kvm` fallback. VirtualBox 측 nested virtualization 요구사항은 호스트 환경 — WinPodX scope 밖이라 새 "최소 요구사항" README 섹션이 커버. - **dockur/windows 이미지 pin v5.15 로 bump (#223, #224).** `DOCKUR_IMAGE_PIN` / `DOCKUR_IMAGE_ARM_PIN` + `config/oem/VERSIONS.txt` 가 신규 digest 추적 (`sha256:32abe0836aee...` x86_64, `sha256:5775bcfd335b...` aarch64). v5.14 이후 upstream 누적 fix 흡수, WinPodX 측 compose 표면 변경 없음. - **`install.sh` 가 Ctrl+C 시 `Pod is starting, not running. Start the pod now and apply? [Y/n]` 단계 넘어 진행하던 문제 수정.** 옛 install loop 의 단일 `EXIT INT TERM` trap 이 install marker 정리만 하고 다음 단계로 떨어졌음. trap 3개로 분리 (`EXIT` cleanup-only, `INT` cleanup + `exit 130`, `TERM` cleanup + `exit 143`) + `wait-ready | tee` 파이프라인을 `set +e` 로 래핑 — `|| true` rescue 가 `PIPESTATUS[0]` 를 덮어쓰기 전에 실제 wait-ready rc 보존, SIGINT 인지 실제 실패인지 분기 가능. - **`winpodx setup --non-interactive` 플래그 — install.sh 가 사용하는 setup migration 용.** install.sh 는 최초 설치 후 setup migration 호출. `--non-interactive` 없으면 storage-path prompt 에서 막혀 curl-install 가 stall. setup 이 플래그를 받고, 설정시 모든 interactive prompt 를 "accept default" 로 처리. - **`uninstall.sh` 가 uninstall 중간에 tray 가 다시 살아나던 문제 수정.** Section 0a 가 tray 를 `pkill` 했지만, section 0b 가 reverse-open teardown 위해 `winpodx host-open stop-listener` / `unregister-guest` 호출. `cli/main.py` 가 `{setup, gui, tray}` 외 모든 subcommand 에서 tray 자동 spawn 시키므로 `host-open` 이 새 tray spawn 트리거 → section 0a 작업 즉시 무효화. `uninstall.sh` 가 이제 `WINPODX_NO_TRAY_SPAWN=1` export; `maybe_spawn_tray()` 가 이 env 감지하면 short-circuit, uninstall.sh 가 실행하는 모든 `winpodx` CLI subprocess 가 env 상속 → spawn skip. ## [0.5.4] - 2026-05-19 사용자 리포트 follow-up 릴리스. 느린 회선 compose timeout (#212), #213 / #214 가 드러낸 silent "launched but no window" 실패 모드 제거, CI matrix 에 Ubuntu 26.04 추가 (#206), Windows 언어 / 지역 / 키보드 설정 (#201), 동적 데스크톱 해상도 (#197) 수정. > **메모:** v0.5.3 이 같은 날 먼저 태그됐으나 구식 `hatchling` 의 Trove classifier whitelist 미스매치로 Debian/Ubuntu `.deb` 빌드가 실패. v0.5.4 가 동일 feature set + 패키징 fix 로 대체; 같은 날 두 릴리스 페이지 피하려고 v0.5.3 릴리스 페이지는 삭제. v0.5.3 태그는 history 로 유지. ### Highlights **pull 친화적 compose timeout, 가시적 FreeRDP launch 오류, CI 에 Ubuntu 26.04, 그리고 모든 지원 distro 에서 `.deb` 빌드 그린으로 만든 패키징 fix.** - `podman-compose up -d` / `down` 기본 timeout 120초 → 1800초; `WINPODX_COMPOSE_TIMEOUT_SECS` 환경 변수 지원 (`0` = 제한 없음). @jimed-rand 가 제보한 #212 수정. - `launch_app` 가 spawn 500ms 후 프로세스를 폴링해서 stderr 로그 경로와 함께 `FreeRDP exited immediately with rc=...` raise. @mozjacksson 의 #213 + @ismikes 의 #214 수정. - CI matrix 에 Ubuntu 26.04 패키지 + Python 3.14. #213 / #214 의 미테스트 플랫폼 공백을 닫음 (by @juampe, #206). - Windows `[pod] language / region / keyboard` 키; `user` / `password` / `home` 와 동일한 YAML 인젝션 hardening (by @juampe, #201). - 데스크톱 세션이 이제 FreeRDP 클라이언트 창 크기에 맞춰 동적으로 리사이즈 (by @Zeik0s, #197). - `.deb` 의 `/usr/share/winpodx/` 에 LICENSE / README 배포; Debian/Ubuntu 의 GUI License 탭이 다시 동작 (by @juampe, #201). - GUI 시작 시 Qt 스타일시트 파서 경고 제거 (by @juampe, #203). - **패키징 fix:** `pyproject.toml` 에서 minor-version Python Trove classifier (`:: 3.9` … `:: 3.13`) drop. 구식 `hatchling` (Debian 12 는 1.12.0, Ubuntu 24.04 는 1.18.0) 이 자체 classifier whitelist 를 유지해서 release 이후 추가된 minor-version classifier 를 거부 — v0.5.3 `.deb` 빌드 실패 원인. version-agnostic `:: 3 :: Only` 로 대체. 지원 버전 정보는 그대로 `requires-python = ">=3.9"` 와 CI matrix 에서 제공. ### Added - **Windows 언어 / 지역 / 키보드 설정.** Pod 설치 언어, 지역 포맷, 키보드 레이아웃을 `winpodx.toml` 의 `[pod] language / region / keyboard` 로 설정 가능. 기본값 `English` / `en-001` / `en-US` (기존 config 와 호환); 한국어 Windows 설치는 `Korean` / `ko-KR` / `ko-KR` 처럼 지정. 세 필드 모두 `PodConfig.__post_init__` 의 `_DANGEROUS_YAML_CHARS` sanitisation 과 compose 렌더 시점의 `_yaml_escape` 두 단계 방어를 거침 — 기존 `user` / `password` / `home` 와 동일한 패턴. `docs/INSTALL.md` 와 `docs/INSTALL.ko.md` 에 자주 쓰는 10개 언어 조합 표 추가. **fresh Windows 설치에만 적용**; 이미 설치된 pod 는 volume 삭제 후 `winpodx setup` 재실행 필요. (by @juampe, #201) ### Changed - CI matrix 가 이제 Python 3.14 를 테스트하고 Ubuntu 26.04 패키지도 빌드합니다 (#206). #213 / #214 에서 드러난 미테스트 플랫폼 공백을 닫습니다. (by @juampe) ### Fixed - **compose up/down timeout 을 이미지 pull 친화적으로 조정.** 기본 compose timeout 을 120초 → 1800초로 늘리고 `WINPODX_COMPOSE_TIMEOUT_SECS` 환경 변수를 지원합니다 (`0` = 제한 없음). @jimed-rand 가 보고한 #212 수정. - **`.deb` 패키지에 LICENSE 및 README.md 가 누락되던 문제 수정.** `pyproject.toml` 의 `[tool.hatch.build.targets.wheel.shared-data]` 가 이제 `LICENSE` 와 `README.md` 를 `/usr/share/winpodx/` 에 배포. GUI License 탭이 Debian/Ubuntu 설치본에서 `FileNotFoundError` 로 깨지지 않음. (by @juampe, #201) - **GUI 시작 시 Qt 스타일시트 파서 경고 수정.** `main_window.py` 가 bare property 선언 (`background: ...`) 과 full QSS selector 룰 (`QPushButton { ... }`) 을 혼합해서 Qt 스타일시트 엔진이 파싱을 거부하던 문제. 중앙 widget 에 `objectName="centralRoot"` 부여하고 배경 룰을 `QWidget#centralRoot { ... }` 로 스코프화. 시각 변화 없음, 로그의 `Could not parse stylesheet of object QWidget(...)` 경고만 제거. Library 페이지 Launch/Edit 버튼도 파서 경고 추적 중 가독성을 위해 인라인 QSS 를 멀티라인 블록으로 변환. (by @juampe, #203) - **동적 데스크톱 창 해상도** — 이제 데스크톱 세션을 생성할 때 해상도가 FreeRDP 클라이언트 창 크기에 맞춰 동적으로 조정됩니다. 이 기능이 데스크톱 세션의 기본 동작으로 추가되었습니다. 테스트를 통해 검증되었습니다. (@Zeik0s 작성, #197) - **빠른 종료 시 FreeRDP stderr 를 launch 오류로 표시.** "실행됨" 메시지만 보이고 창이 뜨지 않는 silent failure 를 제거합니다. Plasma 6 / Wayland 및 Python 3.14 edge case 진단에 도움. @mozjacksson 이 제보한 #213 및 @ismikes 가 제보한 #214 수정. - **Debian/Ubuntu `.deb` 빌드가 `Unknown classifier: Programming Language :: Python :: 3.X` 로 실패.** Debian/Ubuntu 가 패키징한 구식 `hatchling` 이 자체 Trove classifier whitelist 를 유지하기 때문에 해당 hatchling release 이후 추가된 minor-version Python classifier 를 거부. minor-version classifier 는 cosmetic 이었고 실제 Python 버전 커버리지는 `requires-python` 과 CI matrix 에서 제공. 모든 hatchling release 가 받아들이는 `Programming Language :: Python :: 3 :: Only` 로 대체. ## [0.5.2] - 2026-05-14 당일 install-path hot-fix 릴리스. 2025+ 인기 distro (Debian 13 Trixie, Ubuntu 24.10+) 에서 신규 설치를 차단하던 `install.sh` 회귀 두 건을 수정하고, Atomic Fedora 지원을 단일 트랜잭션 OBS-layered 설치 경로로 추가. ### Highlights - **`install.sh` 가 Debian 13 / Ubuntu 24.10+ 에서 다시 동작** — 하드코드된 `freerdp2-x11` 의존성을 해당 distro 에서 해결할 수 없었음 (stock repo 에 `freerdp3-x11` 만 존재). `pkg_name()` 의 Debian/Ubuntu 분기가 이제 `apt-cache show` 로 probe 해서 가능하면 `freerdp3-x11` 선택, 옛 시스템에서만 `freerdp2-x11` 로 fallback. 이전에는 Debian 13 / Ubuntu 24.10+ 신규 사용자가 설치 자체 불가. (reported by @basti189, #198) - **Atomic Fedora 설치 경로** (Silverblue / Kinoite / Sericea / Bluefin / Bazzite) via OBS `rpm-ostree install --apply-live` — 단일 트랜잭션, 패키지마다 재부팅하는 루프 없음. winpodx RPM 의 `Requires:` 가 FreeRDP / podman / python3 / tomli 를 transitively 끌어와서 `winpodx` 만 layer 하면 충분. 부팅된 deployment 가 라이브 적용을 거부하면 staged install + 재부팅 안내로 fallback. (by @Zeik0s, #163) ### Added - **Atomic Fedora 설치 지원** (Silverblue / Kinoite / Sericea / Bluefin / Bazzite). `install.sh` 가 `rpm-ostree` 를 autodetect 하고 per-package dnf 루프 대신 단일 트랜잭션 OBS-layered 설치로 분기: 호스트의 Fedora `VERSION_ID` 에 대한 OBS repo probe (현재 published: Fedora_42 / Fedora_43 / Fedora_44), `/etc/yum.repos.d/` 에 `.repo` 파일 drop, `rpm-ostree install --apply-live --idempotent winpodx` 로 부팅된 deployment 에 재부팅 없이 레이어링. 실행 중인 deployment 가 라이브 적용을 받지 못하는 경우 (kernel / init 건드리는 레이어 등) `rpm-ostree install --idempotent winpodx` 로 staged + 재부팅 안내로 fallback. winpodx RPM 의 `Requires: freerdp >= 3.0` + `python3-tomli` 와 `Recommends: podman` + `python3-PySide6` 가 transitively 끌어옴 — 별도 의존성 리스트 유지 불필요. README "Supported distros" 표에 Atomic Fedora 행 + Fedora Atomic Desktops "Works on" 배지 추가; `docs/INSTALL.md` 에 `install.sh` 없이 수동 설치 절차 문서화. 한국어 미러 동시 갱신. (by @Zeik0s, #163) ### Fixed - `install.sh` 가 Debian 13 (Trixie) 및 최신 Ubuntu (24.10 / 25.04 / 25.10) 에서 `freerdp2-x11 not found` 로 실패하던 문제 수정. 해당 배포판은 stock apt repo 에 `freerdp3-x11` 만 포함하고 `freerdp2-x11` 패키지 자체가 제거됨. `pkg_name()` 의 Debian/Ubuntu 분기를 `apt-cache show` 로 probe 해서 `freerdp3-x11` 우선 선택, 오래된 시스템 (Debian <=12, Ubuntu 22.04 stock) 에서만 `freerdp2-x11` 로 fallback 하도록 변경. FreeRDP 3 이 어차피 권장 타겟 — v0.5.1 launcher 가 시작 시 major 버전 감지해서 어느 쪽이든 적절한 `/app:` syntax 사용. (reported by @basti189, #198) ## [0.5.1] - 2026-05-14 유지보수 + 사용성 릴리스. 큐레이트 Windows 에디션 확장, headless 설치 ergonomics, Ubuntu 22.04 LTS 사용자용 FreeRDP 2 호환 fix, 그리고 GUI 코드 베이스를 future-proof 하는 내부 Qt 리팩토링. v0.5.0 과 이 태그 사이에 9 PR 머지. ### Highlights **LTSC IoT, Win10 LTSC, Tiny, Server, 또는 직접 만든 ISO 선택 가능 — 그리고 Ubuntu 22.04 의 기본 FreeRDP 2.11 에서도 `winpodx app run ` 작동.** 이번 릴리스는 주로 v0.5.0 사용자 리포트 follow-up: 한 곳에서 더 많은 Windows 에디션 선택, `install.sh` 와 `winpodx setup` 의 정식 `--win-version` 플래그, FreeRDP 2.x 에서 Microsoft Store 가 뜨던 RemoteApp launch 버그 수정. - **Windows 에디션 picker** (#178, #183, #185, #186) — `cfg.pod.win_version` 이 GUI 설정 → Container/VM 카드, `winpodx setup --win-version`, `install.sh --win-version` 에서 전체 Win10+ 큐레이트 셋 (11, 10, ltsc11, ltsc10, iot11, tiny11, tiny10, 2025, 2022, 2019, 2016) 을 받습니다. 커스텀 ISO 는 `docs/ARCHITECTURE.md` 의 수동 워크어라운드로 문서화. (reported by @gabe39, #178) - **FreeRDP 2 / Ubuntu 22.04 LTS 호환** (#189) — `winpodx app run ` 이 FreeRDP 2.11.x 에서 더 이상 Microsoft Store 를 띄우지 않음. Win32 RemoteApp 실행이 FreeRDP 3 의 조합 syntax `/app:program:X,name:Y,cmd:Z` 대신 separate-flag 형식 (`/app:` + `/app-name:` + `/app-cmd:`) 사용. (reported by @poetman, #158) - **ARM64 (aarch64) 호스트 지원 — Pi 5 / Ampere 시스템 정상 설치** (#176, #177). 호스트 아키텍처를 install / setup 단계에서 감지; aarch64 호스트는 `dockurr/windows-arm` 이미지로 기본 라우팅, compose 템플릿이 x86 전용 `arch_capabilities=off` 서브옵션 없이 `-cpu host` 만 emit — 이전엔 QEMU 의 `host-arm-cpu` 머신 모델이 `Property 'host-arm-cpu.arch_capabilities' not found` 로 거부하면서 크래시. 신규 aarch64 CI job 이 회귀 감지. 남은 Phase 4 (Windows-on-ARM RDP listener 안정화) 는 #141 / #140 에서 추적. (reported by @tslpre, #140) - **Discovery 정리** (#182) — `winpodx app refresh` 가 reverse-open Linux 앱 shim 을 Windows 앱으로 재임포트하지 않음. 다음 refresh 시 기존 오염된 항목 self-heal pass 로 자동 제거. - **주간 Docker Hub digest watcher** (#180) — release-tag-only watcher 가 놓치는 silent dockur rebuild (태그 없는 보안 패치) 를 감지. - **내부**: `WinpodxWindow` Qt 클래스가 2745 줄에서 148 줄 orchestrator + 10 single-responsibility mixin 으로 분해 (#181). 동작 변경 0; 미래 Rust 포팅에 깔끔하게 매핑. - **항상 켜진 하단 로그바 + Live 로그 레벨 picker.** 두 piece 가 함께 작동: - 메인 윈도 맨 아래에 영구적 2-line 로그 ticker — 어떤 페이지를 보고 있든 표시됨. 최신 2개 `log_signal` 라인 (winpodx logger + `cfg.logging.level == "RAW"` 일 때 pod tail) 이 실시간으로 flash. - Terminal 탭에 `DEBUG / INFO / WARNING / ERROR / CRITICAL / RAW` 드롭다운 — 실행 중인 logger 즉시 retarget + `cfg.logging.level` 에 persist. **RAW** 는 평행 `podman logs -f` 스트림 (`[pod]` 접두사) 도 활성화 — dockur / QEMU / Windows 측 메시지가 Terminal 풀뷰 + 하단바 둘 다 흐름. "Windows 안 부팅" / "ISO 다운로드 멈춤" / agent down 진단에 유용. - tail 들은 GUI 시작 시 한 번 켜져서 계속 실행 → 어떤 페이지든 하단바 갱신. Terminal 의 `Live (app)` / `Live (pod)` / `Stop tail` 버튼은 제거됨 (always-on 디자인에서 잉여). - 컨테이너 재생성을 동반하는 설정 저장이 이제 전체 bring-up (Windows 부팅 대기 → 에이전트 settle → 런타임 fix 적용 → 앱 discovery → reverse-open 매니페스트 push) 을 진행 다이얼로그와 함께 자동 실행. "에디션 바꿨더니 앱이 안 떠요" 워크플로 종료. ### Added - `winpodx setup --win-version EDITION` + `install.sh --win-version VER` 플래그. 새 `WINPODX_WIN_VERSION` 환경변수. `docs/INSTALL.md` "Windows 에디션 선택" 섹션 참고. (#178, #185, #186) - GUI 설정 → Container/VM 카드에 `_KNOWN_WIN_VERSIONS` 로 채워진 "Windows Edition" 콤보 추가. Read-only dropdown; 큐레이트 리스트 밖의 커스텀 dockur 태그는 `winpodx.toml` 직접 편집으로 설정 (`docs/ARCHITECTURE.md` "Advanced: Custom Windows ISO" 참고). (#178, #183) - `core/config.py` 의 `_KNOWN_WIN_VERSIONS` 상수 (12 큐레이트 에디션). `PodConfig.__post_init__` 가 공백/case 정규화 + 모르는 값 WARN (reject 안 함). (#178, #183) - **ARM64 (aarch64) Linux 호스트 지원 — #141 Phase 1+2+3.** `install.sh` 와 `core/config.py:_default_pod_image` 가 `uname -m` 으로 호스트 아키텍처 감지 → aarch64 호스트는 `dockurr/windows-arm` 이미지로 라우팅 (dockur 프로젝트의 별도 ARM 빌드와 매칭). `core/pod/compose.py` 가 aarch64 에서 `-cpu host` 의 `arch_capabilities=off` 서브옵션 제거 — 그건 x86 전용 KVM CPUID 하드닝이고 QEMU `host-arm-cpu` 머신 모델은 `Property 'host-arm-cpu.arch_capabilities' not found` 로 거부 (Pi 5 사용자가 `winpodx pod start` 에서 본 정확한 증상). 신규 aarch64 CI 매트릭스 (#177) 가 `ubuntu-24.04-arm` 에서 테스트 스위트 실행해서 아키텍처 인지 코드 경로 회귀 silent 차단. 남은 Phase 4 (Windows-on-ARM RDP listener 안정화, `xtajit` x86 에뮬레이션 위 rdprrap 호환성) 는 #141 + #140 에서 별도 추적. (reported by @tslpre, #140 — Pi 5 / RaspiOS Trixie repro + 전체 QEMU 에러) - `check-windows-updates` 워크플로우에 `:latest` digest drift 감지 추가 — release-tag bump 없이 digest 가 바뀌면 "silent rebuild" 추적 이슈 오픈. `config/oem/VERSIONS.txt` 에 두 새 필드 `dockur-digest=` / `dockur-arm-digest=`. (#180) - `docs/ARCHITECTURE.md` 의 새 "Advanced: Custom Windows ISO" 섹션 — 자체 ISO 사용자를 위한 수동 `win_version = "custom"` + `compose.yaml` 마운트 워크어라운드 문서화. 한국어 미러 갱신. (#178, #184) - `CONTRIBUTING.md` "Crediting contributors in Highlights" 섹션 — CHANGELOG Highlights 의 inline `(by @user, #PR)` / `(reported by @user, #issue)` 컨벤션 명시. 한국어 미러 갱신. (#187) - 새 `[logging]` config 섹션 + `level` 필드 (`DEBUG | INFO | WARNING | ERROR | CRITICAL | RAW`). `setup_logging()` 가 시작 시 읽음 — 모든 winpodx CLI / GUI 실행이 선택된 레벨 사용. 기본 `INFO`; 모르는 값은 reject 없이 `INFO` fallback. Terminal 탭 드롭다운이 live logger 변경 + TOML persist. `RAW` 는 winpodx-only meta-level: Python logger 를 `DEBUG` 로 고정 + Terminal 에 평행 `podman logs -f` tail 동시 시작 → dockur / QEMU / Windows 측 출력이 winpodx 로그와 같은 줄 흐름에 노출 (pod 로그는 `[pod]` 접두사 + 어둡게). - 새 **License** GUI 탭. MIT 라이선스 본문 + "Third-party components" 섹션 (winpodx 가 의존하거나 ship 하는 upstream 프로젝트 목록: dockur, FreeRDP, PySide6/Qt, rdprrap, stascorp/rdpwrap, rcedit, Catppuccin) 과 각자의 라이선스 명시. LICENSE 파일은 `bundle_dir()` 로 해소해서 모든 설치 모드 (소스 체크아웃, wheel, FHS, installer 드롭) 에서 렌더링됨. - **컨테이너 재생성 후 auto bring-up.** GUI 설정 페이지에서 CPU / RAM / 포트 / 사용자 / Windows 에디션 변경 시 이전에는 `_generate_compose` + `_recreate_container` 만 호출하고 끝났습니다 — 새로 만든 게스트에는 부팅된 Windows (에디션이 바뀌면 새 ISO 다운로드부터), 에이전트, rdprrap 멀티세션 apply, 앱 discovery, reverse-open 매니페스트가 모두 없는 상태였고 사용자가 수동으로 여러 CLI 명령을 돌려야 했습니다. 이제 저장 핸들러가 워커 스레드에서 전체 bring-up 을 자동 체인합니다: (1) `pod_status == RUNNING` + `check_rdp_port` 대기 (`cfg.install.wait_ready_stage2_secs`, 기본 900초), (2) `AgentClient.health()` + host token ready 대기 (`cfg.install.wait_ready_stage3_secs`, 기본 1800초 — 새 ISO 다운로드 + Sysprep 시간이 길어서 큼), (3) rdprrap 활성화 + RDP 타임아웃 + OEM 베이스라인을 위한 `winpodx.core.provisioner.apply_windows_runtime_fixes`, (4) `discovery.scan` + `persist_discovered`, (5) reverse-open 매니페스트용 `winpodx host-open refresh`. 새 `BringUpProgressDialog` (window-modal, 무한 진행률 바, Cancel 버튼) 가 각 phase 의 라벨 + sub-detail 을 표시; 동일한 라인이 기존 `log_signal` fan-out 통해 always-on 하단 로그바에도 흐릅니다. Cancel 은 best-effort — phase 1/2 폴링 루프는 2초 사이클 안에서 이벤트 honour 하지만 phase 3-5 의 긴 blocking apply / discover / sync 호출은 도중 중단 불가 (버튼은 "Cancelling..." 텍스트로 disable 되어 요청이 등록됐음을 사용자에게 알림). 새 `BringUpMixin` (`src/winpodx/gui/_main_window_bringup.py`), `WinpodxWindow` 에 세 새 시그널 (`bringup_phase`, `bringup_done`, `bringup_started`), 그리고 `_main_window_settings.py` 의 기존 `_recreate` 워커 끝에서의 kick 으로 구현. +5 tests (`test_main_window_bringup.py`) — happy-path 순서, phase-1 cancel, phase-3 failure surfacing, non-blocking 엔트리, fresh-cancel-event-per-run 커버. ### Changed - **Qt `WinpodxWindow` 클래스가 mixin 으로 분해.** `src/winpodx/gui/main_window.py` 가 2745 줄에서 148 줄로 (-95%); page builder, page behavior, chrome 구성, pod 제어, worker orchestration 이 각각 자신의 private `_main_window_*.py` mixin 모듈로 이동. 순수 구조 리팩토링 — 기능 변경 0, 모든 method resolution 이 MRO 통해 보존됨. (#181) - `core/discovery._is_junk_entry` 가 `C:\Users\Public\winpodx\reverse-open\bin\` 하위 실행파일 reject (새 `reverse_open.sync.is_guest_shim_path` helper 통해 query) — 호스트의 reverse-open Linux 앱 shim 이 Windows 앱으로 잘못 임포트되지 않도록. `persist_discovered()` 가 `_purge_reverse_open_entries` self-heal pass 실행해서 다음 refresh 시 기존 오염 항목 제거. `scripts/windows/discover_apps.ps1` 의 `Add-Result` 가 게스트 측에서도 같은 경로 필터 — defense in depth. (#182) - `docs/USAGE.md` 의 `win_version` TOML 예시가 pre-#183 서브셋 대신 새 큐레이트 셋 반영. 한국어 미러 갱신. (#184) - **License 탭 third-party acknowledgments 완성.** `libvirt-python` (LGPL-2.1+), `docker` / docker-py (Apache-2.0), `tomli` (MIT), reverse-open shim 에 정적 링크되는 `getrandom` Rust crate (MIT OR Apache-2.0) 항목 추가. 선택 의존성인 `[libvirt]` / `[docker]` extras 가 이제 upstream 라이선스 조건에 따라 LGPL / Apache attribution 을 GUI 에 노출; vendored `.exe` 에 정적 링크된 reverse-open shim 의 dual-licensed Rust 의존성도 parity 차원에서 명기. - **BringUpProgressDialog 디테일 강화.** auto bring-up 다이얼로그가 이제 5-phase 체크리스트 (단계별 elapsed 타이머 포함), 폴링 단계 (Pod ready / Agent ready) 동안의 attempt 카운터, 그리고 dockur 컨테이너의 `podman logs -f` 실시간 tail 을 펼침 가능한 영역으로 보여줍니다. 신규 에디션의 ISO 다운로드 + Sysprep 단계 (Phase 2 — 최대 30분+) 동안 Windows 가 실제로 무엇을 하고 있는지 사용자가 볼 수 있게 한 목적입니다. pod-log expander 는 `cfg.logging.level` 과 무관하게 라이브 tail 을 보여주기 위해 다이얼로그 lifetime 에 한정된 자체 `podman logs -f` subscription 을 spawn 하며, 항상 켜진 하단 로그바용 fan-out 으로 `WinpodxWindow.log_signal` 을 통해 흘러오는 `[pod]`-prefix 라인도 같이 수신합니다. `bringup_phase` 시그널의 첫 인자가 이제 안정적인 phase-ID slug (`phase_1_pod` / `phase_2_agent` / `phase_3_fixes` / `phase_4_discovery` / `phase_5_refresh`) — 표시 문구와 독립적으로 다이얼로그가 emission 을 올바른 체크리스트 행에 라우팅합니다. +6 dialog-side 테스트 추가. ### Fixed - `winpodx app run ` 가 FreeRDP 2.x 호스트 (Ubuntu 22.04 LTS) 에서 Microsoft Store 를 띄우던 문제 + FreeRDP 3.x 호스트 (Tumbleweed, Fedora 42/43 의 FreeRDP 3 PPA 등) 에서 `Unexpected keyword` 로 실패하던 문제 둘 다 수정. Win32 RemoteApp 이 감지된 FreeRDP major 버전에 따라 분기: FreeRDP 3 은 조합 syntax `/app:program:X,name:Y,cmd:Z` 유지 (FreeRDP 3 가 `/app:` 를 `:,...` 로 파싱해서 bare path 의 `C:` drive prefix 를 `Unexpected keyword` 로 reject); FreeRDP 2 는 separate `/app:PATH` + `/app-name:NAME` + `/app-cmd:CMD` flag 형식 사용 (FreeRDP 2 가 조합 string 을 literal program path 로 파싱, launch 실패, Windows shell handler fallback 으로 Microsoft Store 띄움). 버전은 `xfreerdp --version` 으로 프로세스당 한 번 감지 후 캐시. UWP 경로는 변경 없음. (reported by @poetman, #158) - `src/winpodx/__init__.py` 의 `__version__` 이 `0.4.3` 으로 stale 상태였고 패키지 버전과 안 맞았음. 이번 릴리스에 `0.5.1` 로 bump; `winpodx --version`, migrate 버전 비교, 설치 마커가 이제 `pyproject.toml` 과 일치. - **YAML 인젝션 hardening** — compose 템플릿의 double-quoted scalar 에 들어가는 4개 `[pod]` config 필드. 손편집 `winpodx.toml` 의 `win_version = '11"\nEVIL: "x'` (또는 같은 형태의 `container_name` / `image` / `disk_size`) 이 YAML scalar 를 깨고 dockur 서비스에 임의 env key 주입 가능했음. 2-layer fix: `core/config.py:PodConfig.__post_init__` 가 위험 문자 (`"`, `\\`, `\n`, `\r`, `$`, `` ` ``) reject + `disk_size` regex 검증; `core/pod/compose.py:_build_compose_content` 가 4개 값 모두 `_yaml_escape` 통과 (defense in depth). `cli/setup_cmd.py` 가 `--win-version` 적용 후 `__post_init__` 재실행 — direct 속성 변경으로 검증 우회 불가. 5개 새 테스트 추가. - **License 탭 acknowledgment 표 정정.** 스모크 + legal review 에서 3개 오류 발견: `rdprrap` 이 GPL-3.0 으로 표기됨 (실제 MIT, 같은 메인테이너); `Catppuccin Mocha` 를 GUI palette 로 credit (실제 theme 은 GitHub Primer Dark — `src/winpodx/gui/theme.py` 참고); `PySide6` 라이선스 표기 부정확. Catppuccin 제거; rdprrap MIT 로 이동; GitHub Primer Dark 추가 (GitHub Inc. 2013 copyright); PySide6 정확하게 `LGPL-3.0-only WITH Qt-LGPL-exception-1.1`. rdprrap NOTICE 의 추가 upstream 3개 (`stascorp/rdpwrap` Apache-2.0, `llccd/TermWrap` MIT, `llccd/RDPWrapOffsetFinder` MIT) 도 GUI 에 노출. 선택 의존성 (Pillow, cairosvg, pyxdg) 도 각자 라이선스로 추가. ### Changed - `.gitignore` 확장 — CLAUDE.md Section 8 ("AI Tooling Footprint Ban") 에 따라 `.mcp.json`, `AGENTS.md`, `CLAUDE.local.md`, `demo.png` 추가 + 표준 scratch / editor / OS 패턴 + 방어적 secret 모양 denylist (`*.env`, `*.pem`, `*.key`, `id_rsa*` 등 — 현재 트리에 해당 파일 없음). - **Arch Linux 지원 상태 정정.** `README.md` "지원 distro" 표가 `pacman / AUR | Supported` 라고 표기했고, `docs/INSTALL.md` 가 `yay -S winpodx` 안내했지만 — 둘 다 현재 사실이 아님. AUR publish 워크플로우는 준비되어 있지만 `AUR_SSH_PRIVATE_KEY` 시크릿이 프로비저닝되지 않아 secret-gated, 따라서 `winpodx` AUR 패키지가 출시된 적 없음. README 행을 "소스 설치 지원 · AUR 패키지는 메인테이너 온보딩 대기" 로 변경, `INSTALL.md` "Arch Linux (AUR)" 섹션을 `install.sh` 안내로 교체, 네이티브 패키지 매니저 도입부 명확화. 한국어 미러도 함께 업데이트. ## [0.5.0] - 2026-05-13 Reverse-open (#48) 이 end-to-end 출시 — 이 릴리스의 headline 기능. Linux 앱이 Windows guest 우클릭 "Open with…" 메뉴에 기본으로 노출되고, **짧은 메뉴 + 긴 "다른 앱 선택" 다이얼로그 양쪽에 앱별 아이콘이 정상 표시**됨. Phase 2 시리즈 완료 (a / b / c / d) + 전체 fix-forward 스택: per-app `.cmd` wrapper, Rust `.exe` shim, 짧은 메뉴 가시성, Firefox handoff, uninstall `--purge` scope, multi-resolution ICO, Desktop 폴더 단축키 + Quick Access 핀, 그리고 embedded-EXE-icon chooser 수정. Chooser 아이콘 문제에 대한 부연: Win10/Win11 Explorer 는 chooser entry 아이콘을 EXE 의 embedded PE resource 에서만 읽고, `Applications\\DefaultIcon` 도 per-slug ProgID 의 `\DefaultIcon` 도 무시함. PR #171 이 유일하게 동작하는 경로 — per-slug `.ico` 를 per-slug `.exe` 의 PE resource 에 직접 embed (vendored rcedit, electron/rcedit v2.0.0, MIT). PR #165 의 hard-link inode 공유 최적화는 의도적으로 폐기 (~500 KB × N 앱 디스크) — Win10/Win11 에서 chooser 아이콘과 inode 공유를 동시에 유지하는 경로는 없음. ### Highlights **Reverse-open 이 end-to-end 출시 (#48).** Linux 앱이 Windows 게스트 우클릭 "Open with…" 메뉴에 기본으로 노출, 양쪽 chooser surface 에서 앱별 정확한 아이콘. 선택 시 호스트 `xdg-open` 으로 파일 열기 round-trip. - Reverse-open Phase 2 시리즈 완료 (#157, #159, #160, #161, #162) — host 측 discovery, listener daemon, Qt Settings 카드, guest handler + sync transport, default-on. - Reverse-open fix-forward 스택 (#164–#171) 이 real-Windows smoke 거치고 — 최종 #171 의 vendored rcedit 통한 EXE embed 아이콘 (동작하는 chooser-icon fix) 으로 마무리. - PR #165 의 hard-link inode 공유는 의도적으로 폐기 (~500 KB × N 앱 디스크); Win10/Win11 에서 chooser 아이콘과 공유 inode 를 동시 유지하는 경로 없음. - `cfg.reverse_open.enabled` 기본값 `True`; 끄려면 `winpodx host-open disable` 또는 GUI Settings 패널. ### 추가 - **Reverse-open Phase 2 chooser 아이콘 fix-forward (#164 → #171).** Phase 2c registration 이 #161 에 들어갔는데 real-Windows smoke 에서 3가지 문제 발견 — EXE 경로 dedup 때문에 chooser 항목이 하나로 collapse, 짧은 메뉴에서 항목 누락, 항목이 generic .exe glyph 로 렌더링. fix-forward 스택: - **#171 — vendored rcedit 으로 per-slug `.exe` 에 per-slug 아이콘 embed.** 각 `winpodx-.exe` 가 shim 의 독립 copy 가 되고 PE resource section 에 매칭되는 `.ico` 를 들고 있음. #170 의 ProgID `DefaultIcon` surface 는 chooser 가 무시; embedded EXE 아이콘이 Win10/Win11 이 honour 하는 유일한 surface. `rcedit.exe` (1.36 MB) 는 `config/oem/reverse-open/shim/bin/` 에 vendor 되고 base64 sync payload 로 guest 에 push. `register-apps.ps1` 가 `-RcEditExe` 파라미터 추가, 슬러그 staging 은 `Copy-Item` + `rcedit --set-icon`. `unregister-apps.ps1` 도 rcedit 청소 미러링. - **#170 — Scaffolding (#171 로 대체됨).** Per-slug ProgID `HKCU\Software\Classes\winpodx-` 추가 (긴 다이얼로그에서 `\OpenWithProgids\winpodx-` value link 의 anchor), shell 아이콘 캐시 invalidate 위한 `SHChangeNotify(SHCNE_ASSOCCHANGED)` P/Invoke, 그리고 listener spawn 사이트에 `slug=…` + 치환된 `argv=…` 캡쳐하는 1줄 INFO 로그 추가 (잘못 동작하는 spawn 의 사후 디버깅용). 이 PR 의 ProgID `DefaultIcon` 쓰기는 #171 에서 제거 (EXE 가 아이콘 담고 있어서 중복); ProgID 자체, SHChangeNotify 호출, spawn-argv 로그는 유지. - **#169 — Desktop 폴더 단축키 + Quick Access 핀.** Start Menu 의 `Linux Apps` 폴더를 가리키는 `Desktop\Linux Apps.lnk` 폴더 단축키와 `Shell.Application` "pintohome" verb 통한 Quick Access 핀. 두 surface 가 Win11 의 "다른 앱 선택 → 이 PC 에서 다른 앱 찾기" 파일 브라우즈 다이얼로그에 노출되어, 사용자가 `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Linux Apps\` 까지 직접 네비게이션 안 해도 됨. - **#168 — Multi-resolution ICO + 전체 freedesktop mimeapps walk.** 아이콘 변환이 16/24/32/48/64/128/256 px 프레임을 담은 ICO 를 생성 — chooser, taskbar, 설정 > 기본 앱 모두 비흐릿한 이미지 선택. Discovery 가 freedesktop 스펙대로 모든 `mimeapps.list` 를 walk (XDG_CONFIG_HOME → XDG_CONFIG_DIRS → `$XDG_DATA_HOME/applications` → `$XDG_DATA_DIRS/applications`), 사용자의 primary `mimeapps.list` 뿐 아니라 — default-handler 감지가 `xdg-mime query default` 가 resolve 하는 것과 일치. - **#167 — `uninstall.sh --purge` 가 guest reverse-open scrub 생략.** `--purge` 는 "host nuke, container 도 사라질 것" 케이스용 — pod 가 곧 삭제될 거면 guest 에 닿아 `unregister-apps.ps1` 돌리는 건 의미없고 느림. - **#166 — 짧은 메뉴 가시성 + chooser 캐시 + Firefox handoff.** `OpenWithList` 쓰기를 sub-key 로 수정 (value 가 아니라 — Windows 는 `OpenWithList` 의 value 들을 MRU 리스트로, sub-key 이름을 populating 리스트로 취급). 레지스트리 쓰기 뒤 `ie4uinit.exe -show` 호출로 logoff 없이 chooser 캐시 invalidate. Firefox 오픈 파일 handoff 를 전용 경로로 라우팅 — 다중 인스턴스 launch 가 기존 인스턴스로 정상 dispatch. - **#165 — Rust `.exe` shim + NTFS hard link.** 기존 PowerShell/.cmd shim 을 `windows_subsystem = "windows"` Rust 바이너리로 교체 — chooser 항목 launch 시 콘솔 창 flash 없음. Per-slug 항목은 원래 source shim 바이너리의 NTFS hard link 로 landing — 같은 inode, flat 디스크 footprint (이 최적화는 #171 에서 폐기, 위 참조). - **#164 — 앱별 `.cmd` wrapper + Linux-default scope + Start Menu 단축키.** 이전 ProgID 기반 등록은 Windows 가 EXE 경로로 dedupe 해서 여러 앱이 chooser 항목 하나로 collapse — 앱별 `.cmd` shape 가 각 slug 에 distinct 경로 제공. 등록 scope 가 "사용자가 실제로 Linux default 로 설정한 앱" 으로 narrow (`is_default_for` 사용) — 모든 MIME-handling 앱 등록은 chooser 를 flood. `install.sh` 가 reverse-open sync 자동 trigger; `uninstall.sh` 에 `host-open unregister-guest` 단계 포함. Start Menu 단축키는 모든 discovered Linux 앱에 생성 (default-handler 상태 무관) — non-default 앱이 "다른 앱 선택 → 이 PC 에서 다른 앱 찾기" 통해 도달 가능 유지. - **Reverse-open Phase 2c — guest 측 핸들러 + sync transport + default-on.** #48 의 end-to-end 루프 완성: 사용자 opt-in 없이 Linux 앱이 Windows guest 의 우클릭 "Open with…" 메뉴에 등장. `cfg.reverse_open.enabled` 기본값 **True**. 3개 guest 측 스크립트 (`config/oem/reverse-open/register-apps.ps1`, `unregister-apps.ps1`, `shim/winpodx-reverse-open-shim.ps1`) + 새 host 측 push 모듈 (`src/winpodx/reverse_open/sync.py`). shim 은 apps manifest + ICO 와 함께 sync payload 에 base64 임베드되어 — 이 PR 이전에 만들어진 기존 pod 에서도 작동 (dockur 의 OEM 번들 스테이징에 의존하지 않음). sync transport 는 기존 bearer-auth `/exec` endpoint 위에서 180초 budget 으로 동작, 성공 시 `cfg.reverse_open.last_synced_at` 영속. `winpodx pod start` 가 host 측 listener daemon 을 자동 spawn (idempotent — 이미 돌면 no-op), `winpodx pod stop` 이 pod 중지 전에 teardown. `winpodx host-open refresh` 가 agent 도달 가능하면 guest 로 push 하고 `pushed N app(s) + M icon(s) → registered` 보고. PowerShell shim 은 `\\tsclient\home\.local\share\winpodx\reverse-open\incoming\` 에 atomic `.json.tmp` 쓰고 `.json` 으로 rename — host listener 가 부분적 request 를 절대 픽업 안 함. guest 측 레지스트리 shape: 앱별 `HKCU\Software\Classes\winpodx-` ProgID + `shell\open\command` 의 shim 호출, 그리고 앱의 MIME 타입이 매핑되는 모든 확장자에 대한 `HKCU\Software\Classes\\OpenWithProgids\winpodx-` (built-in curated 테이블이 top ~25 명확 매핑 커버, 나머지는 타입 문자열에서 per-MIME 추정). +13 tests (`test_reverse_open_sync.py`). reverse-open Phase 2 시리즈 완료, 남은 follow-up 은 shim 의 Go 포팅 (PowerShell 의 ~300-500 ms cold-start 보다 빠름; 추적 중이지만 차단 아님). References #48. - **Reverse-open Phase 2d — Qt GUI 의 Settings 페이지 카드.** Phase 2a/2b 의 모든 CLI 기능을 GUI 로 노출 — enable 토글, allowlist + denylist (`+ Add` / `− Remove` 버튼이 CLI 와 같은 `validate_slug` 문법 통과), 라이브 daemon 상태 (running / pid, cached app 수, generated-at 타임스탬프), 4개 액션 버튼이 기존 `host_open` 핸들러로 디스패치 (`refresh & sync`, `start daemon`, `stop daemon`, `refresh status`). Widget 빌더 (`winpodx.gui.reverse_open_panel.build_panel`) 는 Qt 를 lazy import 해서, 단위 테스트 가능한 순수 Python 헬퍼 (`PanelStatus`, `build_panel_status`, `validate_slug`, `add_slug`, `remove_slug`, `format_status_line`) 가 headless 테스트 환경에서 동작. slug + list mutation 헬퍼는 CLI 의 `add` / `remove` 와 같은 semantics 라 slug 가 GUI 와 `winpodx host-open` 사이에서 round-trip. Settings 페이지가 RDP / Pod 카드 옆 컬럼 레이아웃에 패널을 wiring, 저장은 기존 `_save_settings` 흐름에 통합 — 별도 "Save" 버튼 없이 패널이 공유 `cfg.reverse_open` 에 쓰고 페이지 전체 save 가 원자적으로 영속. 패널 생성 실패는 Settings 페이지를 막지 않음 (logged + skipped) — 이 모듈의 Qt 회귀가 나머지 Settings 를 다운시킬 수 없음. +17 tests (`test_reverse_open_panel.py`). Phase 2c (Go shim + guest `register-apps.ps1` / `unregister-apps.ps1` + agent `/reverse-open/sync` endpoint) 만 남고 real-Windows smoke gate 에 묶여 있음. References #48. - **Reverse-open Phase 2b — inotify 대체 폴링 listener, daemon lifecycle, apps DB hot-reload, 신규 `host-open` subcommand 4종.** Phase 2a (PR #157) 가 apps.json manifest + 앱별 `.ico` 를 stage 했고, Phase 2b 는 guest 가 쓴 request 파일을 소비할 host 측 daemon 을 켬. 새 모듈: (1) `reverse_open/apps_db.py` — `AppsDatabase` + `AppEntry` (apps.json 에서 schema 검증 load), `substitute_path` argv 빌더 (`%f`/`%u`/`%F`/`%U` 첫 개만 치환, 나머지 drop, never re-shelled). (2) `reverse_open/listener.py` — 500 ms 폴링 루프, group/world-writable 디렉토리 또는 다른 uid 소유면 preflight 거부, 64 KB 크기 cap + 8단계 JSON 깊이 cap (파서 exhaustion 방어), Phase 1 의 `SeenUUIDs` 링 버퍼로 replay 방어, schema 검증 (`version=1`, slug-shape `app`, NUL-free `path`, `\\\\tsclient\\` prefix), spawn 전 `safe_open_unc` TOCTOU-safe path 해소, 200건 in-flight cap (오래된 것 우선), 300초 janitor 사이클, 9가지 카운터 stats. subprocess spawning 은 매개변수화되어 테스트가 fork 없이 capture 가능. (3) `reverse_open/lifecycle.py` — fork-fork-pipe daemon spawn (5초 ready sentinel), `$XDG_RUNTIME_DIR/winpodx/reverse-open.pid` 에 atomic 0o600 pid 파일, SIGTERM/SIGINT graceful shutdown, **SIGHUP 가 apps.json reload** (그래서 `host-open refresh` 가 daemon 을 재시작 안 함), stale-pid 회복 (프로세스 사라졌는데 파일만 남으면 status 쿼리에서 정리), idempotent `start_listener`, daemon body 에 `os._exit` 사용 (fork-without-exec parent stack 으로 SystemExit 누수 방지). 새 `winpodx host-open` 명령 4개: `start-listener`, `stop-listener`, `daemon-status` (모두 `--json` 지원), 그리고 `refresh` 가 daemon 동작 중이면 자동으로 SIGHUP 보내고 JSON 요약에 `daemon_reloaded` 보고. +60 tests across `test_reverse_open_apps_db.py` (12), `test_reverse_open_listener.py` (27, Phase 1 의 `safe_open_unc` 를 통한 실제 fork-and-process 통합 테스트 포함), `test_reverse_open_lifecycle.py` (14, 실 daemon spawn 후 request 처리 round-trip 포함), `test_cli_host_open.py` (+5). Phase 2c (Go shim + guest `register-apps.ps1` / `unregister-apps.ps1` + agent `/reverse-open/sync` endpoint — real-Windows smoke gate 필요) 와 Phase 2d (GUI Settings card) 는 deferred 유지. References #48. - **Reverse-open Phase 2a — host 쪽 `.desktop` discovery, ICO 변환, `winpodx host-open` CLI.** Phase 1 의 dormant foundations 를 host-only 사용 가능한 흐름으로 끌어올림. 새 `winpodx host-open refresh` 명령이 `xdg-open` 처럼 `$XDG_DATA_HOME/applications` + `$XDG_DATA_DIRS` 를 walk, 기존 `cfg.reverse_open` 의 allow / denylist 적용, freedesktop 아이콘을 16/24/32/48/64/128/256 px Windows `.ico` 로 변환 (raster 는 Pillow, SVG 는 cairosvg → svglib fallback, 둘 다 없으면 generic placeholder), `~/.local/share/winpodx/reverse-open/` 에 `apps.json` manifest 를 stage — Phase 2b 의 sync layer 가 guest 로 push 할 준비. Discovery 는 input 을 강하게 검증 — `Exec=` 라인의 shell metacharacter 거부, Wine / winapps / winpodx wrapper 제거 (재귀 방어), `Hidden` / `NoDisplay` / `OnlyShowIn` / `NotShowIn` 존중, `TryExec=` 검증, `mimeapps.list` 의 user default 표시. 그래서 `~/.local/share/applications/` 의 악성 `.desktop` 이 staged manifest 를 통해 Phase 2b 의 spawn 경로로 shell 명령을 smuggling 할 수 없음. CLI 는 또한 `status`, `list` (live-scan 또는 `--cached`), `enable` / `disable`, `add` / `remove` (allow / denylist 관리, `--deny` 토글 + slug-shape 검증) 도 노출. 새 optional extra `winpodx[reverse-open]` 가 `Pillow`, `cairosvg`, `pyxdg` 를 가져옴; 없으면 `host-open refresh` 가 여전히 manifest 는 만들지만 앱당 placeholder ICO 를 쓰고 경고 로깅. +60 tests across `test_reverse_open_discovery.py` (25), `test_reverse_open_icons.py` (14), `test_cli_host_open.py` (21). Phase 2b (inotify listener + `lifecycle` daemon + agent `/reverse-open/sync` endpoint), Phase 2c (Go shim + guest 쪽 `register-apps.ps1` / `unregister-apps.ps1`), Phase 2d (GUI Settings card) 는 deferred. References #48. ## [0.4.4] - 2026-05-10 패치 릴리스. v0.4.3 에서 의도치 않게 buffered 됐던 `pod wait-ready --logs` 실시간 출력 복구 + half-uninstalled 자동 복구 + cachyos rotation/sync-password agent-first 우선 + codec flag allowlist 정정 + `--extra-args` escape hatch + btrfs warning suppression + agent-first install / reverse-open 의 dormant Phase 1 foundations. ### 수정 - **`install.sh` 의 `pod wait-ready --logs` 출력이 다시 실시간으로 흐름.** v0.4.3 가 half-uninstalled-state 진단을 위해 wait-ready 호출을 `tee` 로 캡처하도록 바꿨는데, 의도치 않은 부작용으로 Python 의 stdout 이 line-buffered (terminal) 에서 4KB-block-buffered (pipe) 로 전환됨. 모든 `[1/3] container ...`, `[container] ...`, `[2/3] RDP`, `[3/3] activation` 라인이 buffer 에 누적됐다가 wait-ready 종료 시점에 한 번에 flush — 사용자는 ISO 다운로드 내내 아무 출력도 못 보다가 끝에서 한꺼번에 wall of text 받음. `install.sh` 가 이제 wait-ready 호출 앞에 `PYTHONUNBUFFERED=1` 을 prepend, 파이프가 있어도 stdout 을 line-buffered 로 강제. `tee` 캡처 파일 내용은 변하지 않아 `no such container` 구분은 그대로 작동. 2026-05-10 smoke test 중 보고됨. - **config 는 살아있는데 컨테이너만 사라진 half-uninstalled 상태가 `winpodx setup --non-interactive` (install.sh 가 호출) 에서 자동 복구됨.** 이전에는 비-purge `uninstall.sh` (winpodx 파일은 지우되 컨테이너는 남기는 모드) 후 `install.sh` 재실행 시 `Existing config found, skipping setup.` 만 찍고 곧바로 `pod wait-ready` 로 진행, 거기서 `Error: no container with name or ID "winpodx-windows" found: no such container` 로 폭사하는 흐름이었음 — 컨테이너도 함께 날아간 상태였기 때문 (사용자가 인터랙티브 모드에서 모르고 `--purge` 를 골랐거나, 외부 정리 작업이 winpodx 컨테이너까지 잡아간 경우). 유일한 복구 경로가 전체 `--purge` 재설치였는데, 사용자가 stack trace 만 보고 알아내야 할 일이 아님. setup 이 이제 non-interactive 의 existing-config 분기에서 missing-container 상태를 감지 — `cfg.pod.backend in ("podman","docker")` 이고 `pod_status` 가 RUNNING/PAUSED/STARTING 이외를 반환하며 ` ps -a` 직접 프로빙이 `cfg.pod.container_name` 을 찾지 못할 때 — `ensure_ready(cfg)` 를 호출해서 기존 `compose.yaml` 로 컨테이너를 idempotent 하게 재생성. `install.sh` 의 `pod wait-ready` 실패 경로도 stderr 를 구분: 캡처된 출력에 `no such container` 가 포함되면 이전의 generic 60-min-timeout 메시지 대신 "container is missing — likely from a partial uninstall" 힌트 + 정확한 `--purge` 복구 명령을 출력. - **`winpodx rotate-password` 와 `winpodx pod sync-password` 가 in-guest agent 를 먼저 시도하고 실패 시에만 FreeRDP RemoteApp 으로 fallback.** cachyos 에서 xfreerdp3 의 bidirectional drive redirect 가 winpodx 가 `run_in_windows` 용으로 쓰는 headless RAIL 세션에서 깨져 있음 — 스크립트는 guest 에서 실행되지만 `\tsclient\home\...\windows-exec\` 에 쓰인 결과 파일이 host 로 surface 안 됨. 결과적으로 `_change_windows_password` (자동 rotation + 명시적 `rotate-password` 명령) 와 `sync-password` 복구 경로 양쪽 모두 45초 timeout 까지 block 후 channel failure 로 끝남. redirect 가 정상인 openSUSE Tumbleweed 에서는 같은 코드 경로가 잘 동작했기 때문에 오랫동안 놓쳤던 버그. `sync-password` 가 둘 중 더 critical — rotation 이 이미 깨져서 cfg.rdp.password 가 Windows 와 sync 안 되었을 때 사용자가 마지막으로 쓰는 복구 명령인데, 그것마저 hang 되면 출구가 없음. `core/rotation/_change_windows_password` 와 `cli/pod/_sync_password` 모두 이제 `dispatch(cfg, prefer="agent")` 를 먼저 호출 — agent 가 아직 설치 전이면 (`TransportUnavailable`) 이전과 동일하게 FreeRDP 경로로 fallback, agent auth 실패 (`TransportAuthError`) 는 fallback 없이 즉시 보고 (전송 채널 transient state 가 아니라 config drift 신호이므로). 부수 효과로, agent 가 reachable 일 때 `sync-password` 는 recovery password prompt 자체를 skip — agent 는 bearer-token 인증이라 사용자가 원래 install.bat 비밀번호를 잊어도 lock-out 안 됨. pre-rotation `_ROTATION_PENDING_MARKER` recovery 계약은 그대로 — 어느 transport 가 돌았든 mid-rotation disconnect 면 trigger. `docs/TRANSPORT_ABC.md` rule #6 ("NEVER use Transport for password rotation") 을 supersede; rule 의 두 원래 논거 — FreeRDP authenticate 에 OLD 비밀번호가 필요하다는 점, agent 프로세스 메모리에 NEW 비밀번호가 노출된다는 점 — 모두 더 이상 성립하지 않음 (AgentTransport 는 bearer token 인증; 두 transport 모두 guest 에서 PowerShell argv + `net user` argv 로 동등하게 새 비밀번호를 노출하며, 차이는 agent 경로의 in-memory HTTP buffer 1개 vs FreeRDP 경로의 on-disk script file 1개뿐). - **`_BARE_FLAGS` allowlist 에서 invalid 한 OPTIONAL-typed codec 플래그 제거 (`+/-gfx-h264`, `+/-rfx`, `+/-nsc`, `+/-jpeg`, `+/-avc444`).** v0.4.3 스택 머지 직후 xiyeming 의 새 escape hatch 첫 테스트 (`winpodx app run notepad-exe --extra-args="-gfx-h264"`) 가 FreeRDP 자체의 cmdline 파서에서 `[ERROR][com.winpr.commandline]: Failed at index 15: Unexpected keyword` 로 실패. 근본 원인: FreeRDP 3.x 가 플래그를 `COMMAND_LINE_VALUE_BOOL` (진짜 `+/-name` 토글) 과 `COMMAND_LINE_VALUE_OPTIONAL` (`/name:value` 만 허용) 로 나누는데, 위 codec 플래그들은 모두 OPTIONAL — bare-toggle 문법은 처음부터 실패할 운명. 원래 allowlist 패치는 그걸 BOOL 처럼 허용해서 unsafe-looking 인풋이 winpodx 필터를 통과한 뒤 FreeRDP 가 거부하는 confusing log 만 남겼음. 이제 filter 레이어에서 명확한 `Blocked unsafe extra_flag` 경고와 함께 거부, GUI tooltip / CLI help 텍스트도 올바른 우회 syntax 안내: `--extra-args="/gfx:RFX"` (RemoteFX 강제 → H.264 협상 자체를 안 함; 기존 `/gfx` value-regex 가 이미 통과시킴). 진짜 BOOL 토글 (`+/-gfx-progressive`, `+/-gfx-thin-client`, `+/-gfx-small-cache`, `+/-wallpaper` 등) 은 기존대로 작동. ### 추가 - **Agent-first install Phase 1 foundations — host-side state model, config schema, redactor, JSON Schema, CLI stubs.** `docs/design/AGENT_FIRST_INSTALL_DESIGN.md` (PR #144) 에 기술된 agent-first install 아키텍처의 5단계 롤아웃 중 첫 번째. 모두 foundation 코드 — `cfg.install.agent_first = False` (기본) 인 한 동작 변경 0. 새 모듈: (1) `core/install_state.py` — `GuestInstallStep` / `GuestInstallState` dataclass + `fetch_install_state(cfg)` 가 agent 통해 `C:\winpodx\install-state\*.done` 마커를 읽고, agent unreachable 시 `$XDG_STATE_HOME/winpodx/last_install_state.json` cache 로 fallback, never raises. (2) `core/agent_install_state.py` — atomic 마커 파일 primitives (`atomic_write_marker`, `read_marker`, `list_completed_steps`) + corruption-tolerant `RetryCounter` 클래스 + install-failure redactor (`redact_log_line` / `redact_payload`) 가 `net user ` argv, `Authorization: Bearer ...` 헤더, `password=` / `token=` / `apikey=` 쿼리 패턴, 40자 초과 base64 블롭을 디스크 저장 전에 제거하고 `write_install_failure` 가 사용. (3) `core/config.py` 에 `InstallConfig` 추가 (`agent_first`, `wait_ready_stage2_secs`, `wait_ready_stage3_secs`, `auto_resume`, `watchdog_max_respawns`, `watchdog_probe_debounce_count`, `watchdog_probe_debounce_secs`) — `__post_init__` 가 모든 숫자 범위를 clamp 하고 hand-edited TOML 의 잘못된 값을 raise 없이 안전한 기본값으로 coerce. (4) `cli/pod_install_status.py` + `cli/pod_install_resume.py` — argparse stub 을 `winpodx pod` 의 subcommand 라우터에 wiring, Phase 3 의 전체 flag surface (`--json`, `--no-color`, `--logs`, `--non-interactive` for status; `--non-interactive`, `--yes/-y`, `--force` for resume) 노출, "Phase 3 implementation pending" 출력 후 0 종료. (5) `docs/design/install_failure.schema.json` — JSON Schema draft-07, `additionalProperties: false`, slug-shape `failed_step` / `error_class`, capped string 길이, 50줄 `last_log_lines` cap. +95 새 tests across `test_install_config.py` (17), `test_install_state.py` (22), `test_agent_install_state.py` (32 — redactor 의 round-trip invariant 를 검증하는 Hypothesis property test 포함: 임의의 입력 문자열에 대해 출력에 redaction 패턴 substring 이 결코 남지 않음), `test_install_cli_stubs.py` (23). 테스트 카운트: 937 passed, 1 skipped (이전 842/2). Phase 2 (install.bat 를 state machine 으로 재작성), Phase 3 (host-side `wait-ready` 5-stage + `install-status` / `install-resume` 본격 구현 + GUI 통합), Phase 4 (`agent_first = True` 로 default flip), Phase 5 (legacy 코드 경로 제거) 모두 deferred. - **Reverse-open Phase 1 foundations — `src/winpodx/reverse_open/` 모듈 스켈레톤 (paths, config, mime, seen_uuids).** `docs/design/REVERSE_OPEN_DESIGN.md` (#48) 에 기술된 guest→host 파일 열기 경로의 4단계 롤아웃 중 첫 번째. 4개 foundation 모듈이 land 되며 기존 사용자에게는 동작 변경 0: (1) `paths.py` 가 guest 의 `\\tsclient\...` namespace UNC 경로를 설정된 share root 아래의 POSIX 경로로 변환하며, TOCTOU-safe `safe_open_unc` atomic context manager 포함 (보안 리뷰가 원래의 validate-then-open 2-함수 API 의 swap window — validate 호출과 open 호출 사이에 symlink 가 바꿔치기될 수 있는 — 을 catch 한 뒤 재작성됨; 새 형태는 `O_NOFOLLOW` 로 열고 fd 를 잡고 있는 동안 realpath 를 허용된 root 와 검증해서 attacker 가 race 할 window 가 없음); (2) `config.py` 가 `ReverseOpenConfig` 스키마 정의 + dangerous-app denylist (cmd/powershell/wscript 등) 를 baked-in, `cfg.reverse_open.enabled = False` 기본 — 이 PR 은 user-visible 동작 변경 0; (3) `mime.py` 는 guest 가 가장 자주 돌려보낼 파일 타입을 커버하는 86-entry curated MIME→extension 매핑 + curated 테이블 밖은 `xdg.Mime` runtime fallback; (4) `seen_uuids.py` 는 최근 처리된 request UUID 의 persistent ring buffer — replay 방어용으로 LRU eviction 포함 capped size + tmp+rename atomic write 라 mid-update crash 가 파일을 손상시키지 못함. 4개 새 테스트 모듈에 +154 tests, `paths.translate_unc_to_posix` traversal invariant 를 다루는 hypothesis property test 포함 (랜덤 component 시퀀스 / 혼합 separator / encoded `..` / percent-encoded null byte 모두 허용된 root 안으로 resolve 되거나 raise). 테스트 카운트: 814 passed, 2 skipped (이전 660). Phase 2 (transport), Phase 3 (host dispatcher), Phase 4 (guest agent integration) 은 deferred. #48 참조. - **`winpodx app run --extra-args` per-launch FreeRDP 인자 override + GUI Settings 의 Extra FreeRDP args 입력.** 깨진 xfreerdp3 빌드 배포한 distro 들을 위한 진단/우회용 escape hatch. Per-launch `--extra-args` 는 `cfg.rdp.extra_flags` 뒤에 append → 일회성 디버그 플래그가 글로벌 기본값을 이김; 양쪽 경로 모두 같은 `_filter_extra_flags` allowlist 통과해서 unsafe 인자 차단. GUI Settings 페이지에 "Extra FreeRDP args" 입력란이 `cfg.rdp.extra_flags` 에 바인딩 — placeholder + tooltip 으로 자주 쓰는 토글 안내. allowlist 자체도 codec/cache/RAIL 토글 pack 으로 확장 — `+/-gfx-h264`, `+/-gfx-progressive`, `+/-gfx-thin-client`, `+/-gfx-small-cache`, `+/-nsc`, `+/-jpeg`, `+/-avc444`, `+/-wallpaper`, `+/-themes`, `+/-decorations`, `+/-grab-keyboard`, `+/-grab-mouse`, `+/-mouse-relative`, `+/-async-update`, `+/-async-channels`, `+/-auto-reconnect`, `+/-bitmap-cache`, `+/-offscreen-cache`, `+/-glyph-cache` — 이전엔 allowlist 에 없어서 xfreerdp3 도달 전에 silently drop 됐음. 단기 핵심 use case: `-gfx-h264` 가 cachyos 의 xfreerdp3 빌드 (`WITH_VAAPI_H264_ENCODING=ON` 을 FreeRDP 자체가 experimental 로 표시) 에서 xiyeming 이 RemoteApp 의 RAIL post_connect 단계에서 silent fail 한 상황의 우회 — 이제 winpodx.toml 의 `extra_flags = "-gfx-h264"` 또는 GUI 에서 RemoteFX fallback 강제 가능, `winpodx app run notepad-exe --extra-args="-gfx-h264"` 가 가설 확인하는 동안에도 사용 가능. ### 변경 - **`winpodx pod wait-ready --logs` 출력에서 dockur 의 cosmetic "you are using the BTRFS filesystem for /storage" 경고 suppress.** dockur 의 `proc.sh` 가 이 경고를 `df --output=fstype` 만 보고 모든 btrfs 호스트에 무조건 출력 — NoCoW (`chattr +C`) 적용 여부는 확인 안 함. v0.4.3 가 사용자별 bind-mount + 자동 chattr +C 마이그레이션 제공하므로, post-migration 호스트에서 이 경고는 false positive — 실제 fragmentation 신호 (`COW (copy on write) is not disabled for disk image file ...`) 는 이미 사라짐. 스트리밍 되는 로그 라인이 `cfg.pod.storage_path` 가 set 일 때 `(btrfs warning suppressed: NoCoW bind mount in use)` 로 재작성됨 — install.sh 출력이 더 이상 경고처럼 보이지 않음. 아직 named volume 쓰는 legacy 사용자 (`storage_path = ""`) 는 원본 경고 그대로 보여서 `winpodx setup --migrate-storage` 안내가 유지됨. ## [0.4.3] - 2026-05-06 btrfs Copy-on-Write fragmentation 을 정조준한 패치 릴리즈. `0.4.x` 라인의 핵심 storage rework: 사용자별 bind mount + 자동 NoCoW 로 Windows VM 디스크 이미지의 매 write (pagefile / boot / swap 바이트 모두) 가 btrfs extent 를 새로 fork 하던 걸 차단. btrfs 호스트에서 이전엔 수분~수십분 걸려 300초 예산을 자주 timeout 시키던 pod recreate 가 ext4 처럼 30초에 끝남. @xiyeming 의 #121, #122 보고 (cachyos 환경의 #126 "Unable to open apps and desktop" 도 actual root cause 이 동일 — install.bat 가 btrfs CoW 로 인한 file ops 정체로 mid-stage 에서 죽고, agent 가 안 올라오니 host 의 모든 FreeRDP fallback 이 multi-session 으로 reset). **Highlights — 5개 PR 가 end-to-end 로 fix 를 넣음.** 마이그레이션 설계가 처음엔 "chattr +C 를 podman graph root 전체에" (PR #124 — 머지 안 하고 close, 호스트의 모든 컨테이너/볼륨/이미지를 NoCoW 로 바꾸면 btrfs snapshot 쓰는 사용자가 깜짝 놀랄 수 있음), 그 다음 사용자별 bind mount 로 pivot 후 chattr 가 그 한 디렉토리에만 적용되도록 변경. 그 다음 4개 follow-up 패치는 스모크 테스트가 매번 새로운 silent failure 를 드러내면서 추가됨: 1. **PR #125** — bind mount + `chattr +C` + named-volume 마이그레이션 도구. Fresh install 은 `~/.local/share/winpodx/storage` 가 기본 (compose-up 전에 chattr 박혀있어서 dockur 의 첫 raw-disk write 가 NoCoW inherit). 기존 사용자는 named volume 그대로; `winpodx setup --migrate-storage` (또는 `winpodx migrate` 통한 자동 트리거) 가 Windows 재설치 없이 bind mount 로 이동. 2. **PR #127** — compose-prefixed volume name resolution. `podman-compose up` 이 named volume 을 project name prefix 로 materialise (`winpodx-data` 가 아니라 `winpodx_winpodx-data`); 마이그레이션의 volume probe 가 prefixed 형식 먼저 시도 후 bare 로 fallback. 3. **PR #128** — chattr +C 결과 surfacing + post-rsync NoCoW 검증. apply / already-off / failed 결과를 `print()` 해서 사용자가 install.sh 도중 직접 확인; rsync 후 디렉토리 와 sample `.img` 둘 다 `lsattr` 재확인 — chattr 가 0 반환했지만 flag 가 persist 안 된 silent-no-op 케이스 catch. 4. **PR #129** — bind-mount target 이 이미 populated 인 상태에서 auto-migration 재실행 거부. install.sh 가 두 번째 실행되어 stale `cfg.pod.storage_path` 때문에 `_maybe_auto_migrate_storage` 가 64 GiB 를 already-migrated NoCoW 디스크 위에 fresh CoW 복사본으로 덮어쓰는 destructive 시나리오 방어. 5. **PR #130** — fresh install 에서 chattr 가 절대 안 박히던 진짜 root cause. `plan_migration` 이 디렉토리 `mkdir` 전에 `detect_path_fs(target)` 호출. openSUSE Tumbleweed 에선 `findmnt --target ` 가 `rc=1` + 빈 stdout 반환 (docs 와 다르게) — `detect_path_fs` 가 `"unknown"` 반환, `chattr_will_run` False 평가, chattr 분기 통째로 silent no-op, PR #128 의 surfacing print 도 분기 진입 안 해서 fire 안 함. 수정: findmnt 호출 전에 input path 에서 가장 가까운 존재하는 ancestor 까지 walk-up (최대 64 step; `/` 는 항상 존재). **openSUSE Tumbleweed btrfs 에서 end-to-end 검증**: clean wipe → v0.4.2 fresh install (named volume `winpodx_winpodx-data` 생성) → main 으로 upgrade → install.sh 가 `Migrating storage...` 와 `OK: migrated 71 GiB` 사이에 `chattr +C applied to /home/.../winpodx/storage (NoCoW for new files)` 출력; 이후 `lsattr -d` + `lsattr data.img` 둘 다 `C` flag 박힘, dockur 의 `Warning: COW (copy on write) is not disabled for disk image file /storage/data.img` 가 더이상 journal 에 안 나옴, named volume 제거됨, 다음 `podman-compose up` 이 bind mount 마운트하고 Windows 가 CoW warning 없이 부팅. ### 추가 - **사용자별 storage bind mount + btrfs 자동 NoCoW.** `winpodx setup` 의 fresh install 이 이제 Windows VM 디스크 이미지를 legacy `winpodx-data` named volume 대신 사용자 소유 host 디렉터리 (`~/.local/share/winpodx/storage` 기본) 에 bind mount. 그 경로가 btrfs 면 setup 이 compose-up 전 빈 디렉터리에 `chattr +C` 적용 — dockur 가 처음 Windows raw disk image 쓸 때 부모 디렉터리에서 NoCoW 상속해서 Copy-on-Write fragmentation 완전히 우회. flag 는 **이 특정 디렉터리에만** 영향; host 의 다른 podman 컨테이너/볼륨/이미지는 일체 안 건드림 (graph-root 전체에 적용하는 PR #124 설계는 의도적으로 폐기 — closed comment 참조). 기존 사용자는 named volume 그대로 유지 + 그 볼륨이 btrfs 면 마이그레이션 명령 안내 한 줄 표시. @xiyeming 의 #121, #122 보고. - **`winpodx setup --migrate-storage`.** 기존 `winpodx-data` named volume 을 per-user bind mount 경로로 이동 — Windows 재설치 없음. Pre-flight plan 이 source/target 경로, source 크기, target 파일시스템, chattr 실행 여부, target 빈 공간을 표시. 비용: NVMe 에서 ~5-10분 (`rsync -aS` ~60 GiB). Windows 설치 보존 — Sysprep 재실행 없음, ISO 재다운로드 없음. Idempotent: pod stop → 빈 target 에 btrfs `chattr +C` (복사되는 파일이 NoCoW 상속) → rsync → `cfg.pod.storage_path` 저장 → compose 재생성 → 성공 시에만 old named volume 제거 → pod restart. copy 실패 시 source 볼륨 그대로 두고 target 만 wipe — 재시도 시 빈 상태에서 시작. `--migrate-storage-target PATH` 로 destination override; `--yes` 로 confirmation skip. - **업그레이드 시 자동 마이그레이션.** `winpodx migrate` (install.sh 가 매 업그레이드마다 호출) 가 같은 조건을 감지 — `winpodx-data` named volume + mountpoint 가 btrfs + `cfg.pod.storage_path` 비어있음 — 시 자동 마이그레이션. 인터랙티브 실행은 confirmation prompt (default Yes); 비인터랙티브 실행 (install.sh post-upgrade 경로) 은 그냥 진행 — 이미 degraded 상태고 migrate 실행 자체가 fix 적용 목적이라. 실패 시 원본 named volume 보존되고 사용자에게 `winpodx setup --migrate-storage` 수동 재시도 안내. - **`cfg.pod.storage_path` config 필드.** 빈 문자열은 legacy named-volume 모드 유지; absolute path 면 그 경로로 host bind mount. `Config.load` 가 필드 검증하고 잘못된 값은 빈 문자열로 fallback; `_render_storage_blocks` 가 unsafe 값 (newline, quote, brace) 감지하면 named volume 으로 회귀 — hand-edit 한 `winpodx.toml` 이 compose YAML 깨뜨릴 수 없음. ### 수정 - **compose-managed 설치에서 자동 마이그레이션이 silent skip 되던 버그.** `storage_migration.named_volume_exists("podman", "winpodx-data")` 가 `podman-compose up` 으로 생성된 install 에서 항상 False — 실제 볼륨은 `winpodx_winpodx-data` (compose 가 `name: "winpodx"` 프로젝트명으로 prefix). `winpodx migrate` 의 auto-migrate 와 `winpodx setup` 의 Case-2 경고 모두 bare name 기준이라 모든 compose-managed btrfs install 을 "fresh-install" / "nothing to migrate" 로 잘못 분류 — 자동 마이그레이션이 몇 시간 동안 잠자고 있었음. kernalix7 가 openSUSE Tumbleweed 에서 catch (2026-05-06): named volume 이 `winpodx_winpodx-data` 에 있었고, auto-migrate hook 이 dormant 상태였음. 새 `resolve_named_volume(backend)` 가 prefixed 형식 먼저 probe 하고 bare 형식으로 fallback — compose-managed / hand-created 양쪽 다 정확히 resolve. `MigrationPlan.source_volume` 이 resolved 이름을 끝까지 carry — migration 끝의 `volume rm` step 이 정확히 같은 볼륨을 target. - **`detect_path_fs` 가 `findmnt` 호출 전에 존재하는 ancestor 까지 walk-up — 마침내 chattr +C silent skip 의 진짜 root cause 잡음.** kernalix7 의 2026-05-06 세 번째 스모크 테스트 (clean wipe → v0.4.2 설치 → main upgrade) 가 드디어 "chattr +C 가 절대 안 박힘" 의 silent root cause 노출: `plan_migration` 이 target 디렉토리 mkdir 되기 전에 `detect_path_fs(~/.local/share/winpodx/storage)` 호출. openSUSE Tumbleweed 의 `findmnt --target ` 가 `rc=1` + 빈 stdout 반환 (docs 는 nearest existing parent 로 fallback 한다고 하지만 실제론 그러지 않음) → helper 가 `"unknown"` 반환 → `chattr_will_run = (target_fs == "btrfs")` → **False** 평가 → `execute_migration` 의 chattr 분기 통째로 silent no-op → PR #128 의 post-rsync NoCoW 검증도 마찬가지로 `chattr_will_run` False 라 nothing to flag → PR #128 의 surfacing `print()` 도 분기 진입 안 해서 절대 안 fire. 세 번 연속 마이그레이션이 CoW-fragmented 디스크 이미지를 만들면서 "OK: migrated N GiB" 로 success 보고, NoCoW 가 silent skip 됐다는 indicator 0. 수정: input path 에서 nearest existing ancestor 까지 최대 64 step walk-up (`/` 에서 종료 — 항상 존재), 그 path 를 findmnt 에 넘김 → not-yet-mkdir 된 target 도 그것을 포함할 fs 로 정확히 resolve. fresh-install 마이그레이션 plan 의 btrfs target 에 대해 `chattr_will_run` 이 이제 True → `chattr +C` apply / 결과 inline `print()` / post-rsync `lsattr` 검증 셋 다 실제로 실행. - **bind-mount target 이 이미 populated 인데도 auto-migration 이 재실행되며 64 GiB 를 silently overwrite 하던 버그.** kernalix7 가 2026-05-06 두 번째 스모크 테스트에서 catch — PR #127 + #128 머지 후 install.sh 가 한 번 더 실행됐을 때 `cfg.pod.storage_path` 가 비어있고 (근본 원인은 조사 중 — 가능성 하나: install.sh 의 git-update 단계에서 install dir 이 stale 한 채 남아 `PodConfig` dataclass 가 `storage_path` 필드를 갖지 않은 상태였고, `Config.load` 가 round-trip 중 값을 silently drop), 동시에 `podman-compose up` 이 named volume `winpodx_winpodx-data` 를 64 GiB raw disk 와 함께 재생성. auto-migration 이 두 번째 `rsync -aS` 64 GiB 를 `~/.local/share/winpodx/storage` 로 실행 — 이미 첫 번째 마이그레이션 결과물 (수동 `cp --reflink=never` 복구로 `chattr +C` 박힌 `data.img` 포함) 이 들어있던 곳을 silently overwrite. 두 번째 rsync 가 NoCoW 디스크를 CoW-fragmented 복사본으로 덮어써서 perf 효과 날림 + 첫 마이그레이션 이후 named volume 과 bind mount 사이에 갈라진 모든 변경사항도 손실. 새로운 방어: `_maybe_auto_migrate_storage` 가 named-volume probe 이전에 `default_target_path()` (`~/.local/share/winpodx/storage`) 부터 확인 — 존재하고 비어있지 않으면 hook 이 bail out 하면서 사용자에게 `storage_path` 를 `winpodx.toml` 에 복구하거나 populated dir 을 옆으로 옮긴 후 재시도하라고 안내. 디스크의 bind-mount 데이터가 source of truth — 한 번 마이그레이션 됐으면 재실행은 안전할 수 없음. - **`chattr +C` 결과가 install.sh 에서 보이지 않고, post-migration 검증도 없던 버그.** kernalix7 의 2026-05-06 openSUSE Tumbleweed 스모크 테스트가 auto-migration 성공 메시지 ("OK: migrated 64 GiB") 까진 정상이었는데, 직후 `lsattr -d` 로 확인하니 bind-mount 디렉토리에도 / 새로 rsync 된 64 GiB `data.img` 에도 **`C` flag 가 안 박혀 있었음** — chattr 결과가 `log.warning` 으로만 흐르고 (install.sh foreground 출력에서 필터링됨), post-rsync sanity check 도 없어서 NoCoW 효과 0 인 채로 완료 메시지 출력. `execute_migration` 이 이제 (a) chattr 결과 (`disabled` / `already_off` / `failed`) 를 `print()` 로도 출력 — 사용자가 inline 으로 NoCoW 적용 여부를 직접 확인 가능, (b) rsync 후 `is_cow_disabled` 를 target 디렉토리 + 그 안의 sample `*.img` 파일에 대해 재실행 → 불일치 ("dir 엔 +C 인데 새 파일이 inherit 못함", "chattr 0 반환했는데 flag 가 persist 안 됨") 감지 시 `MigrationResult.detail` 에 warning + recovery 명령 (pod 정지 후 `cp --reflink=never` 로 해당 파일 재생성) 첨부. `cp --reflink=never` 는 이미 `+C` 인 부모 디렉토리 안에서 새 파일로 복사 — 새 파일이 inception 부터 NoCoW + flag inherit. 64 GiB CoW 디스크 이미지를 Windows 재설치 없이 retroactively 고치는 유일하게 안전한 방법. ## [0.4.2] - 2026-05-05 Patch release. 0.4.1 위에 작은 fix 두 개. ### 수정 - **`wait-ready --logs` 가 dockur 컨테이너 내부 VNC URL 을 호스트 매핑 포트로 rewrite.** dockur 가 컨테이너 안에서 `visit http://127.0.0.1:8006/` 출력 — 컨테이너 내부에선 8006 이 listen 포트 맞음. 호스트는 그 포트를 `cfg.pod.vnc_port` (기본 `8007`, `core/pod/compose.py` 의 `127.0.0.1:{vnc_port}:8006` 스펙) 로 매핑하므로 사용자 터미널에 스트림된 URL 이 컨테이너 안에서만 reachable 한 포트를 가리키고 있었음. `_wait_ready` 의 `_drain` 이 이제 줄마다 `127.0.0.1:8006` → `127.0.0.1:{cfg.pod.vnc_port}` 치환 — 사용자가 `winpodx.toml` 에 설정한 실제 포트 사용 (커스텀 값도 그대로 반영). @xiyeming 보고. Closes #118. - **Packaging 워크플로우가 create-release race 견딤.** `debs-publish` 와 `rhel-publish` 가 `view || create` 패턴으로 release 보장하는데, `v0.4.1` 태그 push 시 두 job 이 동시에 view-failed 분기 → 둘 다 `gh release create` 성공 (GitHub release-create POST 가 tag uniqueness 에 atomic 아님) → 같은 태그에 GitHub Release 두 개 생성 → 후속 `release.yml` 의 `softprops/action-gh-release` 가 `tag_name: already_exists` 로 fail 하면서 sdist/wheel attach 안 됨. create 를 idempotent 로: try create, `already_exists` 에러 swallow, 그 후 view 로 reachable 확인. race 이긴 쪽이 생성, 진 쪽 create 는 no-op, 둘 다 asset upload 진행. ## [0.4.1] - 2026-05-05 `0.4.x` 라인의 첫 stable 릴리스. `0.4.0rc1` (2026-05-05) 이 soak preview 였고, `0.4.1` 은 동일 코드 경로 + smoke-test triage 중 land 된 polish. 기능적으론 `0.4.0` 의 안정성 + UX 집중의 연장선이며, 모두 fresh 사용자가 거치는 install / migrate 경로에 집중. **Highlights — `agent never installs` regression 완전 종결.** kernalix7 가 ~3일 동안 openSUSE Tumbleweed 에서 fresh-install 실패 재현: `setup.log` 자체가 없음, `C:\OEM\agent.ps1` 복사 안 됨, `qwinsta` 에 disconnected User session 누적 — agent 안 떠서 메뉴 안 채워짐. 3개 독립 실패 모드가 같은 증상으로 수렴: (1) install.bat 첫 PowerShell 엔진 로드 시점에 Windows Defender 실시간 스캔이 `C:\OEM\` 의 파일을 lock (`PS Expand-Archive` 가 rdprrap zip 에서 deadlock); (2) agent 안 떠 있을 때 host-side `migrate` apply chain 이 FreeRDP RemoteApp 으로 fallback 해서 Windows session-takeover 통해 install.bat autologon session kick; (3) apply chain 직후 transient agent-respawn window 가 inline `app refresh` 와 매번 race. 셋 다 fix, 어느 한 layer regress 해도 deadlock 안 돌아오게 다층 방어. **openSUSE Tumbleweed 에서 end-to-end smoke 통과**: install.bat 깔끔 완료, agent transport 통한 `Password sync OK` (`ERRINFO_LOGOFF_BY_USER` cascade 없음), agent transport 통한 apply chain, discovery inline 완료 (`Discovered N app(s)` + retry-on-respawn-race) — `install.sh` exit 전 메뉴 채워짐. `v0.3.0-RTM1` 에서 업그레이드도 동일: image-pin 마이그레이션, agent 통해 apply chain 성공, interactive discovery prompt 가 메뉴 inline 채움. ### 변경 - **Release 트리거가 `*RTM*` glob 에서 별도 `REL-` 마커 태그로 전환.** "RTM" 접미사가 원래 release 마커였지만 (`v0.3.0-RTM1`) 프로젝트가 PEP 440 / SemVer-hyphen 버전 (`0.4.0`, `0.4.0rc1`) 으로 이동하면서 RTM-suffix 태그 ship 안 함. 마커를 버전 태그 안에 섞으면 (`v0.4.0-REL` 같이) PEP 440 valid 아니라 버전 체계 오염 — 새 디자인은 둘을 분리: 버전 태그는 깨끗 (`v0.4.0`, `v0.4.0rc1`), 같은 commit 의 두 번째 태그 (`REL-v0.4.0`, `REL-v0.4.0rc1`) 가 release-trigger 마커. `release.yml` 이 `REL-*` 매치하고 prefix 떼고 버전 추출 — public Release title `v0.4.0`, CHANGELOG lookup `## [0.4.0]` 정확 매치. Prerelease 감지는 PEP 440 + SemVer 둘 다. 사용 흐름: `git tag v0.4.1 && git tag REL-v0.4.1 && git push origin v0.4.1 REL-v0.4.1`. plain version tag 는 4개 packaging 워크플로우 트리거; REL 마커는 GitHub Release 페이지 전용 opt-in. 기존 v0.3.0-RTM1 release 들은 그대로. - **Release notes 가 미공개 중간 섹션 자동 포함.** `v0.4.0rc1` release 페이지가 처음엔 @Mic92 의 모든 contribution (PR #62-#65) 를 놓침 — `## [0.3.1]` 섹션에 credit 있었는데 v0.3.1 이 CHANGELOG 에만 있고 태그/release 안 됐음. 워크플로우가 이제 실제 git tag 가 있는 가장 최근 CHANGELOG 섹션 (= published release) 을 찾아서 현재 섹션부터 그 이전 published 섹션 직전까지 emit — 미공개 중간 섹션의 credit 도 자동으로 다음 release 페이지에 노출. v0.4.0rc1 페이지는 `gh release edit` 로 수동 backfill. ### 수정 - **install.bat 가 first boot PS Expand-Archive 에서 hang 안 함 (`agent never installs` 진짜 root cause).** `Add-MpPreference -ExclusionPath C:\OEM,C:\winpodx -ExclusionProcess rdprrap-installer.exe` 가 install.bat 의 가장 첫 단계 — staging dir 안 어떤 파일도 실시간 스캔 안 됨. `PS Expand-Archive` 를 Windows 내장 `tar -xf` 로 교체 — PowerShell engine 우회로 PS-script 분석 레이어가 추출 가로챌 수 없음 (다른 보안 도구 추가돼도 동일 deadlock 안 일어남). `_probe_password_sync` 가 `WINPODX_REQUIRE_AGENT=1` 받아들여서 agent down 일 땐 probe skip — PR #104 가 빠뜨린 마지막 host-side FreeRDP 경로. v0.3.0 (마지막 정상 baseline) 과 diff: install.bat 코드 거의 동일, rdprrap zip blob hash 동일; 변한 건 OEM bundle 6 → 13+ 파일 + dockur image pin 이 더 엄격한 Defender 정책의 새 Windows 11 빌드 — 조합이 deadlock 트리거. OEM bundle 22 → 23. - **Fresh install 에서 host-side FreeRDP fallback 이 install.bat autologon session 을 더 이상 kick 안 함.** `_apply_runtime_fixes_to_existing_guest` 가 apply chain 호출 전 agent /health 응답 요구 — 응답 없으면 skip. fresh install 에선 OEM bundle 이 이미 같은 state 적용했으니 no-op. `discover_apps` 가 `WINPODX_REQUIRE_AGENT=1` 보면 FreeRDP fallback 대신 `agent_unavailable` raise. `wait_for_windows_responsive` 가 agent /health 예산 60s → 180s. PR #82 (release/0.3.1) 의 gate 제거가 들어온 regression — 0.3.1 이전엔 chain 이 0.1.x → 0.1.9 boundary 에서만 돌아서 race 가 안 보였음. - **install.bat 가 rdprrap TermService cycle 중간에 죽지 않음.** dockur autologon 흐름에서 Sysprep first-logon User session 자체가 TermService 통해 관리됨 — `rdprrap-activate.ps1` 끝의 cycle 이 install.bat session kill. cycle 을 OEM 모드에서 SKIP, install.bat 가 cycle 을 마지막 action 으로 (launchers, `HKCU\Run`, agent spawn, `setup_done.txt` 모두 commit 후). cycle 이 죽여도 setup 작업 다 디스크에 있고, autologon retry 가 새 session + `termwrap.dll` 로드, agent 깔끔히 시작. OEM bundle 21 → 22. - **`app refresh` 와 migrate 의 인터랙티브 discovery prompt 가 agent-respawn race 에서 retry.** `_apply_vbs_launchers` 끝에 `agent-respawn.ps1` detached spawn — ~3s 후 기존 agent kill, 새 agent 시작. 그 윈도우와 겹치면 `/exec` 가 `Remote end closed connection without response` 로 실패. 두 호출 지점 모두 3회 retry (10s spacing); `install.sh` exit 전 메뉴 채워지고 migrate 인터랙티브 경로도 inline 완료. - **install.sh 가 migrate 와 app refresh 사이에 agent settle 대기.** 두 step 사이 `curl /health` poll — agent 가 `_apply_multi_session` 의 TermService cycle 후 돌아올 시간 (최대 60s) 확보. retry 와 합쳐 메뉴가 안정적으로 inline 채워짐. - **`setup.log` 가 redirected PowerShell block 안에서 `Add-Content` 로 self-lock 안 함.** stderr redirect 잡힌 핸들에 `Add-Content` 가 sharing violation → exception 이 같은 redirect 통해 로그됨, 루프. 안쪽 `Add-Content` 모두 `Write-Output` 으로 교체, cmd-level redirect 도 `2>>` → `>>"%SETUP_LOG%" 2>&1`. - **Info 화면이 fresh-install warm-up 중 "Overall: FAIL" 안 깜빡임.** `probe_agent_health` / `probe_guest_exec` / `probe_guest_summary` 의 timeout 모양 에러는 이제 `WARN("agent warming up or busy")` 반환. `run_all()` 이 dependency-aware skip — `agent_health` 가 OK 아니면 downstream probe 들이 `status="skip"`. 단일 warm-up 신호, cascade FAIL 폭주 없음. - **AgentTransport 선택이 host-side token 가용성 존중.** `/health` 는 unauthenticated 이지만 `/exec` 는 token 필요 — token 파일 race 시 `dispatch()` 가 green `/health` 보고 agent 선택 후 첫 `/exec` 에서 token-missing. `AgentClient.auth_ready()` 가 `(bool, detail)` 반환, `AgentTransport.health()` 가 이거 확인 — token 안 ready 면 `available=False`. 기본 dispatch 가 `FreerdpTransport` 로 fallback. token 자체는 노출 안 됨. - **OEM bind mount 가 bundle 이 user-writable 일 때 복사 안 함.** PR #95 가 항상 user-owned path 로 복사하게 만든 게 majority case (curl install / source / Nix) 에서 `cp: Permission denied` 일으킴. `os.access(bundle_oem, R_OK | W_OK)` 분기 — 사용자 소유면 그대로 반환, 아니면 user-space 복사 + 명시적 `chmod`. - **Defense-in-depth hardening 일괄.** Config TOML load 시 재검증; `_ensure_config()` 가 password + timestamp 자동 생성; `PasswordFilter` 가 FreeRDP `/p:` 도 redact; rotation 이 `generate_compose` → `cfg.save` 순서로 half-state 방지. `install.bat` ASCII-only + tar 절대경로. `tests/test_oem_install_bat.py` 가 정적 invariant 검증. ### 추가 - **README 에 Star History chart** (light/dark ``). 클릭하면 star-history.com 인터랙티브 차트. ### Contributors Thanks to @Mic92 (Nix flake packaging, default-image switch, FreeRDP argv[0] match, Wayland XWayland gate — PRs #62-#65) and @pgarciaq (SELinux OEM bind mount fix — PR #95). ## [0.4.0] - 2026-05-03 설치 / 마이그레이션 경로의 안정성 + UX 에 집중한 주요 릴리스. dockur 의 `:latest` push cadence 가 더 이상 컨테이너 재생성을 트리거하지 않음 (이미지 SHA pinned). 앱 launch 와 agent autostart 에서 PowerShell 콘솔 깜빡임이 모두 제거됨. fresh install 이 Windows 준비될 때까지 정직하게 대기. multi-session 활성화가 `apply-fixes` / `migrate` 로 hands-free. SELinux-enforcing 시스템 (Fedora) 가 out of the box 동작. RTM-suffix pod (`0.3.0-RTM1`) 정상 마이그레이션. `winpodx app refresh` discovery race 가 3계층으로 차단. Contributing 정책 + 라이프사이클 문서 추가. ### 수정 - **Discovery 에러 분류 + GUI 다이얼로그가 더 이상 session-disconnect 실패를 "Pod Not Running" 으로 잘못 표시 안 함.** 이 fix 이전엔 분류기가 `"no result file"` 또는 `"auth"` 포함 메시지를 무조건 ``DiscoveryError(kind="pod_not_running")`` 로 매핑. pod 가 멀쩡히 살아있는 session-side 에러도 같이 잡혀버림 -- 구체적으로 `ERRINFO_LOGOFF_BY_USER` (0x0001000C) 와 `ERRINFO_RPC_INITIATED_DISCONNECT` (0x00010001). 둘 다 outer 메시지가 "no result file written" 인데 (스크립트가 다 못 쓰고 끝나니까), 의미는 게스트가 FreeRDP RemoteApp 세션을 mid-call 종료 -- multi-session 활성화의 TermService cycle 직후 흔함. GUI 가 살아있는 pod 에 "Start Pod" 버튼 띄움 (kernalix7 2026-05-03 fresh install 직후 발생). 수정: `_classify_channel_error()` 가 3개 상태로 명시적 구분 -- transport-layer 실패 (connection refused / connect_transport_failed / agent unavailable) → `pod_not_running`; session-layer disconnect (LOGOFF / RPC_INITIATED_DISCONNECT) → 새 `session_disconnected` kind; 그 외 → `script_failed`. GUI 핸들러가 새 kind 에 대해 "Discovery Session Disconnected" 다이얼로그 + Retry 버튼 (잘못된 "Pod Not Running" / "Start Pod" 안내 대신). - **`winpodx pod start` 가 Fedora / SELinux-enforcing 환경에서 더 이상 `lsetxattr: operation not permitted` 로 실패 안 함.** 3개 버그 체인: (1) `bundle_dir()` 의 marker check (`scripts/`, `config/`, `data/`) 가 `any()` 사용 — 셋 중 하나만 있어도 winpodx 번들로 인식, RPM 제거 후 남은 `/usr/share/winpodx/config/` skeleton (`agent_token.txt` 가 RPM 트래킹 안 돼서 잔존) 이 curl-only 설치에서도 path resolution 가로챔. (2) `_find_oem_dir()` 가 번들 path 직접 반환; RPM/wheel 설치 시엔 `/usr/share/winpodx/config/oem/` (root-owned), compose template 가 `:Z` 로 SELinux relabel 마운트 → rootless Podman 이 root-owned 파일 relabel 못해서 `lsetxattr: operation not permitted` 실패. (3) `stage_token_to_oem()` 이 `agent_token.txt` 를 *번들* OEM 디렉터리에 작성 → RPM 설치에서 runtime token 이 `/usr/share/winpodx/` 아래로 (RPM untracked, `rpm -e` 후 잔존) → 버그 1 로 피드백. 수정: `_find_oem_dir()` 가 번들 OEM 트리를 `~/.config/winpodx/oem/` (user-owned, `:Z`-relabel 가능) 으로 복사 후 그 path 반환; agent token 도 같은 indirection 통해 user 영역에 작성. `bundle_dir()` marker check 를 `any()` → `all()` 로 변경 → 부분 leftover 가 더 이상 매치 안 함. (Thanks @pgarciaq — PR #95, fixes #93.) - **install.bat 가 끝에서 agent 를 *직접* 시작 -- HKCU\Run 미래 등록만 하지 않음.** HKCU\Run 은 *user logon 당 1회* 발사. autologon User 세션은 install.bat (FirstLogonCommands) 가 실행될 때 이미 logon 완료된 상태 -- 그래서 우리가 HKCU\Run 에 등록한 건 *다음* 세션의 agent 만 셋업. install.bat 가 도는 *현재* 세션엔 agent 없음, 사용자 (또는 호스트 RDP probe) 가 새 세션 열기 전엔. install.sh wait-ready phase 3 가 매 fresh install 마다 /health 에서 timeout (kernalix7 가 `OK Windows ready (17:57)` + agent-missing 경고 보고, 이후 migrate apply 체인이 FreeRDP rc=12 LOGOFF_BY_USER + rc=1 RPC_INITIATED_DISCONNECT 로 실패 -- agent 없는 Windows 에 명령 시도한 정확히 그 패턴). 수정: install.bat 끝에 PowerShell `Start-Process` 블록이 현재 세션에 agent spawn (`wscript.exe + hidden-launcher.vbs` 경유, wrapper 없으면 직접 PS fallback -- HKCU\Run 등록과 동일 setup.log 진단). install.bat 끝나는 시점에 agent /health up -> phase 3 가 몇 초 안에 성공 -> migrate apply 체인이 healthy agent 에 실행 -> FreeRDP-fallback cascade 없음. HKCU\Run 등록은 그대로 남음 -> 미래 user logon (호스트 재시작 후 RDP 재접속, multi-session 앱 launch) 에서도 agent 발사. OEM 번들 20 -> 21. - **`check_rdp_port` 가 단순 TCP-accept 대신 실제 RDP 핸드셰이크 수행.** dockur 의 QEMU slirp 가 호스트의 매핑된 RDP 포트로의 TCP 포워드를 QEMU 프로세스 시작 즉시 accept -- Windows 게스트의 TermService 가 올라오기 한참 전에. 이 fix 이전엔 helper 가 `socket.create_connection` 만 함 -- 그래서 fresh install 의 ISO 다운로드 중간 (또는 QEMU 부팅했지만 Windows 가 RDP 리스너까지 못 온 어떤 pod) 이면 컨테이너 시작 즉시 "RDP port open" 보고. install.sh `wait-ready` 가 phase 2 를 0초에 통과, phase 3 (PR #91 이후 /health miss 시 warning + True 반환) 도 통과, 그리고 migrate 의 apply 체인을 Windows 없는 상태에서 실행 -- FreeRDP rc=147 / "Connection reset by peer" 폭격으로 표면화. kernalix7 2026-05-02 21:53 fresh install 에서 [container] 로그가 Windows ISO 7% 다운로드 중인데 호스트는 이미 다음 단계로 진행한 것 보고. 수정: `check_rdp_port` 가 이제 minimal X.224 Connection Request (TPKT-wrapped) 보내고 2바이트 읽음. 진짜 RDP 서버 = TPKT 응답 (first byte `0x03`, second byte `0x00`); QEMU-forwarding-with-no-guest = slirp TCP 스택의 SYN-ACK 받지만 recv 가 timeout / EOF 반환. ~1초 안에 구분. 새 helper `check_tcp_port` 가 옛 TCP-accept-only 동작 노출 -- 이게 정당하게 필요한 한 곳 (`recover_rdp_if_needed` 안의 VNC liveness 체크 -- VNC 는 RDP 안 쓰니까 X.224 probe 가 자연스럽게 fail) 위해. - **`wait_for_windows_responsive` 가 더 이상 agent /health 안 떠도 `install.sh` phase 3 에서 deadlock 안 함.** 이전엔 RDP 포트 열림 AND agent.ps1 /health 응답 둘 다 있어야 return True. agent 가 안 뜨는 어떤 경로 (HKCU\Run 등록 오류, autologon 중간, agent token 불일치, port-mapping blip, install.bat staging 실패) 든 helper 가 caller timeout 전체 (`install.sh` 의 `winpodx pod wait-ready --timeout 3600` = 3600초) spin. kernalix7 이 2026-05-02 fresh install 에서 VNC 로 데스크톱은 보이는데 `[3/3] Waiting for Windows activation...` 에서 30+분 멈춤 보고. 이제: stage 1 (RDP 포트) 는 필수 그대로; stage 2 (agent /health) 는 best-effort, `min(timeout, 60s)` 로 cap. /health 응답 = True (agent 경로 live). /health 응답 X = True 반환 (Windows 자체는 응답 — host code 가 FreeRDP RemoteApp 으로 `transport.dispatch` 폴백) + warning 로그 (`C:\winpodx\setup.log` 진단 안내). RDP 포트 자체 안 열린 경우만 False. 다른 코드 경로의 agent-first 선호는 동일, boot probe 에서만 더 이상 block 안 함. - **install.bat staging 에 per-file 검증 + structured fallback 추가 — "Cannot find script file" wscript 다이얼로그가 fresh install 막는 일 차단.** OEM v20 이전 install.bat 가 5개 launcher 파일을 `C:\Users\Public\winpodx\launchers\` 로 복사할 때 `2>nul` 로 에러 무시 + `HKCU\Run\WinpodxAgent` / `WinpodxMedia` 를 `reg add` 로 *무조건* 그 경로 가리키게 등록. copy 가 silent fail (Sysprep 중 네트워크 share blip, AV 간섭 등) 하면 registry 가 존재하지 않는 파일 가리킴; 다음 user logon 때 wscript.exe 가 "Cannot find script file" 다이얼로그 띄우고 세션 무기한 block (kernalix7 2026-05-02 ~19:58 fresh install 에서 hit). `reg add` 자체도 취약: cmd 파싱 시 `/d "..."` 안의 `\"escaped quotes\"` 가 살아남지만 reg.exe 가 literal backslash-quote pair 로 저장, logon 시 CommandLineToArgvW 가 실제 quoted argument 와 다르게 평가. 3방향 수정: (1) per-file 복사 검증 — 각 `copy /Y` 다음에 `if exist` 체크 + `C:\winpodx\setup.log` 에 결과 기록 (파일별 `launcher OK:` / `launcher FAILED:`). (2) HKCU\Run 등록을 PowerShell `Set-ItemProperty` 한 블록으로 이동 — 깨끗한 .NET 문자열 저장, cmd-quoting 지옥 없음. (3) 존재성 기반 fallback — `hidden-launcher.vbs` 가 copy 안 살아남으면 (LAUNCHERS_OK 미설정), HKCU\Run 을 legacy 직접 `powershell.exe -WindowStyle Hidden -File ...` 형태로 등록. 그 fallback 은 짧은 PS 콘솔 (~50ms) flash 있지만 agent 는 시작됨; 없이는 사용자가 다이얼로그 막혀서 데스크톱 자체를 못 봄. 동일 setup.log 가 어느 경로 선택됐는지도 기록 → apply-fixes 가 나중에 probe 가능. OEM 번들 19 → 20. - **VBS 파일 ASCII-only — 영어 외 Windows 에서 Windows Script Host 가 더 이상 파싱 실패 안 함.** `config/oem/hidden-launcher.vbs` 와 `config/oem/launch_uwp.vbs` 의 선두 주석에 em-dash 문자 있었음. wscript.exe 가 `.vbs` 파일을 시스템 default codepage (한국어 Windows 면 CP-949) 로 읽음. em-dash UTF-8 바이트 시퀀스 (0xE2 0x80 0x94) 가 multi-byte 시퀀스로 디코드되며 statement 중간에서 끝남 -> HKCU\Run 발사 시점에 "Windows Script Host" 에러 다이얼로그 -> Sysprep 에서 install hang. PR #88 이 `.ps1` 에 대해 잡은 것과 동일한 root cause; 이 PR 이 `.vbs` 까지 ASCII-ify 해서 audit 완료. 전체 스크립트 파일 (PS 5개 + VBS 2개) 모두 strict ASCII. - **`agent-respawn.ps1` 의 legacy fallback 제거.** 옛 코드가 `Test-Path hidden-launcher.vbs` 체크 + else-branch 가 `Start-Process powershell.exe -WindowStyle Hidden -File agent.ps1` (PR #58 가 `HKCU\Run\WinpodxAgent` 에서 고친 것과 동일한 broken 패턴) fallback. OEM v13 이후엔 wrapper 가 항상 staging (install.bat + `_apply_vbs_launchers` 둘 다 push) 되므로 fallback 은 파일시스템 손상 / 수동 삭제 케이스에만 발사 — 그런데 발사하면 매 apply-fixes 사이클마다 console flash. wrapper 없으면 `exit 1` 로 교체: 다음 user logon 때 HKCU\Run 재발사 (wscript+hidden-launcher.vbs 경유) 로 agent 복구, flash 없음. flash 발생 경로 모두 차단. - **rdprrap 이 이미 patched 됐는데 - **첫 부팅 앱 discovery 가 일관적 — "지난 설치엔 메뉴 떴는데 이번 설치엔 UWP 빠짐" 같은 stochastic 동작 사라짐.** install.sh 시점 `winpodx app refresh` 가 들쭉날쭉했던 race 조건 3개가 독립적으로 발사돼서, 셋 중 하나만 걸려도 빈/부분 결과: (1) Sysprep 직후 AppX deployment 가 아직 진행 중이라 `Get-AppxPackage` 가 부분만 반환; (2) Start Menu indexer 가 `.lnk` 들 propagation 중; (3) install.sh 가 app refresh 직전에 migrate 돌리는데 (multi-session 활성화 TermService cycle 트리거 가능) agent 가 mid-respawn 상태에서 discovery `/exec` 발사. 3-계층으로 race 자체 제거: - **게스트 readiness gate** (`scripts/windows/discover_apps.ps1`): `AppXSvc.Status -eq 'Running'` AND ProgramData Start Menu `.lnk` count > 0 이 1초 간격 3샘플 연속 안정될 때까지 대기 (StartPending → Running blip 잡음), 최대 60초 budget. "stable, proceeding" / "budget exceeded" 로그를 stderr 로 출력해서 게스트 타이밍을 apply-fixes / refresh 출력에서 볼 수 있음. - **호스트 transport readiness** (`core.discovery.discover_apps`): 스크립트 호출 전 agent `/health` + RDP 포트 응답을 최대 30초 polling. 이거 없으면 multi-session 활성화로 갓 죽인 agent 한테 discovery 발사 → channel-failure cascade 로 멀쩡한 pod 에서 실패. - **호스트 retry-on-empty**: 첫 pass 가 suspiciously empty (< 5 total OR 0 UWP — stock Win11 에선 둘 다 불가능) 면 8초 대기 후 1회 재시도. 큰 결과 선택해서 retry 가 regression 안 됨. - Default discovery timeout 120초 → 180초 로 bump (새 readiness gate 흡수). Retry-on-empty 는 1회로 bounded — 진짜 앱 적은 stripped 이미지가 무한 loop 안 함. - **rdprrap 이 이미 patched 됐는데 marker 만 stale 인 상태에서 `apply-fixes` 가 더 이상 TermService cycle (+ agent kill) 안 함.** PR #81 이 `_apply_multi_session` 을 self-heal 하게 만들면서 `.activation_status` marker 가 `enabled` 외 값이면 무조건 `rdprrap-activate.ps1` spawn 했음. 마커 없는 pod (pre-OEM-v15) / `not-activated` / `installer-failed` 에서는 정확하게 작동. 그런데 kernalix7 처럼 marker = `installer-failed` (OEM-time 부분 실패의 잔재) 지만 `ServiceDll` 은 OEM-time 에 이미 `termwrap.dll` 로 성공적으로 flip 돼서 multi-session 동작하는 pod 도 같이 트리거. 이 상태에서 활성화 재실행 = 불필요한 TermService cycle → agent 의 RDP 세션 죽음 → (`HKCU\Run` 은 user logon 때만 발사라) 사용자가 앱 안 띄우면 agent 영구 dead. 반복 apply-fixes 호출 (`install.sh --main` 업그레이드마다 migrate 의 apply 체인 재실행) 이 매번 agent 죽임. 수정: detached activator spawn 하기 전에 `HKLM\SYSTEM\CurrentControlSet\Services\TermService\Parameters\ServiceDll` 도 같이 probe. 이미 `termwrap` 매치면 marker 를 `enabled` 로 reconcile 하고 return — 이후 apply-fixes 는 fast path 로 들어가고 *TermService cycle 안 함*. - **`HKCU\Run\WinpodxMedia` 가 더 이상 매 앱 launch 마다 검정 PS 콘솔 깜빡 안 함.** OEM v19 이전 install.bat 은 `media_monitor.ps1` (USB 자동 매핑 백그라운드 프로세스) 을 `powershell.exe -WindowStyle Hidden ...` 그대로 등록 — `-WindowStyle Hidden` 은 conhost 가 자식한테 console 잠깐 할당한 후에야 적용돼서 ~50ms 검정 flash 가 새어 나옴. multi-session 켜진 상태에선 매 앱 launch 가 새 RDP 세션 만들고 HKCU\Run 처음부터 발사하므로, 사용자는 모든 launch 마다 flash 봄 — 보이는데 글자 안 읽힘, 전형적인 Hidden-flag race. 수정: install.bat 이 이제 `wscript.exe hidden-launcher.vbs powershell.exe ... media_monitor.ps1` 형태로 등록 (PR #58 가 `WinpodxAgent` 에 적용한 wscript+SW_HIDE 래퍼 동일). 마이그레이션: `_apply_vbs_launchers` 가 wrapping 안 된 항목 발견 시 `HKCU\Run\WinpodxMedia` 재작성 — 기존 pod 도 다음 `winpodx pod apply-fixes` (또는 `winpodx migrate`) 에서 자동 적용. *다음* RDP 세션 / 앱 launch 부터 효과 — 현재 세션의 이미 실행 중인 media_monitor 는 살아있지만 재-spawn 안 하므로 추가 flash 없음. OEM 번들 18 → 19. - **`cfg.pod.image` 가 SHA-pinned dockur image 로 default; migrate 가 기존 pod 도 정렬.** 이전엔 `cfg.pod.image` 가 `docker.io/dockurr/windows:latest` (또는 v0.3.0 이하 설치는 `ghcr.io/dockur/windows:latest`) 로 default 였음. 매 `podman-compose up` 마다 tag 가 dockur 가 그 사이 push 한 최신으로 재해상도됨. resolved digest 가 바뀌면 (자주 — dockur 릴리스 주기가 거의 일별), podman-compose 가 spec mismatch 로 판단해서 **컨테이너 재생성**. kernalix7 이 2026-05-02 정확히 이 상황 만남: dockur 가 proc.sh substring failure (`proc.sh: line 137: -1: substring expression < 0`) 가 든 `:latest` push 한 직후, 일상적인 `install.sh --main` 업그레이드 가 멀쩡한 pod 위에 컨테이너 rebuild + 7.5GB ISO 재다운로드 + Sysprep 초기화 트리거. Pin: `cfg.pod.image` default 가 `docker.io/dockurr/windows@sha256:20b398ab935465f97ec8ab06489f7a85a5ad58e74e036ce66cc3c9172e7dbea8` (릴리스 시점에 Docker Hub registry 에서 조회 후 `core.config` 의 `DOCKUR_IMAGE_PIN` 으로 보관). Migrate 의 "already current" + cross-version 경로 모두 새 `_ensure_canonical_image_pin` 단계 호출 — 기존 pod 의 `cfg.pod.image` + `compose.yaml` 을 main fresh install 과 동일한 canonical pin 으로 재작성. 다음 `pod start` 에서 컨테이너 1회 재생성 (~30초, storage volume 보존 → ISO 재다운로드 없음, Sysprep 없음), 이후 dockur :latest 변동 영향 없음. Idempotent — 이미 pinned 된 config 에 migrate 재실행하면 rewrite 전에 short-circuit. - **`winpodx setup --update-image` 명시적 dockur 버전 갱신.** 기존 `setup` 서브커맨드에 새 플래그. 사용자의 container backend 로 `docker.io/dockurr/windows:latest` pull → 로컬 image 의 repo-digest 해결 → `cfg.pod.image` 에 저장 → `compose.yaml` 재생성. 새 pin 을 출력해서 사용자가 무엇으로 잠그는지 확인 가능. 다음 `pod start` 시 migrate 경로와 동일한 recreate 비용 (~30초, volume 보존). **fresh `:latest` 를 pull 하는 유일한 경로** — 다른 모든 경로는 bundled / persisted pin 사용. ## [0.3.1] - 2026-05-02 v0.3.0-RTM1 → main 마이그레이션 경로가 컨테이너 재생성 없이 multi-session 활성화 갭을 실제로 self-heal 하도록 만든 maintenance 릴리스. OEM-time 과 runtime rdprrap 활성화 경로도 단일 스크립트로 통합. ### 추가 - **Nix flake.** `nix run github:kernalix7/winpodx`, `nix profile install github:kernalix7/winpodx`, 또는 `inputs.winpodx.url = "github:kernalix7/winpodx"`. Wrapper 가 FreeRDP, podman / podman-compose, iproute2, libnotify 를 번들로 포함해 기본 podman 백엔드는 추가 설정 없이 동작; docker 와 libvirt 는 opt-in 유지. devShell 에 동일한 런타임 툴 + ruff + mypy + `src/` 를 `PYTHONPATH` 에 노출. README (en + ko) 에 Nix install 섹션 추가. (Thanks @Mic92 — PR #65.) ### 변경 - **`paths.bundle_dir()` — 번들 리소스 트리 단일 resolver.** 이전엔 7개 호출 사이트가 각자 `__file__.parent.parent…` walk + 일관성 없는 fallback 손수 굴림: discovery 스크립트 lookup, OEM 번들 버전, compose mount 용 OEM 디렉터리, VBS launcher 마이그레이션, debloat 스크립트 (CLI + GUI), data 에셋, rdprrap 버전 pin. 각자 따로 drift — discovery 가 이미 parent count off-by-one 으로 한 번 깨졌고, OEM 디렉터리 resolver 는 wheel install 을 놓쳤고, data-asset lookup 은 Nix 에서 아이콘을 못 찾음. `winpodx.utils.paths` 의 `bundle_dir()` 단일 resolver 로 통합 — `$WINPODX_BUNDLE_DIR` 환경변수 → 소스 체크아웃 → `sys.prefix/share/winpodx` → `~/.local/bin/winpodx-app` 순서로 검색. 각 후보는 `scripts/`, `config/`, `data/` 중 하나를 포함해야 적격; 어느 후보도 통과 안 하면 소스 체크아웃 추측치로 폴백 (안정적 에러 메시지용). 영향받은 helper 의 테스트는 `HOME` + `sys.prefix` + `__file__` 저글링 대신 `bundle_dir` 직접 monkeypatch. (Thanks @Mic92 — PR #65.) ### 수정 - **`winpodx pod apply-fixes` 가 multi-session 활성화 안 됐으면 자동 활성화.** Multi-session 은 WinPodX 핵심 기능 — 없으면 multi-app 띄울 때마다 "Select a session to reconnect to" dialog 뜸. 이전엔 apply 체인의 `multi_session` 스텝이 상태 probe 만 (PR #77, mid-apply rdprrap-conf 가 agent 세션 죽이고 /exec 타임아웃되던 hang 방지용). PR #80 으로 활성화가 안전해짐 — rdprrap-activate.ps1 을 *detached* 로 spawn 해서 /exec 응답이 TermService cycle 전에 반환됨 — 그래서 apply 체인이 self-heal 가능: `.activation_status` 가 `enabled` 면 no-op (추가 /exec round-trip 없음, disconnect 없음, churn 없음); 아니면 detached activator 큐. apply 체인 순서 재배치 — `vbs_launchers` (rdprrap-activate.ps1 + hidden-launcher.vbs staging) 가 `multi_session` 보다 먼저 실행. 활성화 필요한 경우 비용: TermService cycle 동안 RDP 세션 잠시 disconnect (~10초), 재접속하면 multi-session 활성. OEM-time 경로가 내는 1회성 비용을 pre-OEM-v17 pod 의 마이그레이션 시점으로 미룬 것일 뿐. - **`winpodx pod multi-session on` 이 컨테이너 재생성 없이 기존 pod 에서 rdprrap 활성화.** 활성화는 `rdprrap-installer install` + `net stop/start TermService` cycle 이 필요 — 패치된 `ServiceDll` 을 새 TermService 가 읽어야 하므로. 그런데 그 cycle 은 모든 활성 RDP 세션 (agent 자기 user 세션 포함) 을 죽여서, inline `/exec` 로는 활성화 못 함 (응답 돌려보내기 전에 agent 가 죽음). 이전엔 OEM-time 활성화 실패한 v0.3.0-RTM1 pod 가 패치 적용하려면 `podman rm -f` + 재-Sysprep 이 유일한 길 — 정상 케이스에선 30초짜리 레지스트리 tweak 인데 게스트 디스크 수백 MB churn. 이제: 새 `rdprrap-activate.ps1` 스크립트 (idempotent — installer 바이너리 staging 안 되어 있으면 `C:\OEM\` 의 번들된 rdprrap-*.zip 추출 fallback, 3회 재시도, 재시작 후 `ServiceDll` flip 검증, install.bat OEM v15+ 가 쓰는 동일한 `.activation_status` 마커 기록) 가 `C:\Users\Public\winpodx\launchers\` 에 staged 되고, `winpodx pod multi-session on` 이 wscript+hidden-launcher.vbs 로 *detached* spawn (agent-respawn 패턴 동일). `/exec` 호출은 "OK: activation queued" 즉시 반환; 사용자가 잠시 끊긴 후 재접속; agent 가 HKCU\Run 으로 자동 재시작; 이후 `winpodx pod multi-session status` (이제 apply-fixes 의 status 표면과 동일한 marker probe) 로 `enabled` 확인. `winpodx pod apply-fixes` 가 다른 VBS 런처들과 함께 `rdprrap-activate.ps1` 도 push 하므로, 기존 v0.3.0-RTM1 pod 도 다음 마이그레이션 때 재생성 없이 받음. (`status` 는 더 이상 `rdprrap-conf.exe` shell-out 안 함; `disable` 은 여전히 inline — disable 은 레지스트리 패치 clear 뿐이라 TermService cycle 불필요.) install.bat 의 인라인 ~80 라인 installer-retry / TermService-cycle / ServiceDll-verify / marker 로직도 같은 스크립트로 통합 — install.bat 은 SHA 핀된 번들 추출만 하고 `rdprrap-activate.ps1` 에 위임 (OEM 시점엔 cmd.exe 가 로컬 콘솔 세션이라 TermService cycle 이 부모 안 죽이므로 `-Detached` 없이 동기 호출). Single source of truth: 활성화 동작 fix 가 OEM-time / runtime 경로 모두에 drift 없이 반영. OEM 번들 16 → 18. - **앱 launch 가 CLI parent 종료 후 silent 사망하던 문제 해결.** Doomed FreeRDP 가 설명 없이 사라지는 경로 2가지: (1) `stderr=subprocess.PIPE` 가 parent 프로세스에 read-end 를 남겨서, CLI 종료 후 다음 stderr 쓰기에서 SIGPIPE → detached 클라이언트 사망. 이제 stderr 를 `$XDG_RUNTIME_DIR/.stderr` 파일로 기록 — 세션이 parent 보다 오래 살고 tail 도 inspect 가능; `RDPSession.stderr_tail` 은 그 파일의 마지막 2KB 를 lazy 하게 읽어서 기존 caller interface 유지. (2) `$DISPLAY` 없는 순수 Wayland 세션에서 `xfreerdp` (RAIL 동작하는 유일한 클라이언트 — `sdl-freerdp` 는 RAIL 없음 (FreeRDP #9078), `wlfreerdp` 는 deprecated 에 RAIL repaint 깨짐) 가 detach 후 "failed to open display" 로 사망. `launch_app` 이 이제 그 조합을 거부하고 명확한 에러로 XWayland (compositor 내장 또는 niri / river 는 `xwayland-satellite`) 를 안내. (Thanks @Mic92 — PR #64.) - **`is_freerdp_pid()` 가 무관한 프로세스를 live RDP 세션으로 잘못 인식하던 거 해결.** 이전엔 `/proc//cmdline` 안 어디든 `b"freerdp"` 또는 `b"xfreerdp"` 부분 문자열만 있으면 매치 — `~/freerdp-notes/run.sh` 같은 경로의 스크립트, `--deselect=test_freerdp_pid` 인자 가진 pytest 호출, 또는 어쩌다 인자에 freerdp 언급한 도구까지 다 잡혀서, WinPodX 가 그것들을 본인이 spawn 한 FreeRDP 로 착각해 stale `.cproc` 마커가 영원히 reap 안 됐음. 이제 cmdline 을 null-byte 로 파싱해서 argv[0] basename 만 검사 — `find_freerdp()` 가 실제로 실행하는 바이너리들 (`xfreerdp{,3}`, `sdl-freerdp{,3}`, `flatpak run com.freerdp.FreeRDP` 폴백) 과만 매치. 부분문자열 매치로 새던 케이스 2개에 대한 회귀 테스트 추가. 하위 PID-reuse 테스트는 `bash -c "exec -a … sleep 30"` 없이 다시 작성 — 멀티콜 coreutils 환경에서도 안 깨짐. (Thanks @Mic92 — PR #63.) ### 변경 - **기본 컨테이너 이미지 `docker.io/dockurr/windows:latest` 로 전환** (이전 `ghcr.io/dockur/windows:latest`). upstream 공식 compose / `docker run` 레퍼런스와 정렬 (upstream README 와 예제 `compose.yml` 모두 `dockurr/windows` 사용). 동일 이미지 — digest 로 검증됨. 일부 사용자가 GitHub Container Registry 경로에서 token / 4xx 에러 만남; Docker Hub 가 canonical artifact 를 안정적으로 제공. **기존 설치는 자동 마이그레이션 안 됨**: `~/.config/winpodx/winpodx.toml` 이 resolved 값을 persist 해서 이미 `winpodx setup` 돌린 사용자는 옛 레퍼런스 유지. 새 기본값 적용하려면 `winpodx.toml` 에서 `image = "ghcr.io/..."` 라인 삭제 후 `winpodx setup` 재실행 (`compose.yaml` 재생성), 또는 `~/.config/winpodx/compose.yaml` 직접 편집. (Thanks @Mic92 — PR #62.) - **PowerShell 창 깜빡임 0 — 게스트 경로가 hidden VBS 런처와 agent 트랜스포트로 모두 통합.** 3가지 수정 합쳐짐: - **Agent 자동시작이 `hidden-launcher.vbs` 경유.** HKCU\Run 이 `powershell.exe -WindowStyle Hidden -File C:\OEM\agent.ps1` 을 등록했는데, Hidden 플래그는 PowerShell 이 conhost 할당한 *후에* 적용되므로 사용자 로그인마다 ~50ms 짜리 PS 콘솔이 깜빡였음. 새 VBS wrapper 는 GUI 서브시스템 (자체 콘솔 없음) 이고 `WshShell.Run intWindowStyle=0` 이 `SW_HIDE` 를 `CreateProcess` 에 전달해서 spawn 된 PowerShell 이 windowless 로 시작됨. - **UWP launch 가 `IApplicationActivationManager` 경유.** 기존 `/app:program:explorer.exe,cmd:shell:AppsFolder\` 가 UWP 프레임이 뜨기 전에 explorer.exe RemoteApp 윈도를 ~300ms 보여줬음 — Calculator / Settings / Terminal 에서 사용자가 보던 "PowerShell 같은 깜빡임" 이 그거. RemoteApp 이 이제 `wscript.exe launch_uwp.vbs ` 호출 → `IApplicationActivationManager::ActivateApplication` 직접 호출. UWP 프레임이 transition 없이 RemoteApp 윈도로 바로 등장; ~300ms 도 단축. - **잔여 `run_in_windows` 호출자들이 agent 트랜스포트 경유.** `core.updates`, `core.daemon.sync_windows_time`, `cli.pod.multi-session`, `cli.main.debloat`, GUI Tools 페이지 debloat 핸들러 — 이제 모두 `winpodx.core.windows_exec.run_via_transport` 경유. v0.3.0 agent 의 `/exec` (CreateNoWindow=$true) 를 우선 시도하고 `/health` 응답 없을 때만 FreeRDP RemoteApp 폴백. 비밀번호 회전 (rule #6) 과 `winpodx pod sync-password` 복구 경로는 직접 credential 인증이 필요해서 의도적으로 FreeRDP 유지. OEM 번들이 13 으로 bump (새 VBS 파일들은 `C:\Users\Public\winpodx\launchers\` 에 stage — Public 이 User 권한으로 쓸 수 있어서 agent 가 나중에 admin 없이 재작성 가능). **0.3.0-RTM1 기존 pod 마이그레이션 자동화**: 새 `_apply_vbs_launchers` apply 스텝이 agent `/exec` 한 번으로 3개 파일 + `HKCU\Run\WinpodxAgent` 모두 갱신; `apply_windows_runtime_fixes` 가 `multi_session` 뒤에 체이닝. 트리거: `winpodx pod apply-fixes` 또는 업그레이드 후 `winpodx migrate` — **컨테이너 재생성 불필요**. 자동시작 변경은 다음 user 세션 로그인 (또는 `winpodx pod restart`) 시 적용; UWP launch fix 는 host 의 다음 launch 즉시 반영. ### 추가 - **하이브리드 디스커버리 필터 — 필수앱 항상 표시, 시스템 shim 기본 hide.** Windows 11 기본 install 에서 자동 디스커버리가 ~45개 entry 를 만드는데 두 종류 노이즈 같이 발생 — OS 필수앱 (File Explorer / Calculator / Settings) 은 Start Menu .lnk 로 enumerate 안 돼서 누락, 시스템 shim (`LicenseManagerShellExt`, `WindowsPackageManagerServer`, `DesktopPackageMetadata`, `microsoft-store-server` …) 들은 grid 어지럽힘. 필터가 이제 큐레이션된 essentials allowlist (스캔이 놓친 필수앱은 stub 합성) 와 noise denylist (`hidden = true` 자동 stamp 해서 GUI grid 가 거름) 를 같이 가짐. 사용자 override 가 우선 — 타일에 Hide / Show 토글하면 같은 TOML 에 기록돼서 다음 디스커버리 sweep 에서도 유지. discover_apps.ps1 가 essentials 3개를 실제 Windows 아이콘과 같이 명시적으로 emit (File Explorer 는 `C:\Windows\explorer.exe` 에서, Calculator + Settings 는 AppxManifest Square logo 에서 추출) — 사용자가 generic 한 letter avatar 가 아닌 진짜 Windows 아이콘 봄. - **Win32 launch args.** RDP RemoteApp builder 가 `app.toml` 의 per-app `args` 문자열을 honor 해서 FreeRDP `cmd:` 필드로 forward. 오래된 "explorer.exe RemoteApp 뜨면 아무것도 안 보임" 문제 해결 — File Explorer essential 이 `args = "shell:MyComputerFolder"` 같이 emit 돼서 `This PC` view 가 정상 윈도로 열림 (user shell 점령 시도 안 함). 기존 `args = ""` 앱은 영향 없음. - **앱별 .desktop description.** 디스커버리가 게스트에서 한 줄짜리 description 추출 (`.lnk` Comment 필드, exe `ProductName`, UWP ``) 해서 각 앱의 `.desktop` 파일 `Comment=` 키에 넣음. 이전엔 모든 entry 가 `Comment=Windows application via winpodx` 라는 똑같은 스탬프 쓰던 거 — 이제 메뉴/파일 매니저 tooltip 에서 실제 앱 description 보임. description 추출 안 되는 앱은 여전히 generic 스탬프. - **Known-good UWP allowlist.** `DisplayName` 이 `ms-resource:` 간접 참조라 PowerShell 비대화형 세션에서 풀리지 않는 UWP 패키지들 (Calculator, Terminal, Paint, Snipping Tool, Camera, Alarms, Maps, Sound Recorder, Notepad UWP, Sticky Notes, Get Help, Your Phone, To Do, Settings) 이 fallback 으로 dotted `PackageFamilyName` 받아서 host 의 UWP-dot 체크에 junk 로 분류돼 빠지던 문제 — 이제 이 패키지들은 명시적 allowlist 로 통과시켜서 AAD/BrokerPlugin 같은 shim 만 거름. - **GUI Apps 페이지 "Hidden (N)" 토글.** Hidden entry 는 기본 접힘; toolbar 의 count chip 이 몇 개 거른지 표시. chip 클릭하면 hidden 포함해서 grid 펼침 — denylist 가 과도하게 거른 항목 promote 가능. - **README 히어로에 데모 스크린샷.** `docs/images/demo.png` (Windows 정보 / 작업 관리자 / PowerShell 각각 Linux 창으로 winpodx Apps grid 와 나란히) 가 이제 README 상단에 — 처음 방문자가 통합 모습 바로 봄. ## [0.3.0] - 2026-04-30 메이저 릴리스 — 모듈형 core 재구조, HTTP guest agent, 통합 헬스체크 surface. FreeRDP RemoteApp 파이프라인을 기본 host→guest 채널에서 대체. ### 배경 v0.2.2 / v0.2.2.1 가 같은 기능들의 첫 시도였지만 실설치에서 깨짐 (PS창 폭주, "Another user is signed in" 다이얼로그, install timeout, compose 의 `8765` 포트 매핑 누락으로 `/exec` RST). 2026-04-29 main 을 v0.2.1 로 롤백하고 명시적 anti-goal 와 함께 agent + transport 처음부터 재설계 (`docs/AGENT_V2_DESIGN.md` 참고). v0.3.0 이 그 재설계 구현; `0.2.2.x` 태그는 혼란 방지를 위해 삭제됨 — `v0.2.1` 에서 바로 `v0.3.0-RTM1` 로. ### 추가 - **HTTP guest agent (rev4).** `agent.ps1` 가 Windows 안 `127.0.0.1:8765` 에서 동작, `+:8765` 로 바인드해서 QEMU user-mode NAT 통과. Bearer-authed `/exec` (base64 인코딩 PowerShell 페이로드) 가 FreeRDP RemoteApp 을 기본 host→guest 채널에서 대체; `/health` 는 readiness probe 위해 unauthenticated 유지. child PS 는 `[Diagnostics.Process]` + `CreateNoWindow=$true` + 비동기 `ReadToEndAsync` 로 spawn — PS창 깜빡임 없음, pipe buffer deadlock 없음. 토큰은 OEM bind mount 로 전달 (호스트 mode `0600`, gitignored). - **Transport ABC v1** (`core/transport/{base,agent,freerdp,dispatch}`). `dispatch()` 가 agent 우선, `/health` 응답 없으면 FreeRDP 폴백. Password rotation 은 명시적으로 Transport 통하지 **않음** (`docs/TRANSPORT_ABC.md` 규칙 #6) — rotation 은 자체 credential 소유 + `run_in_windows` 직접 호출 (stale-password 복구 시 bootstrap loop 회피). - **`winpodx check` 헬스 프로브.** 새 CLI 명령어가 멀티 소스 헬스 점검을 한 번에 실행하고 각 프로브를 `OK` / `WARN` / `FAIL` / `SKIP` 와 측정 시간으로 출력. `--json` 로 머신 판독용 출력. exit code 는 어떤 프로브든 `FAIL` 일 때만 `1`. 프로브: - `pod_running`, `rdp_port`, `agent_health` — bring-up 상태 - `guest_exec` — `Write-Output ok` 페이로드를 `/exec` 로 보내 rc=0 + stdout="ok" 검증. host→guest 채널이 실제로 round-trip 하는지 (단순히 `/health` 응답만이 아니라) 증명 - `guest_summary` — `/exec` 한 번으로 Windows 버전 / uptime / 현재 사용자 / 활성 세션 수 / C: 여유 공간 가져옴 - `oem_version`, `password_age`, `apps_discovered`, `disk_free` — 호스트 측 상태 - **GUI Info 페이지 Health 카드 자동 갱신.** Info 페이지 최상단의 새 "Health" 섹션이 각 프로브를 색 배지 + 전체 verdict 로 렌더. 페이지가 보이는 동안 30초마다 자동 갱신, 페이지 떠나면 타이머 일시정지 (idle 시 guest poll 안 함). - **사이드바 트랜스포트 표시기.** 상단 pod chip 에 글자 점 2개 추가 — `A` (guest agent) 와 `R` (RDP 포트) — 도달 가능하면 녹색, 안 되면 빨강. tooltip 으로 "agent OK (version)" / "host→guest 명령어가 FreeRDP RemoteApp 으로 폴백됨" 표시 — 다음 launch 가 어느 채널로 갈지 한눈에 보임. 기존 15초 pod-status 타이머가 같이 갱신. - **`install.sh` ref 선택.** `--main` 은 `origin/main` 에서 설치 (개발용), `--ref TAG` 는 특정 git ref / 릴리스 태그에서 설치. 플래그 없으면 최신 GitHub Release 사용. RTM-only 릴리스 게이트와 같이 추가 — RTM 사이의 rapid iteration 태그가 AUR / OBS / Debian publish 를 트리거해서 동작하는 install 을 덮어쓰지 않게 함. ### 수정 - **discovery 스크립트 경로 한 단계 어긋남.** `_ps_script_path` 가 `.parent` 를 4번 거슬러 `/src/scripts/windows/discover_apps.ps1` 를 만들었는데 어떤 layout 에도 없는 경로. 5번 거슬러 실제 `/scripts/windows/` 로 resolve 되게 수정 — 이제 GUI Refresh 가 pod 정상일 때 "Pod Not Running" dialog 를 띄우지 않음. - **GUI 가 `script_missing` 을 `pod_not_running` 으로 오분류.** `_looks_like_pod_down` 가 `winpodx-app/...` 같은 install path 의 "pod" 부분 substring 에 매칭돼서 path 가 들어간 모든 DiscoveryError 가 잘못된 dialog 로 라우팅. RefreshWorker 가 이제 명시적인 `DiscoveryError.kind` 를 먼저 읽고, kind 없을 때만 substring 휴리스틱 폴백. - **Agent `/exec` 가 child clean exit 후에도 `rc:null` 반환.** PowerShell `Start-Process -PassThru` + `WaitForExit(timeout)` 가 child 가 정상 종료해도 `$proc.ExitCode` 를 `$null` 로 둘 수 있는 알려진 동작. agent (rev4) 가 이제 source 에서 null → 0 강제, 호스트 `AgentClient` 도 `rc:null` 을 0 으로 처리해서 rev2 / rev3 가 baked-in 된 기존 pod 도 동작. - **Agent 가 `/exec` 마다 PowerShell 창을 깜빡임.** `Start-Process -NoNewWindow` 가 hidden parent (HKCU\Run 의 `-WindowStyle Hidden`) 에서 fast-exit child 의 콘솔을 새로 띄우는 동작. agent.ps1 (rev4) 가 이제 `[Diagnostics.Process]` + `ProcessStartInfo` 로 `CreateNoWindow=$true` 와 `UseShellExecute=$false` 로 spawn; stdio 는 비동기 `ReadToEndAsync` 로 drain (pipe buffer deadlock 방지). `WINPODX_OEM_VERSION 11 → 12` — 다음 pod recreate 시 새 agent 가 install path 에 들어감. ## [0.2.1] - 2026-04-28 마이너 버전 (0.2.0.x → 0.2.1) — UX 개선 묶음: install 이 부분 완료 상태로 끝나도 다음 실행 시 자동 재개, GUI 로그가 WinPodX 자체 로그를 실시간으로 표시, GUI 첫 실행 시 시스템 체크 안내. ### 추가 - **`utils.pending` 재개 시스템.** 새 `~/.config/winpodx/.pending_setup` 마커가 install.sh 가 못 끝낸 단계 (`wait_ready` / `migrate` / `discovery`) 추적. 다음 CLI 호출 (version/help/uninstall/config/info 외 모든 서브커맨드) 과 GUI 시작 시 마커 픽업해서 미완료 단계를 canonical 순서로 실행. 각 단계는 성공 시 마커에서 자체 제거; 빈 상태 되면 파일 삭제. 10개 단위 테스트가 순서, 멱등성, 부분 완료, "게스트 부팅 중 → 후속 단계 시도 안 함" 가드 커버. - **GUI 첫 실행 Quick Start 다이얼로그.** 최초 launch 시 5-bullet 스냅샷 표시 — backend / FreeRDP / pod 상태 / RDP listener / 디스커버리된 앱 수 — 백그라운드 resume 진행 여부도 안내. dismiss 시 `~/.config/winpodx/.welcomed` 작성하여 재방문 사용자에게는 안 띄움. - **GUI 로그 페이지가 winpodx 앱 로그 자동 tail.** Tools/Terminal 페이지로 이동하면 기본으로 `tail -F ~/.config/winpodx/winpodx.log` 스트림 시작 — 사용자가 내부 프로그램 로그 (apply / probe / refresh / pod 상태 전이) 를 기존 on-demand 컨테이너 로그 버튼과 함께 봄. 페이지 떠나면 streamer 자동 종료. ### 변경 - **install.sh wait-ready timeout 1800s → 3600s.** 예산을 1시간으로 늘려, 느린 하드웨어 신규 설치 (Windows ISO 다운로드 + Sysprep + OEM apply 첫 실행) 가 인라인으로 완료될 수 있게 함 (이전엔 timeout 후 resume 훅에 미룸). 1시간 초과 작업은 여전히 resume 훅이 picking up. - **`pod.max_sessions` 기본값 10 → 25, `pod.ram_gb` 기본값 4 → 6.** 10은 실제 사용 (Office + Teams + Edge + 사이드 앱 몇 개 동시) 에 빡빡함. 새 RAM 기본값이 25 sessions 에서 session-budget 경고 안 띄움 (2.0 + 25 × 0.1 ≈ 4.5 GB 필요). 아래 tier auto-detect 가 머신별로 추가 조정. ### 추가 (보충) - **Setup 의 호스트 스펙 auto-tier.** 새 `utils.specs.detect_host_specs` 가 `/proc/meminfo` + `os.cpu_count()` 읽고 `recommend_tier` 가 3개 preset 중 하나 매핑: 호스트 RAM 호스트 CPU 티어 VM CPU VM RAM ≥32 GB ≥12 thr 상 8 12 GB 16-32 GB 6-12 thr 중 4 6 GB <16 GB <6 thr 하 2 4 GB 두 축 모두 임계값 통과해야 상위 티어 — 64 GB / 4-core 호스트는 CPU 가 병목이라 "하" 받음. 대화형 setup 은 추천값을 기본으로 표시, 비대화형은 즉시 적용. 10개 단위 테스트가 양축-통과, 단축-부족, 임계 경계 커버. ### 수정 (보충) - **앱 실행할 때마다 "Select a session to reconnect to" 다이얼로그 발생 — zombie disconnected 세션 누적 원인.** `install.bat` 와 `_apply_rdp_timeouts` 양쪽이 `MaxDisconnectionTime` 을 `0` 으로 설정. RDP 의미론에서 `0` = **timeout 없음** = disconnect 된 세션이 영원히 살아있음. 사용자가 FreeRDP 창 닫을 때마다 `Disc` 상태 세션 누적 → 다음 launch 시 Windows 가 그동안 쌓인 세션 리스트로 재연결 다이얼로그 띄움. rdprrap 멀티세션은 세션 동시 실행은 허용하지만 이 prompt 는 못 막음 — auto-logoff 만이 답. v0.2.1 에서 `30000` (30초) 으로 변경 — disconnect 후 30초 뒤 자동 logoff, 사용자가 앱 닫고 다시 열어도 zombie 누적 안 됨. `install.bat` (신규 컨테이너) + `_apply_rdp_timeouts` (런타임 apply 로 기존 컨테이너 패치) 양쪽 수정. - **`_apply_max_sessions` 가 틀린 레지스트리 키에 씀.** 런타임 apply 가 `HKLM\...\Terminal Server\MaxInstanceCount` 에 썼지만 Windows 는 실제로 `HKLM\...\Terminal Server\WinStations\RDP-Tcp\MaxInstanceCount` 를 읽음. 결과: session-cap 도입 이후 모든 릴리스가 cfg 변경 시 silent no-op — `install.bat` 의 OEM 시점 값만 authoritative 였음. v0.2.1 이 올바른 subkey 에 쓰고 (`fSingleSessionPerUser` 는 Terminal Server root 에 있는 게 맞음, 그대로 유지), OEM 시점 install.bat 천장도 10 → 50 으로 상향해서 cfg 값이 [1, 50] clamp 안에서 install time 에 silent cap 안 되게. ### 수정 - **GUI Refresh 두 번째 SEGV 경로 — Python ref / Qt deleteLater race.** v0.2.0.10 이 QImage-워커스레드 크래시는 잡았지만 두 번째 SEGV 가 남아있었음: `_on_refresh_succeeded` 와 `_on_refresh_failed` 슬롯이 즉시 `self._refresh_worker = None` 실행. Python 의 ref drop 이 Qt 의 queued `worker.deleteLater()` 이벤트와 race — 둘 중 나중에 실행되는 쪽이 free 된 `QObject` 만나서 worker 스레드의 `~QObject()` 에서 크래시. 2026-04-28 코어덤프로 확인: 워커 스레드 2282062 의 top frame 이 `QObject::~QObject`, 메인 스레드 2281803 은 슬롯의 PySide6 `callPythonMetaMethod` 디스패치 중. 수정: `_refresh_worker` / `_refresh_thread` Python ref drop 을 `_cleanup_refresh_worker` 로만 옮김, `thread.finished` 에 바인딩되어 Qt 객체 둘 다 완전 해제된 후 실행. Worker `deleteLater` 는 워커 스레드 자체 이벤트 루프에서 정상 처리 — Python GC 간섭 없음. ### 수정 - **GUI Refresh 버튼 SEGV.** `_DiscoveryWorker.run()` (Qt 워커 스레드) 가 `persist_discovered` → `_validate_png_bytes` → `QImage.loadFromData` 호출. Wayland 의 Qt + libgallium / Mesa state 가 메인 스레드 외에서 QImage 만지면 race → `Signal: 11 (SEGV)` 코어 덤프. v0.2.0.10 에서 `_validate_png_bytes` 가 `threading.current_thread() is not threading.main_thread()` 일 때 stdlib 청크 워커로 단축 회귀. 워커도 여전히 CRC + 크기 캡 + IEND terminator 강제하므로 off-main-thread 호출자는 약간 느리지만 크래시 없는 경로. - **install.sh wait-ready 600s → 1800s.** 신규 설치 (`uninstall --purge` 후 재설치) 는 ~7.5GB Windows ISO 다운로드 + 추출 + Sysprep + OEM apply + 최종 재부팅 = 첫 실행 15~30분. 600초 timeout 이 Windows VM 부팅 전에 발화 → `[FAIL] Timeout waiting for Windows ready (09:56)` 로 끝남. 1800초 예산이 일반적 환경에서 신규 설치 커버; 후속 설치는 캐시된 ISO 재사용해서 2~5분. - **GUI Refresh 가 `.desktop` 엔트리 자동 설치** (`winpodx app refresh` CLI 와 parity). 기존엔 CLI 경로만 인라인 등록했고 GUI Refresh 는 discovered 트리만 갱신, `~/.local/share/applications/` 는 안 건드림. v0.2.0.10 의 `_DiscoveryWorker` 가 `_sync_desktop_entries` 호출 — `cli/app._register_desktop_entries` 의 워커-스레드-안전 형제 함수. ### 추가 - **첫 부팅 GUI 자동 디스커버리.** Pod 가 `running` 으로 전이 + 앱 리스트 비어있을 때, 메인 윈도가 2초 settle 후 Refresh 워커 자동 발화. install.sh 의 wait-ready 가 Sysprep 끝나기 전에 timeout 한 케이스 해결 — 사용자가 나중에 GUI 열면 pod 살아있는 거 확인 후 디스커버리가 알아서 발화. - **GUI 실시간 로그 스트리밍.** Tools/Terminal 페이지에 4개 버튼 추가: `Live (pod)` 와 `Live (app)` 가 컨테이너 또는 `~/.config/winpodx/winpodx.log` 에 `tail -F` 걸어 새 라인을 패널로 스트리밍; `App log` 는 WinPodX 자체 앱 로그 마지막 200줄 표시; `Stop tail` 은 활성 streamer 종료. 기존엔 pod 로그 100줄 one-shot 스냅샷만 있었음. ### 수정 - **두 번째 앱 실행 시 독립 윈도 대신 Windows "Select a session to reconnect to" 다이얼로그 발생.** Windows 기본값이 사용자당 동시 FreeRDP RemoteApp 세션 거부 → 첫 앱 이후의 모든 launch 가 기존 세션에 묻히거나 reconnect 다이얼로그 띄움. v0.2.0.9 에서 self-heal apply 체인에 `_apply_multi_session` 추가 — 게스트 안에서 `rdprrap-conf --enable` 호출해 termsrv.dll 패치 활성화 → launch 마다 독립 세션. 멱등 (이미 활성화돼있으면 no-op), 구 OEM 번들에 rdprrap-conf 없으면 best-effort skip. - **앱이 Windows 에서 삭제됐는데 DE 메뉴에 `.desktop` 엔트리가 계속 남음.** v0.2.0.8 이 refresh 자동 설치는 추가했지만 사라진 앱의 엔트리 제거는 안 했음. v0.2.0.9 에서 refresh 진짜 양방향 동기화: `list_available_apps()` 에 없는 모든 `winpodx-*.desktop` 파일이 (해당 아이콘과 함께) 제거됨 → Windows 에서 Office 지우면 다음 refresh 때 launcher 에서도 Word/Excel/PowerPoint 사라짐. `~/.local/share/winpodx/data/apps/` 의 사용자 작성 엔트리는 보존. ### 변경 - **README 정보량 강화.** 상단에 `for-the-badge` 스타일 "Status: Beta" + "Latest release" 배지. 그 아래 표준 shields (license, Python, backend, language, tests, CI). 소셜 행 (stars, forks, watchers, unique visitors). 활동 행 (issues, PRs, last commit, code size). EN + KO 동기화. ### 수정 - **`winpodx app refresh` 가 앱 발견은 하지만 데스크톱 메뉴에는 등록 안 함.** refresh 경로는 `app.toml` + 아이콘을 `~/.local/share/winpodx/discovered/` 에 저장만 하고, 실제 `.desktop` 엔트리는 별도 `winpodx app install-all` 명령으로만 생성됐음 → 사용자가 "Discovered N app(s)" 메시지 본 후 DE 메뉴에 앱이 안 떠서 혼란. v0.2.0.8 부터 refresh 가 발견된 앱들의 .desktop 엔트리를 자동 설치 (best-effort, 실패는 warn 만 하고 refresh 자체는 계속) + 아이콘 캐시 갱신. - **앱 실행할 때마다 PowerShell 창 깜빡임.** `ensure_ready` 의 self-heal apply 경로가 매 앱 실행마다 FreeRDP RemoteApp PowerShell payload 3개 발화. `-WindowStyle Hidden` 으로 작아져도 여전히 매번 눈에 띄게 깜빡임. apply 자체는 레지스트리 멱등이라 warm pod 에서 재실행해도 가시적 효과 없음 — 순수 노이즈. v0.2.0.8 부터 self-heal 성공 후 `~/.config/winpodx/.applies_stamp` 에 `:` 기록 → 이후 launch 는 단축 회귀, pod 재시작 (TermService / NIC 설정 재적용 필요) 또는 winpodx 업그레이드 시에만 다시 발화. ### 수정 - **빠른 컨테이너에서 `pod wait-ready --logs` 가 `[container]` 라인 하나도 안 띄움.** 두 가지 문제: (1) tail 을 `--tail 0` 으로 시작했는데 이건 "지금부터의 로그만 표시" 의미. 하지만 dockur 는 Windows ISO 다운로드 / 부팅 단계 메시지를 wait-ready 실행 *전에* 이미 출력 → 사용자에게 아무것도 안 보임. (2) `stdout` 만 drain. dockur 는 진행 메시지를 stdout (다운로드 byte/s) 과 stderr (부팅 단계) 로 나눠 출력해서 절반이 사라짐. v0.2.0.7 에서 `--tail 100` 으로 최근 컨텍스트 즉시 표시 + stdout/stderr 둘 다 병렬 스레드로 drain. ### 수정 - **`wait_for_windows_responsive` 가 부팅 중 게스트에서 1초도 안 되어 무너져 `pod wait-ready` UX 가 통째로 망가짐.** 헬퍼가 RDP TCP 포트 열림은 제대로 대기했지만, 그 다음 FreeRDP RemoteApp probe 를 **단 한 번만** 발화. 한 번 실패하면 (부팅 중 게스트는 항상 rc=147 connection-reset 반환) 즉시 False return → 호출자가 넘긴 600초 timeout 이 무시됨. v0.2.0.6 에서 probe 를 retry loop 로 변경: 5-20초짜리 probe 를 deadline 까지 반복 (FreeRDP 프로세스 CPU 점유 막기 위해 3초 간격). 이제 `pod wait-ready --timeout 600` 이 진짜 10분까지 기다림 — phase 3 의 elapsed time 이 증가하는 게 보임. ### 추가 - **`winpodx pod wait-ready [--timeout SEC] [--logs]`** — Windows VM 첫 부팅 다단계 wait gate. 세 체크포인트를 elapsed time 과 함께 표시해서 사용자가 침묵 속 몇 분간 hang 대신 실제 진행 상황을 봄: - `[1/3] Container running` (~5초) - `[2/3] RDP port open` (보통 30-90초) - `[3/3] Windows ready (RemoteApp probes OK)` (첫 부팅 시 보통 2-8분) `--logs` 옵션 시 컨테이너 stdout 을 백그라운드 스레드로 tail 해서 `[container] ...` 라인으로 surfacing — Windows 가 실제로 뭐 하는지 (Sysprep, OEM apply 등) 보임. 블랙박스 → 가시성. ### 변경 - **`install.sh` 가 진짜 single-shot 으로 바뀜 — 설치 정말 끝났을 때 exit, 컨테이너 시작했다고 거짓말 안 함.** 새 흐름: `setup` → `pod wait-ready --logs` (최대 10분, progress + 컨테이너 로그) → `migrate` (게스트 ready 라 apply 깔끔히 통과) → `app refresh` (디스커버리 즉시 통과). 기존에는 Windows 가 아직 부팅 중인데 `Installation complete!` 표시 후, 사용자가 첫 앱 실행 시 또 기다리는 구조였음. CI / 비대화형 환경에서는 `WINPODX_NO_WAIT=1` 로 wait 우회, `WINPODX_NO_DISCOVERY=1` 로 디스커버리 우회. - install.sh 의 중복 `winpodx pod apply-fixes` 호출 제거 — v0.1.9.3 이후 `migrate` 의 "always-apply" 경로가 이미 apply 를 돌리므로 한 번 더 부르면 wait 만 두 배가 됨. ### 수정 - **신규 `--purge` 설치마다 가짜 "cfg.password does not match Windows" 경고.** v0.1.9.5 가 추가한 `_probe_password_sync` (cfg/Windows 비밀번호 drift 사전 감지) 의 에러 분류기가 FreeRDP 에러 문자열에 `"no result file"` 또는 `"auth"` 가 들어있으면 drift 로 판정. 하지만 부팅 중 게스트 (모든 신규 설치가 거치는 상태) 는 FreeRDP 가 rc=147 `ERRCONNECT_CONNECT_TRANSPORT_FAILED` (connection reset) 반환 → host wrapper 가 `"No result file written"` 으로 감쌈 → 분류기가 `"no result file"` 매칭 → 가짜 drift 경고 발화. v0.2.0.4 가 두 방향으로 수정: 1. probe 가 `wait_for_windows_responsive(timeout=180)` 로 먼저 대기. 게스트 미준비면 `(probe deferred — guest still booting; will retry on next ensure_ready)` 메시지로 skip. 2. 분류기가 transport-level 실패 (`rc=131`, `rc=147`, `transport_failed`, `connection reset`) 와 실제 auth 실패 (`logon_failure`, `STATUS_LOGON_FAILURE` 등) 를 구분. 후자만 sync-password 경고 발화. ### 수정 - **Discovery 가 apply path 와 동일한 부팅 race 에 노출.** v0.2.0.1 이 `_apply_*` 와 `pod apply-fixes` 만 `wait_for_windows_responsive` 로 게이팅하고, `winpodx migrate` 의 "Run app discovery now?" 프롬프트와 `provisioner._auto_discover_if_empty` (첫 부팅 시 ensure_ready 가 발화) 는 probe 없이 FreeRDP RemoteApp 채널 호출. 신규 `--purge` 설치 시 QEMU 안 Windows VM 이 여전히 부팅 중인데 discovery 가 떠서 `ERRCONNECT_CONNECT_TRANSPORT_FAILED [0x0002000D]` (rc=147, connection reset) 으로 무너지고 사용자는 빈 앱 메뉴로 끝남. v0.2.0.3 이 두 discovery 호출 지점 모두에 동일 probe 적용 — wait 후 scan 또는 "Re-run later with: winpodx app refresh" 안내로 skip. - **첫 부팅 timeout 90s → 180s.** 실제 환경의 신규 설치는 느린 하드웨어에서 Windows + RDP + activation 핸드셰이크에 90초 초과 가능. 세 개의 apply / discovery probe 의 wait 예산을 180초로 상향 — one-shot install 이 첫 시도에 apply round 까지 완료할 수 있게 함. ### 수정 - **`--purge` 신규 설치가 가짜 "0.1.7 -> X detected" 업그레이드 메시지 표시.** `winpodx setup` 이 `winpodx.toml` 만 저장하고 `installed_version.txt` 마커는 안 써서, `install.sh` 가 자동으로 이어 호출하는 `winpodx migrate` 가 "config 있고 marker 없음" 상태 보고 pre-tracker fallback (baseline 0.1.7 가정) 발동. 실제 마커 도입 전 업그레이드에서는 맞는 동작이지만, 신규 설치에서는 모든 마이그레이션 스텝을 불필요하게 재실행하면서 "What's new in 0.1.8 / 0.1.9 / ..." 안내까지 띄움. v0.2.0.2 에서 setup 이 마커가 없을 때만 현재 버전을 `installed_version.txt` 에 기록하도록 수정 — 신규 설치는 현재 버전으로 보고되어 마이그레이션 스텝 발화 안 함, 실제 업그레이드 흐름은 그대로 동작. ### 수정 - **차가운 컨테이너에서 apply cascade 가 무너짐.** v0.2.0 은 `pod_status` 가 `RUNNING` 이 되는 즉시 세 개 idempotent runtime apply (`max_sessions`, `rdp_timeouts`, `oem_runtime_fixes`) 를 발화. dockur Linux 컨테이너는 몇 초 안에 `RUNNING` 도달하지만, QEMU 안 Windows VM 은 RDP 리스너가 FreeRDP RemoteApp activation 받기까지 30~90초 더 필요. 그 윈도우 안에서 모든 apply 는: - 신규 설치 시: `ERRCONNECT_CONNECT_TRANSPORT_FAILED [0x0002000D]` (rc=147, RDP 소켓은 열렸지만 서버 미초기화 — connection reset by peer) - `winpodx pod restart` 시: `ERRCONNECT_ACTIVATION_TIMEOUT [0x0002001C]` (rc=131, FreeRDP 연결됐지만 activation 단계 미완료) 로 무너짐. 각 apply 가 60초 timeout 풀로 대기 → cascade 3분 → 사용자가 앱 실행 시 Launch Error 다이얼로그 또는 `winpodx setup` → `winpodx migrate` 도중 "3 of 3 applies failed" 패닉 메시지로 표면화. - 새 헬퍼 `wait_for_windows_responsive(cfg, timeout=90)`: `check_rdp_port` 폴링 후 20초 no-op `Write-Output 'ping'` 프로브로 FreeRDP RemoteApp 채널이 실제로 살아있는지 확인. 다음 경로의 precondition 으로 사용: - `ensure_ready()` warm-pod 경로 — 게스트가 미응답이면 self-heal apply 블록 통째로 skip. - `winpodx pod apply-fixes` CLI — 명시적 "Waiting for Windows guest to finish booting (up to 90s)…" 메시지로 hang 아님을 표시. - `winpodx migrate` apply 단계 — 같은 wait + 채널 실패 스택트레이스 3개 대신 "게스트 부팅 중; 나중에 apply-fixes 실행하거나 그냥 앱 실행해도 됨" 명확한 메시지. - `_self_heal_apply()` (신규) — warm-pod ensure_ready apply 블록을 `WindowsExecError` swallow 로 감싸서 transient 채널 실패가 cascade 안 되고 warning 만 로그 후 같은 호출 내 추가 시도 중단. 다음 ensure_ready 가 이어받음. ### 수정 - **`oem_runtime_fixes` 가 `AllowComputerToTurnOffDevice` 파라미터 오류로 첫 적용 실패.** v0.1.9.5 가 runtime apply 를 FreeRDP RemoteApp PowerShell 로 넘겼지만 payload 는 여전히 `Set-NetAdapterPowerManagement -AllowComputerToTurnOffDevice $false` 호출. 이 cmdlet 은 enum 문자열 `'Disabled'` / `'Enabled'` 를 요구하고, QEMU 안 가상 NIC (virtio) 는 이 파라미터 자체가 노출 안 되는 경우 잦음. v0.2.0 은 `try/catch` 로 감싸고 enum 형태로 전환, 미지원 어댑터는 건너뜀 — NIC 토폴로지 무관 apply 성공. - **migrate 의 password-drift 프로브가 20초에 timeout.** 차가운 pod 의 FreeRDP 첫 연결 (TLS + 인증 + RemoteApp 실행) 은 20초 자주 넘김. v0.2.0 은 프로브 예산 60초로 상향, cold-start 지연 때문에 실제 drift 가 가려지지 않게 함. ### 추가 - **Refresh 진행 상황 스트리밍.** 기존 `winpodx app refresh` 는 게스트 enumerator 가 Registry App Paths / Start Menu / UWP 패키지 / choco·scoop shim 을 30~90초 동안 도는 동안 침묵. v0.2.0 은 스트리밍 진행 채널 추가 — `windows_exec.run_in_windows` 가 `progress_callback` 받고, wrapper 가 `$Global:WinpodxProgressFile` + `Write-WinpodxProgress` 정의, `discover_apps.ps1` 가 소스별로 한 줄씩 출력. 호스트 CLI 는 stderr 로 `... Scanning Registry App Paths...` 식으로 표시 (JSON 출력은 그대로 깨끗). - **`winpodx pod multi-session {on|off|status}`** — 번들 rdprrap 다중 세션 RDP 패치 런타임 토글. FreeRDP RemoteApp 로 Windows 게스트 안에서 `rdprrap-conf.exe` 호출하므로 패치 enable/disable/inspect 위해 컨테이너 재생성 불필요. `C:\OEM\rdprrap\rdprrap-conf.exe`, `C:\OEM\rdprrap-conf.exe`, `C:\Program Files\rdprrap\rdprrap-conf.exe` 순으로 탐색. - **디스커버리 junk 필터.** Refresh 가 그동안 uninstaller (`unins000.exe`, "Uninstall …"), 재배포 패키지 (`vc_redist.x64.exe`, "Microsoft Visual C++ …"), 헬퍼 (`crashpad_handler.exe`), inbox 접근성 도구 (`narrator.exe`, `magnify.exe`, `osk.exe`), 시스템 plumbing (`ApplicationFrameHost.exe`, `RuntimeBroker.exe`), DisplayName 미해결 UWP fallback (예: `Microsoft.AAD.BrokerPlugin`) 을 모두 노출했음. v0.2.0 은 호스트측 denylist 패턴 + 실행 파일 basename 매칭 + UWP fallback 감지로 모두 drop. 디버깅 시 `WINPODX_DISCOVERY_INCLUDE_ALL=1` 로 우회 가능. - **GUI 앱 아이콘.** 디스커버리한 앱이 launcher 의 grid 카드와 리스트 타일에서 실제 Windows 아이콘 (PNG / SVG) 으로 렌더링됨 — 기존 색상+첫글자 avatar 대신. 아이콘은 v0.1.8 부터 `~/.local/share/winpodx/data/discovered//icon.{png,svg}` 에 저장되어 있었고, GUI 가 이제 `QPixmap` (PNG, smooth scaled) + `QSvgRenderer` (SVG, 모든 크기 crisp) 로 읽음. 아이콘 없는 앱은 letter avatar 로 fallback. ### 테스트 - 스트리밍 progress wrapper: Popen 기반 테스트가 progress-file 쓰기 인터리브된 3-poll lifecycle 시뮬레이션. - Junk 필터: 11 가지 쓰레기 케이스 drop, 4 가지 실제 앱 보존, env-bypass 동작 검증. ### 수정 - **결과 파일의 BOM 으로 인한 거짓 "fail" 보고.** v0.1.9.4 가 runtime apply 를 FreeRDP RemoteApp PowerShell 로 라우팅했는데, wrapper 가 `Out-File -Encoding utf8` 사용 → Windows PowerShell 5.1 은 UTF-8 BOM 을 붙임 → 호스트가 기본 utf-8 코덱으로 `json.loads` 시 BOM 거부 → "result file unparseable: Unexpected UTF-8 BOM". 사실 rdp_timeouts 와 oem_runtime_fixes 의 레지스트리 변경은 **실제 적용 성공**했고, 파싱만 실패해서 사용자가 "안 됐다" 고 본 것. `windows_exec.run_in_windows` 가 이제 `utf-8-sig` 로 읽어 BOM 자동 흡수. - **`_apply_max_sessions` 가 자기 RDP 세션을 죽이던 문제.** payload 가 `Restart-Service -Force TermService` 호출했는데, TermService 가 바로 그 FreeRDP RemoteApp 세션 호스팅 중. 재기동 → 세션 강제 종료 → wrapper 가 결과 파일 쓰기 전 죽음 → 호스트는 `ERRINFO_RPC_INITIATED_DISCONNECT [0x00010001]` 봄. 레지스트리 쓰기는 됐을 수도 있지만 채널 실패로 잘못 분류. v0.1.9.5 는 in-script `Restart-Service` 제거; 레지스트리만 쓰면 충분, TermService 가 다음 자연 사이클 (다음 부팅 / 사용자 수동 `winpodx pod restart`) 때 새 값 반영. ### 변경 (아키텍처) - **모든 host→Windows 명령 경로를 깨진 `podman exec ... powershell.exe` 에서 `windows_exec.run_in_windows` 로 마이그레이션**. 6개 함수가 0.1.0 ~ 0.1.9.4 동안 silent no-op 이었음 — `podman exec` 는 QEMU 호스팅하는 Linux 컨테이너만 도달, 그 안 Windows VM 에는 못 가서 `powershell.exe` 호출이 모두 `rc=127 executable file not found in $PATH` 로 실패하는데 헬퍼들이 warning 만 로그하고 return 했음. v0.1.9.5 는 모두 마이그레이션: - `provisioner._change_windows_password` (비밀번호 회전 — 수년간 silent fail) - `pod.recover_rdp_if_needed` (Bug B TermService 재기동 — 작동 안 했음; FreeRDP 도 죽은 RDP 리스너 인증 못하므로 컨테이너 재시작으로 대체) - `daemon.sync_windows_time` (w32tm) - `core.updates._exec_toggle` (Windows Update 활성화/비활성화/상태) - `cli/main._cmd_debloat` 와 `gui/main_window._on_debloat` (debloat.ps1 — `podman cp` + `podman exec` 둘 다 깨졌었음) - `core/discovery.discover_apps` (Bug A 의 stdin pipe "수정" 도 같은 깨진 경로; 이제 FreeRDP RemoteApp 로 진짜 적용) ### 추가 - **`winpodx pod sync-password`** CLI — 이전 릴리즈에서 누적된 비번 drift 복구. "마지막으로 작동한" 비번 (보통 초기 설치 시 또는 `compose.yml` 의 `PASSWORD` env var 값) 을 입력받아 FreeRDP 인증 → Windows 안에서 `net user` 실행 → 계정 비번을 현재 cfg.password 로 설정. 동기화 완료 후 비번 회전이 정상 작동. - **migrate 자동 drift 감지.** `winpodx migrate` 가 "already current" 경로에서 작은 `Write-Output 'sync-check'` payload 를 FreeRDP 채널로 먼저 발사. auth/no-result-file 로 실패하면 "`winpodx pod sync-password` 실행" 안내 메시지 출력 → 그 이후 3개 apply 가 혼란스러운 채널 에러로 실패하는 것 방지. - **Lint 테스트 `tests/test_no_broken_podman_exec.py`** — 향후 `src/winpodx/` 아래 (`windows_exec.py` 자체 제외) 에 `podman exec ... powershell.exe` 패턴 재도입 시 CI 실패. Windows-측 명령은 단일 채널로 강제. ## [0.1.9.4] - 2026-04-26 ### 수정 - **Runtime apply 가 드디어 실제로 적용됨.** kernalix7 이 2026-04-26 에 v0.1.9.1 / v0.1.9.2 / v0.1.9.3 의 runtime apply 가 silently 실패하고 있다고 보고: `podman exec winpodx-windows ...\powershell.exe` 가 `rc=127 executable file not found in $PATH` 반환. 근본 원인: `podman exec` 는 QEMU 를 호스팅하는 **Linux 컨테이너 안**에서 명령을 실행하지, QEMU 안에서 도는 **Windows VM** 에선 안 돈다. Linux 컨테이너엔 `powershell.exe` 가 없음. 헬퍼들 (`_apply_max_sessions`, `_apply_rdp_timeouts`, `_apply_oem_runtime_fixes`, `_change_windows_password`) 이 모두 warning 만 로그하고 return 하는데, 공개 `apply_windows_runtime_fixes` 는 헬퍼가 raise 안 하니까 "ok" 로 보고. → 3개 릴리즈가 silent no-op 을 ship 했음. 3개 변경: 1. **신규 `core/windows_exec.py`** — `run_in_windows(cfg, ps_payload)` 가 FreeRDP RemoteApp 으로 PowerShell 을 띄우고 기존 `\\tsclient\home` 리다이렉션으로 스크립트 파이핑. wrapper 가 `{rc, stdout, stderr}` JSON 을 같은 share 로 다시 씀. 호스트가 파싱해서 `WindowsExecResult` 반환. 채널 실패 (FreeRDP missing / auth fail / timeout / no result file) 는 `WindowsExecError` raise; 스크립트 non-zero rc 는 `WindowsExecResult.rc` 로 표면화. 2. **`_apply_max_sessions`, `_apply_rdp_timeouts`, `_apply_oem_runtime_fixes` 재작성** — 각각 PS payload 빌드 → `run_in_windows` 호출 → `rc != 0` 시 `RuntimeError` raise 해서 실패가 진짜 전파됨. 3. **`apply_windows_runtime_fixes` 정직한 보고** — `try/except` 구조 동일하지만, Windows VM 내부 rc가 실제로 fake `ok` 대신 `failed: rc=2 ...` 로 보고. 비용: 호출당 ~5–10 초 (RDP handshake + auth + script + disconnect) + `-WindowStyle Hidden` 으로 최소화한 PowerShell 창 깜박임. 대신 기존 pod 에서 작동 (재생성 불필요) + rc 체크가 진짜 의미 있음. **주의사항**: `cfg.rdp.password` 가 Windows 게스트 실제 비밀번호와 일치해야 함. 이전 릴리즈에서 password rotation 이 같은 `podman exec` 원인으로 silently 실패해 왔다면, 첫 호출이 auth error 로 실패. 사용자가 `winpodx app run desktop` 으로 Windows 들어가서 `net user User ` 로 동기화 필요. ### 테스트 - `tests/test_windows_exec.py` 에 9개 신규 테스트 — FreeRDP missing / 비번 missing / timeout / no-result-file (auth fail) / happy path / non-zero rc propagation / FreeRDP `/app:program:` cmd 형태 검증 / flatpak 바이너리 splitting / unparseable JSON. - `tests/test_provisioner.py` 재작성 — `subprocess.run` 대신 `windows_exec.run_in_windows` mock. 신규 테스트가 `rc != 0` 에서 `RuntimeError` raise + 채널 실패에서 `WindowsExecError` raise 검증. ## [0.1.9.3] - 2026-04-26 ### 수정 - **Patch 버전 migrate 가 Windows-측 apply 를 건너뛰던 "already current" 트랩.** kernalix7 이 0.1.9.x 에서 0.1.9.2 로 업그레이드 후 `winpodx 0.1.9.2: already current. Nothing to migrate.` 만 보고 실제 Windows 게스트엔 v0.1.9.1 RDP-timeout / v0.1.9.2 OEM v7-baseline runtime 수정이 들어가지 않음. 원인: `_version_tuple(...)[:3]` 이 `0.1.9.1` 과 `0.1.9.2` 를 같은 `(0, 1, 9)` 튜플로 자르므로 `inst_cmp >= cur_cmp` 가 runtime apply 단계 **앞에서** early-return 시킴. 이제 "already current" 경로에서도 idempotent runtime apply 가 항상 실행됨. ### 추가 - **`winpodx pod apply-fixes`** 독립 CLI 명령. Idempotent — `_apply_max_sessions`, `_apply_rdp_timeouts`, `_apply_oem_runtime_fixes` 를 실행 중인 pod 에 호출하고 헬퍼별 OK/FAIL 테이블 출력. 성공 시 exit 0, pod 미실행/백엔드 미지원 시 2, 헬퍼 실패 시 3. 언제든 재실행 안전. - **GUI Tools 페이지 "Apply Windows Fixes" 버튼.** 동일한 runtime apply 를 Qt GUI 에서 트리거 — worker thread 에서 헬퍼 호출, 기존 toast/info-label 채널로 성공/실패 표시. CLI 안 쓰고 GUI 만으로 적용 가능. - **install.sh 가 매 설치 마지막에 `winpodx pod apply-fixes` 자동 호출.** migrate 위자드 다음 단계로 실행. `|| true` 로 실패 무해 — pod 안 켜져 있으면 silent skip. `curl | bash` 한 번이면 항상 최신 Windows-측 수정사항이 기존 게스트에 적용됨, migrate 의 버전 비교가 "진짜" 업그레이드를 봤는지와 무관. - **공개 API `provisioner.apply_windows_runtime_fixes(cfg)`** — `{helper_name: "ok" | "failed: ..."}` 맵 반환. CLI / GUI / migrate 경로가 단일 진입점 공유. ## [0.1.9.2] - 2026-04-26 ### 수정 - **v0.1.9 / v0.1.9.1 의 Windows-측 수정사항이 기존 게스트에 적용 안 되던 버그.** kernalix7 보고: "마이그레이션 잘 되는거 맞아? 윈도에 적용 안되는거같은데" — 사실이었음. install.bat (OEM 스크립트) 은 dockur 무인 설치 첫 부팅 시에만 도므로, 0.1.6 / 0.1.7 / 0.1.8 / 0.1.9 / 0.1.9.1 사용자는 컨테이너 재생성 없이 NIC power-save off (OEM v7), TermService failure-recovery (OEM v7), RDP timeout 비활성 + KeepAlive (OEM v8) 를 받을 수 없었음. 추가로, v0.1.9.1 의 `_apply_rdp_timeouts` runtime 헬퍼가 `provisioner.ensure_ready` 의 `check_rdp_port` early-return **뒤에** 와이어돼 있어서 이미 정상 동작 중인 포드에는 절대 도달하지 못했음. - `provisioner.ensure_ready`: 함수 상단에서 `pod_status` 한 번 probe 해서 idempotent runtime apply 들 (`_apply_max_sessions`, `_apply_rdp_timeouts`, 신규 `_apply_oem_runtime_fixes`) 을 RDP early-return **앞에서** 실행. cold-pod 경로에서는 pod 시작 후 재적용. 호출당 약 1.5s 오버헤드, 재실행은 모두 no-op. - 신규 `provisioner._apply_oem_runtime_fixes(cfg)`: OEM v7 baseline (NIC `Set-NetAdapterPowerManagement -AllowComputerToTurnOffDevice $false`, `sc.exe failure TermService` recovery 액션) 을 기존 게스트에 `podman exec powershell` 로 적용 — `discover_apps.ps1` 가 쓰는 stdin-pipe transport 재사용. - `winpodx migrate`: 0.1.9 boundary 를 넘는 업그레이드 감지 시 세 apply 헬퍼를 proactive 호출 (pod 상태 probe + stopped 시 interactive 시작 옵션). 헬퍼별 성공/실패 출력 — 컨테이너 재생성 없이 어떤 게 적용됐는지 사용자가 직접 확인 가능. ## [0.1.9.1] - 2026-04-26 ### 수정 - **Apps "Refresh Apps" 버튼 누르면 GUI SEGV (pod 안 켜진 상태).** kernalix7 보고: `_on_refresh_failed` 가 queued-signal 콜백 프레임 안에서 `QMessageBox(self)` 를 바로 생성했는데 PySide6 + Qt 6.x 가 dialog 의 폰트 상속 경로 (`QApplication::font(parentWidget)` → `QMetaObject::className()`) 에서 부모 metaObject 가 콜백 중에 조회되며 SEGV. 이제 `QTimer.singleShot(0, ...)` 으로 dialog 생성을 다음 이벤트 루프 틱으로 미뤄서 signal handler 프레임이 먼저 풀림. Info 페이지의 첫 fetch 도 같은 이유로 `__init__` 에서 빠져나간 뒤 실행. Info worker 클래스를 모듈 레벨로 hoisting (refresh 마다 재정의되던 것), 재진입 busy guard 추가, worker + QThread 모두 정상 `deleteLater`. - **호스트 suspend / 장기 유휴 후에도 RDP 세션이 사용 중에 끊기던 문제.** v0.1.9 Bug B 수정은 "RDP 도달 불가" 경로만 다뤘는데, Windows TermService 의 1시간 `MaxIdleTime` 기본값 등으로 활성 세션 자체가 종료될 수 있었음. install.bat (OEM v7 → v8) 와 새 `_apply_rdp_timeouts` provisioner 단계가 `MaxIdleTime=0`, `MaxDisconnectionTime=0`, `MaxConnectionTime=0`, `KeepAliveEnable=1` + `KeepAliveInterval=1` 을 `HKLM\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services` 와 `RDP-Tcp` WinStation 양쪽에 기록 + WinStation 에 `KeepAliveTimeout=1` (TCP keep-alive 1분 간격). 기존 0.1.x 게스트도 다음 `ensure_ready` 에서 자동 적용 — 컨테이너 재생성 불필요. ## [0.1.9] - 2026-04-25 ### 변경 - **Discovery-first 리팩터.** `data/apps/` 아래 14개 번들 앱 프로필 (`word-o365`, `excel-o365`, ..., `notepad`, `cmd`, ...) 을 전부 제거. 이제 Linux 앱 메뉴는 `winpodx app refresh` 결과로만 채워지며, 첫 부팅 시 발견 트리가 비어 있을 때 `provisioner.ensure_ready` 가 자동 실행. 수동 재실행은 동일: CLI 의 `winpodx app refresh` 또는 GUI Apps 페이지의 "Refresh Apps" 버튼. `AppInfo.source` 에서 `"bundled"` enum 값 제거 — `"discovered"` 와 `"user"` 만 남음. 0.1.x < 0.1.9 에서 업그레이드 시 `winpodx migrate` 가 기존 `~/.local/share/applications/winpodx-{14-bundled-slug}.desktop` 파일 정리 여부를 물음 (`--non-interactive` 에서는 자동 스킵). ### 추가 - **Info 페이지 (CLI + GUI).** 새 `core.info.gather_info(cfg)` 가 5섹션 스냅샷 반환 — System (winpodx 버전, OEM 번들 버전, rdprrap 버전, 배포판, 커널), Display, Dependencies, Pod (상태, 실행 시작 시각, RDP/VNC 도달성 probe, 활성 세션 수), Config (기존 budget 경고 포함). `winpodx info` 가 5개 섹션 모두 출력하도록 재작성. Qt 메인 윈도우에 5번째 탭 "Info" 추가 — 섹션당 카드 + "Refresh Info" 버튼이 `QThread` 로 `gather_info` 재실행. 모든 probe 가 하드 타임아웃되어 아픈 pod 가 패널을 멈추지 않음. ### 수정 - **Bug A: Windows 게스트 대상 `winpodx app refresh`.** v0.1.8 에서 `podman cp host:discover_apps.ps1 container:C:/winpodx-discover.ps1` 가 실패 — dockur/windows 는 QEMU 안에서 실제 Windows 게스트를 돌리는 Linux 컨테이너이고, C: 드라이브는 가상 디스크 안에 있어 `podman cp` 로는 도달 불가. 이제 스크립트 본문을 `podman exec -i container powershell -NoProfile -ExecutionPolicy Bypass -Command -` 의 stdin 으로 파이핑하므로 staging 단계 자체가 사라짐. 컨테이너 런타임 stderr 에 "no such container", "is not running" 등이 보이면 `kind="pod_not_running"` 으로 재분류 — cli 는 exit code 2 + "run `winpodx pod start --wait`" 힌트로 라우팅 유지. - **Bug B: 호스트 suspend / 장기 유휴 후 RDP 도달 불가.** 증상: VNC 포트 8007 은 살아있는데 RDP 포트 3390 만 응답 없음 — Windows TermService 가 멈추거나 가상 NIC 가 절전으로 빠짐. 새 `core.pod.recover_rdp_if_needed(cfg)` 가 이 비대칭을 감지하고 `podman exec powershell Restart-Service -Force TermService; w32tm /resync /force` 실행 후 RDP 재 probe (최대 3회, 백오프). `provisioner.ensure_ready` 의 `_ensure_pod_running` 직후에 와이어. OEM 번들 6 → 7 — `install.bat` 에 예방 조치 추가: `Set-NetAdapterPowerManagement -AllowComputerToTurnOffDevice $false` 와 `sc.exe failure TermService reset=86400 actions=restart/5000/restart/5000/restart/5000` 로 Windows 자체 복구. ## [0.1.8] - 2026-04-25 ### 추가 - **Windows 앱 동적 발견.** 새 CLI `winpodx app refresh` 서브커맨드와 Qt GUI Apps 페이지의 "Refresh Apps" 버튼이 Windows 게스트에 실제 설치된 앱을 열거하고 기본 번들 14개 프로필과 함께 등록합니다. 컨테이너 내부에서 `scripts/windows/discover_apps.ps1` 이 Registry `App Paths` (HKLM + HKCU), Start Menu `.lnk` 재귀, UWP/MSIX (`Get-AppxPackage` + `AppxManifest.xml`), Chocolatey / Scoop shim 4개 소스를 스캔하고 실제 바이너리/패키지 로고에서 추출한 base64 아이콘을 포함한 JSON 배열을 반환합니다. Linux 호스트 (`winpodx.core.discovery`) 는 `podman cp` 로 스크립트를 복사하고 `podman exec powershell` 로 실행한 뒤, 결과를 `~/.local/share/winpodx/discovered//` 아래 TOML + PNG/SVG 아이콘 파일로 저장합니다. 번들 / 사용자 직접 추가 / 발견 앱은 세 디렉토리로 분리 관리되며 로딩 시 "사용자 > 발견 > 번들" 우선순위로 병합됩니다 — 재발견 실행은 발견 트리만 건드립니다. - **UWP RemoteApp 실행.** `rdp.build_rdp_command` 가 `launch_uri` + 엄격 정규식 검증된 AUMID (`!`) 를 받아 UWP 앱을 `/app:program:explorer.exe,cmd:shell:AppsFolder\` 로 매핑합니다. `/wm-class` fallback 이 `winpodx-uwp-` 로 슬러그당 고유하게 지정되어 두 UWP 앱이 같은 힌트를 공유할 때도 Linux 태스크바 그루핑이 분리됩니다. - **CI PowerShell Core smoke 테스트.** 새 `discover-apps-ps` 잡이 Ubuntu runner 에 `pwsh` 를 설치하고 모든 PR 에서 `discover_apps.ps1 -DryRun` 을 실행해 `core.discovery` 가 기대하는 JSON 배열 shape 을 stdout 이 파싱 가능한지 검증합니다. - **업그레이드 후 마이그레이션 위자드.** 새 CLI `winpodx migrate` 가 사용자가 건너뛴 모든 버전의 릴리즈 노트를 순차적으로 보여주고, 원하면 `winpodx app refresh` 를 바로 실행해 Windows 앱 메뉴를 한 번에 채워 줍니다. `install.sh` 는 업그레이드 감지 시 (`~/.config/winpodx/winpodx.toml` 존재) `winpodx migrate` 를 자동 호출합니다 — 건너뛰려면 `WINPODX_NO_MIGRATE=1` 설정. 자동화용 플래그: `--no-refresh` (discovery 만 스킵), `--non-interactive` (모든 프롬프트 비활성화). 위자드는 `~/.config/winpodx/installed_version.txt` 에 현재 버전을 기록하며, 이 파일이 없는 사전-0.1.8 설치는 `0.1.7` 에서 업그레이드하는 것으로 간주합니다. - **`pod.max_sessions` 설정 노출.** 기본값 10 유지, `[1, 50]` 범위로 clamp. `ensure_ready()` 가 매 provisioning 시 값을 읽어 게스트의 `HKLM:\...\Terminal Server\MaxInstanceCount` 와 비교하고, 다를 때만 레지스트리 재작성 + `TermService` 재기동 — 활성 RemoteApp 세션이 매번 끊기지 않습니다. 적용 시 `fSingleSessionPerUser=0` 도 함께 재확정. `winpodx.core.config` 의 `estimate_session_memory` / `check_session_budget` 헬퍼가 `winpodx config show`, `winpodx config set`, `winpodx info`, 그리고 GUI Settings 페이지에서 **`max_sessions` 가 `ram_gb` 예산을 초과할 때에만** 경고를 표시합니다 — 기본 설정은 조용합니다. - **오프라인 / 에어갭 설치용 `install.sh` 로컬 경로 플래그.** `--source PATH` 는 git clone 대신 로컬 디렉토리에서 winpodx 를 복사합니다 (`pyproject.toml` + `src/winpodx/` 존재 검증). `--image-tar PATH` 는 `podman load -i` (또는 `docker load -i`) 로 Windows 컨테이너 이미지를 사전 로드해 최초 부팅 시 레지스트리 접근이 필요 없게 합니다. `--skip-deps` 는 배포판 의존성 설치 단계를 완전히 스킵하며 필수 도구가 이미 설치돼 있지 않으면 즉시 실패합니다. 각 플래그에 대응하는 환경 변수 (`WINPODX_SOURCE`, `WINPODX_IMAGE_TAR`, `WINPODX_SKIP_DEPS`) 도 제공 — `curl | bash` 호출도 조합 가능. `install.sh --help` 로 전체 사용법 확인. ### 변경 - `AppInfo` 에 `source: "bundled" | "discovered" | "user"`, `args`, `wm_class_hint`, `launch_uri` 필드 추가. GUI 가 발견 엔트리를 뱃지로 구분할 수 있고 RDP 실행이 UWP 앱을 타겟팅할 수 있게 됩니다. - `desktop.entry._install_icon` 이 아이콘 파일 확장자에 따라 `hicolor/scalable/apps/` (SVG) vs `hicolor/32x32/apps/` (PNG) 로 분기 설치. 발견 앱의 추출된 PNG 아이콘이 번들 SVG 아이콘과 나란히 깔끔하게 설치됩니다. ## [0.1.7] - 2026-04-23 ### 변경 - **번들된 rdprrap 을 v0.1.3 로 갱신 (라이선스 컴플라이언스 릴리즈).** 업스트림이 0.1.0, 0.1.1, 0.1.2 GitHub 릴리즈 자산을 모두 철회했습니다. 0.1.0 / 0.1.1 은 rdprrap 이 코드를 포팅해 온 세 업스트림(`stascorp/rdpwrap` Apache-2.0, `llccd/TermWrap` MIT, `llccd/RDPWrapOffsetFinder` MIT) 이 요구하는 소스 레벨 저작자 고지(attribution notices) 가 누락되어 있었습니다. 0.1.2 는 `NOTICE` + `vendor/licenses/` 를 추가해 법적 공백은 해소했지만, rdpwrap 파생 Rust 소스 16개 중 9개만 나열하고 `rdprrap-conf` About 다이얼로그의 copyright 라인이 `LICENSE` 와 불일치하는 위생 문제가 남아 있었습니다. 0.1.3 은 `NOTICE` 를 업스트림 바이너리별(RDPWInst / RDPConf / RDPCheck)로 재편해 16개 전부를 열거하고, About 다이얼로그 copyright 를 `LICENSE` 와 정렬했으며, 채택한 Contributor Covenant 텍스트에 CC BY 4.0 출처를 명시합니다. 0.1.1 의 레지스트리 readback 수정(`OriginalServiceDll` 이 `termsrv.dlll` 로 저장되던 문제) 도 그대로 포함합니다. 새 번들 SHA256 은 `config/oem/rdprrap_version.txt` 에 고정되며, 기존 게스트도 컴플라이언스 번들로 재설치되도록 first-boot OEM 버전을 6 으로 올렸습니다. ### 문서 - 최상위 [`THIRD_PARTY_LICENSES.md`](../THIRD_PARTY_LICENSES.md) 추가. 번들된 rdprrap 바이너리와 런타임/선택 Python 의존성(PySide6 LGPL, libvirt-python LGPL, docker-py Apache-2.0, tomli MIT) 을 문서화합니다. - `debian/copyright` 가 번들된 rdprrap 파일을 별도 선언하도록 보강했고, ZIP 내부의 `NOTICE` / `vendor/licenses/` 텍스트가 업스트림 Apache-2.0 / MIT 저작자 고지 요건을 충족한다는 사실을 명시했습니다. ### 수정 - **`install.sh` 가 `curl … | bash` 경로에서 정상 동작.** 파이프로 실행되면 bash 가 stdin 에서 스크립트를 읽으므로 `BASH_SOURCE[0]` 가 unset 상태가 되고, 파일 상단의 `set -u` 가드와 결합되어 install.sh 205 줄에서 `BASH_SOURCE[0]: unbound variable` 로 리포 클론 전에 중단되었습니다. 로컬/원격 분기가 소스 경로를 빈 값으로 기본 처리하도록 변경되어, 로컬 소스 트리가 없을 때 자연스럽게 git clone 경로로 폴백합니다. CachyOS + Python 3.14 + fish shell 환경에서 리포트 ([#3](https://github.com/kernalix7/winpodx/issues/3)). ### 보안 / 컴플라이언스 - rdprrap 0.1.0 을 번들한 WinPodX 0.1.6 은 동일한 저작자 고지 누락 결함을 그대로 가지고 있었습니다. 0.1.6 GitHub 릴리즈 자산은 철회되었으며(태그는 보존), 0.1.7 이 Windows 게스트에 컴플라이언스 rdprrap 번들(0.1.3, `NOTICE` + `vendor/licenses/` 포함) 을 내려주는 첫 WinPodX 릴리즈입니다. ## [0.1.6] - 2026-04-22 ### 추가 - **멀티세션 RDP — 번들/완전 오프라인.** [rdprrap](https://github.com/kernalix7/rdprrap) v0.1.0 zip (~1.6 MB, `config/oem/` 내부) 을 winpodx 패키지에 동봉하며, Windows 무인 설치 단계에서 자동 적용합니다. 번들은 게스트 최초 부팅 시 `C:\OEM\` 로 스테이징되고, 핀 파일의 sha256 과 일치 여부를 확인한 뒤 압축이 풀립니다. 설치 시점에 네트워크 접근은 필요하지 않습니다. 실패 시 조용히 단일 세션으로 폴백합니다. 게스트 측 관리 채널(설치 후 enable/disable/status)은 향후 릴리즈로 예정되어 있습니다. ## [0.1.5] - 2026-04-21 ### 추가 - **AlmaLinux 9 / AlmaLinux 10** 용 prebuilt RPM 추가 (RHEL 9/10, Rocky 9/10 에도 그대로 설치 가능). 모든 GitHub Release 에 자동 첨부. - Arch Linux AUR 패키징 인프라 추가 (메인테이너 1회 세팅 후 활성화 — 자세한 절차는 [`packaging/aur/README.md`](../packaging/aur/README.md)). ### 변경 - **최소 Python 버전을 3.11 → 3.9 로 낮춤.** 기본 `python3` 가 3.9 인 배포판 (RHEL 9 / AlmaLinux 9 / Rocky 9) 에 별도 Python 모듈 없이 바로 설치 가능. ### 수정 - OBS RPM 자동 다운로드가 새로 퍼블리시된 에셋을 제대로 수거하도록 수정. ## [0.1.4] - 2026-04-21 ### 수정 - `.deb` 빌드가 "missing files" 로 실패하던 문제 해결. - 타겟 매트릭스 외의 마이너 아키텍처에서 발생하는 빌드 서비스 측 문제로 인해 OBS 퍼블리시가 실패로 찍히지 않도록 개선. ## [0.1.3] - 2026-04-21 ### 수정 - OBS 퍼블리시 단계가 빌드 대기 중 인증 에러 루프에 빠지지 않도록 수정. - `.deb` 빌드가 테스트 스위트를 돌리지 않도록 수정 (테스트는 GitHub Actions 업스트림에서 실행). ## [0.1.2] - 2026-04-21 ### 수정 - 태그 푸시 이후 RPM / `.deb` 퍼블리시 워크플로우가 제대로 실행되어 Release 에 아티팩트가 첨부되도록 수정. - 업스트림 `pyproject.toml` 버전이 최신 git 태그보다 앞서있어도 RPM 빌드가 실패하지 않도록 개선. ## [0.1.1] - 2026-04-21 ### 추가 - **Release 별 prebuilt 패키지**: - RPM: openSUSE Tumbleweed, Leap 15.6, Leap 16.0, Slowroll, Fedora 42, Fedora 43. - `.deb`: Debian 12 / 13, Ubuntu 24.04 / 25.04 / 25.10. - 소스 dist + wheel. - README "설치" 섹션에 배포판별 설치 방법 추가. ### 변경 - AppImage 패키징 제거: Python + Qt + FreeRDP + Podman 의존성 때문에 단일 파일 배포의 이점이 거의 없음. ### 수정 - 주간 업스트림 업데이트 체크가 권한 에러로 실패하지 않고 추적용 Issue 를 생성하도록 변경. ## [0.1.0] - 2026-04-21 첫 공개 릴리즈. ### 추가 - **Zero-config 자동 프로비저닝**: 첫 앱 실행 시 설정 파일 생성, compose 파일 생성, 컨테이너 시작, 데스크탑 엔트리 등록이 자동으로 수행됨. - **14개 번들 앱 정의**: Word, Excel, PowerPoint, Outlook, OneNote, Access, 메모장, 탐색기, CMD, PowerShell, 그림판, 계산기, VS Code, Teams. - **자동 서스펜드 / 리줌**: 유휴 시 컨테이너 일시정지, 다음 앱 실행 시 자동 복구; 종료 시 정상 셧다운. - **패스워드 자동 로테이션**: 암호학적 난수 20자 패스워드, 7일마다 교체 (설정 가능), 실패 시 자동 롤백. - **수동 패스워드 로테이션**: `winpodx rotate-password`. - **Office 락 파일 정리**: `winpodx cleanup` 이 홈 디렉터리의 `~$*.*` 락 파일 제거. - **Windows 시간 동기화**: `winpodx timesync` 로 호스트 sleep/wake 후 시계 재동기화. - **Windows 디블로트**: `winpodx debloat` 로 텔레메트리, 광고, Cortana, 검색 인덱싱 비활성화. - **전원 관리**: `winpodx power --suspend/--resume` 로 컨테이너 수동 일시정지/복구. - **시스템 진단**: `winpodx info` 로 디스플레이, 의존성, 설정 상태 확인. - **데스크탑 알림** (D-Bus / `notify-send`) 앱 실행 시 자동 표시. - **스마트 DPI 스케일링**: GNOME, KDE Plasma 5/6, Sway, Hyprland, Cinnamon, env var, xrdb 에서 스케일 자동 감지. - **Qt 시스템 트레이**: pod 제어, 앱 런처, 유지보수 도구, 유휴 모니터, 자동 새로고침. - **멀티 백엔드**: Podman (기본), Docker, libvirt/KVM, manual RDP — 통일된 인터페이스. - Podman/Docker 백엔드용 **compose 파일 자동 생성** (`dockur/windows` 이미지 사용). - **앱별 작업표시줄 분리**: 각 앱이 고유한 WM_CLASS / `StartupWMClass` 보유. - **Windows 빌드 고정**: `TargetReleaseVersion` 정책으로 기능 업데이트 차단, 보안 업데이트는 유지. - **업스트림 업데이트 모니터링**: `dockur/windows` 신규 릴리즈를 매주 체크. - **동시 실행 보호**: 쓰레딩 락으로 동시 앱 실행 시 크래시 방지. - GUI 의 **Windows Update 토글** (서비스 + 예약 작업 + hosts 파일 3중 차단). - **사운드 + 프린터** 리다이렉션 기본 활성화. - **USB 드라이브 공유** + hot-plug (재연결 없이 subfolder 로 표시). - FreeRDP `urbdrc` 사용 가능 시 **USB 장치 리다이렉션**; 없으면 드라이브 공유로 graceful fallback. - Windows 측 **USB 자동 드라이브 문자 매핑** (이벤트 기반, 폴링 없음). - 데스크탑 통합: `.desktop` 엔트리, hicolor 아이콘, MIME 등록, 아이콘 캐시 리프레시. - 자격 증명 보호용 제한 권한 (`0600`) TOML 설정 파일. - 프로세스 추적 + 좀비 리퍼 포함 FreeRDP 세션 관리. - `winapps.conf` 임포트 (기존 winapps 설정 마이그레이션용). ### 보안 - RDP 를 **127.0.0.1** 에만 바인딩; 네트워크 노출 없음. - **TLS 전용** RDP 채널 (SecurityLayer=2); NLA 는 loopback 바인딩 환경에서만 비활성화.