Files
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

35 KiB
Raw Permalink Blame History

WinPodX 라이프사이클 & 프로세스

WinPodX pod 의 설치 / 업그레이드 / 마이그레이션 / 헬스 유지 전 과정을 코드 경로 단위로 설명. 각 섹션은 한 단계씩 — 누가 발사하나, 무엇을 하나, 코드가 어디 있나, 어떤 실패 모드를 다루나.

대상. WinPodX 의 어떤 코드 경로든 이해하거나 디버그해야 하는 메인테이너 / 고급 사용자. 일상 사용은 README.ko.md 참고.


목차

  1. 전체 단계 개요
  2. 신규 설치
  3. Sysprep 첫 부팅 (install.bat)
  4. 업그레이드 설치 (기존 config 있음)
  5. 마이그레이션 (winpodx migrate)
  6. Apply 체인 (apply_windows_runtime_fixes)
  7. Multi-session 활성화
  8. 컨테이너 이미지 pinning
  9. Discovery (winpodx app refresh)
  10. Transport 선택
  11. Guest sync (winpodx pod sync-guest)
  12. Disk auto-grow
  13. 로그인 시 pod 자동 시작
  14. 복구 시나리오

1. 전체 단계 개요

                 ┌─────────────────┐
                 │  install.sh     │
                 │  (호스트 측)     │
                 └────────┬────────┘
                          │
        ┌─────────────────┴──────────────────┐
        │                                    │
   기존 config 없음                기존 config 있음
        │                                    │
        ▼                                    ▼
┌───────────────┐               ┌─────────────────────┐
│ winpodx setup │               │ setup 스킵           │
│  (대화형 또는   │               │ agent token staging │
│   default)    │               └──────────┬──────────┘
│ 작성:         │                          │
│  winpodx.toml │                          │
│  compose.yaml │                          │
│  agent_token  │                          │
└───────┬───────┘                          │
        │                                  │
        └──────────────┬───────────────────┘
                       │
                       ▼
            ┌─────────────────────┐
            │ winpodx pod         │
            │   wait-ready        │ (3 단계: 컨테이너,
            │                     │  RDP 포트, FreeRDP probe)
            └──────────┬──────────┘
                       │
                       ▼
            ┌─────────────────────┐
            │ winpodx migrate     │ (기존 config 있을 때만)
            │  - 버전 비교         │
            │  - image pin 정렬    │
            │  - apply 체인        │
            └──────────┬──────────┘
                       │
                       ▼
            ┌─────────────────────┐
            │ winpodx app refresh │
            │  (3 계층 race-free  │
            │   discovery)        │
            └─────────────────────┘

상단 박스 3개가 진입점, 나머지는 구현. 아래에 각각 상세 설명.


2. 신규 설치 (기존 config 없음)

트리거. ~/.config/winpodx/winpodx.toml 부재. 보통 처음 사용자가 curl -sSL .../install.sh | bash 돌릴 때.

흐름.

  1. install.sh 가 distro 확인, 누락 의존성 설치 (podman, podman-compose, freerdp, libnotify), Python ≥ 3.9 검증.
  2. install.sh 가 winpodx 소스를 ~/.local/bin/winpodx-app/ 에 추출 + winpodx 런처를 ~/.local/bin/winpodx 에 작성.
  3. install.shpython3 -m winpodx setup --non-interactive 호출 (src/winpodx/cli/setup_cmd.py::handle_setup).
  4. setup 이 ~/.config/winpodx/winpodx.toml 작성:
    • cfg.pod.image default = DOCKUR_IMAGE_PIN (SHA-pinned docker.io/dockurr/windows@sha256:… digest — §8 참고).
    • cfg.rdp.password 무작위 생성.
    • cfg.pod.backend 자동 감지 (podman > docker > libvirt).
  5. setup 이 generate_compose(cfg) 실행 → ~/.config/winpodx/compose.yaml 작성.
  6. setup 이 _ensure_oem_token_staged() 실행 → ~/.config/winpodx/agent_token.txt 작성 + OEM bind-mount source 디렉터리에 복사.
  7. install.shwinpodx pod wait-ready --timeout 3600 --logs 호출. dockur 가 pinned image 받고, Windows ISO (~7.5GB) 다운로드, 우리 OEM bundle 로 Sysprep (§3) — 보통 5-10분.
  8. migrate 스킵 (installed_version.txt 비교 대상 없음).
  9. install.shwinpodx app refresh 호출 (§9).

최종 상태. 완전 프로비저닝된 pod, multi-session 활성, agent 가 wscript wrapper 로 동작, 앱 메뉴 채워짐.


3. Sysprep 첫 부팅 (install.bat)

위치. config/oem/install.bat. OEM bind-mount 안에 있음 → dockur 가 첫 부팅 시 C:\OEM\ 으로 복사 → unattend.xmlFirstLogonCommands 가 자동로그온 사용자의 로컬 콘솔 세션에서 1회 호출.

WINPODX_OEM_VERSION (파일 상단) 이 번들 버전. install.bat 또는 형제 리소스 변경 시 릴리스마다 bump.

하는 일, 순서대로.

  1. TermService 복구 동작sc.exe failure TermService reset= 86400 actions= restart/5000/.... transient TermService crash 자동 복구.
  2. MaxInstanceCount + multi-session 레지스트리HKLM:\...\Terminal Server\WinStations\RDP-Tcp\MaxInstanceCount, fSingleSessionPerUser = 0. OEM 시점의 권위 있는 cap; runtime apply 체인이 cfg.pod.max_sessions 변경 시 동기화.
  3. rdprrap install (multi-session 활성화):
    • SHA256 검증된 번들을 C:\OEM\rdprrap-*.zipC:\winpodx\rdprrap\ 추출.
    • install + verify + marker 는 rdprrap-activate.ps1 에 위임 (OEM-time + runtime 활성화의 single source of truth; §7 참고).
  4. NIC / RDP 타임아웃 설정 — idle / disconnect / connection 타임아웃 비활성화해서 RemoteApp 세션이 1시간 후 끊기지 않도록.
  5. media_monitor.ps1 staging + autostartmedia_monitor.ps1C:\winpodx\ 에 복사하고 HKCU\Run\WinpodxMediawscript+hidden-launcher.vbs wrapper 로 등록 (OEM v19 부터 — 이전 버전은 bare powershell.exe -WindowStyle Hidden 이라 ~50ms conhost flash 새어나옴).
  6. VBS 런처 staginghidden-launcher.vbs, launch_uwp.vbs, launch_uwp.ps1, agent-respawn.ps1, rdprrap-activate.ps1C:\Users\Public\winpodx\launchers\ 에 복사. Public 디렉터리는 누구나 쓰기 가능 → User-level agent 가 나중에 runtime 마이그레이션 때 덮어쓸 수 있음.
  7. Agent autostartHKCU\Run\WinpodxAgent 를 wscript+hidden-launcher.vbs wrapper 가 C:\OEM\agent.ps1 가리키게 등록.
  8. URL ACL 사전 등록 — agent 의 http://+:8765/ 리스너용 netsh http add urlacl. User-level agent 가 admin 권한 없이 bind 가능.
  9. OEM 마커C:\winpodx\oem_version.txt 작성 → 호스트가 어떤 번들로 프로비저닝됐는지 probe 가능.

Idempotency. install.bat 은 1회 만 실행. Sysprep 끝나면 dockur 가 절대 재실행 안 함. 이후 모든 유지보수는 호스트의 apply 체인이 처리 (§6).


4. 업그레이드 설치 (기존 config 있음)

트리거. ~/.config/winpodx/winpodx.toml 이미 존재. 보통 curl -sSL .../install.sh | bash -s -- --main 로 기존 WinPodX 업데이트.

흐름.

  1. install.sh 가 소스를 ~/.local/bin/winpodx-app/ 재추출 (호스트 코드 업데이트).
  2. winpodx setup --non-interactive 실행 — 기존 config 감지, Existing config found ..., skipping setup 출력, _ensure_oem_token_staged() 실행, return. 여기서 compose 재생성 안 함, 즉 동작 중인 pod 은 소스 코드 업데이트로 흔들리지 않음.
  3. winpodx pod wait-ready 실행 — 컨테이너가 이미 떠 있고 따뜻하니 보통 몇 초 안에 완료.
  4. winpodx migrate 실행 (§5) — 기존 pod 마이그레이션의 canonical 위치.
  5. winpodx app refresh 실행 → 발견된 앱 메뉴 갱신.

게스트를 변경하는 건 migrate 만. install.sh 자체는 step 2 이후 순수 호스트 측.


5. 마이그레이션 (winpodx migrate)

코드. src/winpodx/cli/migrate.py::run_migrate.

목표. 기존 pod 을 main fresh install 이 만들 상태로 정렬. 3개 독립 단계가 순서대로 실행:

        installed_version vs current
                    │
       ┌────────────┼─────────────┐
       │            │             │
     None        already       cross-version
   (fresh)      current      (예: 0.1.7 → 0.3.1)
       │            │             │
       ▼            ▼             ▼
   버전 기록      apply 체인     whats-new 출력
                  실행          + apply 체인
                                + (선택) refresh

5.1 버전 감지

_detect_installed_version()~/.config/winpodx/installed_version.txt 읽음. 부재 + config 존재 = pre-tracker (v0.1.7) baseline 가정.

_version_tuple() 이 dot-segment 별 leading digit 추출 (0.3.0-RTM1, 0.3.0rc1, 0.3.0+dev 모두 [:3] 비교용으로 (0, 3, 0) 파싱). PR #82 이전엔 첫 non-int segment 에서 멈춰서 RTM-suffix 가 (0, 3) 반환 → 모든 shipped (0, 3, 0) 보다 작게 lex-비교 → RTM 사용자 모두에게 apply 체인 빠짐.

5.2 항상 실행되는 단계

기존 config 있으면 버전 무관 실행:

  1. _probe_password_sync — 사전 FreeRDP auth probe. password 가 drift 됐으면 (cfg vs Windows 계정 불일치) winpodx pod sync-password 안내 진단 출력.
  2. _ensure_canonical_image_pin — 일치 안 하면 cfg.pod.imagecompose.yamlDOCKUR_IMAGE_PIN 으로 재작성. 다음 pod start 시 컨테이너 1회 recreate (volume 보존 — ~30초, ISO 재다운로드 없음). Idempotent: pin 일치 → no-op.
  3. _apply_runtime_fixes_to_existing_guestapply_windows_runtime_fixes(cfg) 호출 (§6).

5.3 cross-version 업그레이드에서만 실행

  • _print_whats_new_VERSION_NOTES 에서 (installed, current] 사이 모든 버전 release notes 출력.
  • _maybe_cleanup_legacy_bundled — v0.1.9 경계 넘을 때만, 번들 프로필 시대의 stale .desktop 14개 제거 제안.

5.4 왜 "already current" 도 apply 체인 실행하나

Patch 버전 (0.1.9.x) 들은 [:3] 자르면 같은 (0, 1, 9) 튜플로 collapse 됨. 이 경로에서 apply 안 발사하면 0.1.9.0 → 0.1.9.2 업그레이드가 0.1.9.x 의 모든 수정을 silently 스킵. helper 들 idempotent 라 healthy pod 에 재실행해도 helper 당 marker probe + no-op return.


6. Apply 체인 (apply_windows_runtime_fixes)

코드. src/winpodx/core/provisioner.py::apply_windows_runtime_fixes. winpodx pod apply-fixes, GUI Tools 페이지 버튼, migrate 가 호출.

순서 중요 — 각 단계는 이전 단계 실행 가정:

1. max_sessions          MaxInstanceCount 레지스트리 sync
2. rdp_timeouts          idle / disconnect / connection 타임아웃 비활성화
3. oem_runtime_fixes     NIC 절전 off, TermService 복구, …
4. vbs_launchers         VBS 파일 push + agent-respawn + WinpodxMedia 재작성
5. multi_session         marker probe + (필요 시) detached activation

Helper 계약. 각 helper 가 단일 PowerShell payload 만들어서 _apply_via_transport 로 전송 (agent /exec 우선, FreeRDP RemoteApp 폴백) 후 return. 모든 helper idempotent — 이미 적용된 pod 에 실행하면 marker probe + no-op return.

Helper 별 상세.

6.1 _apply_max_sessions

HKLM:\...\WinStations\RDP-TcpMaxInstanceCount 작성, Terminal Server root 의 fSingleSessionPerUser 클리어. TermService restart 안 함 — apply 자체가 그 서비스가 제공하는 RDP 세션 안에서 실행 중이라, 재시작 = apply mid-flight 사망. 레지스트리 쓰기만 충분; 새 값은 다음 자연 주기에 적용.

6.2 _apply_rdp_timeouts

RDP idle / disconnect / max-session 타임아웃 비활성화 + keep-alive 활성화 레지스트리 키 작성. 없으면 Windows 가 1시간 default idle 후 RemoteApp 세션 drop, NAT/firewall idle-cleanup 이 underlying TCP 죽일 수 있음.

6.3 _apply_oem_runtime_fixes

OEM 시점에 유지돼야 하지만 가끔 안 그런 설정들의 catch-all:

  • NIC 절전 off (Set-NetAdapterPowerManagement)
  • TermService 복구 동작 (5초 재시작, 3회 시도)
  • ApplicationFrameHost / explorer.exe 안정성 tweak

6.4 _apply_vbs_launchers

5개 파일을 단일 /exec round-trip 으로 push:

파일 용도
hidden-launcher.vbs 일반 GUI-subsystem wrapper, 자식한테 SW_HIDE 전파
launch_uwp.vbs RemoteApp-friendly UWP 런처, launch_uwp.ps1 hidden 호출
launch_uwp.ps1 C# helper 클래스 IApplicationActivationManager activator (PS-level COM cast 이슈 없음)
agent-respawn.ps1 Detached agent 재시작 (옛 거 죽이고 wscript wrapper 로 새거 spawn)
rdprrap-activate.ps1 Runtime rdprrap activator (§7 참고)

그 다음 HKCU\Run\WinpodxAgentHKCU\Run\WinpodxMedia 를 wscript+hidden-launcher.vbs wrapper 사용하도록 작성. WinpodxMedia 재작성은 legacy 항목 존재 조건부 (install.bat 이 media_monitor staging 스킵한 pod 에 stale 항목 만들지 않도록).

마지막으로 agent-respawn.ps1 detached spawn → 새 wrapper 즉시 적용 (사용자 로그아웃 불필요). /health 가 3-4초 blip 후 새 wrapper 로 복구.

6.5 _apply_multi_session

§7 참고.


7. Multi-session 활성화

목표. rdprrap 이 termsrv.dll 을 patch 해서 같은 사용자의 여러 RDP 세션이 공존 가능. 없으면 새 RDP 연결마다 이전 세션 대체 — 무서운 "Select a session to reconnect to" 다이얼로그.

Single source of truth. config/oem/rdprrap-activate.ps1 이 OEM-time (synchronous, install.bat 에서) 과 runtime (detached, _apply_multi_session 또는 winpodx pod multi-session on 에서) 둘 다에서 호출됨.

7.1 활성화 메커니즘

2단계:

  1. rdprrap-installer install --skip-restartHKLM:\SYSTEM\CurrentControlSet\Services\TermService\Parameters\ServiceDlltermwrap.dll (rdprrap 의 wrapper DLL) 가리키도록 patch.
  2. net stop TermService /y && net start TermService — TermService 가 fresh start 시 새 DLL 로드.

step 2 가 모든 활성 RDP 세션 죽임 (TermService 가 그것들 관리하니까).

7.2 OEM-time 경로 (synchronous)

install.bat 이 FirstLogonCommands로컬 콘솔 세션에서 실행. TermService 는 RDP 세션만 관리하므로, step 2 의 cycle 이 cmd.exe 부모 안 죽임. install.bat 이 -Detached 없이 rdprrap-activate.ps1 호출, 스크립트 종료까지 동기 대기, rc 로 분기.

7.3 Runtime 경로 (detached)

agent 는 user RDP 세션 안에서 실행 — step 2 에서 죽는 그 세션. inline /exec 면 응답 반환 전에 죽음.

_apply_multi_sessionwinpodx pod multi-session on 이 따라서 wscript+hidden-launcher.vbs 로 rdprrap-activate.ps1 -Detached spawn:

host /exec  ──► agent.ps1  ──► Start-Process wscript.exe ...rdprrap-activate.ps1 -Detached
                    │                   │
              host 에 OK 반환       (2초 sleep — host 응답 시간)
                                        ↓
                                 install + TermService cycle
                                        ↓
                                 marker := 'enabled' / 'installer-failed' / 등
                                        ↓
                                 (agent 세션은 ~중간에 죽음)

사용자 재접속 → HKCU\Run 발사 → wscript wrapper 로 새 agent.

7.4 Idempotency: marker + ServiceDll 교차확인

_apply_multi_sessionC:\winpodx\rdprrap\.activation_status 읽음 (rdprrap-activate.ps1 이 작성):

Marker 값 동작
enabled No-op return. Fast path.
부재 / not-activated / installer-failed / extract-failed ServiceDll 교차확인. 이미 termwrap.dll 면: marker 에 enabled 작성, no-op return (PR #85 — install.bat 이 failed 로 마킹했지만 patch 는 어쨌든 land 한 케이스 처리). 아니면: detached activator spawn.

이 belt-and-suspenders 가 이미 동작 중인데 OEM-time 부분 실패한 pod 에서 TermService cycle 발사 방지. PR #85 이전엔 그런 pod 의 매 apply-fixes 호출이 agent 죽임.

7.5 winpodx pod multi-session on/off/status

  • on — apply 체인 단계와 동일 코드 경로. Detached spawn, "OK: activation queued" 반환 + ~10초 disconnect 비용 명시.
  • off — inline rdprrap-conf --disable. Disable 은 레지스트리 patch clear 만; TermService 는 다음 reboot 까지 cycle 불필요, agent 세션 안전.
  • status — marker probe. apply-fixes multi_session 단계가 쓰는 동일 source, 표면 일관성.

8. 컨테이너 이미지 pinning

코드. src/winpodx/core/config.pyDOCKUR_IMAGE_PIN 상수.

왜. Pin 이전 (≤ v0.3.0), cfg.pod.image default 가 :latest. 매 podman-compose up 마다 dockur 가 푸시한 무엇이든에 대해 tag 재해상. digest 가 바뀌면 (자주 — dockur 릴리스 주기가 daily-ish), podman-compose 가 spec 다르다고 판단해서 컨테이너 재생성 → fresh ISO 다운로드 → 분 단위 Sysprep → 게스트 상태 손실.

포맷. docker.io/dockurr/windows@sha256:<64-char-hex>.

갱신 절차 (릴리스 시점).

TOKEN=$(curl -sSL "https://auth.docker.io/token?service=registry.docker.io&scope=repository:dockurr/windows:pull" | jq -r .token)
curl -sSL -H "Authorization: Bearer $TOKEN" -I \
  -H 'Accept: application/vnd.docker.distribution.manifest.list.v2+json' \
  -H 'Accept: application/vnd.oci.image.index.v1+json' \
  "https://registry-1.docker.io/v2/dockurr/windows/manifests/latest" \
  | grep -i '^docker-content-digest:'

digest 를 DOCKUR_IMAGE_PIN 에 붙여넣고, 버전 bump, ship.

마이그레이션. Migrate 의 _ensure_canonical_image_pin 이 기존 pod 의 cfg.pod.image + compose.yaml 재작성. 다음 pod start 시 컨테이너 1회 recreate (volume 보존 — ~30초, ISO 재다운로드 없음, Sysprep 없음). Idempotent.

사용자 opt-in 갱신. winpodx setup --update-image 가 fresh :latest 를 pull 하는 유일한 경로:

  1. podman pull docker.io/dockurr/windows:latest
  2. podman image inspect ... -f '{{json .RepoDigests}}' → digest 해결
  3. docker.io 항목으로 필터 → cfg.pod.image := <digest>
  4. compose.yaml 재생성
  5. "다음 pod start 시 컨테이너 recreate ~30초, volume 보존" 출력

9. Discovery (winpodx app refresh)

코드. src/winpodx/core/discovery/__init__.py::discover_apps (host) + scripts/windows/discover_apps.ps1 (guest).

기본 timeout. 180초.

3 race 회피 계층.

9.1 Layer 1: 게스트 readiness gate

discover_apps.ps1 머리:

1초 간격 polling:
  AppXSvc.Status -eq 'Running'  AND
  ProgramData Start Menu .lnk 개수 > 0

3 연속 안정 sample 필요
60초 budget

Sysprep-방금-끝난 윈도우 (AppX 가 아직 inbox 앱 설치 중, Start Menu indexer 가 mid-propagation) 잡음.

9.2 Layer 2: 호스트 transport readiness

_wait_for_transport_ready(cfg, max_wait_sec=30) 이 agent /health 와 RDP port polling. 둘 중 하나 응답하면 즉시 return. migrate-방금-cycle-한-TermService 윈도우 (agent mid-respawn) 잡음.

9.3 Layer 3: retry-on-empty

첫 pass 후, _looks_suspiciously_empty(apps):

  • 총 개수 < 5 (stock Win11 항상 15+ 개)
  • OR UWP 개수 == 0 (Calculator / Settings / Terminal 항상 있음)

Suspicious 면: 8초 대기, 1회 재시도. 큰 결과 선택 → retry 가 절대 regression 안 됨.

9.4 Discovery 소스

스크립트가 5개 소스 union, lowercase exec path 또는 UWP AUMID 로 dedupe:

  1. Registry App Paths (HKLM + HKCU)
  2. Start Menu .lnk 재귀 (ProgramData + 모든 user profile)
  3. UWP / MSIX 패키지 via Get-AppxPackage + AppxManifest.xml
  4. Chocolatey + Scoop shim
  5. Essentials allowlist — File Explorer / Calculator / Settings 는 항상 emit (synthesized stub 으로) — .lnk 로 enum 안 되니까.

Junk 필터: uninstaller, redistributable, LicenseManagerShellExt, WindowsPackageManagerServer 등 숨김. 사용자 override (hidden = true in app.toml) 는 후속 refresh 에서도 보존.

9.5 출력

~/.local/share/winpodx/discovered/ 에 persist. 각 앱이 .toml + .desktop 항목 → ~/.local/share/applications/ 에 등록 → 사용자 런처 메뉴 즉시 반영.


10. Transport 선택 (agent vs FreeRDP)

코드. src/winpodx/core/transport/__init__.py::dispatch.

2개 transport.

Transport 메커니즘 창 깜빡임 기본 timeout Latency
Agent HTTP /exec on 127.0.0.1:8765, bearer 인증 없음 (CreateNoWindow=$true) 60초 100-300ms
FreeRDP RemoteApp PS invocation via xfreerdp 불가피한 PS 콘솔 30초 3-5초

선택 규칙. dispatch(cfg) 가 1-2초 timeout 으로 agent /health 호출. agent 응답하면 AgentTransport 반환. 아니면 FreerdpTransport.

사용처. core.updates, core.daemon.sync_windows_time, cli.pod.multi-session, cli.main.debloat, GUI Tools 페이지 debloat 핸들러 — 모두 windows_exec.run_via_transport 통과.

의도적 미사용. Password rotation 과 winpodx pod sync-password rescue 경로 — 둘 다 직접 credential 인증 필요, FreeRDP 강제.

10.1 어느 transport 가 활성인지 확인

PYTHONPATH=src python3 -c "
from winpodx.core.config import Config
from winpodx.core.transport import dispatch
print(type(dispatch(Config.load())).__name__)"

AgentTransport → /exec 경로. FreerdpTransport → 폴백.

10.2 Agent 프로세스 트리

HKCU\Run\WinpodxAgent 가 user logon 시 트리거:
  wscript.exe hidden-launcher.vbs  (GUI subsystem, 콘솔 없음)
    └─ powershell.exe -File C:\OEM\agent.ps1  (SW_HIDE 상속)
       └─ /exec 호출마다 child PS
          (ProcessStartInfo + CreateNoWindow=$true)

Agent listener: http://+:8765/ + netsh http add urlacl 사전 등록 (User-level, admin 불필요). Token: C:\OEM\agent_token.txt (호스트에서 bind-mount).


11. Guest sync (winpodx pod sync-guest)

코드. src/winpodx/core/guest_sync.py::sync_guest. 전체 설계 노트: docs/design/GUEST_SYNC_DESIGN.md.

목표. 호스트의 WinPodX 를 업그레이드하면 호스트 바이너리는 갱신되지만, 첫 설치 때 staging 된 게스트 측 아티팩트는 사용자가 Windows 를 밀고 재설치하기 전까지 stale 상태로 남음: C:\OEM\agent.ps1, urlacl 예약, rdprrap / shim.exe / rcedit.exe, 헬퍼 스크립트. apply 체인 (§6) 은 일부 idempotent 레지스트리 fix 만 재적용할 뿐 agent.ps1 / urlacl 예약 / 게스트 바이너리는 갱신 안 함. Guest sync 가 재설치 없이 이 간극을 메움.

핵심 enabler. /oem 은 호스트 config/oemlive bind mount (compose.py{oem_dir}:/oem:Z) — 즉 호스트 업그레이드 후 동작 중인 컨테이너의 /oem이미 새 파일이 들어 있음 (이미지 재빌드 없음). 게스트 전달은 winpodx pod recover-oem 과 동일 채널 (컨테이너에서 /oem tar → 127.0.0.1:8766 일회성 HTTP 서버 → 게스트가 QEMU NAT 게이트웨이 10.0.2.2 로 pull), 단 sync 는 agent 가 살아 있으므로 bearer-auth /exec 엔드포인트로 동작.

언제 실행되나. pod start 당 1회, pod 이 responsive 해진 뒤 (provisioner.ensure_ready() / pod wait-ready 꼬리), cfg.pod.guest_autosync (default True) 면서 게스트 stamp 가 stale 일 때. podman/docker 로 gate.

Staleness gate. 호스트 current = winpodx.__version__ + core.info._bundled_oem_version(). read_guest_version()C:\winpodx\install-state\guest_version.json ({winpodx, oem_bundle}) 을 /exec 로 읽음. guest_sync_needed(cfg) 는 stamp 가 존재하고 호스트 쌍보다 오래된 경우에만 True 반환. stamp 부재 는 기록만, sync 안 함 — 아직 진행 중인 첫 부팅 install (install.bat 이 아직 자기 state 미작성) 을 방해 안 하려는 것. stamp 가 존재하고 동일하면 cheap no-op.

Sync 흐름 (모든 단계 idempotent; 부분 실패 후 재실행 안전하도록 순서 잡힘):

  1. /oem 게스트 전달 — 게스트 Invoke-WebRequest http://10.0.2.2:8766/oem.tar.gztar -xzfC:\OEM. agent.ps1, rdprrap, shim.exe, rcedit.exe, 헬퍼 스크립트를 한 번에 refresh. install.bat 은 재실행 안 함 — autologon / 계정 설정 같은 one-shot 첫 부팅 로직을 담고 있어 live install 에서 재발사하면 안 됨.
  2. urlacl 예약 (#269) — install.bat 의 netsh 블록을 /exec 로 포팅: 겹치는 :8765 예약 삭제 후 WD SID SDDL 로 http://+:8765/ 재등록.
  3. Idempotent 레지스트리 / runtime fixapply_windows_runtime_fixes(cfg) 호출 (§6 과 동일 체인). 갱신된 바이너리에 대해 rdprrap 도 재활성화.
  4. agent 재시작 — agent 는 자신이 실행되는 /exec 를 제공하므로 동기적으로 스스로를 Stop-Process 할 수 없음. one-shot scheduled task 가 ~5초 뒤 발사해 현재 agent 프로세스를 멈추고 HKCU\Run 명령 (C:\OEM\agent.ps1) 을 재기동. /exec 호출이 task 발사 전에 반환; 새 agent 가 교정된 urlacl 로 :8765 재바인드.
  5. 버전 stamp 작성guest_version.json 은 13 단계 성공 후에만 작성 (step 4 는 fire-and-forget; readiness 는 호출자가 재확인). 부분 실패 시 stamp 가 stale 로 남아 다음 pod start 가 재시도.

sync_guest 는 CLI 와 GUI Tools → Sync Guest 액션이 행을 렌더링할 수 있도록 단계별 결과 맵 반환 (apply_windows_runtime_fixes 처럼).

수동. winpodx pod sync-guest [--force]. --force 는 stamp 가 호스트 쌍과 일치해도 재sync (up-to-date 게스트에서 clean no-op — 동일 아티팩트 재전달, 세션 중단 없음).


12. Disk auto-grow

코드. src/winpodx/core/disk.py (sizing + 게스트 extend), src/winpodx/core/daemon.py (idle 경로) 및 pod start 에서 호출.

목표. dockur 는 cfg.pod.disk_size 가 증가하고 컨테이너가 재생성될 때만 가상 디스크 이미지를 키울 뿐, 게스트의 C: 파티션은 절대 확장 안 하고 online resize 도 없음. auto-grow 가 양쪽을 처리해서 차오르는 게스트가 공간 고갈 안 되게 함.

트리거. pod start / idle 시, C: used% 가 cfg.pod.disk_autogrow_threshold_pct (default 80) 를 넘고 동시에 pod 이 idle 일 때.

왜 idle 전용. dockur 에 online resize 가 없으므로 매 grow 가 컨테이너를 재생성 (빠른 게스트 reboot). idle 전용 스케줄링이 live RemoteApp 세션을 절대 중단 안 함을 보장 — daemon 이 idle 경로에서만 발사.

Sizing (compute_grow_target). 플랫 step 이 아니라, used%cfg.pod.disk_autogrow_target_free_pct 여유 (default 30%) 로 돌아갈 만큼만 키우되 cfg.pod.disk_autogrow_increment 단위 (default 32G) 로 올림. ceiling 은 다음 중 작은 쪽:

  • 선택적 cfg.pod.disk_max_size (빈 값 = 고정 cap 없음), 그리고
  • 호스트가 실제로 back 가능한 양current + (host_free reserve) (reserve 가 auto-grow 의 호스트 디스크 고갈 방지).

어느 headroom 도 없으면 (cap 도달 / 호스트 여유 없음) 로그 한 줄과 함께 grow 스킵, pod 은 현재 크기로 계속.

게스트 extend. 이미지가 커지고 컨테이너가 재생성되면 새 공간은 디스크 에 붙지만 C: 는 원래 위치에서 끝남. extend 는 /exec 로 동작: Resize-Partition -DriveLetter C. dockur 의 Windows 레이아웃은 C: 바로 뒤에 작은 WinRE Recovery 파티션을 두어 extend 를 막으므로 — 단계가:

  1. reagentc /disable (WinRE detach),
  2. 막는 recovery 파티션 삭제 (C: 끝의 그것),
  3. Resize-Partition -DriveLetter C 로 새 공간 채움,
  4. reagentc /enable (WinRE 재활성화; 전용 파티션 없으면 C:\Windows 로 폴백).

13. 로그인 시 pod 자동 시작 (opt-in)

설정. cfg.pod.auto_start (bool, default off), 그리고 tray 의 autostart .desktop 항목.

목표. 매 로그인 후 사용자가 winpodx pod start 를 수동 실행하지 않아도 Windows pod 이 준비되게 함.

흐름. cfg.pod.auto_start 가 켜지면 tray 의 autostart .desktop 항목이 등록되어 로그인 시 tray 가 뜸; 시작 시 tray 가 pod 을 올림 — 멈춰 있으면 start, suspend 됐으면 resume (auto resume). 작업은 best-effort + 백그라운드: pod 도달 실패가 로그인을 막거나 blocking 에러를 띄우지 않고, 다음 on-demand 실행이 올리도록 pod 을 그대로 둠.

이게 opt-in 인 이유는 cold pod start 가 (이미 있는) 이미지를 pull 하고 Windows 를 부팅하는데, 가끔만 Windows 앱을 띄우는 사용자에겐 낭비된 작업이기 때문.


14. 복구 시나리오

14.1 Agent 가 죽고 안 살아남

증상. curl http://127.0.0.1:8765/health exit 56 / 무응답.

원인. TermService 가 cycle 됨 (multi-session 활성화, 수동 재시작 등). agent 의 RDP 세션이 같이 죽음. HKCU\Run 은 user logon 시점만 발사, service 재시작 시점 아님.

해결. Windows 앱 아무거나 띄움 — 새 RDP 세션이 HKCU\Run 트리거 → wscript wrapper 로 새 agent. 앱 안 띄우고 pod idle 이면 agent dead 유지.

예방. PR #85 (활성화 전 ServiceDll 교차확인) 이 가장 흔한 redundant 활성화 케이스 차단. install.bat OEM v15+ 가 marker 정확히 작성 → 후속 apply 가 fast path 로.

14.2 매 앱 launch 마다 PS 콘솔 깜빡

증상. 검정 콘솔 ~50ms 깜빡 (UWP 든 Win32 든 무관).

진단. agent /exec 로 HKCU\Run 값 probe. wscript.exe ... hidden-launcher.vbs 로 안 감싼 항목 찾기. 흔한 culprit:

  • WinpodxMedia — PR #84 에서 수정 (옛날엔 bare powershell.exe -WindowStyle Hidden).
  • WinpodxAgent — PR #58 에서 수정.

해결. winpodx pod apply-fixes 가 둘 다 재작성 (vbs_launchers 단계가 legacy 항목 존재 조건부라 재실행 안전).

14.3 "Select a session to reconnect to" 다이얼로그

증상. Multi-session 활성 안 됨. 매 새 앱 launch 가 이전 세션 대체.

진단. HKLM:\SYSTEM\CurrentControlSet\Services\TermService\Parameters\ServiceDll probe:

  • C:\Program Files\RDP Wrapper\termwrap.dll → rdprrap registry-patched. Multi-session 이 동작해야 함; 그래도 다이얼로그 뜨면 TermService 가 옛 termsrv.dll 로드한 채 cycle 안 함. winpodx pod multi-session on 으로 cycle (~10초 disconnect 후 OK).
  • C:\Windows\System32\termsrv.dll → patch 안 됨. winpodx pod multi-session on 으로 install + 활성화.

14.4 컨테이너 예상 외 재생성

증상. Pod 예기치 않게 재시작. dockur log 에 fresh Windows install / ISO 재다운로드.

원인 (PR #83 이전). image: :latest 가 podman-compose 에 의해 재해상. dockur 가 마지막 up 이후 새 :latest push. 새 digest → spec mismatch → recreate.

해결. Migrate (PR #83 의 _ensure_canonical_image_pin) 가 compose 를 pinned digest 로 재작성. 미래 :latest push 가 사용자 안 흔듬.

14.5 Discovery 가 빈/부분 결과 반환

증상. winpodx app refresh 끝나는데 메뉴에 몇 개만 / UWP 항목 없음.

원인 (PR #86 이전). 첫 부팅 race — AppXSvc 아직 deploying / Start Menu indexer 아직 propagating / agent mid-respawn.

해결. PR #86 의 계층화된 race 회피. 스크립트 stderr (apply-fixes 로그에 보임) 에서 [discover] stable (...) — proceeding (좋음) vs [discover] stability budget exceeded (pod 이 진짜 60초+ 걸렸음) 확인. budget 적중 의심되면 winpodx app refresh 재실행.

14.6 Agent token 불일치

증상. 모든 /exec 호출 401 실패.

원인. ~/.config/winpodx/agent_token.txt (호스트) 와 C:\OEM\agent_token.txt (게스트) 불일치. 보통 수동 편집 또는 부분 복원 후.

해결. winpodx setup --non-interactive 재실행 — _ensure_oem_token_staged() 가 fresh token 재생성 + staging. agent 가 받아들이도록 pod 재시작.

14.7 Pod 시작 안 됨

증상. winpodx pod start 가 컨테이너 up 보고하지만 wait-ready 가 phase 1/2 통과 못함.

진단. podman logs winpodx-windows --tail 50. 찾을 것:

  • proc.sh: line 137: -1: substring expression < 0 → dockur 의 :latest push 내부 버그. PR #83 이후엔 pin 이 보호하니 발생 불가.
  • mknod: /dev/net/tun: File exists → 무해 경고, root cause 아님.
  • BdsDxe boot loop 인데 Windows started successfully 없음 → 게스트 mid-Sysprep. 그냥 대기.

컨테이너 자체가 시작 안 함 (podman 즉시 exit) 이면, podman ps -a --filter name=winpodxpodman inspect winpodx-windows -f '{{.State.Error}}' 확인.


참고