v0.6.0
[0.6.0] - 2026-06-05
Removed
- The libvirt backend was dropped (breaking).
backend = "libvirt"is no longer valid — the default dockur backend is QEMU/KVM in a container and now covers device passthrough too (#286), so the thin "bring-your-own libvirt domain" wrapper had no remaining justification.--backendacceptspodman | docker | manual; an existing config withbackend = "libvirt"falls back topodmanon load (with a warning). Thelibvirtpip extra (libvirt-python) and the install.sh/AUR/RPM/DEB libvirt references are gone;winpodx[all]is now libvirt-free (nolibvirt-devbuild dependency). Run Windows in your own libvirt domain? Keep using winpodx ≤ 0.5.x, or point themanualbackend at its RDP endpoint.
Added
- A redesigned, Start-menu-style GUI — resource Dashboard home + left navigation sidebar (#460–#471). The window is now a Start-menu shell: a left vertical navigation sidebar (one row per page, with the active page highlighted) and a new Dashboard home showing live Pod / RAM / CPU ring gauges + disk usage, an auto-recovery status card, pinned/recent workspace tiles, and a reverse-open toggle; the app launcher moved to an "All apps" page. It also brings a unified design system, an in-house SVG icon set (replacing the old unicode-glyph "icons"), responsive layouts that reflow (stack columns, drop grid tiles) on narrow / fractionally-scaled windows, fit-to-screen window sizing, and a hero search that doubles as a command bar. The display name is now WinPodX across the UI, site, and docs (the lowercase
winpodxcommand / package identifier is unchanged). - A project website — winpodx.org (#436–#451). A landing page plus Features / Get-started / FAQ pages, deployed via GitHub Pages and localized to the same languages as the app.
- Hide / show individual apps in the Linux menu (#319, #415). A Hide action in the GUI app library (grid + list) removes a Windows app from your Linux application menu; hidden apps persist in their profile and a Hidden toggle brings them back. CLI:
winpodx app hide <name>/winpodx app show <name>. winpodx pod recreate --keep-iso— reinstall Windows without re-downloading the ISO (#416). Wipes the Windows disk + install markers but keeps the cached install ISO, so dockur rebuilds from it instead of re-fetching ~5–8 GB from Microsoft (falls back to a normal download when no ISO is cached).- Host USB / PCI device passthrough to the Windows guest — CLI + GUI (#286). You can now hand a host device (a USB security dongle, a capture card, a non-GPU PCI card, …) to the Windows guest. Because the default backend is dockur (QEMU/KVM in a container), passthrough is wired at the QEMU layer — no libvirt needed.
winpodx device listshows host USB/PCI devices and which are assigned;device attach <id>/detach <id>assign/release them (persisted incfg.pod.devices), and a Devices tab in the GUI is a two-column host↔guest mover. USB hot-plugs live into the running guest (cfg.pod.usb_live, default on):device attach <usb>adds it with no restart by driving dockur's built-in QEMU-monitor(reached via<backend> exec ... bash -c '/dev/tcp') — no custom-qmpsocket and nodevice_cgroup_rules(both crash-looped Windows boot on rootless Podman). usb_live just binds the host USB bus (/dev/bus/usb) into the container, which boots cleanly rootless; QEMU (root, via dockur) attaches the device to its existingqemu-xhcicontroller. Setusb_live = falseto keep the USB bus out of the container. (USB mass storage also still works via the\\tsclient\mediadrive share.) PCI binds tovfio-pci, which can't be hot-plugged into a container QEMU, so it's boot-added and needs a guest restart, gated behind a safety check — risky devices (the primary GPU, the boot-disk controller, the active NIC) require an explicit--force(CLI) or a confirmation dialog (GUI), and the whole IOMMU group is flagged since it moves together. Device ids are hex-validated, so nothing dangerous reaches the generated compose / QEMU args. - System-tray USB switcher (#300). The tray menu now has a USB Devices submenu with a checkable entry per host USB device — tick it to redirect the device into the running guest, untick to hand it back to the host. It's the quick-access surface alongside the CLI (
device attach/detach) and the GUI Devices tab: the toggle runs the persist + live usbredir attach/detach off the UI thread (so the tray never freezes, and apkexecprompt can still appear), and the submenu rebuilds on each open so it tracks hot-plugged devices and the current assignment. The persist half is now a single shared helper (core.devices.assign_device/unassign_device) behind all three surfaces. - Multi-monitor RAIL now works by default — a remote app window keeps working input when dragged onto a second monitor (
cfg.rdp.multimon, defaults tospan). Without it, a RAIL app launch sizes the session desktop to a single monitor, so a window dragged onto a second monitor lands at host-virtual-screen coords outside that desktop — clicks miss, then stop registering entirely. winpodx now adds/spanto RAIL app launches, sizing the session desktop to the bounding box of all host monitors (one wide rectangle, no per-monitorMonitorDefArray)./multimonwas tried first but sends the full monitor layout, which the guest'srdprrapRAIL helper can't handle — it kills input outright — so the default isspan, notmultimon. Setcfg.rdp.multimon = "off"(orwinpodx setup --multimon off) for non-rectangular layouts where the spanned bounding box leaves dead space;multimonis kept as a diagnosis-only value. --extra-args/cfg.rdp.extra_flagsnow allow the multi-monitor + repaint knobs (/multimon,/multimon:force,/span,/gdi:sw|hw,/smart-sizing[:WxH],/monitors:0,1), each value-validated. These are the manual levers for the RAIL window-move corruption some multi-display setups hit (a remote app window blurs / breaks when dragged between monitors of different resolution or DPI). Withcfg.rdp.multimonnow defaulting tospan(above), the input-loss case is handled out of the box; these flags remain for per-launch experimentation with the repaint / scaling knobs.winpodx doctor --fix— idempotent auto-remediation for common findings (0.6.0 item K).winpodx doctorstays read-only by default (it prints suggested commands and never mutates state), but the new--fixflag turns each finding that has a known fixer into an automatic, idempotent repair: doctor collects findings, runs the registered fixer for every warn/fail finding, then re-probes that single check and reports[fixed]/[still failing](findings with no fixer print[skip] no auto-fix available). Every fixer is a no-op when the underlying state is already healthy, so--fixis safe to run repeatedly. Four remediations ship: (1) dead agent — when the pod is RUNNING but the in-guest agent/healthis down, doctor kicks the in-guestWinpodxAgentKeepAlivescheduled task (over the agent transport, which falls back to FreeRDP when the agent itself is unreachable) and polls/healthuntil the agent is back; (2) stale lock files —.cprocmarkers in~/.local/share/winpodx/run/whose owning PID is no longer a live FreeRDP process are purged (live sessions are left untouched); (3) missing desktop entries — apps present in the index but with no installed.desktopfile are re-registered through the existing desktop-entry install path (entry + icon + MIME + cache refresh); (4) oem-version drift — when the host'soem_bundlestamp is newer than the guest's recorded version, doctor triggersguest_sync.maybe_autosyncto push the refreshed guest scripts.--fiximplies the slow container-health / guest-exec probes so the two guest-touching fixers are reachable. The stale-lock and missing-desktop-entry fixers are host-only; the dead-agent and oem-drift fixers touch the Windows guest. Without--fix,winpodx doctorbehaviour is unchanged. Seedocs/design/ROADMAP-0.6.0.mditem K.- Clean, interactive install progress (and
install.sh --verbosefor the raw firehose). The Windows first-boot wait used to dump hundreds of raw dockur/wget lines (…K …… 78% 4.55M 21m22s) plus UEFIBdsDxe:boot-loader noise. By defaultpod wait-ready --logsnow shows the ISO download as a single self-erasing line that updates in place —Downloading Windows ISO [#########-----] 78% 4.55 MB/s ETA 21m22s— with aWindows is booting…heartbeat for the non-download phases and UEFI/mknodnoise hidden; only real dockur milestones stay on screen. The live line is written straight to/dev/tty, so it animates on the terminal without pollutinginstall.sh'stee-captured log (non-TTY consumers fall back to occasional percentage lines).install.sh --verbose/-v(orwinpodx pod wait-ready --logs --verbose) streams the full raw container output instead. The installer also ends with a tidy summary box (version / backend / GUI / venv path) + concrete next-step commands. Deadline auto-extension on slow downloads is unchanged. install.shv2 — a pre-sudo system check, install modes, a mandatory private venv, and rollback on failure (resolves #271). Thecurl | bashinstaller now scans the host before touchingsudo: it prints distro + version, presence/version of podman / docker / libvirt / freerdp / python3, whether/dev/kvmexists, and whetherpython3 -m venvactually works (Debian/Ubuntu splitpython3-venv/ensurepip out, so it can be missing). It flags podman older than major 4 as too old — dockur/winpodx need rootlessgroup_add: keep-groups+ modern compose, and Ubuntu 22.04 ships podman 3.4 (#271) — and suggests either upgrading podman (Kubic /devel:kubic:libcontainers) or using the Docker backend. It then offers four modes (preselect with--mode r|a|c|n/WINPODX_MODEfor piped/non-interactive runs): [R]ecommended (today's behaviour — Podman backend, install all missing deps), [A]utomatic (reuse what's installed, pick an already-working docker/podman/libvirt backend, minimal sudo), [C]ustom (choose backend + GUI yes/no), [N]o (cancel cleanly, no changes). Two new flags feedwinpodx setup:--backend podman|docker|libvirt|manual(WINPODX_BACKEND) and--no-gui(WINPODX_NO_GUI=1, headless — skips PySide6). Python now always runs from a private venv under~/.local/bin/winpodx-app/.venv— thewinpodx-runlauncher execs the venv's interpreter instead of the systempython3+PYTHONPATH, so there's no--user/--break-system-packagessystem-python pollution and the reverse-open icon deps (cairosvg+pyxdg) install cleanly into the venv (no more distro-package best-effort). And a failed fresh install now rolls back — it removes only winpodx's own artifacts created that run (the venv,winpodx-run+winpodxlauncher/symlink, desktop entry + icon, the in-progress marker), never system packages and never a pre-existing~/.config/winpodxconfig; on an upgrade a failure leaves the working install untouched.--manual,--skip-deps,--source,--ref/--main,--win-version, and--image-tarall behave as before.winpodx-gitAUR package — install the latestmainfrom source (#482, #483, #484). Alongside the stablewinpodxAUR package (tagged release tarballs), a newwinpodx-gitVCS package builds from the GitHubmainbranch:yay -S winpodx-git, with the version derived from git soyay -Syu --develrebuilds it whenevermainmoves. Itprovides/conflictswinpodx, so install one or the other — not both. Recipe + maintainer notes live inpackaging/aur-git/; documented indocs/INSTALL.md.
Changed
- GUI UX overhaul — clearer feedback, safer actions, less jargon. A broad pass over the desktop app (no new telemetry, no network, no bloat): a shared toast/notification + "busy" dialog + inline warning-callout + actionable-error layer (
_widget_helpers) now backs consistent feedback everywhere. Highlights: app launches and device attach/detach show a non-blocking toast ("Launching …" → success/fail) instead of silent RDP spawns; the app library shows a context-specific empty state (Windows not running / no search match / all hidden / none discovered) and a unified grid-vs-list card layout (labelled Launch, consistent Show/Hide, confirmed delete), a labelled search box, a "+N more" overflow for categories beyond the first 8, and an "X of Y" count that reconciles with the status bar; refresh failures use an actionable dialog (Start pod / Retry / View logs); the bring-up dialog disables Cancel during the non-interruptible phases (with a tooltip) and shows honest per-phase ETA hints + a clear "✓ Ready" finish; the status banner now distinguishes "running but transport degraded" (RDP/agent unreachable) from "stopped", and a down agent that still has working FreeRDP fallback shows amber (not red); pod state is no longer triplicated across chip/banner/info-bar; Settings marks which controls apply immediately, surfaces a wipe/recreate warning callout with a time estimate before destructive saves, shows an always-on RAM-budget line, and adds inline help for the power-user knobs; long maintenance ops (grow-disk / sync-guest / debloat / apply-fixes) show a busy dialog with an ETA; PCI passthrough spells out in plain language which host devices the IOMMU group gives up; the Add-app dialog explains its fields and lightly validates the Windows executable path; keyboard shortcuts (Alt+N to switch pages, Ctrl+F to search) were added; and the License page links each third-party project. (Funding stays in the repoFUNDING.yml/ README — never surfaced in the app.) - Windows apps are now grouped under a single "winpodx" menu folder instead of being scattered across native categories. Previously each installed app inherited its discovered categories (Office, Graphics, …) and landed loose among your native Linux apps; on a 50-app guest that buried the menu. winpodx now creates a dedicated winpodx (Windows Apps) submenu — the same mechanism Wine uses: a freedesktop
.directoryfile names the folder, anapplications-merged/winpodx.menufragment maps a customX-winpodxcategory into it, and every generated.desktopcarries that one category. The folder is created on first app install and torn down when the last Windows app is removed (both idempotent + self-healing onwinpodx app refresh). Honored by KDE Plasma, XFCE, Cinnamon, MATE and LXQt; GNOME's overview is a flat grid that ignores menu folders, so there the apps still appear but aren't grouped. Menu search still finds apps by name (Keywords=windows;winpodx;<name>). Re-runwinpodx app refreshto migrate an existing install. install.shprints an install plan before it touches the system. Once the mode (R/A/C/N) and every dependency source are resolved, the installer shows a short plan that lists every major component evenly —python3, thevenvprobe, the container backend, FreeRDP,/dev/kvm, and the GUI — each with its detected state and the action this run will take (use existing / install / host-requirement), plus the exact packages it will install via the distro package manager and the Windows-VM provisioning steps — before any package install orsudo. The run is transparent instead of jumping straight from the mode prompt into installing things.- FreeRDP client source is now selectable; the launcher prefers the Flatpak client, with the native client as a fallback / opt-in (#269, #366, #393). Previously
install.shalways installed the nativefreerdp3package even when the Flatpakcom.freerdp.FreeRDPwas already there (redundant — #269). Now: (1) the launcher prefers the Flatpakcom.freerdp.FreeRDP(core/rdp.find_freerdpauto order is flatpak-first) — it ships a self-contained FreeRDP 3+ with no host package skew. Its earlier RAIL multi-display rough edges (a remote app window losing input when dragged to a second monitor) are now handled bycfg.rdp.multimon = "span", so the Flatpak is viable as the preferred client; the nativexfreerdpis the fallback when the Flatpak is absent, or when explicitly pinned via--freerdp-source native. (2)install.shnever installs a redundant client — if any FreeRDP (native or Flatpak) is already present it installs nothing; when none is presentautoinstalls the native package (lightweight, no Flatpak-runtime pull), and the launcher's auto order then prefers an existing Flatpak only when one is actually present. (3) Custom install mode lets you choose each major dependency's source — container backend (podman/docker/libvirt), FreeRDP client (auto / native / flatpak), and the GUI — andwinpodx setup --freerdp-source <auto|native|flatpak>persists the choice tocfg.rdp.freerdp_source(setnativeto pin the native client on hosts where the Flatpak sandbox is a problem). - Reverse-open refresh output now states its direction and the deprecation / summary plumbing is de-duplicated.
winpodx host-open refreshprints aReverse-open (host apps → Windows "Open with")header so itsDiscovered/staged/skippedcounts aren't confused with the opposite-direction Windows-app discovery that runs just above it duringprovision, plus a pointer towinpodx host-open listfor the staged set. Separately, thewinpodx provisionsummary no longer dumps the rawapply_fixesdict repr (it renders a compactN/N fixes OK/k: vline), and the two byte-identicalpod-deprecation-notice helpers (_deprecate_pod/_emit_deprecation) are collapsed to one. install.sh --verbosenow reaches the upgrade path too.install.sh --verbose(andWINPODX_VERBOSE=1) used to forward towinpodx provision --verboseon a fresh install but was silently dropped on the upgrade branch, becauseinstall.shinvokedwinpodx migrate --non-interactivewithout a verbose flag andmigrate's injected_rich_waithardcodedverbose=False.winpodx migratenow accepts--verbose/-v,install.shforwards$WINPODX_VERBOSEto bothprovisionandmigrate, and the migrate wait-ready stage honours the flag so an upgrade run streams the raw container firehose just like a fresh install. No behaviour change when--verboseis off (the clean self-erasing line stays the default for both paths).agent_keepaliveno longer fails the migrate apply burst on a post-restart agent flicker. During an upgradewinpodx migrate, the container restarts (OEM reboot pass) and the in-guest agent goes through the TermService cycle that rdprrap (re)activation triggers. A single/healthOK during that window can be the agent momentarily up — it dies again right after, and the apply burst then hit a closed socket (agent_keepalive: channel failure: /exec socket error: Remote end closed connection without response, seen in 0.6.0 upgrade smoke). Two layers of defence now: (A) therequire_agentsettle stage waits for 3 consecutive/healthOK probes (≈ up to 30 s) before proceeding, so the chain starts only once the agent has actually stabilised rather than on the first flicker; (B)_apply_via_transport— the shared channel used by every_apply_*runtime fix (agent_keepalive,oem_runtime_fixes,multi_session,vbs_launchers,max_sessions,rdp_timeouts) — now retries aTransportError(closed socket //healthtimeout) twice with a 5 s backoff, re-dispatching each attempt so a recovered agent is re-picked (or a still-dead one falls to FreeRDP). A genuinerc != 0from a payload that actually ran is still surfaced on the first try, unretried. No behaviour change on a healthy first try.- Provisioning unification (0.6.0 item B) restored four behaviours its first cut had homogenised away. Folding the four post-create paths into
finish_provisioninghad conformed everything to a silent wait + a single agent gate, dropping behaviours each path had accumulated for specific issues: (1) the dynamic wait — the live self-erasing progress line + wget-ETA deadline auto-extension for slow links (#126) — so a fresh install no longer shows one line then a silent multi-minute hang; it's restored via an injectedwait_fnthat routes the wait-ready stage through the richpod wait-readymachinery. (2) agent-first install protection (#271):require_agent=Truenow exportsWINPODX_REQUIRE_AGENT=1across the apply + discovery stages (not just the one-shot settle re-probe), so discovery/apply defer instead of falling back to a FreeRDP RemoteApp connect that can kick install.bat's autologon session during first boot; a persistent agent-unavailable during discovery now defers cleanly to the pending machinery (exit 5) instead of recording a generic failure. (3) upgrade →winpodx migrate:install.shnow branches fresh vs upgrade — fresh runsprovision --require-agent, upgrade runsmigrate(which syncs the refreshed guest scripts /agent.ps1into the existing guest, pins the image, then runs the same apply → discovery → reverse-open chain); the first cut ranprovisionfor both and left upgraded guests on stale guest scripts. (4) thepending.resume"migrate" step now runsguest_synctoo, so a deferred upgrade resumed later also refreshes guest scripts.migrateadditionally gained the reverse-open stage (it's now the upgrade path's sole driver). Seedocs/design/PROVISION_UNIFY_FIDELITY_AUDIT.md. - Command taxonomy reorganised —
guest,install, anddoctorare the new canonical homes (0.6.0 item G). Thepodjunk-drawer carried 14 subcommands across three unrelated domains; the diagnostics surface had three partially-overlapping commands (info,check,doctor). As of this release:winpodx podkeeps only the lifecycle subcommands (start,stop,status,restart,recreate,wait-ready).winpodx guestis the new canonical home for guest-side operations:apply-fixes,sync(renamed fromsync-guest),sync-password,multi-session,recover-oem.winpodx installis the new canonical home for install-progress and storage operations:status(frompod install-status),resume(frompod install-resume),grow-disk(frompod grow-disk),disk-usage(frompod disk-usage).winpodx doctoris the canonical diagnostic command; it gains two new flags:--json(machine-readable Finding list as a JSON array) and--quick(skip slow container-health / guest-exec probes, run only cheap local checks in < 1 s).winpodx infoandwinpodx checkkeep their exact current output and behaviour, but now print a one-line deprecation notice to stderr:[deprecated] 'winpodx info' will be removed in 0.7.0; use 'winpodx doctor'(and analogously forcheck). All oldpod <x>subcommands remain registered and keep working through 0.6.x — they print the same deprecation pattern before delegating to the shared handler function; aliases are removed in 0.7.0. Shell scripts (install.sh,uninstall.sh,postinst,postrm,postrm-common.sh) contained no live invocations of the moved subcommands, so no update was needed. Full old→new command mapping: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. Seedocs/design/ROADMAP-0.6.0.mditem G. Deprecated aliases will be removed in 0.7.0. - Backend auto-selection unified through
backend/select.choose_backend. Three places used to decide which container backend to use, and they disagreed on Ubuntu 22.04:install.sh's Automatic-mode picker walkedpodman → docker → libvirtand gatedpodman < 4as unusable (#271), butcli/setup_cmd.py's non-interactive branch did a one-line"podman" if which("podman") else "docker"with no version gate — so the setup wizard happily picked the broken 3.4 podman the install path had just declined.choose_backend(prefer, deps, podman_min_major=4)is now the single Python source of truth: explicit--backendwins, otherwise walkAUTO_PRIORITY = ("podman", "docker", "libvirt")and the first usable wins (podman gated onpodman_major_version() >= 4); fall back to"podman"when nothing is present so the recommended install path can install it.install.sh's bash picker stays as the one intentional shell mirror (same pattern as the pre-venv deps probe — it runs before Python is installed) with a comment pointing back at the Python SoT, and the test suite pins both copies to the same priority order + minimum major so they cannot silently drift. Part of the 0.6.0 consolidation work (docs/design/ROADMAP-0.6.0.mditem E). - Host dependency detection unified through
utils/deps.py:check_all. Pre-0.6.0 each consumer (utils/deps.py,core/deps_quickcheck.py,cli/doctor.py,cli/setup_cmd.py) carried its ownshutil.which()list for FreeRDP binaries, and they had drifted —deps_quickcheckanddoctormissedsdl-freerdp3and the Flatpak fallback, so a host with only those reported MISSING in the GUI Quick Start and inwinpodx doctor.check_all()now also reports/dev/kvm(via a newcheck_kvm()), sosetup_cmddrops its inline KVM probe anddoctor's_check_kvmdelegates too. Every Python consumer now goes throughcheck_all()/check_freerdp()/check_kvm(); the shell side ofinstall.shkeeps a minimal pre-venv probe (genuinely shell-unique — it runs before Python exists) and that's the single intentional duplicate. A regression test pins it: an AST scan overdeps_quickcheck.pyanddoctor.pyfails CI the moment a hardcodedshutil.which("xfreerdp...")call comes back. Part of the 0.6.0 consolidation work (docs/design/ROADMAP-0.6.0.mditem D). - Version + Windows-edition lists collapsed to a single source each.
pyproject.tomlis now the only place the project version is declared;src/winpodx/__init__.pyderives__version__viaimportlib.metadata.version("winpodx")(with a clearly-marked0.0.0+sourcefallback for a non-installed source checkout), so a release-prep bump can't go missing from one of the two files. The curated Windows-edition list moved to aWIN_VERSION_LABELSdict incore/config.py; the CLI help text (winpodx setup --win-version), the interactivesetupprompt, and the GUI Settings dropdown all derive from it, so adding a new edition is a one-line change. The packaging-version CI guard (scripts/ci/verify_versions.py) drops the obsolete__init__.pyliteral check, picks uppackaging/rpm/winpodx.spec, and adds a round-trip assertion againstimportlib.metadata; the spec's staleVersion: 0.1.5literal is bumped to current and gains a pointer to the new guard. Part of the 0.6.0 consolidation work (docs/design/ROADMAP-0.6.0.mditems F + N). winpodx.tomlnow carries aschema_versionmarker (groundwork for future config migrations). The on-disk config file has changed shape across versions (renamed keys, moved sections) and we've been relying on_apply()'s "unknown keys are silently ignored" fallback. That works for adding fields but loses data the moment we rename or move one. Addingschema_version = 1at the top of every saved file (and a_migrate_config(data, from_version)hook incore/config.py) gives a future 0.7.0+ rename a clean upgrade path without dropping user settings. The hook is intentionally a no-op today — 0.6.0 didn't change the layout — but the marker lands now so existing 0.5.x files get tagged on first load + save, and a later release that does restructure can transform them safely. A hand-edited file without the marker is treated as schema 0 (pre-0.6.0). Part of the 0.6.0 consolidation work (docs/design/ROADMAP-0.6.0.mditem J).- Agent port 8765 is a single Python constant now.
core/agent.AGENT_PORTis the single host-side source of truth; the compose template, the urlacl strings pushed into the guest, and theAgentClientURL all derive from it. The guest-side files (agent.ps1,install.bat,agent-keepalive.ps1,agent-respawn.ps1) keep their own literal because PowerShell can't import a Python constant — that pairing is now documented and locked by tests, andinstall.sh's/healthcurl carries a comment pointing back at the constant. Pure internal cleanup; no behaviour change. Part of the 0.6.0 consolidation work (seedocs/design/ROADMAP-0.6.0.mditem C). - The legacy FreeRDP host→guest fallback is now logged (groundwork for retiring it). Before the in-guest agent existed, host→guest commands ran PowerShell over a FreeRDP RemoteApp; that path still exists as the silent fallback when the agent isn't reachable. It was logged at
debug, so there was no way to tell how often it actually fires in practice. Every fallback now logs aWARNINGtowinpodx.logtaggedFreeRDP-fallback, with the reason (agent/healthdetail) and the operation that fell back. Count them withgrep -c FreeRDP-fallback ~/.local/state/winpodx/winpodx.log. No behaviour change — this is measurement so we can decide, from real usage, how aggressively to strengthen agent recovery and shrink the fallback (rather than removing it blindly and risking breakage). - Logging hygiene pass — one consistent logger variable name across modules (0.6.0 item L). The
reverse_open/package andgui/reverse_open_panel.pydefined their module logger aslogger, while the other ~50 modules uselog(both vialogging.getLogger(__name__)); the call sites are now standardised onlog. A one-pass audit of log levels confirmed they are already consistent — failures / fallbacks / refusals log atWARNING(the console threshold), routine milestones atINFO, and diagnostics atDEBUG— so no level changes were needed. Pure internal cleanup with no behaviour change: the logger name is the module path regardless of the Python variable, so log output is byte-for-byte identical. Part of the 0.6.0 consolidation work (docs/design/ROADMAP-0.6.0.mditem L). - First-run discovery retries trimmed from 6 to 2 (0.6.0 item M). When the provisioning chain (
winpodx provision, run byinstall.shon a fresh install, and by thesetupauto-provision path) scans the guest for installed apps, it retries discovery with exponential backoff if the guest agent is briefly not ready. The fixed 6× loopinstall.shhistorically used predated theWinpodxAgentKeepAlivewatchdog (#359); now that the keep-alive keeps the agent reliably up, 6 attempts is overkill and just slows a clean first run. Thewinpodx provision --retriesdefault and thefinish_provisioning(retries=...)default both drop to 2, so discovery does at most one 2 s-backoff retry before reporting. Themigrate/pending.resumeupgrade paths keep their existingretries=3(deliberately set during item B and out of scope here). Override withwinpodx provision --retries Nif a slow guest needs more. Part of the 0.6.0 consolidation work (docs/design/ROADMAP-0.6.0.mditem M). - Documentation refresh for 0.6.0 (0.6.0 item H).
README.md,docs/USAGE.md,docs/ARCHITECTURE.md,docs/FEATURES.md,docs/INSTALL.md, and every matchingdocs/*.ko.mdmirror now reflect the 0.6.0 shape: the newwinpodx guest/winpodx install/winpodx doctorcommand surface (withpod <x>aliases noted as deprecated and removed in 0.7.0), the Thin AppImage redesign (only FreeRDP + Python + Qt + winpodx; host container runtime required; #357 / #363 root-caused), andwinpodx provisionas the single post-pod-running chain. TheUSAGE.mdcheat-sheet splits the old "Pod management" block into three sections (podlifecycle /guestops /installops) and the System section gains the newdoctorflags (--json,--quick,--fix),provision, andmigrate. The version blurb in bothREADME.mdfiles is rewritten for 0.6.0.
Fixed
- UWP / Store apps now appear in the Linux taskbar (#472). A UWP app's visible frame is owned by
ApplicationFrameHostand arrives over FreeRDP RAIL marked_NET_WM_STATE_SKIP_TASKBAR/SKIP_PAGER, so it stayed off the panel even though its window class already matched the launcher. winpodx now re-lists the window viawmctrlafter launch (best-effort, X11 / XWayland; no-op otherwise). - Opening a host file whose path contains a space now works (#473). The UNC path (
\\tsclient\home\…) handed to FreeRDP's RemoteApp command is now double-quoted, so the guest no longer splits it on spaces — previously the app received only the first token and reported "path not found". - A mixed-scale dual-monitor host no longer breaks app launch / leaves frozen windows (#474). On KDE Plasma 6 Wayland (XWayland) with two monitors at different fractional scales, the default
/spanmulti-monitor desktop was doubly broken: the compositor's sub-pixel rounding leaves the monitors' logical rectangles non-tileable (a 1 px gap, mismatched heights,desktopScale: 0), so FreeRDP rejected/spanatpre_connect(ERRCONNECT_PRE_CONNECT_FAILED) and nothing launched; and even when a session did come up, a RAIL window dragged onto the differently-scaled monitor froze and stopped taking input — FreeRDP RAIL + XWayland can't remap a window across per-monitor fractional scales (an upstream limit no client flag fixes). winpodx now detects the per-monitor scales (kscreen-doctoron KDE,swaymsg/hyprctlon wlroots) and, when they differ, pins the RemoteApp to the primary monitor — so the app launches and stays responsive there — and logs the one real fix for using both monitors together: set them to the same scale (or, on KDE, Display → Legacy Applications → Apply scaling themselves, which makes XWayland hand xfreerdp a uniform pixel grid). With uniform scales/spanis used exactly as before (a window moves freely across monitors). Single monitor / undetectable scales are unaffected;cfg.rdp.multimon = "off"forces single-monitor regardless. As a connect-time backstop, a launch that still dies with that pre_connect monitor error retries on a single-monitor desktop. - Terminate a running RDP session from the GUI and the tray (#450, #452, #453). A Running sessions strip on the GUI Dashboard lists each live app session and terminates it on click; the tray gained a Terminate Session submenu. Terminating now signals the whole FreeRDP process group rather than just the leader (#458) and can force-kill a stuck session (#459); the tray also retries showing itself until the desktop's StatusNotifier host is up and re-asserts on its heartbeat so it survives a host restart (#455, #465).
- The Dashboard resource centre now actually shows CPU / RAM / disk, and refreshes fast. Three problems compounded on real machines: (1)
podman/docker statswas queried by the configured container name, but podman-compose prefixes it (winpodx_winpodx-windows_1), so the probe hit "no such container" and every gauge stayed blank — the name is now resolved via<cli> ps --filter name=first; (2) the CPU/RAM probe and the guest-disk probe ran sequentially, so a slow disk read stalled the whole snapshot — they now run concurrently with a hard cap, the disk is sampled on a slower cadence, and its last good value is cached instead of blanking between reads; (3) the disk probe fell back to a FreeRDP RemoteApp PowerShell that flashed a visible window in the guest on every poll — it's now agent-only (windowless/exec), so a passive dashboard never pops a console. Finally, rootless podman routinely reports CPU% (and sometimes MEM) as--instats, leaving those gauges empty even with a healthy pod; winpodx now back-fills them by reading the container's cgroup v2 files directly (memory.currentfor RAM, acpu.statusage_usecdelta for CPU%), so CPU and RAM populate on the rootless setups wherestatswon't. Many rootless slices delegate onlycpu/pids(notmemory) to the user, somemory.currentis absent even whencpu.statreads fine — RAM then falls back to summing theVmRSSof every process in the container cgroup (kernel-accounted, controller-agnostic; the dockur QEMU process dominates), so the RAM gauge fills there too. - The Dashboard RAM gauge now reflects the guest's actual usage, and the panel paints faster. Every host-side memory read (cgroup
memory.currentor summed processVmRSS) reported the VM's memory — for the dockur (QEMU) backend the host sees nearly all the guest RAM resident, so the gauge sat pinned near 100 %. RAM now comes from inside Windows via the guest agent (Win32_OperatingSystemtotal/free physical memory), the only figure that reflects what the guest is really using; it shares the disk probe's single agent round-trip (one/execfor both) and slower cadence, and the last value is cached so it doesn't blank between polls. CPU stays host-side but now reads the cgroupcpu.statdelta first (an instant file read) and only falls back to the multi-secondpodman statssampling, so the panel populates without that stall on every tick. - The Dashboard now auto-refreshes from the moment the window opens. The live-refresh timer was started only by the nav's page-switch handler, but the Dashboard is the default page shown at startup — no switch fires for it — so the panel painted once and then sat frozen until you navigated to another page and back. The timer now starts when the Dashboard is built, so resources and pod status update on their own from launch.
- GUI pages no longer clip / grow a horizontal scrollbar on narrow or fractionally-scaled windows. The "All apps" Pinned/Recent shelves were a non-wrapping row, so a long shelf forced the whole page wider than the viewport and clipped the app grid on its right edge (worst on hi-DPI fractional-scale displays). Every page's scroll area now pins the horizontal scrollbar off, the tile grids budget a touch more width per tile so the last column can't overflow, and the Dashboard/All-apps shelves cap to the number of tiles that actually fit (everything stays reachable in the grid below) and re-flow on resize.
- Settings combo boxes no longer change value when you scroll past them. Hovering a drop-down while scrolling the Settings page used to silently change its value (scale, backend, edition, …). Combo boxes / spin boxes now ignore the wheel unless they're focused — click one first to scroll its value — and the wheel falls through to scroll the page instead.
- Upgrades restart the running GUI / tray so the new version takes effect immediately (#467). The installer gracefully restarts a running
winpodx tray/ GUI at the end of an upgrade (viasetsid, so it outlives the installer); the pod keeps running. A no-op on fresh installs. - File sharing now works on Fedora Atomic / Silverblue / Kinoite (#418, #420). There
/homeis a symlink to/var/home, which made the UNC-path conversion report home-folder files as "outside shared locations". The home and media base paths are now resolved to their real targets before the comparison. - Setup verifies the container backend daemon is actually reachable, not just installed (#395, #419). A podman / docker CLI on
PATHbacked by a dead daemon (e.g. a staleDOCKER_HOST) used to pass the dependency check and then fail at pod start; the check now probes reachability and reports a clear DAEMON DOWN status. manualbackend now reaches the guest agent at the VM's address instead of loopback (#426). The agent client always targeted127.0.0.1:8765, even when the Windows machine lived elsewhere — so on themanualbackend pointed at a VM (e.g. a VMware guest atLTSC11P.local), RDP worked but every agent-backed feature fell back to FreeRDP-only, andwinpodx checkreportedagent_healthunreachable even though the agent answered fine at the VM address.AgentClientnow derives its host fromcfg.rdp.ip— the same address the RDP reachability check uses — so it follows the VM onmanualand stays on127.0.0.1for podman/docker (where the container publishes 8765 to host loopback andcfg.rdp.ipis already loopback). No per-call-site changes; one fix in the client constructor.- Reverse-open's Windows shim is no longer quarantined by Microsoft Defender (#425).
winpodx-reverse-open-shim.exe(a tiny, stripped, unsigned Rust binary) trips Defender's ML heuristic asTrojan:Win32/Rafvartar!rfn— a false positive.install.batalready excludedC:\OEMandC:\winpodx, butregister-apps.ps1stages the shim and its per-slugwinpodx-<slug>.execopies underC:\Users\Public\winpodx, which wasn't excluded — so Defender quarantined them and reverse-open silently broke. The first-boot Defender-exclusion step now also coversC:\Users\Public\winpodxand thewinpodx-reverse-open-shim.exeprocess. The shim is also now built with an embedded Windows VERSIONINFO resource (CompanyName / ProductName / FileDescription / version via awinresourcebuild script) — a metadata-less stripped PE scores worse on AV heuristics, so stamping real publisher info is a cheap legitimacy signal that the per-slug copies inherit. (Longer-term the shim should be Authenticode-signed / submitted to Microsoft as a false positive; the exclusion is the turnkey fix for winpodx's own guest.) - The Setup tab's "Enable reverse-open" checkbox now starts/stops the listener live (#425). Ticking the box only flipped
cfg.reverse_open.enabledand refreshed the status label — the daemon stayed down until the next pod bringup, so users had to also click "Start daemon." The checkbox now starts the listener immediately when enabled and stops it when disabled (and persists the flag). It's best-effort + quiet: if the guest isn't up yet, the flag still persists and the listener comes up on the next pod start, with no error modal for that expected case. - Fresh install no longer hangs forever at
wait-readyon hosts where dockur picks bridge-NAT networking (#269, #387). The agent on guest port 8765 was unreachable from the host after Windows booted —wait-readypolled it indefinitely while noVNC showed a finished desktop. Root cause: the compose setUSER_PORTS: "8765"but left dockur to auto-select its network mode. On hosts where dockur's default bridge-NAT path succeeds (typically rootful backends), that path silently ignoresUSER_PORTS, so QEMU never forwarded 8765 to the guest (the Podman host↔container publish was fine; the container↔guest hop was the gap). dockur only consultsUSER_PORTSin its user-mode (passt) / slirp paths — which is exactly why rootless hosts were unaffected (rootless can't build the bridge, so dockur already fell back to passt and forwarded the port). The compose now pinsNETWORK: "user"so dockur always uses the port-forwarding user-mode path regardless of host. winpodx only ever reaches the guest over forwarded ports (RDP 3389, the web viewer 8006, the agent 8765) and never needs the guest on the host LAN, so user-mode is the correct mode here; on rootless hosts the change is a no-op (they were already on passt). install.shno longer blindly proceeds with a too-old podman in Recommended mode (#271). Automatic mode already walked apodman < 4host (Ubuntu 22.04 ships 3.4) down to docker / libvirt, but Recommended mode and an explicit--backend podmanskipped that fallback — so the install ran to completion and only failed later at provisioning, after installing packages. A guard now runs right after the backend is resolved (before any package install): when the chosen backend is podman but podman is too old, an interactive run offers to switch to an installed docker / libvirt (or continue at your own risk / abort), and a non-interactive run exits cleanly without modifying the system, pointing at--backend docker|libvirt, a podman upgrade, or--allow-old-podman/WINPODX_ALLOW_OLD_PODMAN=1to force. This completes the "graceful exit" part of #271 (the distro/version check and the runtime selector already shipped in theinstall.shv2 rework).- Upgrades no longer prompt for an install mode or replay the old first-boot log. Two upgrade/re-run UX warts: (1)
install.shshowed the[R]ecommended / [A]utomatic / [C]ustom / [N]omode menu even when a winpodx config already existed — pointless, since an upgrade reuses the existing backend/config and runsmigrate. It now detects the existing install and resolves to Automatic without prompting. (2)pod wait-ready --logs(used by the upgrade's migrate path) tailedpodman logs --tail 100, which on an already-running container replays the original first-boot ISO-download + image-build output — under--verbosethat looked alarmingly like Windows was being re-downloaded on every update. wait-ready now replays no history (--tail 0) when the pod is already up (RDP reachable), keeping--tail 100only for a genuine fresh boot where the in-progress download is worth showing. - The Flatpak FreeRDP now does per-app RemoteApp instead of opening the full desktop.
flatpak run com.freerdp.FreeRDPruns the app's default command — the SDL client, which has no RAIL (FreeRDP #9078) — so launching a single app via the Flatpak opened the whole Windows desktop / login screen instead of one app window. winpodx now invokes the Flatpak asflatpak run --command=xfreerdp … com.freerdp.FreeRDP, forcing the X11xfreerdpbinary (the only client with working RAIL), and grants the sandbox the holes every winpodx RDP flag needs so the Flatpak behaves like a native client:--share=network(localhost RDP),--socket=x11/--socket=wayland(RAIL + clipboard),--socket=pulseaudio(sound),--socket=cups(printer),--device=dri(display), and--filesystem=home+ the removable-media mount roots (\\tsclient\home+\\tsclient\mediadrive redirection). Surfaced after 0.6.0 started preferring the Flatpak when present. - The install-mode menu (R/A/C/N) now works under
curl … | bash. The interactive mode prompt — and the Custom-mode backend/GUI sub-prompts and the dependency-install confirm — were gated on[ -t 0 ]and read from stdin, so under the canonicalcurl … | bashinstall (where stdin is the script pipe, not a terminal) they were silently skipped and the install always defaulted to Recommended. install.sh now also detects a reachable controlling terminal via/dev/ttyand reads every prompt from it, so acurl … | bashrun in an attended terminal shows the mode menu and lets you choose (the same/dev/ttytrick the live progress line already uses for output). Fully non-interactive runs (CI / cron / stdin and/dev/ttyboth absent) stay non-interactive and default to Recommended, and--mode/WINPODX_MODEstill preselect without prompting. - Fresh-install provisioning now waits for a flaky guest agent until it actually recovers, instead of giving up on a timer. The agent-first settle gate and the discovery stage used to give up after a fixed window (a ~30 s settle ceiling; 2 discovery retries over a few seconds), so when the in-guest agent flickered down right after the first-boot reboot — it can lag up to a keepalive-watchdog cycle (~60 s) — provisioning deferred and the install only finished later via
winpodx app refresh. Both stages now wait with no time cap, gated on pod liveness: as long as the pod isRUNNING, theWinpodxAgentKeepAlivewatchdog will bring the agent back, so they poll/healthand proceed once it is stably up (settle requires 3 consecutive OK) — completing discovery in-line however long the agent takes. The wait ends only if the pod itself stops (no watchdog → genuinely unrecoverable), the same real-signal-bounded philosophy as the wget-ETA dynamic deadline inpod wait-ready(#126) rather than an arbitrary duration. The bounded retry budget (--retries, 0.6.0 item M) now applies only to non-agent transient discovery errors.winpodx migrate(the upgrade path) gets the same patient recovery. - A deferred fresh install no longer rolls itself back. On a fresh
install.shrun where Windows downloaded + booted fine but the agent-first discovery deferred (winpodx provisionexit 5, e.g. the in-guest agent lagging a minute after the first-boot reboot), install.sh rolled back the entire install — throwing away ~15 minutes of ISO download + boot for a state that finishes itself. Root cause: bash fires theERRtrap on a failing pipeline even underset +e, so the rollback trap fired on the provision pipe before the rc-handling (which already treats a non-zero provision as "record pending, keep install") could run. The provision call now explicitly disarms theERRtrap (trap - ERR/ re-arm after) instead of relying onset +e, and exit 4 (wait-ready ran long) / exit 5 (discovery deferred) are handled as an explicit deferred-not-failed case: the remaining steps are recorded as pending (they auto-resume on the nextwinpodxrun, orwinpodx app refresh) and the install is kept. A genuine early failure (deps / venv / container creation) still rolls back via the trap as before. - Install / provision now lists the apps it registered, not just a count. The discovery stage's shared
_register_desktop_entriesprinted onlyRegistered N app(s) in your desktop menu.— duringwinpodx provision/install.sh/winpodx migratethe user never saw which apps landed, even with--verbose. It now lists each registered app name (matchingwinpodx app refresh), and when the discovery count and the registered count differ — discovered apps that have no bundled profile can't be registered — it prints a one-lineNote: N discovered app(s) had no bundled profile and were not added to the menu: …so the gap is explained instead of silent. migrate --verbosereached only one of its two apply paths (regression of the just-shipped verbose fix).run_migrate's cross-version upgrade branch called_apply_runtime_fixes_to_existing_guest(non_interactive)withoutverbose=verbose, so on a real version-crossing upgrade — exactly the pathinstall.shdrives withmigrate --non-interactive --verbose— the raw container firehose was still dropped (the--verboselocal was dead on that branch). Both call sites now threadverbose=verbose.- The Windows-exec wrapper script is no longer written world-readable.
run_in_windows()wrote~/.local/share/winpodx/windows-exec/<desc>.ps1(and created its directory) at the process umask (commonly0644/0755). On the FreeRDP-fallback password-rotation path that script embeds the freshly-rotated cleartext Windows password, so it was briefly persisted readable by any other local user. The directory is now0700and the script0600, matching how the agent token,compose.yaml, config, and the rotation marker are already written. - The
.cprocsession lock is no longer briefly truncated to empty while a session launches.launch_appopened the PID lock file with mode"w"(truncating it to zero bytes) before the FreeRDP child PID was known, leaving a window where a concurrent reader —list_active_sessions/ the idle monitor — could see an empty file and unlink the live session's lock as corrupt. The lock fd is now opened withoutO_TRUNC; the PID is written as a singleftruncate+write+fsyncmutation underflockonce the child exists, so a reader only ever sees the prior or the new PID, never empty. - Discovered Windows executables with a comma in the path can no longer inject a FreeRDP
/app:sub-key. The FreeRDP-3/app:program:<exe>,name:<name>arg interpolatedapp_executableraw, while the adjacentdefault_argsand the UWP AUMID were already sanitised/validated against exactly this.app_executablenow gets the same comma→space treatment before the combined arg is built. - A failed pod resume now fails fast with a clear message instead of a long opaque RDP timeout.
ensure_pod_awakeignoredresume_pod's result, so a pod that didn't actually unpause was handed straight tolaunch_app. It now re-checksis_pod_pausedafter the resume attempt and raisesProvisionError("failed to resume paused pod …")when the pod is still paused. - First-boot discovery keeps a retry that fixes the UWP gap even when the total app count is unchanged.
discover_apps's retry-on-empty only kept the retry result when it had strictly more apps total, so an equal-size retry that recovered the missing UWP (Store) apps — the exact signal that triggered the retry — was discarded. It now also keeps the retry when it has more UWP entries. - The AppImage no longer ships a bundled container stack that fights the host — Thin AppImage redesign (#357, #363). The pre-0.6.0 fat AppImage bundled the entire podman stack (podman + podman-compose + conmon/crun/netavark/aardvark-dns/pasta/passt/slirp4netns + transitive
.sodeps) into${APPDIR}/usr/binand${APPDIR}/usr/lib, with the entrypoint prepending both toPATHandLD_LIBRARY_PATH. That broke every host that already had a working podman: #357, Ubuntu 26.04 —podman-composeresolved to the bundled copy, which probed for the bundledpodman(can't run standalone: no host/etc/containersconfig, no subuid/subgid, no systemd integration) and died withit seems that you do not have podman installed, hiding the host's working podman 5.7 + podman-compose 1.5; #363, Fedora Bluefin — podman shelled out to the hostsystemd-runfor rootless aardvark-dns, but the prepended${APPDIR}/usr/libforced that host binary to load the AppImage's bundledlibcrypto.so.3→OPENSSL_3.4.0 not found (required by host libsystemd-shared), so aardvark-dns failed and container start died. PR #365's_hostenvhelper patched around the symptoms; 0.6.0 item A removes the root cause by dropping the entire container stack from the AppImage. The AppImage now bundles only what is safe to bundle (FreeRDP 3, Python, Qt, winpodx — leaf binaries that don't spawn host helpers) and requires a host container runtime (podmanrecommended,docker/libvirtsupported) installed via the distro package manager, same model asinstall.sh. Dropping the container stack on its own only saved ~20 MB, though (≈296 MB fat → ≈274 MB): the real bulk is PySide6, which bundles the entire Qt6 stack (QtWebEngine alone is ~195 MB) while winpodx links only QtCore/QtGui/QtWidgets/QtSvg/QtDBus, so a companion pass (packaging/appimage/slim-pyside6.sh) strips the unused Qt6 modules + their plugins/resources + the now-orphaned FFmpeg libs, bringing the AppImage to ~110 MB._hostenvcollapses to a single job — strip${APPDIR}fromLD_LIBRARY_PATHfor every container-backend subprocess so the host runtime + the host helpers it spawns load HOST libcrypto / libssl, not the bundled ones still on the AppImage's ownLD_LIBRARY_PATH(the #363 mitigation that survives because bundled FreeRDP / Python / Qt still need those bundled libs).host_path()andresolve_backend_bin()are gone — nothing in${APPDIR}/usr/binshadows the host container runtime anymore, so standardsubprocessPATH resolution finds the hostpodman/dockerdirectly. A regression test (tests/test_appimage_recipe.py) fails CI the moment the recipe re-introduces any ofpodman/podman-compose/conmon/crun/netavark/aardvark-dns/passt/pasta/slirp4netns/fuse-overlayfs. #357 and #363 are root-cause-fixed but stay open until the original reporters smoke-verify the rebuilt AppImage on the next 0.6.0 release. Outside an AppImage this is a strict no-op (same binaries, environment inherited unchanged), so the ~99% of non-AppImage installs are byte-for-byte unaffected. Seedocs/design/ROADMAP-0.6.0.mditem A. - The guest agent now survives session churn instead of dying until the next reboot. The in-guest HTTP agent (
agent.ps1, the:8765listener the host uses for windowless/exec) had exactly one autostart: anHKCU\Runentry that fires once per interactive logon, with the agent running as a child of the autologon session. When that session was torn down — RDP single-session enforcement kicking it when a FreeRDP connection arrives before rdprrap multi-session is active, or the TermService cycle during rdprrap (re)activation — the agent died with the session andHKCU\Rundid not re-fire, so the agent stayed dead until a pod reboot (/healthtimed out on a pod up for hours;pod restartrevived it). A newWinpodxAgentKeepAlivescheduled task is now the persistent watchdogHKCU\Runnever was: it runs an idempotentagent-keepalive.ps1AtLogOn and every 1 minute, which (re)launches the agent via the existinghidden-launcher.vbswrapper (no console flash) only when noagent.ps1process is running — it never kills a healthy agent. The task runs as the interactive autologon user (not SYSTEM/S4U) so the agent's/execkeeps the user's HKCU + Start Menu context that app discovery and per-user reverse-open registration depend on; the World-SID:8765urlacl reservation and the World-readableC:\OEM\agent_token.txtstay reachable unchanged. It's registered at OEM time (install.bat, OEM bundle bumped 25 → 26) and staged by the apply chain (winpodx pod apply-fixes/ migrate / guest-sync) so already-provisioned pods get it without a container recreate. A crashed-but-session-alive agent is back within ~1 min; the case of a session kick with no re-logon is prevented by the already-idempotent rdprrap activation (the keep-alive's 1-minute repetition is also the backstop after a TermService cycle settles). install.shnow installs the reverse-open icon dependencies, so far fewer apps get a placeholder icon. Thecurl | bashinstaller runs winpodx under the systempython3without a venv, so winpodx's declared icon deps —cairosvg(SVG → PNG) andpyxdg(full freedesktop icon-theme resolution) — weren't present, and every Linux app whose icon is an SVG or lives in a non-Hicolor theme (Papirus, breeze, …) fell back to a generic placeholder in the Windows "Open with" menu (e.g. 13 of 18 placeholders in one smoke were just missingcairosvg). The installer now installspython3-cairosvg+python3-pyxdgvia the distro package manager when they're missing — best-effort and non-fatal (apps still launch; the feature degrades to a placeholder if the packages or sudo aren't available). The AppImage already bundles both, so it was never affected.- Guest sync no longer flashes console windows or strands the post-upgrade chain. When guest sync first actually fires (e.g. a 0.5.8 → 0.5.9 upgrade — stamp older than host), its OEM-pull / urlacl / agent-restart steps went through FreeRDP RemoteApp (
run_in_windows), which pops a visible PowerShell/console window per call — the console-flash regression. They now ride the windowless agent/execchannel (run_via_transport) like the rest of guest sync; the agent is up for all three (the restart's/execreturns before its scheduled task kills the agent). Separately, the agent restart was fire-and-forget, so the install's downstream work (migrate apply chain, app discovery, reverse-open) raced the relaunch, found the agent unreachable, and degraded to a pending-resume.sync_guestnow waits — generously, bounded — for the agent's/healthto answer again after the restart before returning, so those steps see a live agent. Pending-resume stays the backstop if the relaunch is unusually slow. - Reverse-open icons that only ship as a multi-colour XPM (e.g. veracrypt) no longer fall back to a blank placeholder. Pillow's bundled XPM decoder only handles one char per pixel (≤256 colours), so veracrypt's icon — its only icon is
/usr/share/pixmaps/veracrypt.xpmat 1770 colours / 2 chars per pixel — raisedKeyErrorand the app got a generic placeholder in the Windows "Open with" menu. winpodx now decodes such XPMs with a small pure-Python reader (parse the colour table + pixel rows, build the RGBA image directly), so those apps get their real icon. No new dependency and no external tool — it works identically on every install method including the AppImage. The remaining placeholder case is genuinely unavoidable (an app that ships no icon at all).
Contributors
Thanks to everyone who reported the issues addressed in this release: @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) — and @ismikes for a code contribution (#423).