diff --git a/scripts/uninstall.ps1 b/scripts/uninstall.ps1 index 9b6e6ebb86..7fc5efab4c 100644 --- a/scripts/uninstall.ps1 +++ b/scripts/uninstall.ps1 @@ -3,8 +3,10 @@ # # Unsloth Studio uninstaller for Windows PowerShell. # Stops running servers and removes install dir, launcher data, CLI shim, -# desktop and Start Menu shortcuts, the user PATH entry, and the PathBackup -# registry key. Honors custom roots set via UNSLOTH_STUDIO_HOME / STUDIO_HOME +# desktop and Start Menu shortcuts, the user PATH entry, the PathBackup +# registry key, and WebView2 runtime data keyed by the app bundle id +# (EBWebView caches created at first desktop-app launch, not by install.ps1). +# Honors custom roots set via UNSLOTH_STUDIO_HOME / STUDIO_HOME # at install time (read back from share\studio.conf). # # Usage: irm https://raw.githubusercontent.com/unslothai/unsloth/main/scripts/uninstall.ps1 | iex @@ -398,6 +400,32 @@ function Uninstall-UnslothStudio { _RemovePath $defaultUnslothHome } + # WebView2/app runtime data keyed by the Tauri bundle id. Created at first + # desktop-app launch, not by install.ps1. LOCALAPPDATA holds the EBWebView + # profile (a leftover copy serves a stale frontend to the next install); + # APPDATA holds the app config dir. + _Step "Removing WebView caches and app data (ai.unsloth.studio)..." + $bundleId = "ai.unsloth.studio" + $webviewDataDirs = @() + if ($env:LOCALAPPDATA) { $webviewDataDirs += Join-Path $env:LOCALAPPDATA $bundleId } + if ($env:APPDATA) { $webviewDataDirs += Join-Path $env:APPDATA $bundleId } + # Stop the desktop app and any msedgewebview2.exe helper using these dirs + # first; WebView2 keeps open handles that make the delete fail. + try { Stop-Process -Name "unsloth-studio" -Force -ErrorAction SilentlyContinue } catch { } + try { + foreach ($proc in (Get-CimInstance Win32_Process -Filter "Name = 'msedgewebview2.exe'" -ErrorAction SilentlyContinue)) { + $cl = $proc.CommandLine + if (-not $cl) { continue } + foreach ($d in $webviewDataDirs) { + if ($cl -ilike "*$d*") { + try { Stop-Process -Id $proc.ProcessId -Force -ErrorAction SilentlyContinue } catch { } + break + } + } + } + } catch { } + foreach ($d in $webviewDataDirs) { _RemovePath $d } + # ── Remove desktop and Start Menu shortcuts ── _Step "Removing desktop and Start Menu shortcuts..." try { diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index 957d2b7af2..303b5225dc 100755 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -4,7 +4,9 @@ # # Unsloth Studio uninstaller (macOS / Linux / WSL). # Stops running servers and removes install dir, launcher data, -# CLI shim, desktop shortcut, .app bundle, and Launch Services entry. +# CLI shim, desktop shortcut, .app bundle, Launch Services entry, and +# WebView runtime data keyed by the app bundle id (WebKit/webkit2gtk +# caches created at first app launch, not by install.sh). # Honors custom roots set via UNSLOTH_STUDIO_HOME / STUDIO_HOME at # install time (read back from studio.conf). # @@ -80,6 +82,13 @@ $_roots_from_conf" pkill -KILL -f "$_pat" 2>/dev/null || true done done + + # Tauri desktop app (binary "unsloth-studio"): stop it so its WebView + # helpers can't hold or re-create the runtime caches removed below. + # -x is an exact name match, so the "unsloth" CLI shim is never touched. + pkill -TERM -x unsloth-studio 2>/dev/null || true + sleep 0.5 + pkill -KILL -x unsloth-studio 2>/dev/null || true } _remove_path() { @@ -261,6 +270,25 @@ case "$_os" in if [ -x "$_lsr" ]; then "$_lsr" -u "$HOME/Applications/Unsloth Studio.app" 2>/dev/null || true fi + # WebView/app runtime data keyed by the bundle id. Created by macOS + + # WKWebView at first app launch, not by install.sh, so it survives an + # uninstall and a leftover WebKit cache then serves a stale frontend + # to the next install. + _bid="ai.unsloth.studio" + echo "Removing WebView caches and app data ($_bid)..." + _remove_path "$HOME/Library/Caches/$_bid" + _remove_path "$HOME/Library/WebKit/$_bid" + _remove_path "$HOME/Library/Application Support/$_bid" + _remove_path "$HOME/Library/HTTPStorages/$_bid" + _remove_path "$HOME/Library/HTTPStorages/$_bid.binarycookies" + _remove_path "$HOME/Library/Cookies/$_bid.binarycookies" + _remove_path "$HOME/Library/Saved Application State/$_bid.savedState" + # Delete via defaults first: cfprefsd caches plists in memory and can + # re-write the file after a bare rm. + if command -v defaults >/dev/null 2>&1; then + defaults delete "$_bid" >/dev/null 2>&1 || true + fi + _remove_path "$HOME/Library/Preferences/$_bid.plist" ;; Linux) if [ "$_is_wsl" = "1" ]; then @@ -403,6 +431,15 @@ case "$_os" in echo " sudo rm -rf /opt/rocm /opt/rocm-* && sudo ldconfig" fi fi + # WebView/app runtime data keyed by the Tauri bundle id. Created by + # webkit2gtk at first desktop-app launch, not by install.sh; a + # leftover cache serves a stale frontend to the next install. + _bid="ai.unsloth.studio" + echo "Removing WebView caches and app data ($_bid)..." + _remove_path "${XDG_CACHE_HOME:-$HOME/.cache}/$_bid" + _remove_path "${XDG_DATA_HOME:-$HOME/.local/share}/$_bid" + _remove_path "${XDG_CONFIG_HOME:-$HOME/.config}/$_bid" + _remove_path "${XDG_STATE_HOME:-$HOME/.local/state}/$_bid" echo "Removing Linux .desktop entry..." _remove_path "$HOME/.local/share/applications/unsloth-studio.desktop" if command -v update-desktop-database >/dev/null 2>&1; then diff --git a/tests/run_all.sh b/tests/run_all.sh index eaa726f73c..bb62582c61 100755 --- a/tests/run_all.sh +++ b/tests/run_all.sh @@ -14,6 +14,7 @@ sh "$TESTS_DIR/sh/test_nvcc_meets_llama_minimum.sh" sh "$TESTS_DIR/sh/test_resolve_cuda_archs.sh" sh "$TESTS_DIR/sh/test_strixhalo_wsl_reroute.sh" sh "$TESTS_DIR/sh/test_uninstall_shared_icon.sh" +sh "$TESTS_DIR/sh/test_uninstall_webview_data.sh" sh "$TESTS_DIR/sh/test_torch_flavor.sh" sh "$TESTS_DIR/sh/test_redact_install_output.sh" sh "$TESTS_DIR/sh/test_install_uv_override_space.sh" diff --git a/tests/sh/test_uninstall_webview_data.sh b/tests/sh/test_uninstall_webview_data.sh new file mode 100755 index 0000000000..14898c05c2 --- /dev/null +++ b/tests/sh/test_uninstall_webview_data.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# SPDX-License-Identifier: AGPL-3.0-only +# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 +# Regression tests for WebView runtime-data cleanup in scripts/uninstall.sh. +# +# WKWebView (macOS) and webkit2gtk (Linux) create data keyed by the bundle id +# at first app launch, not at install time, so the uninstaller used to miss it +# and a leftover cache served a stale frontend to the next install. Runs the +# full script against a fixture HOME (pkill/defaults stubbed via PATH, OS +# branch picked by a stubbed uname) and asserts bundle-id paths are removed +# while unrelated app data survives. +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +UNINSTALL_SH="$SCRIPT_DIR/../../scripts/uninstall.sh" +BID="ai.unsloth.studio" +PASS=0 +FAIL=0 + +_TMP_ROOT=$(mktemp -d) +trap 'rm -rf "$_TMP_ROOT"' EXIT + +assert_gone() { _l="$1"; if [ -e "$2" ]; then echo " FAIL: $_l (still present: $2)"; FAIL=$((FAIL+1)); else echo " PASS: $_l"; PASS=$((PASS+1)); fi; } +assert_present() { _l="$1"; if [ -e "$2" ]; then echo " PASS: $_l"; PASS=$((PASS+1)); else echo " FAIL: $_l (missing: $2)"; FAIL=$((FAIL+1)); fi; } + +# Stub out process kills and macOS pref/LaunchServices tools so the script can +# run against a fixture HOME without touching the real system. +STUB_BIN="$_TMP_ROOT/stubbin" +mkdir -p "$STUB_BIN" +for _tool in pkill defaults; do + printf '#!/bin/sh\nexit 0\n' > "$STUB_BIN/$_tool" + chmod +x "$STUB_BIN/$_tool" +done + +# run_uninstall : run the full script with a stubbed OS. +run_uninstall() { + printf '#!/bin/sh\necho %s\n' "$2" > "$STUB_BIN/uname" + chmod +x "$STUB_BIN/uname" + env -u UNSLOTH_STUDIO_HOME -u STUDIO_HOME \ + -u XDG_CACHE_HOME -u XDG_DATA_HOME -u XDG_CONFIG_HOME -u XDG_STATE_HOME \ + HOME="$1" PATH="$STUB_BIN:$PATH" sh "$UNINSTALL_SH" >/dev/null 2>&1 +} + +# ── 1. macOS: every bundle-id-keyed ~/Library path is removed ── +H=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$H/Library/Caches/$BID/WebKit/NetworkCache" \ + "$H/Library/WebKit/$BID/WebsiteData/CacheStorage" \ + "$H/Library/Application Support/$BID" \ + "$H/Library/HTTPStorages/$BID" \ + "$H/Library/Saved Application State/$BID.savedState" \ + "$H/Library/Preferences" \ + "$H/Library/Cookies" \ + "$H/Library/Caches/com.other.app" +: > "$H/Library/HTTPStorages/$BID.binarycookies" +: > "$H/Library/Cookies/$BID.binarycookies" +: > "$H/Library/Preferences/$BID.plist" +: > "$H/Library/Caches/$BID/stale-frontend.js" +: > "$H/Library/Caches/com.other.app/keepme" +run_uninstall "$H" Darwin +assert_gone "macOS: Caches/$BID removed" "$H/Library/Caches/$BID" +assert_gone "macOS: WebKit/$BID removed" "$H/Library/WebKit/$BID" +assert_gone "macOS: Application Support/$BID removed" "$H/Library/Application Support/$BID" +assert_gone "macOS: HTTPStorages/$BID removed" "$H/Library/HTTPStorages/$BID" +assert_gone "macOS: HTTPStorages/$BID.binarycookies removed" "$H/Library/HTTPStorages/$BID.binarycookies" +assert_gone "macOS: Cookies/$BID.binarycookies removed" "$H/Library/Cookies/$BID.binarycookies" +assert_gone "macOS: Saved Application State removed" "$H/Library/Saved Application State/$BID.savedState" +assert_gone "macOS: Preferences/$BID.plist removed" "$H/Library/Preferences/$BID.plist" +assert_present "macOS: unrelated app cache kept" "$H/Library/Caches/com.other.app/keepme" + +# ── 2. Linux: bundle-id-keyed XDG default paths are removed ── +H=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$H/.cache/$BID" "$H/.local/share/$BID" "$H/.config/$BID" \ + "$H/.local/state/$BID" "$H/.cache/other.app" +run_uninstall "$H" Linux +assert_gone "linux: ~/.cache/$BID removed" "$H/.cache/$BID" +assert_gone "linux: ~/.local/share/$BID removed" "$H/.local/share/$BID" +assert_gone "linux: ~/.config/$BID removed" "$H/.config/$BID" +assert_gone "linux: ~/.local/state/$BID removed" "$H/.local/state/$BID" +assert_present "linux: unrelated app cache kept" "$H/.cache/other.app" + +# ── 3. Linux: XDG_*_HOME overrides are honored ── +H=$(mktemp -d -p "$_TMP_ROOT") +XDG=$(mktemp -d -p "$_TMP_ROOT") +mkdir -p "$XDG/cache/$BID" "$XDG/data/$BID" "$XDG/config/$BID" "$XDG/state/$BID" +printf '#!/bin/sh\necho Linux\n' > "$STUB_BIN/uname" +chmod +x "$STUB_BIN/uname" +env -u UNSLOTH_STUDIO_HOME -u STUDIO_HOME \ + XDG_CACHE_HOME="$XDG/cache" XDG_DATA_HOME="$XDG/data" \ + XDG_CONFIG_HOME="$XDG/config" XDG_STATE_HOME="$XDG/state" \ + HOME="$H" PATH="$STUB_BIN:$PATH" sh "$UNINSTALL_SH" >/dev/null 2>&1 +assert_gone "linux: XDG_CACHE_HOME override honored" "$XDG/cache/$BID" +assert_gone "linux: XDG_DATA_HOME override honored" "$XDG/data/$BID" +assert_gone "linux: XDG_CONFIG_HOME override honored" "$XDG/config/$BID" +assert_gone "linux: XDG_STATE_HOME override honored" "$XDG/state/$BID" + +# ── 4. Nothing to remove is a clean no-op (fresh HOME, exit 0) ── +H=$(mktemp -d -p "$_TMP_ROOT") +if run_uninstall "$H" Darwin; then + echo " PASS: empty HOME -> no-op exit 0"; PASS=$((PASS+1)) +else + echo " FAIL: empty HOME -> nonzero exit"; FAIL=$((FAIL+1)) +fi + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" = 0 ]