feat(linux): show when an upgrade could not restore the Service state

Refs #8515
This commit is contained in:
Tunglies
2026-11-10 07:53:00 +08:00
parent 136385c028
commit aeffb55820
22 changed files with 106 additions and 8 deletions
+14 -2
View File
@@ -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"
+14 -2
View File
@@ -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"
+3
View File
@@ -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")]
+52 -4
View File
@@ -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<ServiceUpgradeNotice<'_>> {
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<ServiceFallbackNotice> {
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"))
);
}
}
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "تعذّر على الخدمة التأكد من أن ملفات النواة ومجلداتها آمنة للتشغيل بصلاحيات مرتفعة.",
"permissionRepairGuide": "عرض خطوات الإصلاح",
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة. تم التبديل إلى وضع Sidecar؛ ولا تزال الخدمة غير متاحة.",
"upgradeStateNotRestored": "أعادت الترقية تثبيت خدمة Clash Verge لكنها لم تتمكن من استعادة حالة بدء التشغيل والتشغيل السابقة ({{state}}). لن تؤدي إعادة تثبيت الخدمة إلى استعادتها؛ تحقق منها باستخدام systemctl status clash-verge-service واضبطها باستخدام systemctl.",
"uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح"
},
"updater": {
+1
View File
@@ -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": {
+1
View File
@@ -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": {
+1
View File
@@ -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": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "سرویس نتوانست ایمن بودن فایل‌ها و پوشه‌های هسته را برای اجرا با دسترسی بالا تأیید کند.",
"permissionRepairGuide": "مشاهدهٔ راهنمای رفع مشکل",
"sidecarFallback": "سرویس نتوانست هسته را اجرا کند. اجرا در حالت Sidecar ادامه می‌یابد؛ سرویس همچنان در دسترس نیست.",
"upgradeStateNotRestored": "ارتقا سرویس Clash Verge را دوباره نصب کرد اما نتوانست وضعیت قبلی راه‌اندازی خودکار و اجرای آن ({{state}}) را بازگرداند. نصب دوباره سرویس آن را بازنمی‌گرداند؛ با systemctl status clash-verge-service آن را بررسی و با systemctl تنظیم کنید.",
"uninstallSuccess": "سرویس با موفقیت حذف نصب شد"
},
"updater": {
+1
View File
@@ -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": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "コアファイルとフォルダーを高い権限で安全に実行できるか確認できませんでした。",
"permissionRepairGuide": "修復方法を見る",
"sidecarFallback": "サービスでコアを起動できなかったため、Sidecar モードで動作を継続しています。サービスは引き続き利用できません。",
"upgradeStateNotRestored": "アップグレードで Clash Verge サービスを再インストールしましたが、以前の自動起動と実行状態({{state}})を復元できませんでした。サービスを再インストールしても復元されません。systemctl status clash-verge-service で状態を確認し、systemctl で調整してください。",
"uninstallSuccess": "サービスのアンインストールに成功しました。"
},
"updater": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "서비스가 코어 파일과 폴더를 높은 권한으로 안전하게 실행할 수 있는지 확인하지 못했습니다.",
"permissionRepairGuide": "해결 방법 보기",
"sidecarFallback": "서비스가 코어를 시작하지 못해 Sidecar 모드로 계속 실행합니다. 서비스는 여전히 사용할 수 없습니다.",
"upgradeStateNotRestored": "업그레이드 중 Clash Verge 서비스를 다시 설치했지만 이전 부팅 시 시작 및 실행 상태({{state}})를 복원하지 못했습니다. 서비스를 다시 설치해도 복원되지 않습니다. systemctl status clash-verge-service로 상태를 확인하고 systemctl로 조정하세요.",
"uninstallSuccess": "서비스가 제거되었습니다"
},
"updater": {
+1
View File
@@ -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": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "Служба не смогла подтвердить безопасность файлов и папок ядра для запуска с повышенными правами.",
"permissionRepairGuide": "Инструкция по исправлению",
"sidecarFallback": "Служба не смогла запустить ядро. Работа продолжена в режиме Sidecar; служба по-прежнему недоступна.",
"upgradeStateNotRestored": "Обновление переустановило службу Clash Verge, но не смогло восстановить её прежнее состояние автозапуска и работы ({{state}}). Переустановка службы его не восстановит; проверьте её командой systemctl status clash-verge-service и настройте через systemctl.",
"uninstallSuccess": "Служба успешно удалена"
},
"updater": {
+1
View File
@@ -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": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "Хезмәт төш файлларын һәм папкаларын югары хокуклар белән имин эшләтеп булачагын раслый алмады.",
"permissionRepairGuide": "Төзәтү күрсәтмәләрен карау",
"sidecarFallback": "Хезмәт төшне эшләтеп җибәрә алмады. Эш Sidecar режимында дәвам итә; хезмәт һаман да эшләми.",
"upgradeStateNotRestored": "Яңарту Clash Verge хезмәтен яңадан урнаштырды, ләкин аның элекке автоҗибәрү һәм эшләү халәтен ({{state}}) кайтара алмады. Хезмәтне яңадан урнаштыру аны кайтармый; аны systemctl status clash-verge-service белән тикшерегез һәм systemctl белән көйләгез.",
"uninstallSuccess": "Сервис уңышлы салдырылды"
},
"updater": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "服务无法确认内核文件及所在目录是否安全,已拒绝高权限运行。",
"permissionRepairGuide": "查看修复方法",
"sidecarFallback": "服务无法启动内核,已降级为 Sidecar 模式运行,服务仍不可用。",
"upgradeStateNotRestored": "升级已重装 Clash Verge 服务,但未能恢复其原有的开机启动和运行状态({{state}})。重新安装服务也无法恢复该状态;请用 systemctl status clash-verge-service 检查服务状态,并用 systemctl 调整。",
"uninstallSuccess": "已成功卸载服务"
},
"updater": {
+1
View File
@@ -697,6 +697,7 @@
"permissionRejectedReason": "服務無法確認核心檔案及所在目錄是否安全,已拒絕以高權限執行。",
"permissionRepairGuide": "查看修復方法",
"sidecarFallback": "服務無法啟動核心,已降級為 Sidecar 模式執行,服務仍無法使用。",
"upgradeStateNotRestored": "升級已重新安裝 Clash Verge 服務,但未能恢復其原有的開機啟動和執行狀態({{state}})。重新安裝服務也無法恢復該狀態;請用 systemctl status clash-verge-service 檢查服務狀態,並用 systemctl 調整。",
"uninstallSuccess": "已成功解除安裝服務"
},
"updater": {
+1
View File
@@ -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'
+6
View File
@@ -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) => {
+1
View File
@@ -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',
+1
View File
@@ -858,6 +858,7 @@ export interface TranslationResources {
permissionWritableReason: string
sidecarFallback: string
uninstallSuccess: string
upgradeStateNotRestored: string
}
updater: {
withClashProxyFailed: string