diff --git a/src-tauri/packages/linux/post-install-rpm.sh b/src-tauri/packages/linux/post-install-rpm.sh index 3f1cd5c56..46c7146c1 100644 --- a/src-tauri/packages/linux/post-install-rpm.sh +++ b/src-tauri/packages/linux/post-install-rpm.sh @@ -14,8 +14,19 @@ 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 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$UPGRADE_MARKER") + ( + umask 022 + { + printf 'gid=%s\nid=%s\n' "$1" "$(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 @@ -62,7 +73,8 @@ reinstall_existing_service() { systemctl stop "$SERVICE_UNIT" || restored=no fi if [ "$restored" = "no" ]; then - echo "Warning: restoring the previous Clash Verge Service state (${enabled:-unknown}, active=$active) failed; check it with systemctl." >&2 + 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" diff --git a/src-tauri/packages/linux/post-install.sh b/src-tauri/packages/linux/post-install.sh index ee007c089..d8e9f5c9e 100644 --- a/src-tauri/packages/linux/post-install.sh +++ b/src-tauri/packages/linux/post-install.sh @@ -10,8 +10,19 @@ 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 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$UPGRADE_MARKER") + ( + umask 022 + { + printf 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" + if [ -n "$UNRESTORED_STATE" ]; then + printf 'restore=%s\n' "$UNRESTORED_STATE" + fi + } >"$UPGRADE_MARKER" + ) } existing_service_gid() { @@ -49,7 +60,8 @@ reinstall_existing_service() { systemctl stop "$SERVICE_UNIT" || restored=no fi if [ "$restored" = "no" ]; then - echo "Warning: restoring the previous Clash Verge Service state (${enabled:-unknown}, active=$active) failed; check it with systemctl." >&2 + 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" diff --git a/src-tauri/src/core/notify/mod.rs b/src-tauri/src/core/notify/mod.rs index 36c3f1c2f..957106a4b 100644 --- a/src-tauri/src/core/notify/mod.rs +++ b/src-tauri/src/core/notify/mod.rs @@ -24,6 +24,9 @@ pub enum NoticeStatus { #[serde(rename = "service_core::sidecar_fallback")] #[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 f656b2b2b..c8ed359e9 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -92,16 +92,42 @@ pub(crate) async fn notify_incomplete_service_upgrade() { return; } let status = SERVICE_MANAGER.current().await; - if !matches!(status, ServiceStatus::Ready) { - logging!(warn, Type::Service, "安装包升级后服务未就绪: {status:?}"); - *PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(SERVICE_UPGRADE_INCOMPLETE.to_owned()); - Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, ""); + match service_upgrade_notice(&marker, matches!(status, ServiceStatus::Ready)) { + Some(ServiceUpgradeNotice::Reinstall) => { + logging!(warn, Type::Service, "安装包升级后服务未就绪: {status:?}"); + *PENDING_SERVICE_FALLBACK_NOTICE.lock() = Some(SERVICE_UPGRADE_INCOMPLETE.to_owned()); + Handle::notice(NoticeStatus::ServiceCoreSidecarFallback, ""); + } + Some(ServiceUpgradeNotice::StateNotRestored(state)) => { + logging!(warn, Type::Service, "安装包升级后未能恢复服务原状态: {state}"); + Handle::notice(NoticeStatus::ServiceCoreUpgradeStateNotRestored, state); + } + None => {} } if let Err(error) = std::fs::write(&ack, id) { logging!(warn, Type::Service, "无法记录服务升级确认: {error}"); } } +#[cfg(target_os = "linux")] +#[derive(Debug, PartialEq, Eq)] +enum ServiceUpgradeNotice<'a> { + Reinstall, + /// A reinstall would enable and start the Service again, so it cannot restore this state. + StateNotRestored(&'a str), +} + +#[cfg(target_os = "linux")] +fn service_upgrade_notice(marker: &str, ready: bool) -> Option> { + if !ready { + return Some(ServiceUpgradeNotice::Reinstall); + } + marker + .lines() + .find_map(|line| line.strip_prefix("restore=")) + .map(ServiceUpgradeNotice::StateNotRestored) +} + pub(crate) fn take_service_fallback_notice() -> Option { let reason = PENDING_SERVICE_FALLBACK_NOTICE.lock().take()?; Some(if reason == SERVICE_UPGRADE_INCOMPLETE { @@ -2167,6 +2193,8 @@ mod tests { generate_service_session_token, mark_service_unavailable_after_owner_loss, owner_recovery_policy, service_core_path_for, session_matches_status, }; + #[cfg(target_os = "linux")] + use super::{ServiceUpgradeNotice, service_upgrade_notice}; #[cfg(unix)] use super::{service_core_path_for_with_publisher, service_tool_path_for}; use crate::core::runstate::{FakeEnv, OwnerRecoveryReason, PendingAction, RunStateStore}; @@ -2824,4 +2852,24 @@ 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_only_for_a_ready_service() { + let restored = "gid=995\nid=1\n"; + let unrestored = "gid=995\nid=1\nrestore=disabled,inactive\n"; + assert_eq!( + service_upgrade_notice(restored, false), + Some(ServiceUpgradeNotice::Reinstall) + ); + assert_eq!( + service_upgrade_notice(unrestored, false), + Some(ServiceUpgradeNotice::Reinstall) + ); + assert_eq!(service_upgrade_notice(restored, true), None); + assert_eq!( + service_upgrade_notice(unrestored, true), + Some(ServiceUpgradeNotice::StateNotRestored("disabled,inactive")) + ); + } } diff --git a/src/locales/ar/settings.json b/src/locales/ar/settings.json index 494c5a059..086378e52 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/settings.json b/src/locales/de/settings.json index 938fd8992..3812c34ac 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": "Das Upgrade hat den Clash Verge-Dienst neu installiert, konnte aber seinen vorherigen Autostart- und Laufzustand ({{state}}) nicht wiederherstellen. 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/settings.json b/src/locales/en/settings.json index 1ef97ab22..96fef1c27 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": "The upgrade reinstalled the Clash Verge service but could not restore its 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/settings.json b/src/locales/es/settings.json index 56a00fa22..3c2e5dc51 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": "La actualización reinstaló el servicio de Clash Verge, pero no pudo restaurar su estado anterior de inicio y ejecución ({{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/settings.json b/src/locales/fa/settings.json index 9c820827d..c17a995ff 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/settings.json b/src/locales/id/settings.json index e4365e895..400d8aedf 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": "Pembaruan telah memasang ulang layanan Clash Verge tetapi tidak dapat memulihkan status mulai otomatis dan berjalan sebelumnya ({{state}}). 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/settings.json b/src/locales/jp/settings.json index 4b0bff37c..b8dd6a981 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/settings.json b/src/locales/ko/settings.json index 12945d42e..3fac83495 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/settings.json b/src/locales/pt/settings.json index 67230c587..58161e103 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": "A atualização reinstalou o serviço do Clash Verge, mas não conseguiu restaurar o estado anterior de inicialização e execução ({{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/settings.json b/src/locales/ru/settings.json index caf1bc347..097d2c065 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/settings.json b/src/locales/tr/settings.json index 21d5a5c2b..96a69d8ae 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ükseltme Clash Verge hizmetini yeniden kurdu ancak önceki açılışta başlatma ve çalışma durumunu ({{state}}) geri yükleyemedi. 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/settings.json b/src/locales/tt/settings.json index a95e305c4..151505219 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/settings.json b/src/locales/zh/settings.json index a03389427..87be8e583 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/settings.json b/src/locales/zhtw/settings.json index a117c3f82..9bb6e8863 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 1310f3a14..1db027d5b 100644 --- a/src/services/bindings.ts +++ b/src/services/bindings.ts @@ -618,6 +618,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' diff --git a/src/services/notice-handlers.ts b/src/services/notice-handlers.ts index af0dd15cf..e9051ce38 100644 --- a/src/services/notice-handlers.ts +++ b/src/services/notice-handlers.ts @@ -140,6 +140,12 @@ const handlers: Record< ) }) }, + 'service_core::upgrade_state_not_restored': (msg) => + showNotice.warning( + 'settings.feedback.notifications.clashService.upgradeStateNotRestored', + { state: msg }, + 0, + ), 'dns_override::auto_disabled': () => { void takeDnsOverrideNotice() .then((pending) => { diff --git a/src/types/generated/i18n-keys.ts b/src/types/generated/i18n-keys.ts index 297e6052d..60c172755 100644 --- a/src/types/generated/i18n-keys.ts +++ b/src/types/generated/i18n-keys.ts @@ -821,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 a516cd6e0..ff44e3b6b 100644 --- a/src/types/generated/i18n-resources.ts +++ b/src/types/generated/i18n-resources.ts @@ -858,6 +858,7 @@ export interface TranslationResources { permissionWritableReason: string sidecarFallback: string uninstallSuccess: string + upgradeStateNotRestored: string } updater: { withClashProxyFailed: string