From d09f78f9b10e2cde7bf4936c0d345ad0f421835f Mon Sep 17 00:00:00 2001 From: Tunglies <77394545+Tunglies@users.noreply.github.com> Date: Mon, 9 Nov 2026 21:28:46 +0800 Subject: [PATCH] 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> --- Changelog.md | 4 + scripts/prebuild.mjs | 23 +++ src-tauri/packages/linux/post-install-rpm.sh | 93 ++++++++++ src-tauri/packages/linux/post-install.sh | 79 +++++++++ src-tauri/packages/linux/service-uninstall.sh | 28 +++ src-tauri/src/cmd/service.rs | 7 + src-tauri/src/core/manager/lifecycle.rs | 14 ++ src-tauri/src/core/notify/mod.rs | 5 +- src-tauri/src/core/service.rs | 162 +++++++++++++++++- src-tauri/src/core/updater.rs | 65 ++++++- src-tauri/src/ipc.rs | 1 + src-tauri/src/utils/resolve/mod.rs | 2 + src-tauri/tauri.linux.conf.json | 10 +- .../layout/sysproxy-privilege-dialog.tsx | 7 +- src/locales/ar/layout.json | 1 + src/locales/ar/settings.json | 1 + src/locales/de/layout.json | 1 + src/locales/de/settings.json | 1 + src/locales/en/layout.json | 1 + src/locales/en/settings.json | 1 + src/locales/es/layout.json | 1 + src/locales/es/settings.json | 1 + src/locales/fa/layout.json | 1 + src/locales/fa/settings.json | 1 + src/locales/id/layout.json | 1 + src/locales/id/settings.json | 1 + src/locales/jp/layout.json | 1 + src/locales/jp/settings.json | 1 + src/locales/ko/layout.json | 1 + src/locales/ko/settings.json | 1 + src/locales/pt/layout.json | 1 + src/locales/pt/settings.json | 1 + src/locales/ru/layout.json | 1 + src/locales/ru/settings.json | 1 + src/locales/tr/layout.json | 1 + src/locales/tr/settings.json | 1 + src/locales/tt/layout.json | 1 + src/locales/tt/settings.json | 1 + src/locales/zh/layout.json | 1 + src/locales/zh/settings.json | 1 + src/locales/zhtw/layout.json | 1 + src/locales/zhtw/settings.json | 1 + src/services/bindings.ts | 4 + src/services/bus/create-bus-handlers.test.ts | 22 +++ src/services/bus/create-bus-handlers.ts | 1 + src/services/cmds.ts | 4 + src/services/notice-handlers.ts | 21 +++ src/services/service-request.ts | 1 + src/types/generated/i18n-keys.ts | 2 + src/types/generated/i18n-resources.ts | 2 + 50 files changed, 572 insertions(+), 13 deletions(-) create mode 100755 src-tauri/packages/linux/service-uninstall.sh diff --git a/Changelog.md b/Changelog.md index 307e88622..d079001c2 100644 --- a/Changelog.md +++ b/Changelog.md @@ -52,4 +52,8 @@ - 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址 +**🐧 Linux** + +- 优化 Linux 升级安装时自动重装已安装的服务并保留启停设置,重装失败时提示 + 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 cb1f3ca75..e2206986a 100644 --- a/src-tauri/packages/linux/post-install-rpm.sh +++ b/src-tauri/packages/linux/post-install-rpm.sh @@ -3,8 +3,101 @@ set -e chmod +x /usr/bin/clash-verge-service-install chmod +x /usr/bin/clash-verge-service-uninstall +# The /usr/bin uninstaller is the service-uninstall.sh wrapper; this is the real binary it runs. +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" + +# Set to the previous state when restoring it fails; the app shows it even if the Service is ready. +UNRESTORED_STATE="" + +write_upgrade_marker() { + ( + umask 022 + { + printf 'id=%s\n' "$(date +%s%N)" + if [ -n "$UNRESTORED_STATE" ]; then + printf 'restore=%s\n' "$UNRESTORED_STATE" + fi + } >"$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)" + [ -n "$start" ] || return 1 + (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)" + if [ -n "$group" ]; then + getent group "$group" | cut -d: -f3 + fi + return 0 +} + +# Failures only warn: dpkg and rpm cannot restore the previous Service from a failed postinst, +# and a failing script would leave the package unconfigured or make the app report a failed update. +# The installer always enables and starts the unit, so the previous enablement and activity are restored. +reinstall_existing_service() { + local gid="$1" enabled active=no + enabled="$(systemctl is-enabled "$SERVICE_UNIT" 2>/dev/null)" + if systemctl is-active --quiet "$SERVICE_UNIT"; then + active=yes + fi + local status=0 restored=yes + if [ "$enabled" = "masked-runtime" ]; then + systemctl unmask --runtime --quiet "$SERVICE_UNIT" || restored=no + fi + if ! CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then + echo "Warning: reinstalling Clash Verge Service failed; Clash Verge will offer to reinstall it." >&2 + status=1 + fi + case "$enabled" in + disabled) systemctl disable --quiet "$SERVICE_UNIT" || restored=no ;; + enabled-runtime) { systemctl disable --quiet "$SERVICE_UNIT" && systemctl enable --runtime --quiet "$SERVICE_UNIT"; } || restored=no ;; + masked-runtime) systemctl mask --runtime --quiet "$SERVICE_UNIT" || restored=no ;; + esac + if [ "$active" = "no" ]; then + systemctl stop "$SERVICE_UNIT" || restored=no + fi + if [ "$restored" = "no" ]; then + UNRESTORED_STATE="${enabled:-unknown},$([ "$active" = yes ] && echo active || echo inactive)" + echo "Warning: restoring the previous Clash Verge Service state ($UNRESTORED_STATE) failed; check it with systemctl." >&2 + status=1 + fi + return "$status" +} + +# 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 previous %preun runs after this script; the record keeps it from removing the Service. +# Without a record that %preun removes the Service, so the marker makes the app offer a reinstall. +if [ "$1" -ge 2 ] && [ -d /run/systemd/system ] && [ -f "$SERVICE_UNIT_FILE" ] && [ ! -L "$SERVICE_UNIT_FILE" ]; then + recorded=yes + record_transaction || recorded=no + gid="$(existing_service_gid)" + if [ -z "$gid" ]; then + echo "Warning: the Clash Verge Service group could not be resolved; Clash Verge will offer to reinstall the Service." >&2 + write_upgrade_marker + elif reinstall_existing_service "$gid" && [ "$recorded" = yes ]; then + rm -f "$UPGRADE_MARKER" + else + write_upgrade_marker + fi +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..ce0d19071 100644 --- a/src-tauri/packages/linux/post-install.sh +++ b/src-tauri/packages/linux/post-install.sh @@ -1,8 +1,87 @@ #!/bin/bash chmod +x /usr/bin/clash-verge-service-install chmod +x /usr/bin/clash-verge-service-uninstall +# The /usr/bin uninstaller is the service-uninstall.sh wrapper; this is the real binary it runs. +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" + +# Set to the previous state when restoring it fails; the app shows it even if the Service is ready. +UNRESTORED_STATE="" + +write_upgrade_marker() { + ( + umask 022 + { + printf 'id=%s\n' "$(date +%s%N)" + if [ -n "$UNRESTORED_STATE" ]; then + printf 'restore=%s\n' "$UNRESTORED_STATE" + fi + } >"$UPGRADE_MARKER" + ) +} + +existing_service_gid() { + local group + group="$(sed -n 's/^Group=//p' "$SERVICE_UNIT_FILE" | head -n 1)" + if [ -n "$group" ]; then + getent group "$group" | cut -d: -f3 + fi + return 0 +} + +# Failures only warn: dpkg and rpm cannot restore the previous Service from a failed postinst, +# and a failing script would leave the package unconfigured or make the app report a failed update. +# The installer always enables and starts the unit, so the previous enablement and activity are restored. +reinstall_existing_service() { + local gid="$1" enabled active=no + enabled="$(systemctl is-enabled "$SERVICE_UNIT" 2>/dev/null)" + if systemctl is-active --quiet "$SERVICE_UNIT"; then + active=yes + fi + local status=0 restored=yes + if [ "$enabled" = "masked-runtime" ]; then + systemctl unmask --runtime --quiet "$SERVICE_UNIT" || restored=no + fi + if ! CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then + echo "Warning: reinstalling Clash Verge Service failed; Clash Verge will offer to reinstall it." >&2 + status=1 + fi + case "$enabled" in + disabled) systemctl disable --quiet "$SERVICE_UNIT" || restored=no ;; + enabled-runtime) { systemctl disable --quiet "$SERVICE_UNIT" && systemctl enable --runtime --quiet "$SERVICE_UNIT"; } || restored=no ;; + masked-runtime) systemctl mask --runtime --quiet "$SERVICE_UNIT" || restored=no ;; + esac + if [ "$active" = "no" ]; then + systemctl stop "$SERVICE_UNIT" || restored=no + fi + if [ "$restored" = "no" ]; then + UNRESTORED_STATE="${enabled:-unknown},$([ "$active" = yes ] && echo active || echo inactive)" + echo "Warning: restoring the previous Clash Verge Service state ($UNRESTORED_STATE) failed; check it with systemctl." >&2 + status=1 + fi + return "$status" +} + +# 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. +if [ "$1" = "configure" ] && [ -n "$2" ] && [ -d /run/systemd/system ] && + [ -f "$SERVICE_UNIT_FILE" ] && [ ! -L "$SERVICE_UNIT_FILE" ]; then + gid="$(existing_service_gid)" + if [ -z "$gid" ]; then + echo "Warning: the Clash Verge Service group could not be resolved; Clash Verge will offer to reinstall the Service." >&2 + write_upgrade_marker + elif reinstall_existing_service "$gid"; then + rm -f "$UPGRADE_MARKER" + else + write_upgrade_marker + fi +fi + . /etc/os-release if [ "$ID" = "deepin" ]; then diff --git a/src-tauri/packages/linux/service-uninstall.sh b/src-tauri/packages/linux/service-uninstall.sh new file mode 100755 index 000000000..417a1e422 --- /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" && [ -n "$txn_start" ]; 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 through this upgrade." + exit 0 + fi + pid="$(stat_field "$pid" 2)" + depth=$((depth + 1)) + done +fi + +exec "$UNINSTALLER" "$@" diff --git a/src-tauri/src/cmd/service.rs b/src-tauri/src/cmd/service.rs index 592df0417..f081c4efd 100644 --- a/src-tauri/src/cmd/service.rs +++ b/src-tauri/src/cmd/service.rs @@ -141,6 +141,13 @@ pub fn take_service_repair_notice() -> bool { crate::core::service::take_service_repair_notice() } +#[tauri::command] +#[specta::specta] +#[cfg_attr(not(target_os = "linux"), allow(clippy::missing_const_for_fn))] +pub fn take_service_upgrade_state_notice() -> Option { + crate::core::service::take_service_upgrade_state_notice() +} + #[tauri::command] #[specta::specta] pub fn take_service_owner_notice() -> Option { diff --git a/src-tauri/src/core/manager/lifecycle.rs b/src-tauri/src/core/manager/lifecycle.rs index 84b0c2346..e4da95d17 100644 --- a/src-tauri/src/core/manager/lifecycle.rs +++ b/src-tauri/src/core/manager/lifecycle.rs @@ -750,6 +750,20 @@ impl CoreManager { self.controlled_stop_core_inner().await } + /// Holds the core lifecycle for a package install, stopping a Service-run core first and returning + /// whether it did, so no core start or restart overlaps the package scripts reinstalling the Service. + #[cfg(target_os = "linux")] + pub(crate) async fn pause_for_package_install( + &'static self, + ) -> Result<(tokio::sync::MutexGuard<'static, ()>, bool)> { + let life = self.lifecycle_lock.lock().await; + if !matches!(*self.get_running_mode(), RunningMode::Service) { + return Ok((life, false)); + } + self.controlled_stop_core_inner().await?; + Ok((life, 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/notify/mod.rs b/src-tauri/src/core/notify/mod.rs index 9a6b7699f..957106a4b 100644 --- a/src-tauri/src/core/notify/mod.rs +++ b/src-tauri/src/core/notify/mod.rs @@ -22,8 +22,11 @@ pub enum NoticeStatus { #[serde(rename = "service_core::app_data_not_owned")] ServiceCoreAppDataNotOwned, #[serde(rename = "service_core::sidecar_fallback")] - #[cfg_attr(not(target_os = "windows"), allow(dead_code))] + #[cfg_attr(not(any(target_os = "windows", target_os = "linux")), allow(dead_code))] ServiceCoreSidecarFallback, + #[serde(rename = "service_core::upgrade_state_not_restored")] + #[cfg_attr(not(target_os = "linux"), allow(dead_code))] + ServiceCoreUpgradeStateNotRestored, #[serde(rename = "dns_override::auto_disabled")] DnsOverrideAutoDisabled, #[serde(rename = "enhance::discarded_keys")] diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 05cf15d57..478264579 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -42,6 +42,8 @@ static ACTIVE_SERVICE_SESSION: Mutex> = Mutex::new( static PENDING_SERVICE_FALLBACK_NOTICE: Mutex> = Mutex::new(None); static PENDING_SERVICE_REPAIR_NOTICE: AtomicBool = AtomicBool::new(false); static PENDING_SERVICE_OWNER_NOTICE: Mutex> = Mutex::new(None); +#[cfg(target_os = "linux")] +static PENDING_SERVICE_UPGRADE: Mutex> = Mutex::new(None); /// Why the Service was unavailable when the core fell back to Sidecar. #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, specta::Type)] @@ -50,6 +52,7 @@ pub enum ServiceFallbackNotice { Unavailable, CoreRejected(String), NotAutoStarted, + UpgradeIncomplete, } /// The release installer registers AutoStart. @@ -66,9 +69,118 @@ pub(crate) fn notify_service_fallback(reason: &str) { Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, ""); } +/// Written by the Linux packages when an upgrade found the Service and may not have kept it. +#[cfg(target_os = "linux")] +const SERVICE_UPGRADE_MARKER: &str = "/run/clash-verge-service-upgrade"; +const SERVICE_UPGRADE_INCOMPLETE: &str = "the package upgrade did not keep the Service ready"; + +/// Offers a reinstall once per package upgrade whose Service is not ready, since package script output never reaches the app. +#[cfg(target_os = "linux")] +pub(crate) async fn notify_incomplete_service_upgrade() { + let Ok(marker) = std::fs::read_to_string(SERVICE_UPGRADE_MARKER) else { + return; + }; + let Some(id) = marker.lines().find_map(|line| line.strip_prefix("id=")) else { + return; + }; + let ack = match dirs::app_home_dir() { + Ok(dir) => dir.join("service-upgrade-ack"), + Err(error) => { + logging!(warn, Type::Service, "无法定位服务升级确认文件: {error:#}"); + return; + } + }; + if std::fs::read_to_string(&ack).is_ok_and(|seen| seen == id) { + return; + } + let status = SERVICE_MANAGER.current().await; + let notice = service_upgrade_notice(&marker, matches!(status, ServiceStatus::Ready)); + if !notice.reinstall && notice.unrestored_state.is_none() { + acknowledge_service_upgrade(&ack, id); + return; + } + // Without a window (silent start) the events are dropped; the frontend replays them on subscription. + *PENDING_SERVICE_UPGRADE.lock() = Some(PendingServiceUpgrade { + ack, + id: id.to_owned(), + reinstall: notice.reinstall, + unrestored_state: notice.unrestored_state.map(str::to_owned), + }); + if notice.reinstall { + logging!(warn, Type::Service, "安装包升级后服务未就绪: {status:?}"); + *PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(SERVICE_UPGRADE_INCOMPLETE.to_owned()); + Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, ""); + } + if let Some(state) = notice.unrestored_state { + logging!(warn, Type::Service, "安装包升级后未能恢复服务原状态: {state}"); + Handle::notice(NoticeStatus::ServiceCoreUpgradeStateNotRestored, ""); + } +} + +#[cfg(target_os = "linux")] +fn acknowledge_service_upgrade(ack: &Path, id: &str) { + if let Err(error) = std::fs::write(ack, id) { + logging!(warn, Type::Service, "无法记录服务升级确认: {error}"); + } +} + +/// An upgrade whose notices are acknowledged on disk only after the frontend has taken all of them. +#[cfg(target_os = "linux")] +struct PendingServiceUpgrade { + ack: PathBuf, + id: String, + reinstall: bool, + unrestored_state: Option, +} + +#[cfg(target_os = "linux")] +fn settle_service_upgrade(take: impl FnOnce(&mut PendingServiceUpgrade) -> T) -> Option { + let (taken, settled) = { + let mut pending = PENDING_SERVICE_UPGRADE.lock(); + let upgrade = pending.as_mut()?; + let taken = take(upgrade); + let settled = !upgrade.reinstall && upgrade.unrestored_state.is_none(); + (taken, if settled { pending.take() } else { None }) + }; + if let Some(upgrade) = settled { + acknowledge_service_upgrade(&upgrade.ack, &upgrade.id); + } + Some(taken) +} + +#[cfg(target_os = "linux")] +pub(crate) fn take_service_upgrade_state_notice() -> Option { + settle_service_upgrade(|upgrade| upgrade.unrestored_state.take()).flatten() +} + +#[cfg(not(target_os = "linux"))] +pub(crate) const fn take_service_upgrade_state_notice() -> Option { + None +} + +#[cfg(target_os = "linux")] +#[derive(Debug, PartialEq, Eq)] +struct ServiceUpgradeNotice<'a> { + reinstall: bool, + /// Reported regardless of readiness: a reinstall enables and starts the Service, so it cannot restore this state. + unrestored_state: Option<&'a str>, +} + +#[cfg(target_os = "linux")] +fn service_upgrade_notice(marker: &str, ready: bool) -> ServiceUpgradeNotice<'_> { + ServiceUpgradeNotice { + reinstall: !ready, + unrestored_state: marker.lines().find_map(|line| line.strip_prefix("restore=")), + } +} + pub(crate) fn take_service_fallback_notice() -> Option { let reason = PENDING_SERVICE_FALLBACK_NOTICE.lock().take()?; - Some(if reason.starts_with(CORE_REJECTED_PREFIX) { + Some(if reason == SERVICE_UPGRADE_INCOMPLETE { + #[cfg(target_os = "linux")] + settle_service_upgrade(|upgrade| upgrade.reinstall = false); + ServiceFallbackNotice::UpgradeIncomplete + } else if reason.starts_with(CORE_REJECTED_PREFIX) { ServiceFallbackNotice::CoreRejected(reason) } else if is_not_auto_started(&reason) { ServiceFallbackNotice::NotAutoStarted @@ -2124,6 +2236,11 @@ pub static SERVICE_MANAGER: ServiceManager = ServiceManager; #[cfg(test)] #[allow(clippy::expect_used, clippy::panic, reason = "tests assert by panicking")] mod tests { + #[cfg(target_os = "linux")] + use super::{ + PENDING_SERVICE_UPGRADE, PendingServiceUpgrade, ServiceUpgradeNotice, service_upgrade_notice, + settle_service_upgrade, take_service_upgrade_state_notice, + }; use super::{ ServiceHealth, ServiceStatus, capture_generation_before, claim_owner_recovery_generation, generate_service_session_token, mark_service_unavailable_after_owner_loss, owner_recovery_policy, @@ -2786,4 +2903,47 @@ mod tests { assert!(!store.state().sidecar_allowed); assert_eq!(status_of(&store), ServiceStatus::Ready); } + + #[cfg(target_os = "linux")] + #[test] + fn upgrade_marker_reports_unrestored_state_regardless_of_readiness() { + let restored = "id=1\n"; + let unrestored = "id=1\nrestore=disabled,active\n"; + let notice = |reinstall, unrestored_state| ServiceUpgradeNotice { + reinstall, + unrestored_state, + }; + assert_eq!(service_upgrade_notice(restored, true), notice(false, None)); + assert_eq!(service_upgrade_notice(restored, false), notice(true, None)); + assert_eq!( + service_upgrade_notice(unrestored, true), + notice(false, Some("disabled,active")) + ); + assert_eq!( + service_upgrade_notice(unrestored, false), + notice(true, Some("disabled,active")) + ); + } + + #[cfg(target_os = "linux")] + #[test] + fn upgrade_marker_is_acknowledged_only_after_every_notice_is_taken() { + let ack = std::env::temp_dir().join(format!("verge-service-upgrade-ack-{}", std::process::id())); + let _ = std::fs::remove_file(&ack); + *PENDING_SERVICE_UPGRADE.lock() = Some(PendingServiceUpgrade { + ack: ack.clone(), + id: "7".to_owned(), + reinstall: true, + unrestored_state: Some("disabled,active".to_owned()), + }); + + assert_eq!(take_service_upgrade_state_notice().as_deref(), Some("disabled,active")); + assert!(!ack.exists()); + assert_eq!(take_service_upgrade_state_notice(), None); + + settle_service_upgrade(|upgrade| upgrade.reinstall = false); + assert_eq!(std::fs::read_to_string(&ack).ok().as_deref(), Some("7")); + assert!(PENDING_SERVICE_UPGRADE.lock().is_none()); + let _ = std::fs::remove_file(&ack); + } } diff --git a/src-tauri/src/core/updater.rs b/src-tauri/src/core/updater.rs index 25a938bc4..13ac3e155 100644 --- a/src-tauri/src/core/updater.rs +++ b/src-tauri/src/core/updater.rs @@ -292,8 +292,61 @@ 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<()> { + #[cfg(target_os = "linux")] + let mut install = Self::install_with_core_paused(update, bytes).await?; + #[cfg(not(target_os = "linux"))] + let mut install = tokio::task::spawn_blocking(move || update.install(&bytes)); + let joined = match timeout { + Some(limit) => match tokio::time::timeout(limit, &mut install).await { + Ok(joined) => Ok(joined), + Err(_) => { + // The caller resumes the old app; once the package manager succeeds, restart on the new version. + #[cfg(target_os = "linux")] + tokio::spawn(async move { + if matches!(install.await, Ok(Ok(()))) { + crate::feat::restart_app().await; + } + }); + Err(anyhow::anyhow!("install timed out ({}s)", limit.as_secs())) + } + }, + None => Ok(install.await), + }; + joined.and_then(|joined| { + joined??; + Ok(()) + }) + } + + /// The package scripts reinstall an existing Service, killing the core it runs and its session, so the + /// core stays paused until the package manager exits, even after the caller stops waiting. + #[cfg(target_os = "linux")] + async fn install_with_core_paused(update: Update, bytes: Vec) -> Result>> { + let manager = crate::core::CoreManager::global(); + let (lifecycle, stopped_service_core) = manager + .pause_for_package_install() + .await + .context("failed to stop the Service core before installing the update")?; + let install = tokio::task::spawn_blocking(move || update.install(&bytes)); + Ok(tokio::spawn(async move { + let result = match install.await { + Ok(result) => result.map_err(anyhow::Error::from), + Err(error) => Err(error.into()), + }; + drop(lifecycle); + if result.is_err() + && stopped_service_core + && let Err(error) = manager.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 +422,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 +466,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) } diff --git a/src-tauri/src/ipc.rs b/src-tauri/src/ipc.rs index 334ef1ccb..24d5dfdf0 100644 --- a/src-tauri/src/ipc.rs +++ b/src-tauri/src/ipc.rs @@ -56,6 +56,7 @@ pub fn builder() -> tauri_specta::Builder { cmd::get_core_startup_error, cmd::take_service_repair_notice, cmd::take_service_owner_notice, + cmd::take_service_upgrade_state_notice, cmd::take_discarded_keys_notice, cmd::get_dns_config_content, cmd::validate_dns_config, diff --git a/src-tauri/src/utils/resolve/mod.rs b/src-tauri/src/utils/resolve/mod.rs index fa408f2af..35cc68c32 100644 --- a/src-tauri/src/utils/resolve/mod.rs +++ b/src-tauri/src/utils/resolve/mod.rs @@ -78,6 +78,8 @@ async fn resolve_setup() { let core_init = AsyncHandler::spawn(|| async { init_core_manager().await; + #[cfg(target_os = "linux")] + crate::core::service::notify_incomplete_service_upgrade().await; }); let _ = futures::join!( 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": [ diff --git a/src/components/layout/sysproxy-privilege-dialog.tsx b/src/components/layout/sysproxy-privilege-dialog.tsx index fb493157c..e4db9d73f 100644 --- a/src/components/layout/sysproxy-privilege-dialog.tsx +++ b/src/components/layout/sysproxy-privilege-dialog.tsx @@ -28,7 +28,9 @@ import getSystem from '@/utils/get-system' type Remedy = 'installAndRestart' | 'reinstallAndRestart' | 'restartOnly' const remedyFor = (reason: ServiceRequestReason): Remedy => - reason === 'serviceLocationRefused' || reason === 'serviceNotAutoStarted' + reason === 'serviceLocationRefused' || + reason === 'serviceNotAutoStarted' || + reason === 'serviceUpgradeIncomplete' ? 'reinstallAndRestart' : reason === 'sysproxySidecarReady' ? 'restartOnly' @@ -43,6 +45,8 @@ const EXPLANATION = { 'layout.components.serviceMigration.locationRefusedMessage', serviceNotAutoStarted: 'layout.components.serviceMigration.notAutoStartedMessage', + serviceUpgradeIncomplete: + 'layout.components.serviceMigration.upgradeIncompleteMessage', } as const const TITLE = { @@ -51,6 +55,7 @@ const TITLE = { tunNeedsService: 'layout.components.sysproxyPrivilege.tunTitle', serviceLocationRefused: 'layout.components.serviceMigration.repair', serviceNotAutoStarted: 'layout.components.serviceMigration.repair', + serviceUpgradeIncomplete: 'layout.components.serviceMigration.reinstall', } as const const stateToRestore = ( diff --git a/src/locales/ar/layout.json b/src/locales/ar/layout.json index ed220dfb4..ccce4230c 100644 --- a/src/locales/ar/layout.json +++ b/src/locales/ar/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "لا يمكن للخدمة تشغيل النواة لأن مسار التثبيت أو أذونات المجلد لم تجتز فحص الأمان. أعد تثبيت الخدمة لاستعادة ملفاتها وأذوناتها، ثم حاول مجددًا. قد يطلب Windows موافقة المسؤول.", "notAutoStartedMessage": "تم تغيير نوع بدء تشغيل خدمة Clash Verge إلى «يدوي» أو «معطّل» (غالبًا بواسطة أدوات التحسين)، لذلك لم تعد تبدأ مع Windows؛ تستخدم هذه الجلسة Sidecar. أعد تثبيت الخدمة لاستعادة التشغيل التلقائي. قد يطلب Windows موافقة المسؤول.", + "upgradeIncompleteMessage": "تمت ترقية Clash Verge، لكن خدمة Clash Verge المثبتة سابقًا غير جاهزة. أعد تثبيت الخدمة لمواصلة استخدامها؛ قد يطلب النظام مصادقة المسؤول.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/ar/settings.json b/src/locales/ar/settings.json index 494c5a059..9ba341749 100644 --- a/src/locales/ar/settings.json +++ b/src/locales/ar/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "تعذّر على الخدمة التأكد من أن ملفات النواة ومجلداتها آمنة للتشغيل بصلاحيات مرتفعة.", "permissionRepairGuide": "عرض خطوات الإصلاح", "sidecarFallback": "تعذّر على الخدمة تشغيل النواة. تم التبديل إلى وضع Sidecar؛ ولا تزال الخدمة غير متاحة.", + "upgradeStateNotRestored": "بعد الترقية تعذّرت استعادة حالة بدء التشغيل والتشغيل السابقة لخدمة Clash Verge ({{state}}). لن تؤدي إعادة تثبيت الخدمة إلى استعادتها؛ تحقق منها باستخدام systemctl status clash-verge-service واضبطها باستخدام systemctl.", "uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح" }, "updater": { diff --git a/src/locales/de/layout.json b/src/locales/de/layout.json index 7f1d75666..08465bffe 100644 --- a/src/locales/de/layout.json +++ b/src/locales/de/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "Der Dienst kann den Kern nicht starten, da sein Installationspfad oder die Verzeichnisberechtigungen die Sicherheitsprüfung nicht bestanden haben. Installieren Sie den Dienst erneut, um Dateien und Berechtigungen wiederherzustellen, und versuchen Sie es erneut. Windows kann eine Administratorbestätigung anfordern.", "notAutoStartedMessage": "Der Starttyp des Clash-Verge-Dienstes wurde auf „Manuell“ oder „Deaktiviert“ geändert (häufig durch Optimierungsprogramme), daher startet er nicht mehr mit Windows; diese Sitzung verwendet Sidecar. Installieren Sie den Dienst erneut, um den automatischen Start wiederherzustellen. Windows kann eine Administratorbestätigung anfordern.", + "upgradeIncompleteMessage": "Clash Verge wurde aktualisiert, aber der zuvor installierte Clash Verge-Dienst ist nicht bereit. Installieren Sie den Dienst neu, um ihn weiter zu verwenden; das System fragt möglicherweise nach einer Administrator-Authentifizierung.", "title": "Dienst-Update erforderlich", "message": "Die Version des Clash-Verge-Dienstes passt nicht zu der Version, die diese App benötigt. Wählen Sie „Dienst neu installieren“, um den vorhandenen Dienst zu deinstallieren und die benötigte Version zu installieren, oder fahren Sie für diese Sitzung mit Sidecar fort.", "checkingMessage": "Der Status des Systemdienstes und der aktuelle Betriebsmodus werden geprüft. Bitte warten…", diff --git a/src/locales/de/settings.json b/src/locales/de/settings.json index 938fd8992..dfe7a5d74 100644 --- a/src/locales/de/settings.json +++ b/src/locales/de/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "Der Dienst konnte nicht bestätigen, dass die Kerndateien und Ordner für die Ausführung mit erhöhten Rechten sicher sind.", "permissionRepairGuide": "Reparaturanleitung öffnen", "sidecarFallback": "Der Dienst konnte den Kern nicht starten. Die Anwendung läuft im Sidecar-Modus weiter; der Dienst bleibt nicht verfügbar.", + "upgradeStateNotRestored": "Nach dem Upgrade konnte der vorherige Autostart- und Laufzustand des Clash Verge-Dienstes ({{state}}) nicht wiederhergestellt werden. Eine Neuinstallation des Dienstes stellt ihn nicht wieder her; prüfen Sie ihn mit systemctl status clash-verge-service und passen Sie ihn mit systemctl an.", "uninstallSuccess": "Service erfolgreich deinstalliert" }, "updater": { diff --git a/src/locales/en/layout.json b/src/locales/en/layout.json index da4042be1..177621dfc 100644 --- a/src/locales/en/layout.json +++ b/src/locales/en/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "The service cannot start the core because its installation path or directory permissions failed a security check. Reinstall the service to restore its files and permissions, then try again. Windows may ask for administrator approval.", "notAutoStartedMessage": "The Clash Verge service startup type was changed to Manual or Disabled (often by system optimizer tools), so it no longer starts with Windows; this session is using Sidecar. Reinstall the service to restore automatic startup. Windows may ask for administrator approval.", + "upgradeIncompleteMessage": "Clash Verge was upgraded, but the Clash Verge service it had installed is not ready. Reinstall the service to keep using it; the system may ask for administrator authentication.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index 1ef97ab22..b81b41ad6 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "The service could not confirm that the core files and folders are safe to run with elevated privileges.", "permissionRepairGuide": "View repair instructions", "sidecarFallback": "The service could not start the core. Switched to Sidecar mode; the service remains unavailable.", + "upgradeStateNotRestored": "After the upgrade, Clash Verge could not restore the service's previous startup and running state ({{state}}). Reinstalling the service will not restore it; check it with systemctl status clash-verge-service and adjust it with systemctl.", "uninstallSuccess": "Service Uninstalled Successfully" }, "updater": { diff --git a/src/locales/es/layout.json b/src/locales/es/layout.json index ff782df40..2f05d2c7d 100644 --- a/src/locales/es/layout.json +++ b/src/locales/es/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "El servicio no puede iniciar el núcleo porque su ruta de instalación o los permisos del directorio no superaron una comprobación de seguridad. Reinstale el servicio para restaurar sus archivos y permisos e inténtelo de nuevo. Windows puede solicitar autorización de administrador.", "notAutoStartedMessage": "El tipo de inicio del servicio de Clash Verge se cambió a Manual o Deshabilitado (a menudo por herramientas de optimización), por lo que ya no se inicia con Windows; esta sesión usa Sidecar. Reinstale el servicio para restaurar el inicio automático. Windows puede solicitar autorización de administrador.", + "upgradeIncompleteMessage": "Clash Verge se actualizó, pero el servicio de Clash Verge que estaba instalado no está listo. Reinstala el servicio para seguir usándolo; el sistema puede solicitar autenticación de administrador.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/es/settings.json b/src/locales/es/settings.json index 56a00fa22..19e28f8e6 100644 --- a/src/locales/es/settings.json +++ b/src/locales/es/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "El servicio no pudo confirmar que los archivos y carpetas del núcleo sean seguros para ejecutarlos con privilegios elevados.", "permissionRepairGuide": "Ver instrucciones de reparación", "sidecarFallback": "El servicio no pudo iniciar el núcleo. Se ha cambiado al modo Sidecar; el servicio sigue sin estar disponible.", + "upgradeStateNotRestored": "Tras la actualización no se pudo restaurar el estado anterior de inicio y ejecución del servicio de Clash Verge ({{state}}). Reinstalar el servicio no lo restaurará; compruébelo con systemctl status clash-verge-service y ajústelo con systemctl.", "uninstallSuccess": "Servicio desinstalado con éxito" }, "updater": { diff --git a/src/locales/fa/layout.json b/src/locales/fa/layout.json index 60438d57a..c9fa3f0dc 100644 --- a/src/locales/fa/layout.json +++ b/src/locales/fa/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "سرویس نمی‌تواند هسته را اجرا کند، زیرا مسیر نصب یا مجوزهای پوشه در بررسی امنیتی تأیید نشده‌اند. برای بازیابی فایل‌ها و مجوزها، سرویس را دوباره نصب و سپس مجدداً تلاش کنید. ممکن است ویندوز تأیید مدیر را درخواست کند.", "notAutoStartedMessage": "نوع راه‌اندازی سرویس Clash Verge به «دستی» یا «غیرفعال» تغییر کرده است (اغلب توسط ابزارهای بهینه‌ساز)، بنابراین دیگر همراه ویندوز اجرا نمی‌شود؛ این نشست از Sidecar استفاده می‌کند. برای بازگرداندن راه‌اندازی خودکار، سرویس را دوباره نصب کنید. ممکن است ویندوز تأیید مدیر را درخواست کند.", + "upgradeIncompleteMessage": "Clash Verge ارتقا یافت، اما سرویس Clash Verge که قبلاً نصب شده بود آماده نیست. برای ادامه استفاده، سرویس را دوباره نصب کنید؛ ممکن است سیستم احراز هویت مدیر را درخواست کند.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/fa/settings.json b/src/locales/fa/settings.json index 9c820827d..81dbc5f81 100644 --- a/src/locales/fa/settings.json +++ b/src/locales/fa/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "سرویس نتوانست ایمن بودن فایل‌ها و پوشه‌های هسته را برای اجرا با دسترسی بالا تأیید کند.", "permissionRepairGuide": "مشاهدهٔ راهنمای رفع مشکل", "sidecarFallback": "سرویس نتوانست هسته را اجرا کند. اجرا در حالت Sidecar ادامه می‌یابد؛ سرویس همچنان در دسترس نیست.", + "upgradeStateNotRestored": "پس از ارتقا، وضعیت قبلی راه‌اندازی خودکار و اجرای سرویس Clash Verge ({{state}}) بازگردانده نشد. نصب دوباره سرویس آن را بازنمی‌گرداند؛ با systemctl status clash-verge-service آن را بررسی و با systemctl تنظیم کنید.", "uninstallSuccess": "سرویس با موفقیت حذف نصب شد" }, "updater": { diff --git a/src/locales/id/layout.json b/src/locales/id/layout.json index 1349f6c0e..0d619409e 100644 --- a/src/locales/id/layout.json +++ b/src/locales/id/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "Layanan tidak dapat memulai inti karena jalur instalasi atau izin direktori tidak lolos pemeriksaan keamanan. Pasang ulang layanan untuk memulihkan berkas dan izinnya, lalu coba lagi. Windows mungkin meminta persetujuan administrator.", "notAutoStartedMessage": "Jenis startup layanan Clash Verge diubah menjadi Manual atau Nonaktif (sering oleh alat pengoptimal), sehingga tidak lagi berjalan saat Windows dimulai; sesi ini menggunakan Sidecar. Pasang ulang layanan untuk memulihkan startup otomatis. Windows mungkin meminta persetujuan administrator.", + "upgradeIncompleteMessage": "Clash Verge telah ditingkatkan, tetapi layanan Clash Verge yang sebelumnya terpasang belum siap. Pasang ulang layanan untuk terus menggunakannya; sistem mungkin meminta autentikasi administrator.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/id/settings.json b/src/locales/id/settings.json index e4365e895..070839e27 100644 --- a/src/locales/id/settings.json +++ b/src/locales/id/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "Layanan tidak dapat memastikan bahwa berkas dan folder inti aman dijalankan dengan hak istimewa.", "permissionRepairGuide": "Lihat panduan perbaikan", "sidecarFallback": "Layanan tidak dapat memulai inti. Beralih ke mode Sidecar; layanan masih tidak tersedia.", + "upgradeStateNotRestored": "Setelah pembaruan, status mulai otomatis dan berjalan sebelumnya dari layanan Clash Verge ({{state}}) tidak dapat dipulihkan. Memasang ulang layanan tidak akan memulihkannya; periksa dengan systemctl status clash-verge-service dan sesuaikan dengan systemctl.", "uninstallSuccess": "Layanan Berhasil Dicopot" }, "updater": { diff --git a/src/locales/jp/layout.json b/src/locales/jp/layout.json index a6bf5e76e..8bfaa28d5 100644 --- a/src/locales/jp/layout.json +++ b/src/locales/jp/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "サービスのインストール先またはフォルダーのアクセス許可がセキュリティチェックに合格しなかったため、コアを起動できません。サービスを再インストールしてファイルとアクセス許可を復元し、再試行してください。Windows が管理者の承認を求める場合があります。", "notAutoStartedMessage": "Clash Verge サービスのスタートアップの種類が「手動」または「無効」に変更されたため(最適化ツールによることが多い)、Windows の起動時に開始されません。このセッションは Sidecar で動作しています。サービスを再インストールすると自動起動に戻ります。Windows が管理者の承認を求める場合があります。", + "upgradeIncompleteMessage": "Clash Verge はアップグレードされましたが、インストール済みの Clash Verge サービスが準備できていません。引き続き使用するにはサービスを再インストールしてください。管理者認証を求められる場合があります。", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/jp/settings.json b/src/locales/jp/settings.json index 4b0bff37c..71b81aa90 100644 --- a/src/locales/jp/settings.json +++ b/src/locales/jp/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "コアファイルとフォルダーを高い権限で安全に実行できるか確認できませんでした。", "permissionRepairGuide": "修復方法を見る", "sidecarFallback": "サービスでコアを起動できなかったため、Sidecar モードで動作を継続しています。サービスは引き続き利用できません。", + "upgradeStateNotRestored": "アップグレード後、Clash Verge サービスの以前の自動起動と実行状態({{state}})を復元できませんでした。サービスを再インストールしても復元されません。systemctl status clash-verge-service で状態を確認し、systemctl で調整してください。", "uninstallSuccess": "サービスのアンインストールに成功しました。" }, "updater": { diff --git a/src/locales/ko/layout.json b/src/locales/ko/layout.json index a07d358e8..850a5460a 100644 --- a/src/locales/ko/layout.json +++ b/src/locales/ko/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "서비스 설치 경로나 폴더 권한이 보안 검사를 통과하지 못해 코어를 시작할 수 없습니다. 서비스를 다시 설치하여 파일과 권한을 복원한 후 다시 시도하세요. Windows에서 관리자 승인을 요청할 수 있습니다.", "notAutoStartedMessage": "Clash Verge 서비스의 시작 유형이 수동 또는 사용 안 함으로 변경되어(최적화 도구가 변경하는 경우가 많음) Windows 시작 시 실행되지 않습니다. 이번 세션은 Sidecar로 실행 중입니다. 서비스를 다시 설치하면 자동 시작이 복원됩니다. Windows에서 관리자 승인을 요청할 수 있습니다.", + "upgradeIncompleteMessage": "Clash Verge가 업그레이드되었지만 설치되어 있던 Clash Verge 서비스가 준비되지 않았습니다. 계속 사용하려면 서비스를 다시 설치하세요. 관리자 인증을 요청할 수 있습니다.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/ko/settings.json b/src/locales/ko/settings.json index 12945d42e..0e08ff6d9 100644 --- a/src/locales/ko/settings.json +++ b/src/locales/ko/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "서비스가 코어 파일과 폴더를 높은 권한으로 안전하게 실행할 수 있는지 확인하지 못했습니다.", "permissionRepairGuide": "해결 방법 보기", "sidecarFallback": "서비스가 코어를 시작하지 못해 Sidecar 모드로 계속 실행합니다. 서비스는 여전히 사용할 수 없습니다.", + "upgradeStateNotRestored": "업그레이드 후 Clash Verge 서비스의 이전 부팅 시 시작 및 실행 상태({{state}})를 복원하지 못했습니다. 서비스를 다시 설치해도 복원되지 않습니다. systemctl status clash-verge-service로 상태를 확인하고 systemctl로 조정하세요.", "uninstallSuccess": "서비스가 제거되었습니다" }, "updater": { diff --git a/src/locales/pt/layout.json b/src/locales/pt/layout.json index 6c1a2142b..df6b0c4d8 100644 --- a/src/locales/pt/layout.json +++ b/src/locales/pt/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "O serviço não pode iniciar o núcleo porque o caminho de instalação ou as permissões do diretório falharam em uma verificação de segurança. Reinstale o serviço para restaurar seus arquivos e permissões e tente novamente. O Windows pode solicitar aprovação de administrador.", "notAutoStartedMessage": "O tipo de inicialização do serviço Clash Verge foi alterado para Manual ou Desativado (geralmente por ferramentas de otimização do sistema), então ele não inicia mais com o Windows; esta sessão está usando Sidecar. Reinstale o serviço para restaurar a inicialização automática. O Windows pode solicitar aprovação de administrador.", + "upgradeIncompleteMessage": "O Clash Verge foi atualizado, mas o serviço do Clash Verge que estava instalado não está pronto. Reinstale o serviço para continuar a usá-lo; o sistema pode pedir autenticação de administrador.", "title": "Atualização do serviço necessária", "message": "A versão do serviço Clash Verge não corresponde à versão exigida por este app. Escolha Reinstalar serviço para desinstalar o serviço existente e instalar a versão exigida, ou continue com o Sidecar nesta sessão.", "checkingMessage": "Verificando o status do serviço do sistema e o modo de execução atual. Aguarde…", diff --git a/src/locales/pt/settings.json b/src/locales/pt/settings.json index 67230c587..9fa5bb890 100644 --- a/src/locales/pt/settings.json +++ b/src/locales/pt/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "O serviço não conseguiu confirmar que os arquivos e pastas do núcleo são seguros para execução com privilégios elevados.", "permissionRepairGuide": "Ver instruções de reparo", "sidecarFallback": "O serviço não conseguiu iniciar o núcleo. Mudou para o modo Sidecar; o serviço continua indisponível.", + "upgradeStateNotRestored": "Após a atualização, não foi possível restaurar o estado anterior de inicialização e execução do serviço do Clash Verge ({{state}}). Reinstalar o serviço não o restaurará; verifique-o com systemctl status clash-verge-service e ajuste-o com systemctl.", "uninstallSuccess": "Serviço Desinstalado com Sucesso" }, "updater": { diff --git a/src/locales/ru/layout.json b/src/locales/ru/layout.json index 479a678df..5a24c11be 100644 --- a/src/locales/ru/layout.json +++ b/src/locales/ru/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "Служба не может запустить ядро: путь установки или права доступа к каталогу не прошли проверку безопасности. Переустановите службу, чтобы восстановить файлы и права доступа, затем повторите попытку. Windows может запросить подтверждение администратора.", "notAutoStartedMessage": "Тип запуска службы Clash Verge изменён на «Вручную» или «Отключена» (часто это делают программы-оптимизаторы), поэтому она больше не запускается вместе с Windows; в этом сеансе используется Sidecar. Переустановите службу, чтобы вернуть автоматический запуск. Windows может запросить подтверждение администратора.", + "upgradeIncompleteMessage": "Clash Verge обновлён, но ранее установленная служба Clash Verge не готова. Переустановите службу, чтобы продолжить её использовать; система может запросить аутентификацию администратора.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/ru/settings.json b/src/locales/ru/settings.json index caf1bc347..82d1a52c0 100644 --- a/src/locales/ru/settings.json +++ b/src/locales/ru/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "Служба не смогла подтвердить безопасность файлов и папок ядра для запуска с повышенными правами.", "permissionRepairGuide": "Инструкция по исправлению", "sidecarFallback": "Служба не смогла запустить ядро. Работа продолжена в режиме Sidecar; служба по-прежнему недоступна.", + "upgradeStateNotRestored": "После обновления не удалось восстановить прежнее состояние автозапуска и работы службы Clash Verge ({{state}}). Переустановка службы его не восстановит; проверьте её командой systemctl status clash-verge-service и настройте через systemctl.", "uninstallSuccess": "Служба успешно удалена" }, "updater": { diff --git a/src/locales/tr/layout.json b/src/locales/tr/layout.json index de00692d5..36fed0936 100644 --- a/src/locales/tr/layout.json +++ b/src/locales/tr/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "Hizmetin kurulum yolu veya dizin izinleri güvenlik denetiminden geçemediği için çekirdek başlatılamıyor. Dosyaları ve izinleri geri yüklemek için hizmeti yeniden kurup tekrar deneyin. Windows yönetici onayı isteyebilir.", "notAutoStartedMessage": "Clash Verge hizmetinin başlangıç türü El ile veya Devre dışı olarak değiştirildi (genellikle optimizasyon araçları tarafından), bu nedenle artık Windows ile başlamıyor; bu oturum Sidecar kullanıyor. Otomatik başlatmayı geri yüklemek için hizmeti yeniden kurun. Windows yönetici onayı isteyebilir.", + "upgradeIncompleteMessage": "Clash Verge yükseltildi, ancak yüklü olan Clash Verge hizmeti hazır değil. Kullanmaya devam etmek için hizmeti yeniden yükleyin; sistem yönetici kimlik doğrulaması isteyebilir.", "title": "Hizmet güncellemesi gerekli", "message": "Clash Verge hizmet sürümü, bu uygulamanın gerektirdiği sürümle eşleşmiyor. Mevcut hizmeti kaldırıp gerekli sürümü yüklemek için hizmeti yeniden kurun ya da bu oturumda Sidecar ile devam edin.", "checkingMessage": "Sistem hizmetinin durumu ve geçerli çalışma modu denetleniyor. Lütfen bekleyin…", diff --git a/src/locales/tr/settings.json b/src/locales/tr/settings.json index 21d5a5c2b..e96563c13 100644 --- a/src/locales/tr/settings.json +++ b/src/locales/tr/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "Hizmet, çekirdek dosya ve klasörlerinin yükseltilmiş yetkilerle güvenle çalıştırılabileceğini doğrulayamadı.", "permissionRepairGuide": "Düzeltme adımlarını görüntüle", "sidecarFallback": "Hizmet çekirdeği başlatamadı. Sidecar moduna geçildi; hizmet hâlâ kullanılamıyor.", + "upgradeStateNotRestored": "Yükseltmeden sonra Clash Verge hizmetinin önceki açılışta başlatma ve çalışma durumu ({{state}}) geri yüklenemedi. Hizmeti yeniden kurmak bunu geri yüklemez; systemctl status clash-verge-service ile kontrol edin ve systemctl ile ayarlayın.", "uninstallSuccess": "Hizmet Başarıyla Kaldırıldı" }, "updater": { diff --git a/src/locales/tt/layout.json b/src/locales/tt/layout.json index 18db1b8ff..abf222b51 100644 --- a/src/locales/tt/layout.json +++ b/src/locales/tt/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "Хезмәтнең урнаштыру юлы яки каталог рөхсәтләре иминлек тикшерүен узмаганга, төшне эшләтеп булмый. Файлларны һәм рөхсәтләрне торгызу өчен хезмәтне яңадан урнаштырыгыз һәм кабатлап карагыз. Windows администратор раславын сорарга мөмкин.", "notAutoStartedMessage": "Clash Verge хезмәтенең башлау төре «Кулдан» яки «Сүндерелгән» итеп үзгәртелгән (еш кына оптимизация программалары тарафыннан), шуңа күрә ул Windows белән бергә башланмый; бу сеанс Sidecar куллана. Автоматик башлауны кайтару өчен хезмәтне яңадан урнаштырыгыз. Windows администратор раславын сорарга мөмкин.", + "upgradeIncompleteMessage": "Clash Verge яңартылды, ләкин элек урнаштырылган Clash Verge хезмәте әзер түгел. Аны куллануны дәвам итәр өчен хезмәтне яңадан урнаштырыгыз; система администратор аутентификациясен сорарга мөмкин.", "title": "Service update required", "message": "The Clash Verge service version does not match the version required by this app. Choose Reinstall Service to uninstall the existing service and install the required version, or continue with Sidecar for this session.", "checkingMessage": "Checking the system service status and current running mode. Please wait…", diff --git a/src/locales/tt/settings.json b/src/locales/tt/settings.json index a95e305c4..181c6874f 100644 --- a/src/locales/tt/settings.json +++ b/src/locales/tt/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "Хезмәт төш файлларын һәм папкаларын югары хокуклар белән имин эшләтеп булачагын раслый алмады.", "permissionRepairGuide": "Төзәтү күрсәтмәләрен карау", "sidecarFallback": "Хезмәт төшне эшләтеп җибәрә алмады. Эш Sidecar режимында дәвам итә; хезмәт һаман да эшләми.", + "upgradeStateNotRestored": "Яңартудан соң Clash Verge хезмәтенең элекке автоҗибәрү һәм эшләү халәтен ({{state}}) кайтарып булмады. Хезмәтне яңадан урнаштыру аны кайтармый; аны systemctl status clash-verge-service белән тикшерегез һәм systemctl белән көйләгез.", "uninstallSuccess": "Сервис уңышлы салдырылды" }, "updater": { diff --git a/src/locales/zh/layout.json b/src/locales/zh/layout.json index 0fc52c288..c5be91993 100644 --- a/src/locales/zh/layout.json +++ b/src/locales/zh/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "系统服务的安装路径或目录权限未通过安全检查,无法启动内核。请重新安装服务以恢复服务文件和权限,随后重试。Windows 可能会请求管理员授权。", "notAutoStartedMessage": "Clash Verge 系统服务的启动类型已被改为手动或禁用(常见于优化软件的开机加速功能),开机后不会自动运行,本次已改用 Sidecar。请重新安装服务以恢复自动启动。Windows 可能会请求管理员授权。", + "upgradeIncompleteMessage": "Clash Verge 已升级,但原先安装的 Clash Verge 服务未就绪。请重装服务以继续使用,系统可能要求管理员认证。", "title": "需要更新系统服务", "message": "Clash Verge 系统服务版本与当前应用要求不符。请选择「重新安装服务」卸载现有服务并安装所需版本,也可以仅在本次会话继续使用 Sidecar。", "checkingMessage": "正在确认系统服务状态及当前运行模式,请稍候……", diff --git a/src/locales/zh/settings.json b/src/locales/zh/settings.json index a03389427..2c9f8c0ba 100644 --- a/src/locales/zh/settings.json +++ b/src/locales/zh/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "服务无法确认内核文件及所在目录是否安全,已拒绝高权限运行。", "permissionRepairGuide": "查看修复方法", "sidecarFallback": "服务无法启动内核,已降级为 Sidecar 模式运行,服务仍不可用。", + "upgradeStateNotRestored": "升级后未能恢复 Clash Verge 服务原有的开机启动和运行状态({{state}})。重新安装服务也无法恢复该状态;请用 systemctl status clash-verge-service 检查服务状态,并用 systemctl 调整。", "uninstallSuccess": "已成功卸载服务" }, "updater": { diff --git a/src/locales/zhtw/layout.json b/src/locales/zhtw/layout.json index cd5b6d642..8b2455513 100644 --- a/src/locales/zhtw/layout.json +++ b/src/locales/zhtw/layout.json @@ -23,6 +23,7 @@ "serviceMigration": { "locationRefusedMessage": "系統服務的安裝路徑或目錄權限未通過安全檢查,無法啟動核心。請重新安裝服務以還原服務檔案和權限,然後重試。Windows 可能會要求管理員授權。", "notAutoStartedMessage": "Clash Verge 系統服務的啟動類型已被改為手動或停用(常見於最佳化軟體的開機加速功能),開機後不會自動執行,本次已改用 Sidecar。請重新安裝服務以恢復自動啟動。Windows 可能會要求管理員授權。", + "upgradeIncompleteMessage": "Clash Verge 已升級,但原先安裝的 Clash Verge 服務未就緒。請重新安裝服務以繼續使用,系統可能要求管理員驗證。", "title": "需要更新系統服務", "message": "Clash Verge 系統服務版本與目前應用程式要求不符。請選擇「重新安裝服務」移除現有服務並安裝所需版本,也可以僅在本次工作階段繼續使用 Sidecar。", "checkingMessage": "正在確認系統服務狀態及目前執行模式,請稍候……", diff --git a/src/locales/zhtw/settings.json b/src/locales/zhtw/settings.json index a117c3f82..5b1186195 100644 --- a/src/locales/zhtw/settings.json +++ b/src/locales/zhtw/settings.json @@ -697,6 +697,7 @@ "permissionRejectedReason": "服務無法確認核心檔案及所在目錄是否安全,已拒絕以高權限執行。", "permissionRepairGuide": "查看修復方法", "sidecarFallback": "服務無法啟動核心,已降級為 Sidecar 模式執行,服務仍無法使用。", + "upgradeStateNotRestored": "升級後未能恢復 Clash Verge 服務原有的開機啟動和執行狀態({{state}})。重新安裝服務也無法恢復該狀態;請用 systemctl status clash-verge-service 檢查服務狀態,並用 systemctl 調整。", "uninstallSuccess": "已成功解除安裝服務" }, "updater": { diff --git a/src/services/bindings.ts b/src/services/bindings.ts index 02e633c49..f08e1951f 100644 --- a/src/services/bindings.ts +++ b/src/services/bindings.ts @@ -111,6 +111,8 @@ export const commands = { __TAURI_INVOKE('take_service_repair_notice'), takeServiceOwnerNotice: () => __TAURI_INVOKE('take_service_owner_notice'), + takeServiceUpgradeStateNotice: () => + __TAURI_INVOKE('take_service_upgrade_state_notice'), takeDiscardedKeysNotice: () => __TAURI_INVOKE('take_discarded_keys_notice'), getDnsConfigContent: () => @@ -618,6 +620,7 @@ export type NoticeStatus = | 'service_core::repair_required' | 'service_core::app_data_not_owned' | 'service_core::sidecar_fallback' + | 'service_core::upgrade_state_not_restored' | 'dns_override::auto_disabled' | 'enhance::discarded_keys' | 'tun_mode::auto_disabled' @@ -1035,6 +1038,7 @@ export type ServiceFallbackNotice = | { kind: 'unavailable' } | { kind: 'coreRejected'; reason: string } | { kind: 'notAutoStarted' } + | { kind: 'upgradeIncomplete' } export type ServiceHealthKind = | 'unknown' diff --git a/src/services/bus/create-bus-handlers.test.ts b/src/services/bus/create-bus-handlers.test.ts index 844e40d33..5f394c321 100644 --- a/src/services/bus/create-bus-handlers.test.ts +++ b/src/services/bus/create-bus-handlers.test.ts @@ -6,6 +6,7 @@ import { takeDnsOverrideNotice, takeServiceFallbackNotice, takeServiceRepairNotice, + takeServiceUpgradeStateNotice, } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { revalidateQueries } from '@/services/query-client' @@ -27,6 +28,7 @@ vi.mock('@/services/cmds', () => ({ takeServiceFallbackNotice: vi.fn().mockResolvedValue(null), takeServiceRepairNotice: vi.fn().mockResolvedValue(false), takeServiceOwnerNotice: vi.fn().mockResolvedValue(null), + takeServiceUpgradeStateNotice: vi.fn().mockResolvedValue(null), })) vi.mock('@/services/service-request', () => ({ requestService: vi.fn() })) vi.mock('@/services/notice-service', () => ({ @@ -198,3 +200,23 @@ it('explains a startup core rejection instead of the generic fallback notice', a 0, ) }) + +it('replays a pending Service upgrade state warning once the frontend subscribes', async () => { + vi.mocked(takeServiceUpgradeStateNotice) + .mockResolvedValueOnce('disabled,active') + .mockResolvedValue(null) + + const bus = await mountBus() + bus.onSubscribed() + bus.handlers['verge://notice-message']([ + 'service_core::upgrade_state_not_restored', + '', + ]) + await flush() + + expect(showNotice.warning).toHaveBeenCalledExactlyOnceWith( + 'settings.feedback.notifications.clashService.upgradeStateNotRestored', + { state: 'disabled,active' }, + 0, + ) +}) diff --git a/src/services/bus/create-bus-handlers.ts b/src/services/bus/create-bus-handlers.ts index c41442704..59f0c045c 100644 --- a/src/services/bus/create-bus-handlers.ts +++ b/src/services/bus/create-bus-handlers.ts @@ -105,6 +105,7 @@ export const createEventBus = (deps: BusHandlerDeps) => { deps.handleNotice(['service_core::sidecar_fallback', '']) deps.handleNotice(['service_core::repair_required', '']) deps.handleNotice(['service_core::app_data_not_owned', '']) + deps.handleNotice(['service_core::upgrade_state_not_restored', '']) deps.handleNotice(['core_start::error', '']) } diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 6b118d34a..ddc48ba64 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -248,6 +248,10 @@ export async function takeServiceOwnerNotice() { return commands.takeServiceOwnerNotice() } +export async function takeServiceUpgradeStateNotice() { + return commands.takeServiceUpgradeStateNotice() +} + export async function takeDiscardedKeysNotice() { return commands.takeDiscardedKeysNotice() } diff --git a/src/services/notice-handlers.ts b/src/services/notice-handlers.ts index 3a8ff5eba..19dbf3a29 100644 --- a/src/services/notice-handlers.ts +++ b/src/services/notice-handlers.ts @@ -7,6 +7,7 @@ import { takeServiceFallbackNotice, takeServiceOwnerNotice, takeServiceRepairNotice, + takeServiceUpgradeStateNotice, } from '@/services/cmds' import type { NoticeStatus } from '@/services/contract' import { hideNotice, showNotice } from '@/services/notice-service' @@ -125,6 +126,8 @@ const handlers: Record< ) } else if (notice?.kind === 'notAutoStarted') { requestService({ reason: 'serviceNotAutoStarted' }) + } else if (notice?.kind === 'upgradeIncomplete') { + requestService({ reason: 'serviceUpgradeIncomplete' }) } else if (notice) { showNotice.warning( 'settings.feedback.notifications.clashService.sidecarFallback', @@ -138,6 +141,24 @@ const handlers: Record< ) }) }, + 'service_core::upgrade_state_not_restored': () => { + void takeServiceUpgradeStateNotice() + .then((state) => { + if (state) { + showNotice.warning( + 'settings.feedback.notifications.clashService.upgradeStateNotRestored', + { state }, + 0, + ) + } + }) + .catch((error) => { + console.error( + 'Failed to read the pending service upgrade state notice', + error, + ) + }) + }, 'dns_override::auto_disabled': () => { void takeDnsOverrideNotice() .then((pending) => { diff --git a/src/services/service-request.ts b/src/services/service-request.ts index 01af78676..c4d6d9aaf 100644 --- a/src/services/service-request.ts +++ b/src/services/service-request.ts @@ -5,6 +5,7 @@ export type ServiceRequestReason = | 'tunNeedsService' | 'serviceLocationRefused' | 'serviceNotAutoStarted' + | 'serviceUpgradeIncomplete' export interface ServiceRequest { readonly reason: ServiceRequestReason diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index ba6eac3a7..60c172755 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -128,6 +128,7 @@ export const translationKeys = [ 'layout.components.navigation.menu.expandNavBar', 'layout.components.serviceMigration.locationRefusedMessage', 'layout.components.serviceMigration.notAutoStartedMessage', + 'layout.components.serviceMigration.upgradeIncompleteMessage', 'layout.components.serviceMigration.title', 'layout.components.serviceMigration.message', 'layout.components.serviceMigration.checkingMessage', @@ -820,6 +821,7 @@ export const translationKeys = [ 'settings.feedback.notifications.clashService.permissionRejectedReason', 'settings.feedback.notifications.clashService.permissionRepairGuide', 'settings.feedback.notifications.clashService.sidecarFallback', + 'settings.feedback.notifications.clashService.upgradeStateNotRestored', 'settings.feedback.notifications.clashService.uninstallSuccess', 'settings.feedback.notifications.updater.withClashProxySuccess', 'settings.feedback.notifications.updater.withClashProxyFailed', diff --git a/src/types/generated/i18n-resources.ts b/src/types/generated/i18n-resources.ts index 3e5a527f9..ff44e3b6b 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -233,6 +233,7 @@ export interface TranslationResources { success: string title: string unavailableMessage: string + upgradeIncompleteMessage: string } sysproxyPrivilege: { alternative: string @@ -857,6 +858,7 @@ export interface TranslationResources { permissionWritableReason: string sidecarFallback: string uninstallSuccess: string + upgradeStateNotRestored: string } updater: { withClashProxyFailed: string