feat(linux): reinstall an existing Service during package upgrades

The deb and rpm packages never refreshed an installed Service, and the
previous package's prerm/%preun uninstalls it unconditionally. When the
Service unit existed before the upgrade, the new postinst/%post now
reinstalls it with its group and restores disabled, enabled-runtime,
masked-runtime and stopped states. First installs and explicit removals
never add a Service.

rpm runs the new %post before the old %preun. %post records the
transaction process (pid and start time) in /run, and
/usr/bin/clash-verge-service-uninstall becomes a wrapper that skips only
that %preun call and otherwise runs the real uninstaller, now under
/usr/lib/clash-verge-rev. deb has no such ordering: an upgrade from a
released version still loses the Service, and later deb upgrades reinstall
it in place only once the prerm upgrade guard from #8240 ships.

The scripts exit 0, because a failing postinst or %post cannot restore the
Service. On failure they write /run/clash-verge-service-upgrade instead;
package script output never reaches the GUI. At startup the app offers a
reinstall if the Service is not ready and warns if its previous state was
not restored. Both notices replay when a window subscribes, and the marker
is acknowledged only after both are taken. In-app updates stop a
Service-run core and hold the core lifecycle until the package manager
exits, including after the 30-second startup timeout.

Refs #8515

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Tunglies
2026-11-09 21:28:46 +08:00
co-authored by Copilot
parent d51e4a95d6
commit d09f78f9b1
50 changed files with 572 additions and 13 deletions
+23
View File
@@ -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 {