diff --git a/scripts/prebuild.mjs b/scripts/prebuild.mjs index d5a3476ef..ac9ad75e2 100644 --- a/scripts/prebuild.mjs +++ b/scripts/prebuild.mjs @@ -550,9 +550,16 @@ const SERVICE_BINARIES = [ 'clash-verge-service-uninstall', ] +// Linux packages install the uninstaller under /usr/lib behind a wrapper sidecar; see +// src-tauri/packages/linux/service-uninstall.sh and the files entries in tauri.linux.conf.json. +const LINUX_UNINSTALLER_FILE = 'clash-verge-service-uninstall-bin' + function serviceFileInfo(name) { const ext = platform === 'win32' ? '.exe' : '' const suffix = platform === 'linux' ? `-${SIDECAR_HOST}` : '' + if (platform === 'linux' && name === 'clash-verge-service-uninstall') { + return { sourceFile: name, targetFile: LINUX_UNINSTALLER_FILE } + } return { sourceFile: `${name}${ext}`, targetFile: `${name}${suffix}${ext}`, @@ -573,6 +580,22 @@ async function findExtractedFile(dir, fileName) { } async function resolveServiceBundle() { + await fetchServiceBundle() + if (platform === 'linux') { + const wrapper = path.join( + SERVICE_DIR, + `clash-verge-service-uninstall-${SIDECAR_HOST}`, + ) + await fsp.copyFile( + path.join(cwd, 'src-tauri', 'packages', 'linux', 'service-uninstall.sh'), + wrapper, + ) + await fsp.chmod(wrapper, 0o755) + await updateHashCache(wrapper) + } +} + +async function fetchServiceBundle() { const files = SERVICE_BINARIES.map((name) => { const info = serviceFileInfo(name) return { diff --git a/src-tauri/packages/linux/post-install-rpm.sh b/src-tauri/packages/linux/post-install-rpm.sh index cbabc4dda..0513f1ece 100644 --- a/src-tauri/packages/linux/post-install-rpm.sh +++ b/src-tauri/packages/linux/post-install-rpm.sh @@ -3,17 +3,28 @@ set -e chmod +x /usr/bin/clash-verge-service-install chmod +x /usr/bin/clash-verge-service-uninstall +chmod +x /usr/lib/clash-verge-rev/clash-verge-service-uninstall chmod +x /usr/bin/clash-verge-service SERVICE_UNIT="clash-verge-service.service" SERVICE_UNIT_FILE="/etc/systemd/system/$SERVICE_UNIT" # Read by the app, which offers a reinstall when the package could not keep the Service. UPGRADE_MARKER="/run/clash-verge-service-upgrade" +# Read by /usr/bin/clash-verge-service-uninstall to keep the Service through the previous %preun. +TRANSACTION_RECORD="/run/clash-verge-service-rpm-upgrade" write_upgrade_marker() { (umask 022 && printf 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$UPGRADE_MARKER") } +# Scriptlets run as children of the process running the rpm transaction; its start time +# keeps a stale record from matching a later process that reuses the PID. +record_transaction() { + local start + start="$(sed 's/.*) //' "/proc/$PPID/stat" | cut -d' ' -f20)" + (umask 022 && printf '%s %s\n' "$PPID" "$start" >"$TRANSACTION_RECORD") +} + existing_service_gid() { local group group="$(sed -n 's/^Group=//p' "$SERVICE_UNIT_FILE" | head -n 1)" @@ -52,9 +63,10 @@ reinstall_existing_service() { # Upgrades keep an existing Service current; first installs and explicit removals never add one. # A symlinked unit is masked or managed by the administrator and is left alone. -# The marker is written first: a previous package whose %preun uninstalls unconditionally -# still removes the Service after this script, and the app only acts on it if the Service is not ready. +# The previous %preun runs after this script; the record keeps it from removing the Service. +# The marker stays as a fallback should it still be removed; the app only acts on it if the Service is not ready. if [ "$1" -ge 2 ] && [ -d /run/systemd/system ] && [ -f "$SERVICE_UNIT_FILE" ] && [ ! -L "$SERVICE_UNIT_FILE" ]; then + record_transaction || true gid="$(existing_service_gid)" write_upgrade_marker "$gid" if [ -z "$gid" ]; then diff --git a/src-tauri/packages/linux/post-install.sh b/src-tauri/packages/linux/post-install.sh index 0a4918a8f..1f9d993b5 100644 --- a/src-tauri/packages/linux/post-install.sh +++ b/src-tauri/packages/linux/post-install.sh @@ -1,6 +1,7 @@ #!/bin/bash chmod +x /usr/bin/clash-verge-service-install chmod +x /usr/bin/clash-verge-service-uninstall +chmod +x /usr/lib/clash-verge-rev/clash-verge-service-uninstall chmod +x /usr/bin/clash-verge-service SERVICE_UNIT="clash-verge-service.service" diff --git a/src-tauri/packages/linux/service-uninstall.sh b/src-tauri/packages/linux/service-uninstall.sh new file mode 100755 index 000000000..5e14543fb --- /dev/null +++ b/src-tauri/packages/linux/service-uninstall.sh @@ -0,0 +1,28 @@ +#!/bin/sh +# Installed as /usr/bin/clash-verge-service-uninstall. During an rpm upgrade the new %post +# reinstalls an existing Service before the previous package's %preun runs, and released +# %preun scripts uninstall it unconditionally through this path. Skip only that call: one +# made by a scriptlet of the transaction that %post recorded. +RECORD=/run/clash-verge-service-rpm-upgrade +UNINSTALLER=/usr/lib/clash-verge-rev/clash-verge-service-uninstall + +# Fields after the parenthesised command name, which may itself contain spaces. +stat_field() { + sed 's/.*) //' "/proc/$1/stat" 2>/dev/null | cut -d' ' -f"$2" +} + +if [ -f "$RECORD" ] && [ ! -L "$RECORD" ] && read -r txn_pid txn_start <"$RECORD"; then + pid=$PPID + depth=0 + while [ "$depth" -lt 4 ] && [ -n "$pid" ] && [ "$pid" -gt 1 ]; do + if [ "$pid" = "$txn_pid" ] && [ "$(stat_field "$pid" 20)" = "$txn_start" ]; then + rm -f "$RECORD" + echo "Keeping the Clash Verge Service reinstalled by this upgrade." + exit 0 + fi + pid="$(stat_field "$pid" 2)" + depth=$((depth + 1)) + done +fi + +exec "$UNINSTALLER" "$@" diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 166faf26b..d0734fdd3 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -11,7 +11,10 @@ "conflicts": ["clash-verge"], "replaces": ["clash-verge"], "postInstallScript": "./packages/linux/post-install.sh", - "preRemoveScript": "./packages/linux/pre-remove.sh" + "preRemoveScript": "./packages/linux/pre-remove.sh", + "files": { + "/usr/lib/clash-verge-rev/clash-verge-service-uninstall": "./sidecar/clash-verge-service-uninstall-bin" + } }, "rpm": { "depends": ["openssl", "libayatana-appindicator-gtk3"], @@ -21,7 +24,10 @@ "conflicts": ["clash-verge"], "obsoletes": ["clash-verge"], "postInstallScript": "./packages/linux/post-install-rpm.sh", - "preRemoveScript": "./packages/linux/pre-remove.sh" + "preRemoveScript": "./packages/linux/pre-remove.sh", + "files": { + "/usr/lib/clash-verge-rev/clash-verge-service-uninstall": "./sidecar/clash-verge-service-uninstall-bin" + } } }, "externalBin": [