Skip to content

v0.5.8

Choose a tag to compare

@github-actions github-actions released this 24 May 12:39
c68a837

[0.5.8] - 2026-05-24

Reliability + reach release: fixes the two big fresh-install failures users reported (#269 agent can't bind 8765, #287 install.bat never runs), ships a distro-agnostic fat AppImage, and rounds out the setup wizard so a standalone winpodx setup provisions end-to-end.

Highlights

Fresh-install reliability + a self-contained AppImage. Two fresh-install dead-ends fixed, slow connections + slow hosts no longer time out, and a single-file AppImage now bundles FreeRDP + Podman for immutable distros.

  • #269 fixed -- the agent's http://+:8765/ urlacl reservation is now created for the World SID so the non-admin guest User can bind it; agent.ps1 retries + logs the urlacl state on failure (thanks @ismikes).
  • Fat AppImage (#227) -- winpodx-fat-x86_64.AppImage bundles Python + Qt + FreeRDP + Podman + podman-compose; winpodx setup-host handles the host-side kvm-group / subuid bits via a single pkexec prompt (requested by @leandromqrs).
  • Dynamic + generous timeouts (#126) -- pod wait-ready auto-extends from dockur's download ETA, compose-up is activity-based, and every fixed timeout was widened so slow links / hosts don't fail mid-op (reported by @xiyeming).
  • winpodx pod recover-oem (#287) -- re-stages C:\OEM + runs install.bat when dockur's first-boot OEM copy fails (driver @ankranidiotis).
  • Setup wizard completion (#255) -- --customize walks edition / language / region / keyboard / tuning, and a standalone winpodx setup now runs the full provision flow (wait-ready + discovery + reverse-open) instead of stopping at "container created".
  • podman-compose required up-front (#288) -- no more silent keep-groups failure when podman compose delegates to docker-compose (reported by @magicdiablo).

Added

  • winpodx pod recover-oem -- manual OEM recovery for failed dockur first-boot copy (#287). When dockur's $OEM$ -> C:\OEM copy silently fails (so install.bat never runs, the agent never installs, and port 8765 RSTs), this command tars /oem out of the container, serves it over a short-lived HTTP server on container port 8766, and prints the exact noVNC PowerShell to download + extract + run install.bat from the full OEM tree. podman/docker only. Driver: @ankranidiotis (Linux Mint 22, Podman 4.9.3).
  • pod wait-ready dynamic deadline + winpodx setup-host (#126, #227). pod wait-ready parses dockur's wget ETA and extends its deadline (no upper bound -- a genuinely stalled download stops producing ETA lines and the wait expires naturally; +60min slack absorbs blips) so slow ISO downloads don't trip the 60min default. winpodx setup-host (also python -m winpodx.setup_wizard) detects + pkexec-fixes the host bits an AppImage can't do from user space: kvm group membership, /etc/subuid + /etc/subgid for rootless podman, persistent kvm module load. Reported by @xiyeming.
  • Fat AppImage with bundled FreeRDP + Podman + pkexec wizard (#227). Initial lean AppImage in #302 (Python + winpodx + Qt only) layered up to a fat bundle in this release: packaging/appimage/ recipe downloads a python-build-standalone portable Python 3.11, installs the winpodx wheel + gui + reverse-open extras into it, then overlays a Fedora 41 binary bundle (xfreerdp / wlfreerdp / sdl-freerdp, podman, podman-compose, conmon, crun, netavark, slirp4netns, passt, pasta) with all transitive .so deps that aren't on the host-critical exclude list (glibc / libX11 / libGL / libwayland / libxkbcommon stay on host -- bundling them would break desktop integration or hard-crash on glibc mismatch). Single winpodx-fat-x86_64.AppImage (~290 MB squashfs / ~920 MB AppDir) self-contained for everything user-space can carry. New winpodx setup-host subcommand (plus python -m winpodx.setup_wizard) wraps the host-side bits that AppImages legitimately can't do from user space -- kvm group membership, /etc/subuid + /etc/subgid for rootless podman, persistent kvm module load -- in a single pkexec polkit prompt. Target audience: immutable distros (Fedora Silverblue / Kinoite / Aeon, Steam Deck) where layering system packages is heavy or unavailable, plus locked-down environments where curl install.sh | bash isn't an option. CI bypasses python-appimage entirely (its bundled appimagetool runs without honouring our PATH wrapper and silently fails on FUSE-less GitHub runners); the new pipeline builds the AppDir layout from scratch, layers the binary overlay via a Fedora 41 docker container, and packs with a preinstalled extract-and-wrap appimagetool at /usr/local/bin. The bundled third-party license texts (FreeRDP / Podman / podman-compose / ...) are vendored in packaging/appimage/licenses/ so they travel inside the AppImage independent of CI's dnf nodocs policy, and the build fail-closes if a required dir is absent. README + INSTALL.md (en + ko) updated. Driver: @leandromqrs.
  • Setup wizard completion -- --customize prompts + full standalone provision (#255). The wizard now prompts for Windows edition, UI language, regional format, keyboard layout, and tuning profile (previously CLI-skipped, GUI-only). A standalone winpodx setup runs the same post-create flow install.sh drives -- wait-ready, apply-fixes, app discovery, reverse-open -- so it finishes like a complete install. New --create-only flag keeps install.sh's create-then-orchestrate path intact.
  • CLI + GUI first-run setup prompt (#255 PR 1). When cfg.pod.initialized is False (or no config exists), the first winpodx <cmd> invocation surfaces a three-way prompt: [Y]es (auto -- host-detected defaults, no prompts), [C]ustom (wizard -- pick every knob), [n]o (skip). Skip-list bypasses introspection / config / uninstall / gui / tray commands and non-TTY stdin. GUI shows the same modal on first launch. After a successful setup, initialized flips to True and the prompt stops firing.
  • winpodx setup --customize flag (#255 PR 1). Opts into the wizard mode (existing interactive prompts; full multi-step wizard with debloat / tuning / anti-detection knobs lands in PR 7). Default winpodx setup is now non-interactive (host-detected defaults).
  • install.sh --manual flag (#255 PR 2). Skips winpodx setup + pod wait-ready + app discovery + reverse-open setup. Binary + desktop entry + icon land normally; provisioning is deferred to the first-run prompt that fires on the next winpodx invocation (CLI Y/C/n or GUI modal). Useful when you want to pick custom knobs (edition / language / debloat / tuning / anti-detection) without sitting through the auto path first. Equivalent: WINPODX_MANUAL=1 env var. docs/INSTALL.md + Korean mirror gain a "Manual install" section.
  • winpodx doctor subcommand (#255 PR 6). Read-only diagnostic. Walks ~9 checks (install source detected, freerdp present, /dev/kvm present, backend on PATH, config + binary state, container health, pending setup marker, autostart entry integrity, cfg.pod.initialized flag) and prints per-check [OK] / [WARN] / [FAIL] findings with the suggested next command. Read-only -- never mutates state; users copy the suggestions. Exit 1 when any FAIL finding; useful in CI / install scripts as a quick health gate. Designed to be cheap (< 2s on a healthy install): every subprocess probe has a short timeout, no network calls.
  • winpodx setup registers the GUI launcher .desktop (#255 G7). Standalone winpodx setup invocations (pip install, dev checkout, or any path where install.sh / packaging didn't drop the GUI entry) now copy data/winpodx.desktop to ~/.local/share/applications/winpodx.desktop, mirroring what install.sh does for curl installs. Skips when /usr/share/applications/winpodx.desktop already exists -- package installs own that path and a user-level copy would shadow the package version on upgrade. install_gui_launcher_desktop() lives next to install_winpodx_icon() in winpodx.desktop.icons.
  • winpodx uninstall consolidated to a single bash script (#255 follow-up). The pre-this-PR #255 PR 3 landed a ~470-line Python implementation in winpodx.cli.uninstall alongside the existing ~300-line uninstall.sh. Two parallel implementations drifted (10+ cleanup steps diverged: MIME cache, gtk-update-icon-cache, guest-registry scrub, final pkill sweep, volume glob, storage bind-mount wipe, GUI launcher .desktop removal, ...), repeatedly producing bin/winpodx-symlink-survives-purge bugs (#278 / #279). Consolidation: uninstall.sh is now the canonical implementation. winpodx uninstall is a ~50-line Python wrapper that locates uninstall.sh (in /usr/share/winpodx/, ~/.local/bin/winpodx-app/, ~/.local/share/winpodx/, sys.prefix/share/winpodx/, or the dev checkout) and os.execvp's it -- so uninstall keeps working even when the Python install is half-broken. The same uninstall.sh now ships in every channel (debian/winpodx.install, rpm %files, AUR PKGBUILD package(), pyproject shared-data for pip wheels, curl install bundle dir) at one well-known path: /usr/share/winpodx/uninstall.sh. Install-source detection (dpkg / rpm / pacman) moved into bash; if a package manager owns the binary, the script prompts to run sudo apt remove / sudo dnf remove / sudo pacman -Rns first, then re-enters the same script via the package's postrm hook with --from-postrm to do user-side cleanup -- the only ordering that keeps dpkg/rpm db consistent with disk state. postrm-common.sh simplified: delegates to /usr/share/winpodx/uninstall.sh --from-postrm --yes [--purge] instead of calling the Python CLI. Flags normalised: --yes (was --confirm -- kept as silent alias), --purge, --from-postrm (internal, set by postrm hooks). The --no-package-prompt flag introduced by PR 3 is removed (its behaviour folds into --from-postrm). Tests: deleted tests/test_uninstall_consolidation.py (Python helpers no longer exist); added tests/test_uninstall_wrapper.py (10 tests: candidate-path coverage, execvp argv forwarding, SystemExit on script-not-found) and tests/uninstall_smoke.sh (sandbox $HOME smoke that asserts every expected path is gone post-run, shellcheck-clean).
  • uninstall.sh detects pip / source installs (#255 G6). Previously fell through to unknown -- pip users saw no hint. Now identifies site-packages or dev-checkout paths and prints pip uninstall winpodx after user-side cleanup as the canonical removal command for that install kind. Detection is heuristic only (the script doesn't know the venv prefix to drive pip uninstall directly), so the hint is informational, not auto-exec'd.
  • Packaging post-install / post-remove hooks across debian / rpm / aur (#255 PR 4). Every package install path now ends with a banner pointing at winpodx setup (or just opening winpodx -- first-run prompt covers it). Every package remove path runs a tiny cleanup that pkill's tray / GUI / helper processes + stops the reverse-open listener via winpodx host-open stop-listener, so users aren't left with orphan processes after apt remove / dnf remove / yay -Rns. debian's apt purge additionally runs winpodx uninstall --purge --yes --no-package-prompt per user with a winpodx config dir, matching apt purge semantics. rpm and aur lack a purge concept; their post-remove hooks tell users how to do the full wipe (winpodx uninstall --purge --yes). Shared logic lives in packaging/scripts/postrm-common.sh, installed to /usr/share/winpodx/packaging/postrm-common.sh by every distro. New files: debian/postinst, debian/postrm, debian/winpodx.install, packaging/aur/winpodx.install. Modified: packaging/rpm/winpodx.spec (%post + %postun added), packaging/aur/PKGBUILD (install=winpodx.install + scripts install), .github/workflows/aur-publish.yml (push winpodx.install alongside PKGBUILD).
  • winpodx --version install-source suffix. Output now reads winpodx 0.5.8 (installed via apt) / (curl install) / (pip install) / (install source not detected) so users see provenance at a glance.
  • winpodx info [System] install-source line. Same detection surfaced in the info command's System block alongside winpodx / OEM bundle / rdprrap / distro / kernel lines.
  • winpodx.utils.install_source -- new helper. Detects install provenance via dpkg -S / rpm -qf / pacman -Qo (3 s timeout each), with ~/.local/bin/winpodx-app curl-install heuristic + site-packages source-checkout fallback. Returns InstallSource(kind, label, package_name, removal_command) for downstream uninstall / version / info / doctor consumers.
  • Extended Windows-on-KVM tuning profile (#245). auto and safe now apply 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) plus virtio-rng-pci backed by /dev/urandom. Significant scheduling / VM-exit win on multi-vCPU guests; entropy pool fills quickly so first-boot CryptoAPI / TLS handshakes don't stall. auto additionally exposes nested-virt CPU features (+vmx Intel / +svm AMD) and hv-evmcs (Intel) when /sys/module/kvm_intel/parameters/nested (or kvm_amd) reads Y -- enabling Hyper-V / WSL2 / Docker Desktop inside the Windows guest. No-op when the host kernel hasn't opted into nested KVM. safe excludes nested-virt + hv-evmcs (those need explicit host-side opt-in). New CLI override: winpodx pod start --tuning {auto,safe,off,manual} for one-shot per-run profile selection (does not persist to winpodx.toml). GUI Settings gains a Tuning Profile dropdown plus a live format_tuning_summary() panel showing what the resolved profile applies on this host. New TuningCapability.nested_kvm probe + TuningProfile.apply_hv_enlightenments / apply_virtio_rng / apply_evmcs / apply_nested_virt flags in winpodx.utils.specs.
  • performance tuning profile + Settings card relocation (#245 follow-up). New profile that behaves like auto but bypasses the dedicated_host gate -- CPU pinning + no-balloon flip on regardless of current host idle CPU / free RAM. Use when the box is mostly dedicated to winpodx and you want minimum guest latency at the cost of other host workloads. Hard-gated knobs (+invtsc, io_uring) still respect capability detection so the profile can't force a CPU flag QEMU would reject. CLI: winpodx pod start --tuning performance. GUI: Settings now has a dedicated "Performance Tuning" card -- dropdown at top, format_tuning_summary() detection panel below in the same card frame.
  • Settings UI polish + design tokens. Split the old "Container / VM" card (10 form rows, taller than the 7-row RDP card next to it) into a "Hardware" card (Backend / Edition / CPU / RAM / Idle / Max Sessions = 6 rows) plus a full-width "Localization" card (Language / Region / Keyboard / Timezone = 4 rows) so the top row reads as a height-balanced two-column layout. Added a design-token layer to winpodx.gui.theme -- SPACE_XS/S/M/L/XL/XXL, RADIUS_XS/S/M/L/XL/XXL, and FONT_CAPTION/BODY/SUBHEAD/HEADER/TITLE/HERO -- so the patchwork of magic numbers (24, 22, 14, 12, 11, ...) across cards, headers, summary panels, and form labels can converge on a single named scale. Settings page + tuning card converted to the new tokens; remaining pages will migrate in a follow-up.

Changed

  • winpodx setup default flipped to non-interactive auto (#255 PR 1). Previously interactive with host-detected defaults + a handful of prompts. Now defaults to fully auto (= existing --non-interactive behaviour). Use winpodx setup --customize for the wizard. The --non-interactive flag stays as a deprecated alias for install.sh and other scripted callers.
  • Timeouts widened across the board + activity-based compose-up (#298, #126). Slow hosts / slow links no longer fail ops that are still making progress: compose up is now activity-based (kills only after 5min of silent output, not a 120s wall clock) with a 4h hard cap; wait_for_windows_responsive 90 -> 300s default / 180 -> 600s at call sites; agent /health + FreeRDP RDP-port probes 2 -> 5s; transport.exec / run_in_windows guest calls bumped 2-3x (sync-password 30 -> 90s, multi-session 20/45 -> 60/120s, etc.); compose down 60 -> 180s; daemon suspend/resume/inspect 30/10 -> 90/30s. Tight TCP poll-loop probes (check_rdp_port inside pod_status) intentionally left short -- the budget there is the poll interval.
  • CPU sub-flags moved from ARGUMENTS to dedicated CPU_FLAGS + VMX env vars; hv- delegated to dockur.* Three problems collapse into one refactor: (1) PR #289's -msg timestamp=on workaround for dockur proc.sh:137 bash slice on empty post-strip ARGUMENTS, (2) PR #281's hv-* enlightenments duplicating dockur's own hv_passthrough (default HV=Y) causing QEMU 10's Ambiguous CPU model string. Don't mix both "-hv-evmcs" and "hv-evmcs=on" warning, (3) PR #281's explicit +vmx / +svm nested-virt injection duplicating dockur's VMX env handling. winpodx now uses dockur's documented env-var interface: CPU_FLAGS: "arch_capabilities=off,+invtsc" on x86 (the two sub-flags dockur doesn't add itself), VMX: "Y" when nested-virt is wanted (dockur picks the right +vmx / +svm per host CPU). ARGUMENTS carries only the virtio-rng device pair now. proc.sh:137 strip code path is never triggered (no -cpu host, in ARGUMENTS) so the -msg timestamp=on marker is removed. The ambiguous-string warning goes away (we don't add hv-evmcs -- dockur owns the entire hv-* set). And winpodx is decoupled from QEMU sub-flag deprecation: when QEMU 11/12 changes hv-* syntax, dockur handles it, we don't.

Fixed

  • Agent 8765 bind failure -- urlacl reserved for the wrong owner (#269). install.bat ran to completion (RDP listener up, HKCU\Run registered, agent spawned) but the agent FATAL'd on every boot: HttpListener.Start() failed: ... 'http://+:8765/' ... conflicts with an existing registration on the machine. The agent runs as the non-admin autologon User, so binding the strong-wildcard prefix needs a urlacl reservation it's permitted to use. install.bat's old netsh http add urlacl ... user=Everyone listen=yes >nul 2>&1 either created a wrong-owner reservation or hit ERROR_ALREADY_EXISTS (183) -- both masked by the >nul redirect. Now: delete all overlapping 8765 reservations first, then netsh http add urlacl url=http://+:8765/ sddl=D:(A;;GX;;;WD) (World SID directly, locale-proof), with every result logged to setup.log + a netsh http show urlacl dump after. agent.ps1 retries the bind 5x (absorbs the reservation-landing race) and dumps the live urlacl state to agent.log on persistent failure. OEM bundle 24 -> 25. Diagnosed end-to-end with @ismikes (Kubuntu 26.04) -- three earlier test branches ruled out the install.bat changes before his agent.log screenshots pinpointed the urlacl conflict.
  • dockur proc.sh:137 ARGUMENTS-strip crash, agent never installs (#287, #269). dockur (via the qemus/qemu base image) parses the compose ARGUMENTS: env, strips out -cpu host,<sub-flags> for its own CPU_FLAGS pipeline, then runs ARGUMENTS="${args::-1}" on the leftover. When the user's tuning profile produces nothing else (tuning_profile = off for example), the leftover is an empty string and the bash slice fails with proc.sh: line 137: -1: substring expression < 0. proc.sh exits on that line; /oem is never copied into the Windows guest; install.bat never runs; the winpodx agent service is never installed; port 8765 accepts TCP but RSTs every handshake; winpodx pod wait-ready times out at 60 min. Reported by @ankranidiotis on v0.5.7 (Linux Mint 22) and @ismikes (Kubuntu 26.04) -- both fresh installs with default tuning. Workaround in _qemu_arguments_for_host(): when no extra QEMU args would be emitted, append -msg timestamp=on (timestamps QEMU log lines, otherwise a no-op) so proc.sh's strip leaves at least one space-separated token in ARGUMENTS and the bash slice sees a non-empty string. The auto / safe / performance tuning profiles already emit hv-* / virtio-rng extras (#245) and aren't affected; the workaround triggers for off and any future profile that produces a pure -cpu host,... line.
  • podman-compose required up-front instead of trusting podman compose (#288). On Fedora-family systems with the docker-compose CLI plugin installed, podman compose delegates to docker-compose, which doesn't understand the group_add: [keep-groups] magic value winpodx uses for rootless /dev/kvm passthrough -- the container fails to start with Unable to find group keep-groups. install.sh's old presence check accepted that delegation (podman compose version succeeds via it). Now install.sh requires the standalone podman-compose binary, and backend.podman._compose_cmd raises a clear per-distro install hint instead of silently delegating. Reported by @magicdiablo (Nobara).
  • -no-hpet removed -- QEMU 10 (dockur v5.15+) rejects it as invalid option. PR #281's hv-enlightenments work appended -no-hpet to the QEMU ARGUMENTS when the auto / safe / performance profiles selected the Hyper-V machine timer. dockur v5.15 ships QEMU 10.x via the qemus/qemu v7.30 base image; QEMU 10 dropped the -no-hpet flag entirely (replacement is -machine ...,hpet=off, which would override dockur's machine type). Container start now bombs with qemu-system-x86_64: -no-hpet: invalid option. Dropping the flag has no functional impact -- the hv-stimer + hv-stimer-direct enlightenments already steer Windows off HPET as the clock source. Reported by @ismikes on Kubuntu 26.04 against main HEAD after PR #289 merged.
  • Structured [agent-install] markers in install.bat (#269 diagnostics). Each agent-install step (hkcu-run-register, spawn, post-spawn-probe, urlacl) is fenced with [agent-install] step=<name> status=<enter|exit> lines, the HKCU\Run write + Start-Process are wrapped in try/catch that log the exception, and a 5s post-spawn probe checks the 8765 listener + tails agent.log -- so a failed agent install localises itself in setup.log without another round-trip.
  • RPM spec License: tag widened to MIT AND Apache-2.0 (#301). packaging/rpm/winpodx.spec declared License: MIT but config/oem/rdprrap-*-windows-x64.zip bundles stascorp/rdpwrap (Apache-2.0). Fedora packaging guidelines require the SPDX expression to enumerate every license that applies to the redistributed binary content. debian/copyright and THIRD_PARTY_LICENSES.md already broke this out correctly; the RPM spec now matches.