项目文件夹

文件
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

241 行
13 KiB
Markdown

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# Architecture
**English** | [한국어](ARCHITECTURE.ko.md)
How WinPodX is put together: the data flow on app launch, the technology stack, and the source tree layout.
## How It Works
```
┌─────────────────────────────┐
Click "Word" │ Linux Desktop (KDE, │
in app menu ───> │ GNOME, Sway, ...) │
└──────────────┬──────────────┘
┌──────────────▼──────────────┐
│ WinPodX │
│ ┌─────────────────────┐ │
│ │ auto-provision: │ │
│ │ config → password │ │
│ │ → container → RDP │ │
│ │ → desktop entries │ │
│ └─────────────────────┘ │
└──────────────┬──────────────┘
│ FreeRDP RemoteApp
┌──────────────▼──────────────┐
│ Windows Container (Podman)│
│ ┌──────────────────────┐ │
│ │ Word Excel PPT ...│ │
│ │ multi-session/rdprrap│ │
│ └──────────────────────┘ │
│ 127.0.0.1:3390 (TLS) │
└─────────────────────────────┘
```
The pod's command channel is a bearer-authed HTTP agent listening on `127.0.0.1:8765` inside the guest (loopback only). RDP itself runs on `127.0.0.1:3390` with TLS encryption. Reverse-open (Linux apps appearing in the Windows "Open with..." menu) runs through a separate host-side listener daemon that receives requests pushed via the `\\tsclient\home` share.
## Tech Stack
| Layer | Technology |
|-------|------------|
| Language | Python 3.9+ (stdlib only on 3.11+; `tomli` fallback on 3.9/3.10) |
| CLI | argparse (stdlib) |
| GUI (optional) | PySide6 (Qt6) |
| Config | TOML (stdlib `tomllib` on 3.11+ / `tomli` on 3.9/3.10; built-in writer) |
| RDP | FreeRDP 3+ (xfreerdp, RemoteApp/RAIL) |
| Guest agent | PowerShell `HttpListener` on `127.0.0.1:8765` (bearer auth, base64-encoded `/exec` payloads) |
| Container | Podman / Docker ([dockur/windows](https://github.com/dockur/windows)) |
| Hypervisor | QEMU / KVM (inside the dockur container; host USB / PCI device passthrough is wired at this layer) |
| Reverse-open shim | Rust (`windows_subsystem = "windows"`, embedded per-slug icon via vendored rcedit) |
| i18n | `winpodx.core.i18n` (English-source-as-key, flat JSON catalogs per language) |
| CI | GitHub Actions (lint + test on 3.9-3.13 + pip-audit) |
## Project Structure
```
winpodx/
├── install.sh # One-line installer (no pip)
├── uninstall.sh # Clean uninstaller
├── src/winpodx/
│ ├── cli/ # argparse commands (app, pod, config, setup, host-open, ...)
│ ├── core/ # Config, RDP, pod lifecycle, provisioner, daemon
│ ├── backend/ # Podman, Docker, manual
│ ├── desktop/ # .desktop entries, icons, MIME, tray, notifications
│ ├── display/ # X11/Wayland detection, DPI scaling
│ ├── gui/ # Qt6 main window, app dialog, theme, reverse-open Settings card
│ ├── reverse_open/ # Discovery, ICO conversion, listener daemon, sync transport
│ └── utils/ # XDG paths, deps, TOML writer, winapps compat
├── data/ # winpodx GUI desktop entry + icon + config example
├── config/oem/
│ ├── install.bat # Windows OEM first-boot orchestration
│ └── reverse-open/ # register-apps.ps1, unregister-apps.ps1, Rust shim, rcedit
├── scripts/windows/ # PowerShell scripts (debloat, time sync, USB mapping, app discovery)
├── packaging/ # OBS / AUR / RHEL spec + maintainer docs
├── debian/ # Debian source package layout
├── docs/ # User docs (English + Korean mirrors)
├── .github/workflows/ # CI: lint + test + publish (OBS / RHEL / deb / AUR)
└── tests/ # pytest test suite
```
## Key Data Flows
- **App launch.** CLI → `provisioner.ensure_ready()` (config + password rotation + compose + resume + pod + bundled apps + desktop entries) → FreeRDP session → `.cproc` tracking + reaper thread + desktop notification.
- **App install (Linux side).** AppInfo (TOML) → `.desktop` file generation → icon install → MIME registration → icon cache refresh.
- **File open (host → guest).** Linux path → UNC path conversion (`\\tsclient\home\...`) → RDP `/app-cmd`.
- **Auto suspend.** `daemon.run_idle_monitor()` → no sessions for N seconds → `podman pause` → lock file cleanup.
- **Auto resume.** `provisioner``daemon.ensure_pod_awake()``podman unpause` → wait for RDP.
- **Password rotation.** `ensure_ready()` → check `password_max_age` → generate new password → save config + compose → recreate container → rollback on failure.
- **Reverse-open (guest → host).** Windows Explorer "Open with..." → per-slug `winpodx-<slug>.exe` shim → atomic JSON write to `\\tsclient\home\.local\share\winpodx\reverse-open\incoming\<uuid>.json` → host listener picks it up → `safe_open_unc` TOCTOU-safe path resolution → `xdg-open` invocation on the host.
- **Device passthrough (host → guest).** `winpodx device list / attach <id> / detach <id>` (also a GUI "Devices" page and a tray USB switcher) → device wired through to the guest at the QEMU (dockur) layer. USB hot-plugs live (`cfg.pod.usb_live`, default on); PCI is boot-added and needs a guest restart plus a safety confirmation (`--force` / dialog).
## Guest sync subsystem
**Code.** `src/winpodx/core/guest_sync.py`. Design notes: [docs/design/GUEST_SYNC_DESIGN.md](design/GUEST_SYNC_DESIGN.md).
Upgrading WinPodX on the host updates the host binary, but the guest-side
artifacts staged at first install (`C:\OEM\agent.ps1`, the urlacl reservation,
rdprrap / `shim.exe` / `rcedit.exe`, helper scripts) would otherwise go stale
until the user wipes and reinstalls Windows. Guest sync closes that gap
without a reinstall.
**Key enabler.** `/oem` is a **live bind mount** of the host's `config/oem`
(`{oem_dir}:/oem:Z` in `compose.py`), so after a host upgrade the running
container's `/oem` *already* holds the new files — no image rebuild. Delivery
into the guest reuses the same channel as `winpodx guest recover-oem`: tar `/oem`
in the container → serve it over a one-shot HTTP server on `127.0.0.1:8766`
→ guest pulls via the QEMU NAT gateway `10.0.2.2`. Because the agent is alive
during sync, the pull and follow-up fixes run over the bearer-authed `/exec`
endpoint rather than the noVNC paste path.
`sync_guest` is ordered so a partial failure is safe to re-run:
1. **Deliver `/oem`** — guest `Invoke-WebRequest` + `tar -xzf` into `C:\OEM`.
`install.bat` is **not** re-run (it carries one-shot first-boot logic —
autologon, account setup — that must not fire on a live install).
2. **urlacl reservation** — re-applies install.bat's netsh block over `/exec`
(delete overlapping `:8765` reservations, re-add `http://+:8765/` with the
`WD` SID SDDL).
3. **Idempotent registry / runtime fixes** — calls
`apply_windows_runtime_fixes(cfg)` (same chain as apply-fixes), which also
re-activates rdprrap against the refreshed binaries.
4. **Restart the agent** — the agent serves the `/exec` it runs through, so it
can't `Stop-Process` itself synchronously. A **one-shot scheduled task**
fires ~5 s later to stop and relaunch `C:\OEM\agent.ps1`; the `/exec` call
returns first, then the new agent rebinds `:8765` under the corrected urlacl.
5. **Stamp version** — writes `C:\winpodx\install-state\guest_version.json`
(`{winpodx, oem_bundle}`) only after steps 1–3 succeed.
**Staleness check.** Host current = `winpodx.__version__` +
`core.info._bundled_oem_version()`. `guest_sync_needed(cfg)` reads the stamp via
`/exec`; a stamp that is present **and** older triggers a sync, a missing stamp
is recorded only (no disruption during a first-boot install still in progress).
Auto-runs after pod readiness when `cfg.pod.guest_autosync` (default `True`) is
set, gated to podman/docker. Manual: `winpodx guest sync [--force]` and a
GUI Tools → Sync Guest action. `sync_guest` returns a per-step result map so
the CLI/GUI can render rows.
## Disk auto-grow subsystem
**Code.** `src/winpodx/core/disk.py` (sizing + guest extend), triggered from
`src/winpodx/core/daemon.py` (idle path).
dockur only grows the virtual disk *image* when `cfg.pod.disk_size` increases
and the container is recreated — it never extends the guest's C: partition, and
it has **no online resize**. WinPodX adds an idle-time auto-grow that handles
both ends.
**Trigger.** On pod start / idle, if C: used% exceeds
`cfg.pod.disk_autogrow_threshold_pct` (default 80) **and** the pod is idle.
**Sizing.** Grows the image just enough to restore
`cfg.pod.disk_autogrow_target_free_pct` free (default 30%), rounded up to whole
`cfg.pod.disk_autogrow_increment` steps (default `32G`). The ceiling is the
smaller of the optional `cfg.pod.disk_max_size` and *what the host can actually
back* — `current + (host_free reserve)`, where the reserve keeps auto-grow
from consuming the last of the host disk. If neither headroom is available the
grow is skipped with a log line.
**Why idle-only.** Since dockur has no online resize, every grow **recreates
the container** (a quick guest reboot). Scheduling it idle-only guarantees it
never interrupts a live RemoteApp session.
**Guest extend.** After the image grows, the new space lands at the end of the
disk but C: still ends where it did. The extend runs over `/exec`:
`Resize-Partition -DriveLetter C`. dockur's Windows layout puts a small WinRE
Recovery partition **right after** C:, blocking the extend — so the step
detaches WinRE (`reagentc /disable`), deletes the blocking recovery partition,
extends C:, then re-enables WinRE (`reagentc /enable`, which falls back to
`C:\Windows` when no dedicated partition is present).
## UI internationalization (i18n)
**Code.** `src/winpodx/core/i18n.py`; catalogs in `src/winpodx/locale/<lang>.json`.
The Linux-side UI text (tray, GUI, CLI) is wrapped in
`winpodx.core.i18n.tr(text)`. The **English string is the catalog key**
`tr()` looks the source string up in the active-language catalog and falls back
to that same English source per-string on a miss, so an incomplete catalog
never blanks the UI. Catalogs are flat `{ "<english>": "<translation>" }` JSON.
The active language is resolved from `[ui] language` (default `auto`, which maps
the host locale from `$LC_ALL` / `$LC_MESSAGES` / `$LANG`, unknown → English).
Seven languages ship: en, ko, zh, ja, de, fr, it. (Distinct from
`pod.language`, which is the *Windows guest* install language.)
## Advanced: Custom Windows ISO
WinPodX ships first-class support for the dockur-curated Windows
editions (Win10 / 11, LTSC, IoT LTSC, Tiny, Server 2016+). The list
lives in `_KNOWN_WIN_VERSIONS` in `src/winpodx/core/config.py` and
the GUI Settings → Container/VM card exposes it as a dropdown.
If you need to boot a Windows ISO that dockur does **not** curate
(your own pre-loaded installer image, an Enterprise edition with
specific debloat preset, a localised build dockur hasn't tagged),
you can pass it through manually. **This path is unsupported**
WinPodX's OEM scripts (`install.bat`, `agent.ps1`, `rdprrap`) are
written against the dockur-curated Win10+ family. A custom ISO may
boot but fail to surface the agent, the multi-session enabler, or
RemoteApp discovery. Bug reports specific to custom-ISO installs
fall on you to debug.
With that disclaimer:
1. Place your `.iso` somewhere readable (e.g. `~/winpodx-custom.iso`).
2. Edit your `winpodx.toml` to set `win_version = "custom"`:
```toml
[pod]
win_version = "custom"
```
WinPodX will log a one-line WARNING that the value isn't on its
known list, then pass it through to dockur as-is.
3. Edit the generated `~/.config/winpodx/compose.yaml` to mount the
ISO at the path dockur looks for:
```yaml
services:
windows:
volumes:
- ~/winpodx-custom.iso:/storage/custom.iso
# ...existing volumes stay
```
4. Recreate the container:
```bash
winpodx pod stop
podman compose -f ~/.config/winpodx/compose.yaml up -d
```
The compose template is regenerated by `winpodx setup` and
`winpodx pod start` on certain code paths (cpu / ram / port / user
changes via the GUI Save button, for example) — your manual edit
will be overwritten there. Re-apply after any such regeneration.
If you find yourself doing this routinely and the upstream dockur
project doesn't carry your edition, file a feature request: a
narrow `cfg.pod.custom_iso_path` field is on the table but is not
shipped today.