From cdd704f9804ef5decb50971ffcc827ecd57007ad Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 9 Nov 2026 18:02:22 +0800 Subject: [PATCH] feat(linux): reinstall an existing Service when upgrading the package The deb postinst and rpm %post now reinstall the Service in place when an upgrade finds its systemd unit, reusing the unit's group so package-manager runs without a sudo/pkexec user keep the same IPC access. First installs and disabled, masked, or group-less units are left alone; failures warn and exit 0 because neither dpkg nor rpm can restore the previous Service from a failed postinst. The in-app updater stops a Service-run core before installing on Linux and restarts it if the install fails, so the post-update restart does not fail on the session lost when the Service restarts. Refs #8515 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Changelog.md | 4 ++ src-tauri/packages/linux/post-install-rpm.sh | 38 +++++++++++++++++++ src-tauri/packages/linux/post-install.sh | 37 ++++++++++++++++++ src-tauri/src/core/manager/lifecycle.rs | 11 ++++++ src-tauri/src/core/updater.rs | 40 ++++++++++++++++---- 5 files changed, 122 insertions(+), 8 deletions(-) diff --git a/Changelog.md b/Changelog.md index 307e88622..d7f0add84 100644 --- a/Changelog.md +++ b/Changelog.md @@ -52,4 +52,8 @@ - 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址 +**🐧 Linux** + +- 优化 Linux 升级安装包后自动重装已安装的服务,无需在应用内手动重装 + diff --git a/src-tauri/packages/linux/post-install-rpm.sh b/src-tauri/packages/linux/post-install-rpm.sh index cb1f3ca75..b740f3413 100644 --- a/src-tauri/packages/linux/post-install-rpm.sh +++ b/src-tauri/packages/linux/post-install-rpm.sh @@ -5,6 +5,44 @@ chmod +x /usr/bin/clash-verge-service-install chmod +x /usr/bin/clash-verge-service-uninstall chmod +x /usr/bin/clash-verge-service +# Failures only warn: dpkg and rpm cannot restore the previous Service from a failed postinst, +# and a failing script would leave the upgraded package unconfigured or report the update as failed. +reinstall_existing_service() { + local unit="/etc/systemd/system/clash-verge-service.service" + # Only a unit kept by an upgrade-aware removal script shows the Service existed before this upgrade. + if [ ! -f "$unit" ] || [ -L "$unit" ]; then + return 0 + fi + if [ ! -d /run/systemd/system ]; then + echo "Warning: systemd is not running, so Clash Verge Service was not reinstalled; reinstall it from Clash Verge." >&2 + return 0 + fi + if ! systemctl is-enabled --quiet clash-verge-service.service; then + echo "Clash Verge Service is disabled; leaving it unchanged." >&2 + return 0 + fi + # Package managers run without the invoking user's identity, so keep the group that may use the Service. + local group gid + group="$(sed -n 's/^Group=//p' "$unit" | head -n 1)" + gid="" + if [ -n "$group" ]; then + gid="$(getent group "$group" | cut -d: -f3)" + fi + if [ -z "$gid" ]; then + echo "Warning: the Clash Verge Service group '$group' could not be resolved, so the Service was not reinstalled; reinstall it from Clash Verge." >&2 + return 0 + fi + if ! CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then + echo "Warning: reinstalling Clash Verge Service failed; reinstall it from Clash Verge." >&2 + fi +} + +# Upgrades keep an existing Service current; first installs never add one. No success is reported: +# a previous package whose %preun uninstalls unconditionally still removes the Service afterwards. +if [ "$1" -ge 2 ]; then + reinstall_existing_service +fi + # The enforcement file exists in both Enforcing and Permissive mode. if [ ! -e /sys/fs/selinux/enforce ]; then exit 0 diff --git a/src-tauri/packages/linux/post-install.sh b/src-tauri/packages/linux/post-install.sh index 25fcf528d..e88808f37 100644 --- a/src-tauri/packages/linux/post-install.sh +++ b/src-tauri/packages/linux/post-install.sh @@ -3,6 +3,43 @@ chmod +x /usr/bin/clash-verge-service-install chmod +x /usr/bin/clash-verge-service-uninstall chmod +x /usr/bin/clash-verge-service +# Failures only warn: dpkg and rpm cannot restore the previous Service from a failed postinst, +# and a failing script would leave the upgraded package unconfigured or report the update as failed. +reinstall_existing_service() { + local unit="/etc/systemd/system/clash-verge-service.service" + # Only a unit kept by an upgrade-aware removal script shows the Service existed before this upgrade. + if [ ! -f "$unit" ] || [ -L "$unit" ]; then + return 0 + fi + if [ ! -d /run/systemd/system ]; then + echo "Warning: systemd is not running, so Clash Verge Service was not reinstalled; reinstall it from Clash Verge." >&2 + return 0 + fi + if ! systemctl is-enabled --quiet clash-verge-service.service; then + echo "Clash Verge Service is disabled; leaving it unchanged." >&2 + return 0 + fi + # Package managers run without the invoking user's identity, so keep the group that may use the Service. + local group gid + group="$(sed -n 's/^Group=//p' "$unit" | head -n 1)" + gid="" + if [ -n "$group" ]; then + gid="$(getent group "$group" | cut -d: -f3)" + fi + if [ -z "$gid" ]; then + echo "Warning: the Clash Verge Service group '$group' could not be resolved, so the Service was not reinstalled; reinstall it from Clash Verge." >&2 + return 0 + fi + if ! CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then + echo "Warning: reinstalling Clash Verge Service failed; reinstall it from Clash Verge." >&2 + fi +} + +# Upgrades keep an existing Service current; first installs never add one. +if [ "$1" = "configure" ] && [ -n "$2" ]; then + reinstall_existing_service +fi + . /etc/os-release if [ "$ID" = "deepin" ]; then diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 84b0c2346..ad47b5ed4 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -750,6 +750,17 @@ impl CoreManager { self.controlled_stop_core_inner().await } + /// Stops the core only when the Service runs it, returning whether it did. Waits for an in-flight start. + #[cfg(target_os = "linux")] + pub(crate) async fn stop_service_core(&self) -> Result { + let _life = self.lifecycle_lock.lock().await; + if !matches!(*self.get_running_mode(), RunningMode::Service) { + return Ok(false); + } + self.controlled_stop_core_inner().await?; + Ok(true) + } + pub(crate) async fn controlled_stop_core_inner(&self) -> Result<()> { self.controlled_stop_core_with_intent(ProxyStopIntent::Clear).await } diff --git a/src-tauri/src/core/updater.rs b/src-tauri/src/core/updater.rs index 25a938bc4..06c63fd0d 100644 --- a/src-tauri/src/core/updater.rs +++ b/src-tauri/src/core/updater.rs @@ -292,8 +292,36 @@ impl SilentUpdater { Ok(updater_builder.build()?.check().await?) } - async fn install(update: Update, bytes: Vec) -> Result<()> { - Ok(tokio::task::spawn_blocking(move || update.install(&bytes)).await??) + async fn install(update: Update, bytes: Vec, timeout: Option) -> Result<()> { + // The package scripts reinstall an existing Service, killing the core it runs and its session; + // stopping it first keeps the post-update restart from failing on that lost session. + #[cfg(target_os = "linux")] + let stopped_service_core = crate::core::CoreManager::global() + .stop_service_core() + .await + .context("failed to stop the Service core before installing the update")?; + + let install = tokio::task::spawn_blocking(move || update.install(&bytes)); + let joined = match timeout { + Some(limit) => tokio::time::timeout(limit, install) + .await + .map_err(|_| anyhow::anyhow!("install timed out ({}s)", limit.as_secs())), + None => Ok(install.await), + }; + let result = joined.and_then(|joined| Ok(joined??)); + + #[cfg(target_os = "linux")] + if result.is_err() + && stopped_service_core + && let Err(error) = crate::core::CoreManager::global().start_core().await + { + logging!( + error, + Type::System, + "Failed to restart the core after the update install failed: {error:#}" + ); + } + result } /// Installs a newer cached update before normal startup, if the user confirms. @@ -369,11 +397,7 @@ impl SilentUpdater { Self::show_update_splash(app_handle, &version); // `install()` may hang (#2558); on Windows NSIS can take over without returning. - let install = tokio::time::timeout(std::time::Duration::from_secs(30), Self::install(update, bytes)); - match install - .await - .unwrap_or_else(|_| Err(anyhow::anyhow!("install timed out (30s)"))) - { + match Self::install(update, bytes, Some(Duration::from_secs(30))).await { Ok(()) => { logging!(info, Type::System, "Update v{version} install triggered at startup"); Self::delete_cache(); @@ -417,7 +441,7 @@ impl SilentUpdater { }; logging!(info, Type::System, "Installing update v{version}..."); - Self::install(update, bytes).await?; + Self::install(update, bytes, None).await?; Self::delete_cache(); Ok(InstallUpdateOutcome::Installed) }