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
592 lines
26 KiB
Bash
Executable File
592 lines
26 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# SPDX-License-Identifier: MIT
|
|
set -euo pipefail
|
|
|
|
# Suppress auto-spawn of the system tray during uninstall. install.sh
|
|
# uses a marker file for this; uninstall.sh uses an env var instead
|
|
# because the marker would block tray spawn far longer than we want
|
|
# (uninstall is expected to be short, and leaving a stale marker would
|
|
# leak into the next install / GUI session). Every ``winpodx`` CLI call
|
|
# this script makes (host-open stop-listener, unregister-guest, etc.)
|
|
# inherits the env, so ``maybe_spawn_tray`` short-circuits cleanly --
|
|
# without this the section-0a pkill below was promptly undone by the
|
|
# next CLI subcommand auto-spawning a fresh tray.
|
|
export WINPODX_NO_TRAY_SPAWN=1
|
|
|
|
###############################################################################
|
|
# winpodx uninstaller -- single source of truth for every install channel.
|
|
#
|
|
# Usage:
|
|
# ./uninstall.sh # Interactive: asks before each step, keeps container + config
|
|
# ./uninstall.sh --yes # Non-interactive: auto-yes, keeps container + config
|
|
# ./uninstall.sh --purge # Non-interactive: full wipe (container, volumes, config)
|
|
# ./uninstall.sh --purge --yes # Same as --purge (purge implies --yes)
|
|
#
|
|
# One-liner (curl | bash):
|
|
# curl -fsSL https://raw.githubusercontent.com/kernalix7/winpodx/main/uninstall.sh | bash -s -- --yes
|
|
# curl -fsSL https://raw.githubusercontent.com/kernalix7/winpodx/main/uninstall.sh | bash -s -- --purge
|
|
#
|
|
# --yes or --purge is required when piping -- interactive prompts cannot
|
|
# read from a terminal while bash is consuming stdin from curl.
|
|
#
|
|
# Internal flag (set by package post-remove hooks, not for end users):
|
|
# --from-postrm Re-entered from a deb/rpm/aur postrm hook. Skips the
|
|
# install-source detect step (avoids re-exec'ing the
|
|
# package manager that's already removing us) and skips
|
|
# reverse-open daemon calls (the winpodx binary may
|
|
# already be gone at this point).
|
|
#
|
|
# Env overrides (for non-default deployments; rarely needed):
|
|
# WINPODX_CONTAINER_NAME default: winpodx-windows
|
|
# WINPODX_STORAGE_PATH default: ~/.local/share/winpodx/storage
|
|
# WINPODX_BACKEND default: auto-detect (podman > docker)
|
|
###############################################################################
|
|
|
|
GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m'
|
|
log() { echo -e "${GREEN}[WinPodX]${NC} $*"; }
|
|
warn() { echo -e "${YELLOW}[warn]${NC} $*"; }
|
|
|
|
AUTO=false
|
|
PURGE=false
|
|
FROM_POSTRM=false
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--yes|--confirm) AUTO=true; shift ;;
|
|
--purge) PURGE=true; AUTO=true; shift ;;
|
|
--from-postrm) FROM_POSTRM=true; AUTO=true; shift ;;
|
|
-h|--help) sed -n '/^# Usage/,/^###/p' "$0" | sed 's/^# \?//'; exit 0 ;;
|
|
*) echo "Unknown flag: $1" >&2
|
|
echo "Usage: $0 [--yes] [--purge] [--from-postrm]" >&2
|
|
exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
# Env-driven config overrides. Defaults match Config()'s factory defaults
|
|
# in src/winpodx/core/config.py so the common case Just Works without
|
|
# parsing the user's winpodx.toml from bash.
|
|
CONTAINER_NAME="${WINPODX_CONTAINER_NAME:-winpodx-windows}"
|
|
STORAGE_PATH="${WINPODX_STORAGE_PATH:-}"
|
|
if [[ -z "$STORAGE_PATH" ]]; then
|
|
# Resolve the ACTUAL configured storage path, not just the default: a user
|
|
# who relocated the VM with `--storage-dir` has their disk elsewhere, and
|
|
# both the #716 non-purge guard and the --purge wipe must act on the real
|
|
# location. Best-effort TOML parse of pod.storage_path; fall back to default.
|
|
_cfg="${XDG_CONFIG_HOME:-$HOME/.config}/winpodx/winpodx.toml"
|
|
if [[ -f "$_cfg" ]]; then
|
|
STORAGE_PATH="$(sed -n \
|
|
's/^[[:space:]]*storage_path[[:space:]]*=[[:space:]]*"\(.*\)"[[:space:]]*$/\1/p' \
|
|
"$_cfg" 2>/dev/null | head -n1)"
|
|
fi
|
|
STORAGE_PATH="${STORAGE_PATH:-$HOME/.local/share/winpodx/storage}"
|
|
fi
|
|
# Harden against a bogus STORAGE_PATH defeating the #716 nesting guard: a
|
|
# relative path or a literal "~/..." (env-set, unexpanded) would not match the
|
|
# DATA_DIR-prefix test and could re-enable rm -rf of the VM disk. Require an
|
|
# absolute path; otherwise ignore it and use the safe default.
|
|
if [[ "$STORAGE_PATH" != /* ]]; then
|
|
STORAGE_PATH="$HOME/.local/share/winpodx/storage"
|
|
fi
|
|
BACKEND_OVERRIDE="${WINPODX_BACKEND:-}"
|
|
|
|
ask() {
|
|
if [[ "$AUTO" == true ]]; then return 0; fi
|
|
echo -n " $1 (y/N): "
|
|
read -r answer
|
|
[[ "$answer" =~ ^[Yy] ]]
|
|
}
|
|
|
|
# Locate the winpodx binary across all install topologies. Used by
|
|
# section 0b (reverse-open teardown). Empty if not installed at all or
|
|
# if the binary was removed by the package manager before postrm fired.
|
|
find_winpodx_bin() {
|
|
local cand
|
|
for cand in "$HOME/.local/bin/winpodx" "/usr/bin/winpodx" "/usr/local/bin/winpodx"; do
|
|
if [[ -x "$cand" ]]; then echo "$cand"; return 0; fi
|
|
done
|
|
command -v winpodx 2>/dev/null || true
|
|
}
|
|
|
|
# Detect which package manager owns the winpodx binary, if any. Echoes
|
|
# one of:
|
|
# apt|<pkg-name>|<sudo-removal-command>
|
|
# dnf|<pkg-name>|<sudo-removal-command>
|
|
# zypper|<pkg-name>|<sudo-removal-command>
|
|
# pacman|<pkg-name>|<sudo-removal-command>
|
|
# curl|| (curl-installed bundle present)
|
|
# unknown|| (pip / source / unknown)
|
|
detect_install_source() {
|
|
local bin
|
|
bin="$(find_winpodx_bin)"
|
|
|
|
if [[ -n "$bin" ]]; then
|
|
local resolved
|
|
resolved="$(readlink -f "$bin" 2>/dev/null || echo "$bin")"
|
|
|
|
if command -v dpkg >/dev/null 2>&1; then
|
|
local pkg
|
|
pkg="$(dpkg -S "$resolved" 2>/dev/null | head -n1 | cut -d: -f1 || true)"
|
|
if [[ -n "$pkg" && "$pkg" != *"no path found"* ]]; then
|
|
echo "apt|$pkg|sudo apt-get remove $pkg"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if command -v rpm >/dev/null 2>&1; then
|
|
local pkg
|
|
pkg="$(rpm -qf --queryformat '%{NAME}' "$resolved" 2>/dev/null || true)"
|
|
if [[ -n "$pkg" && "$pkg" != *"not owned"* && "$pkg" != *"file "* ]]; then
|
|
local mgr="dnf"
|
|
if grep -qiE '^ID=.*(opensuse|suse|sles)' /etc/os-release 2>/dev/null; then
|
|
mgr="zypper"
|
|
fi
|
|
echo "$mgr|$pkg|sudo $mgr remove $pkg"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if command -v pacman >/dev/null 2>&1; then
|
|
local pkg
|
|
pkg="$(pacman -Qo "$resolved" 2>/dev/null | awk '{print $(NF-1)}' || true)"
|
|
if [[ -n "$pkg" && "$pkg" != "winpodx" && "$pkg" != *"No package"* ]]; then
|
|
# awk strip can drop the package name on some pacman outputs;
|
|
# fall back to the literal package name if our parse is wrong.
|
|
if ! pacman -Q "$pkg" >/dev/null 2>&1; then pkg="winpodx"; fi
|
|
echo "pacman|$pkg|sudo pacman -Rns $pkg"
|
|
return 0
|
|
elif [[ "$pkg" == "winpodx" ]]; then
|
|
echo "pacman|winpodx|sudo pacman -Rns winpodx"
|
|
return 0
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Not owned by any package manager. Check for the curl-install
|
|
# bundle directory as a positive signal -- the symlink alone is not
|
|
# enough (we may have already removed the symlink in a prior run).
|
|
if [[ -d "$HOME/.local/bin/winpodx-app" ]]; then
|
|
echo "curl||"
|
|
return 0
|
|
fi
|
|
|
|
# pip / source install detection: resolved binary lands in a
|
|
# site-packages tree (system or venv) or inside a /src/winpodx/
|
|
# dev checkout. Heuristic only -- no package manager owns the
|
|
# file, and we don't know the venv path to drive `pip uninstall`
|
|
# for the user, so we just print the canonical command as a hint.
|
|
if [[ -n "$bin" ]]; then
|
|
local resolved
|
|
resolved="$(readlink -f "$bin" 2>/dev/null || echo "$bin")"
|
|
if [[ "$resolved" == *site-packages* ]] || [[ "$resolved" == */src/winpodx/* ]]; then
|
|
echo "pip||pip uninstall winpodx"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
echo "unknown||"
|
|
}
|
|
|
|
echo ""
|
|
echo "=========================================="
|
|
echo " WinPodX uninstaller"
|
|
echo "=========================================="
|
|
if [[ "$PURGE" == true ]]; then
|
|
echo " Mode: FULL PURGE (container + volumes + storage + config + files)"
|
|
else
|
|
echo " Mode: WinPodX files only (container + volumes + config kept)"
|
|
fi
|
|
[[ "$FROM_POSTRM" == true ]] && echo " (re-entered from package post-remove hook)"
|
|
echo ""
|
|
|
|
REMOVED=0
|
|
|
|
# Detect runtime (podman preferred, fallback to docker, or env override).
|
|
RUNTIME=""
|
|
if [[ -n "$BACKEND_OVERRIDE" ]] && command -v "$BACKEND_OVERRIDE" >/dev/null 2>&1; then
|
|
RUNTIME="$BACKEND_OVERRIDE"
|
|
elif command -v podman >/dev/null 2>&1; then
|
|
RUNTIME="podman"
|
|
elif command -v docker >/dev/null 2>&1; then
|
|
RUNTIME="docker"
|
|
fi
|
|
|
|
# --- Install-source detect + package-manager-first ordering ---
|
|
# If installed via a package manager, the *correct* sequence is:
|
|
# 1. sudo apt remove winpodx (or dnf/zypper/pacman equivalent)
|
|
# 2. post-remove hook fires
|
|
# 3. hook calls /usr/share/winpodx/uninstall.sh --from-postrm [--purge] --yes
|
|
# 4. user-side cleanup runs against still-present $HOME state
|
|
# Running the user cleanup first would leave the dpkg/rpm db inconsistent
|
|
# (db says files exist, disk says otherwise) and postrm may fail to
|
|
# locate the binary it tries to invoke.
|
|
#
|
|
# When --from-postrm is set we skip this block (we ARE the postrm
|
|
# re-entry) and proceed directly to user-side cleanup.
|
|
if [[ "$FROM_POSTRM" != true ]]; then
|
|
SRC="$(detect_install_source)"
|
|
SRC_KIND="${SRC%%|*}"
|
|
case "$SRC_KIND" in
|
|
apt|dnf|zypper|pacman)
|
|
SRC_PKG="$(echo "$SRC" | cut -d'|' -f2)"
|
|
SRC_CMD="$(echo "$SRC" | cut -d'|' -f3)"
|
|
echo " Install source: $SRC_KIND ($SRC_PKG)"
|
|
echo ""
|
|
echo " Recommended order:"
|
|
echo " 1. Remove the package via the system package manager."
|
|
echo " 2. Its post-remove hook will re-run this script for user-side cleanup."
|
|
echo ""
|
|
if ask "Run now: $SRC_CMD ?"; then
|
|
# Forward purge intent so postrm-common.sh re-invokes
|
|
# uninstall.sh with --purge.
|
|
if [[ "$PURGE" == true ]]; then
|
|
export WINPODX_PURGE_REQUESTED=1
|
|
fi
|
|
log "Handing off to package manager: $SRC_CMD"
|
|
# shellcheck disable=SC2086
|
|
exec $SRC_CMD
|
|
else
|
|
warn "Package not removed. Continuing with user-side cleanup only."
|
|
warn " (You can run '$SRC_CMD' later to remove the package itself.)"
|
|
echo ""
|
|
fi
|
|
;;
|
|
pip)
|
|
SRC_CMD="$(echo "$SRC" | cut -d'|' -f3)"
|
|
echo " Install source: pip / source (site-packages or dev checkout)"
|
|
echo ""
|
|
echo " This script will clean up user-side state (containers, configs,"
|
|
echo " desktop entries, launchers). To remove the winpodx Python package"
|
|
echo " itself, run after this script finishes:"
|
|
echo " $SRC_CMD"
|
|
echo ""
|
|
;;
|
|
curl|unknown)
|
|
: # No package manager involved; proceed.
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# --- 0a. Stop running winpodx processes (tray + GUI + any helper) ---
|
|
# Both GUI and tray hold open file handles into
|
|
# ~/.local/bin/winpodx-app/ and the runtime / config directories we're
|
|
# about to remove. The tray additionally owns the flock under
|
|
# $XDG_RUNTIME_DIR/winpodx/tray.lock and drives UNRESPONSIVE recovery
|
|
# notifications -- leaving it alive across the uninstall surfaces
|
|
# recovery-attempt notifications fire against a now-gone container.
|
|
#
|
|
# Uninstall is intentional + user-initiated, so kill every winpodx
|
|
# Python process broadly. Three launcher cmdline shapes exist in the
|
|
# wild:
|
|
# (a) install.sh wrapper: python -m winpodx tray
|
|
# (b) pip / venv entry pt: python /.../venv/bin/winpodx tray
|
|
# (c) source path-style: python /.../src/winpodx/__main__.py tray
|
|
# Common substring: "python" ... "winpodx" ... <subcommand>. The
|
|
# pattern below matches all three. False positives (e.g. pytest
|
|
# tests/test_winpodx_*.py) are acceptable -- uninstall is explicit
|
|
# and ``--purge`` is the expected mode, so over-killing is the
|
|
# safer failure shape than leaving FDs open into the dir we're
|
|
# about to rm -rf.
|
|
#
|
|
# Listing the matched pids before the kill makes the action
|
|
# observable so a surprise hit is at least obvious in the output.
|
|
WINPODX_PROCS=$(pgrep -fa 'python.*winpodx' 2>/dev/null || true)
|
|
if [[ -n "$WINPODX_PROCS" ]]; then
|
|
log "Stopping WinPodX processes:"
|
|
while IFS= read -r line; do
|
|
log " $line"
|
|
done <<<"$WINPODX_PROCS"
|
|
pkill -f 'python.*winpodx' 2>/dev/null || true
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
# Brief grace so the killed processes release their file handles
|
|
# before later steps rm -rf their install directory.
|
|
sleep 1
|
|
|
|
# --- 0b. Reverse-open teardown (BEFORE container removal) ---
|
|
# Stops the host-side listener daemon so the runtime/winpodx/ cleanup
|
|
# below doesn't leave an orphan process when the pid file is deleted.
|
|
#
|
|
# Guest-side registry scrub (unregister-apps.ps1) only runs in non-
|
|
# purge mode -- when --purge is set, the container is destroyed in
|
|
# step 1 below and the HKCU entries vanish with it. Calling the agent
|
|
# in that case is wasted latency (and can hang if the agent isn't
|
|
# reachable, slowing down the uninstall path with no payoff).
|
|
#
|
|
# Skip this entire block when --from-postrm: the package manager has
|
|
# already removed the winpodx binary, and the listener process was
|
|
# already killed by the pkill above.
|
|
if [[ "$FROM_POSTRM" != true ]]; then
|
|
WINPODX_BIN="$(find_winpodx_bin)"
|
|
if [[ -n "$WINPODX_BIN" ]]; then
|
|
if "$WINPODX_BIN" host-open daemon-status --json 2>/dev/null | grep -q '"running": true'; then
|
|
log "Stopping host-side reverse-open listener..."
|
|
"$WINPODX_BIN" host-open stop-listener 2>/dev/null || true
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
# Skip the guest scrub on --purge: container teardown below
|
|
# destroys the registry anyway.
|
|
if [[ "$PURGE" != true ]] && [[ -n "$RUNTIME" ]] && \
|
|
$RUNTIME ps --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER_NAME"; then
|
|
log "Scrubbing reverse-open registry entries on the guest..."
|
|
"$WINPODX_BIN" host-open unregister-guest 2>/dev/null | sed 's/^/ /' || \
|
|
warn " guest scrub skipped (agent unreachable)"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- 1. Container (always) + Volumes + Storage bind-mount (purge only) ---
|
|
if [[ -n "$RUNTIME" ]]; then
|
|
if $RUNTIME ps -a --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER_NAME"; then
|
|
if [[ "$PURGE" == true ]]; then
|
|
log "Stopping and removing container ($RUNTIME)..."
|
|
$RUNTIME stop "$CONTAINER_NAME" 2>/dev/null || true
|
|
$RUNTIME rm "$CONTAINER_NAME" 2>/dev/null || true
|
|
else
|
|
log "Stopping container (keeping disk for re-install): $CONTAINER_NAME"
|
|
$RUNTIME stop "$CONTAINER_NAME" 2>/dev/null || true
|
|
fi
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
|
|
if [[ "$PURGE" == true ]]; then
|
|
for vol in $($RUNTIME volume ls --format "{{.Name}}" 2>/dev/null | grep '^winpodx' || true); do
|
|
log "Removing volume: $vol"
|
|
$RUNTIME volume rm "$vol" 2>/dev/null || true
|
|
REMOVED=$((REMOVED + 1))
|
|
done
|
|
fi
|
|
else
|
|
warn "Neither podman nor docker found; skipping container cleanup"
|
|
fi
|
|
|
|
# Storage bind-mount wipe (purge only). The Windows disk image and any
|
|
# guest-side scratch live under this directory when dockur's STORAGE is
|
|
# bind-mounted from the host (the default for most install topologies).
|
|
if [[ "$PURGE" == true ]] && [[ -d "$STORAGE_PATH" ]]; then
|
|
log "Wiping storage bind-mount contents: $STORAGE_PATH"
|
|
rm -rf "${STORAGE_PATH:?}"/* "${STORAGE_PATH:?}"/.[!.]* 2>/dev/null || true
|
|
rmdir "$STORAGE_PATH" 2>/dev/null || true
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
|
|
# --- 2. Desktop entries ---
|
|
DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
|
# Remove winpodx GUI launcher
|
|
if [[ -f "$DESKTOP_DIR/winpodx.desktop" ]]; then
|
|
rm -f "$DESKTOP_DIR/winpodx.desktop"
|
|
log "Removed WinPodX GUI launcher"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
# Remove app desktop entries. Guard the dir: under `set -euo pipefail` a find
|
|
# over a missing DESKTOP_DIR exits non-zero and, via pipefail, aborts the whole
|
|
# uninstall mid-cleanup (leaving a half-removed state) — seen in the #716 smoke.
|
|
if [[ -d "$DESKTOP_DIR" ]]; then
|
|
DESKTOP_COUNT=$(find "$DESKTOP_DIR" -maxdepth 1 -name "winpodx-*.desktop" 2>/dev/null | wc -l)
|
|
else
|
|
DESKTOP_COUNT=0
|
|
fi
|
|
if [[ "$DESKTOP_COUNT" -gt 0 ]]; then
|
|
if ask "Remove $DESKTOP_COUNT app desktop entries?"; then
|
|
rm -f "$DESKTOP_DIR"/winpodx-*.desktop
|
|
log "Removed $DESKTOP_COUNT app desktop entries"
|
|
REMOVED=$((REMOVED + DESKTOP_COUNT))
|
|
fi
|
|
fi
|
|
# Remove the consolidated "winpodx" menu folder definitions (desktop/menu.py
|
|
# writes both on every app install). Without this an empty "WinPodX (Windows
|
|
# Apps)" submenu lingers in KDE/XFCE/Cinnamon/MATE/LXQt after uninstall, since
|
|
# the .menu fragment auto-merges into applications.menu. Always remove -- both
|
|
# files are winpodx-specific and useless without the app entries.
|
|
MENU_DIRFILE="${XDG_DATA_HOME:-$HOME/.local/share}/desktop-directories/winpodx-windows.directory"
|
|
MENU_FRAGMENT="${XDG_CONFIG_HOME:-$HOME/.config}/menus/applications-merged/winpodx.menu"
|
|
for mf in "$MENU_DIRFILE" "$MENU_FRAGMENT"; do
|
|
if [[ -e "$mf" ]]; then
|
|
rm -f "$mf"
|
|
log "Removed $mf"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
done
|
|
# Update desktop database
|
|
if command -v update-desktop-database >/dev/null 2>&1; then
|
|
update-desktop-database "$DESKTOP_DIR" 2>/dev/null || true
|
|
fi
|
|
|
|
# --- 3. Icons ---
|
|
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor"
|
|
if [[ -d "$ICON_DIR" ]]; then
|
|
ICON_COUNT=$(find "$ICON_DIR" \( -name "winpodx-*" -o -name "winpodx.svg" \) 2>/dev/null | wc -l)
|
|
if [[ "$ICON_COUNT" -gt 0 ]]; then
|
|
if ask "Remove $ICON_COUNT icons?"; then
|
|
find "$ICON_DIR" \( -name "winpodx-*" -o -name "winpodx.svg" \) -delete 2>/dev/null || true
|
|
log "Removed $ICON_COUNT icons"
|
|
REMOVED=$((REMOVED + ICON_COUNT))
|
|
fi
|
|
fi
|
|
# Refresh icon cache
|
|
if command -v gtk-update-icon-cache >/dev/null 2>&1; then
|
|
gtk-update-icon-cache -f -t "$ICON_DIR" 2>/dev/null || true
|
|
fi
|
|
# Rebuild KDE Plasma sycoca cache
|
|
kbuildsycoca6 --noincremental 2>/dev/null || kbuildsycoca5 --noincremental 2>/dev/null || true
|
|
fi
|
|
|
|
# --- 4. MIME associations ---
|
|
MIMEINFO="${XDG_DATA_HOME:-$HOME/.local/share}/applications/mimeinfo.cache"
|
|
if [[ -f "$MIMEINFO" ]] && grep -q "winpodx" "$MIMEINFO" 2>/dev/null; then
|
|
sed -i '/winpodx/d' "$MIMEINFO" 2>/dev/null || true
|
|
log "Cleaned WinPodX MIME associations"
|
|
fi
|
|
# mimeapps.list keeps [Default Applications] / [Added Associations] entries
|
|
# pointing at winpodx-*.desktop (written by `winpodx app install --mime` and
|
|
# `winpodx doctor --fix` via xdg-mime default). Left behind they dangle at
|
|
# removed handlers. Drop any line that references a winpodx handler.
|
|
MIMEAPPS="${XDG_CONFIG_HOME:-$HOME/.config}/mimeapps.list"
|
|
if [[ -f "$MIMEAPPS" ]] && grep -q "winpodx-" "$MIMEAPPS" 2>/dev/null; then
|
|
sed -i '/winpodx-/d' "$MIMEAPPS" 2>/dev/null || true
|
|
log "Cleaned WinPodX entries from mimeapps.list"
|
|
fi
|
|
|
|
# --- 5. App definitions ---
|
|
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/winpodx"
|
|
if [[ -d "$DATA_DIR" ]]; then
|
|
if ask "Remove app definitions ($DATA_DIR)?"; then
|
|
# DATA-LOSS GUARD (#716): the default VM storage lives *under* DATA_DIR
|
|
# (~/.local/share/winpodx/storage/data.img). A non-purge uninstall is
|
|
# documented to KEEP the Windows VM data, so it must never rm -rf the
|
|
# parent DATA_DIR out from under the storage subtree. When STORAGE_PATH
|
|
# is DATA_DIR itself or nested beneath it and we are NOT purging,
|
|
# preserve the top-level storage component and remove only the rest.
|
|
_canon() { readlink -f "$1" 2>/dev/null || echo "${1%/}"; }
|
|
_dd="$(_canon "$DATA_DIR")"
|
|
_sp="$(_canon "$STORAGE_PATH")"
|
|
if [[ "$PURGE" != true ]] && { [[ "$_sp" == "$_dd" ]] || [[ "$_sp" == "$_dd"/* ]]; }; then
|
|
# First path component of STORAGE_PATH relative to DATA_DIR (e.g.
|
|
# "storage"). Keep that whole top-level entry; delete DATA_DIR's
|
|
# other top-level children (apps/, run/, icons cache, …).
|
|
_rel="${_sp#"$_dd"/}"
|
|
_keep="${_rel%%/*}"
|
|
if [[ "$_sp" == "$_dd" ]] || [[ -z "$_keep" ]]; then
|
|
# STORAGE_PATH == DATA_DIR: nothing safe to delete recursively.
|
|
log "Kept $DATA_DIR (holds the Windows VM storage; use --purge to wipe)"
|
|
else
|
|
# Belt-and-braces: always also preserve a top-level "storage"
|
|
# entry, not just the computed $_keep — if STORAGE_PATH ever
|
|
# resolves wrong, the conventional default VM disk still survives
|
|
# a non-purge uninstall.
|
|
find "$DATA_DIR" -mindepth 1 -maxdepth 1 \
|
|
! -name "$_keep" ! -name "storage" \
|
|
-exec rm -rf {} + 2>/dev/null || true
|
|
log "Removed $DATA_DIR contents (preserved VM storage: $STORAGE_PATH)"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
else
|
|
rm -rf "$DATA_DIR"
|
|
log "Removed $DATA_DIR"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# --- 6. Runtime PID files ---
|
|
RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/winpodx"
|
|
if [[ -d "$RUNTIME_DIR" ]]; then
|
|
rm -rf "$RUNTIME_DIR"
|
|
log "Removed runtime files"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
|
|
# --- 6b. Autostart entry (XDG ~/.config/autostart/winpodx-tray.desktop) ---
|
|
# The Settings-page "Launch winpodx tray at login" checkbox writes this
|
|
# file via the XDG autostart spec; leaving it around after uninstall
|
|
# means the user gets a "winpodx tray" command-not-found at next login.
|
|
# Always remove regardless of purge mode -- the .desktop is winpodx-
|
|
# specific and useless without the binary.
|
|
AUTOSTART_DESKTOP="${XDG_CONFIG_HOME:-$HOME/.config}/autostart/winpodx-tray.desktop"
|
|
if [[ -e "$AUTOSTART_DESKTOP" ]]; then
|
|
rm -f "$AUTOSTART_DESKTOP"
|
|
log "Removed $AUTOSTART_DESKTOP"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
|
|
# --- 7. Config (purge only) ---
|
|
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/winpodx"
|
|
if [[ -d "$CONFIG_DIR" ]]; then
|
|
if [[ "$PURGE" == true ]]; then
|
|
rm -rf "$CONFIG_DIR"
|
|
log "Removed $CONFIG_DIR"
|
|
REMOVED=$((REMOVED + 1))
|
|
else
|
|
warn "Config preserved at $CONFIG_DIR (use --purge to remove)"
|
|
fi
|
|
fi
|
|
|
|
# --- 7b. KVM module-load drop-in (purge only) ---
|
|
# install.sh (#541) writes /etc/modules-load.d/winpodx-kvm.conf via sudo when
|
|
# /dev/kvm is missing but the CPU supports virtualization. It's a winpodx-
|
|
# created root-owned file, so a full --purge should remove it. Best-effort:
|
|
# needs sudo and only loads a stock kernel module, so a non-purge run leaves it.
|
|
KVM_MODCONF="/etc/modules-load.d/winpodx-kvm.conf"
|
|
if [[ "$PURGE" == true && -e "$KVM_MODCONF" ]]; then
|
|
if sudo rm -f "$KVM_MODCONF" 2>/dev/null; then
|
|
log "Removed $KVM_MODCONF"
|
|
REMOVED=$((REMOVED + 1))
|
|
else
|
|
warn "Could not remove $KVM_MODCONF (needs sudo); remove it manually if desired"
|
|
fi
|
|
fi
|
|
|
|
# --- 8. Curl-install bundle dir + launcher symlinks (~/.local/bin) ---
|
|
# These exist only when installed via curl install.sh. On package
|
|
# installs, $HOME/.local/bin/winpodx-app/ doesn't exist and the
|
|
# symlinks point at /usr/bin (or wherever the package manager put
|
|
# them) -- those are owned by the package and will be removed by it,
|
|
# not by us. The conditionals below make this a no-op on package
|
|
# installs without special-casing.
|
|
INSTALL_DIR="$HOME/.local/bin/winpodx-app"
|
|
if [[ -d "$INSTALL_DIR" ]]; then
|
|
if ask "Remove curl-install bundle ($INSTALL_DIR)?"; then
|
|
rm -rf "$INSTALL_DIR"
|
|
log "Removed $INSTALL_DIR"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
fi
|
|
for f in "$HOME/.local/bin/winpodx-run" "$HOME/.local/bin/winpodx"; do
|
|
# Only touch symlinks or files clearly owned by curl install (the
|
|
# winpodx-run wrapper script). Don't break a system-wide install
|
|
# by removing /usr/bin/winpodx -- those paths aren't in this list.
|
|
if [[ -L "$f" || -e "$f" ]]; then
|
|
rm -f "$f"
|
|
log "Removed $f"
|
|
REMOVED=$((REMOVED + 1))
|
|
fi
|
|
done
|
|
|
|
# --- Final tray / GUI sweep ---
|
|
# Belt-and-braces against tray respawn paths the section-0a kill +
|
|
# WINPODX_NO_TRAY_SPAWN env didn't cover. Examples:
|
|
# - User had a GUI window open in another terminal that spawned
|
|
# a tray via maybe_spawn_tray() *before* uninstall.sh started.
|
|
# - A KDE/GNOME autostart-triggered launch raced the env export.
|
|
# - dbus-activated launch path bypasses cli/main.py's spawn check.
|
|
# Section 0a only sweeps what's alive at the start of the script;
|
|
# this final pass catches anything that came up during the 10-30 s
|
|
# uninstall window. Quiet by default -- absence of victims is
|
|
# normal and noisy logging would distract from the summary below.
|
|
pkill -f 'python.*winpodx' 2>/dev/null || true
|
|
pkill -f 'winpodx-app' 2>/dev/null || true
|
|
|
|
# --- Summary ---
|
|
echo ""
|
|
if [[ "$PURGE" != true ]]; then
|
|
if [[ -n "$RUNTIME" ]] && $RUNTIME ps -a --format "{{.Names}}" 2>/dev/null | grep -q "$CONTAINER_NAME"; then
|
|
echo " Container '$CONTAINER_NAME' was kept (re-install will reuse the disk)."
|
|
echo " To remove it too: $0 --purge"
|
|
echo ""
|
|
fi
|
|
fi
|
|
if [[ "$FROM_POSTRM" != true ]]; then
|
|
echo " NOT removed: system packages (podman, freerdp, python3)"
|
|
echo ""
|
|
fi
|
|
log "Uninstall complete ($REMOVED items removed)"
|