Files
clash-verge-rev/src-tauri/src/core/notify/mod.rs
T
TungliesandCopilot d09f78f9b1 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>
2026-11-09 21:28:46 +08:00

178 lines
6.6 KiB
Rust

mod announce;
pub use announce::{Refresh, announce};
pub mod desktop;
pub mod handle;
pub mod notification;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, specta::Type)]
pub enum NoticeStatus {
#[serde(rename = "info")]
Info,
#[serde(rename = "import_sub_url::ok")]
ImportSubUrlOk,
#[serde(rename = "import_sub_url::error")]
ImportSubUrlError,
#[serde(rename = "set_config::error")]
SetConfigError,
#[serde(rename = "core_start::error")]
CoreStartError,
#[serde(rename = "service_core::repair_required")]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
ServiceCoreRepairRequired,
#[serde(rename = "service_core::app_data_not_owned")]
ServiceCoreAppDataNotOwned,
#[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")]
EnhanceDiscardedKeys,
#[serde(rename = "tun_mode::auto_disabled")]
TunModeAutoDisabled,
#[serde(rename = "tun_mode::auto_disable_failed")]
TunModeAutoDisableFailed,
#[serde(rename = "app_restart::core_stop_failed")]
AppRestartCoreStopFailed,
#[serde(rename = "app_quit::core_stop_failed")]
AppQuitCoreStopFailed,
#[serde(rename = "update_with_clash_proxy")]
UpdateWithClashProxy,
#[serde(rename = "update_failed_even_with_clash")]
UpdateFailedEvenWithClash,
#[serde(rename = "reactivate_profiles::error")]
ReactivateProfilesError,
#[serde(rename = "update_failed")]
UpdateFailed,
#[serde(rename = "config_validate::boot_error")]
ConfigValidateBootError,
#[serde(rename = "config_validate::error")]
ConfigValidateError,
#[serde(rename = "config_validate::process_terminated")]
ConfigValidateProcessTerminated,
#[serde(rename = "config_validate::script_error")]
ConfigValidateScriptError,
#[serde(rename = "config_validate::script_syntax_error")]
ConfigValidateScriptSyntaxError,
#[serde(rename = "config_validate::script_missing_main")]
ConfigValidateScriptMissingMain,
#[serde(rename = "config_validate::file_not_found")]
ConfigValidateFileNotFound,
#[serde(rename = "config_validate::yaml_syntax_error")]
ConfigValidateYamlSyntaxError,
#[serde(rename = "config_validate::yaml_read_error")]
ConfigValidateYamlReadError,
#[serde(rename = "config_validate::yaml_mapping_error")]
ConfigValidateYamlMappingError,
#[serde(rename = "config_validate::merge_syntax_error")]
ConfigValidateMergeSyntaxError,
#[serde(rename = "config_validate::merge_mapping_error")]
ConfigValidateMergeMappingError,
#[serde(rename = "config_core::change_success")]
ConfigCoreChangeSuccess,
#[serde(rename = "config_core::change_error")]
ConfigCoreChangeError,
#[serde(rename = "mixed_port::fallback")]
MixedPortFallback,
#[serde(rename = "mixed_port::fallback_error")]
MixedPortFallbackError,
}
impl NoticeStatus {
/// Every variant in golden order. The wire-contract export and the golden
/// test both derive their variant list from here; expected strings stay
/// hand-written in the test so renames cannot slip through.
#[cfg(test)]
pub const ALL: [Self; 34] = [
Self::Info,
Self::ImportSubUrlOk,
Self::ImportSubUrlError,
Self::SetConfigError,
Self::CoreStartError,
Self::ServiceCoreRepairRequired,
Self::ServiceCoreAppDataNotOwned,
Self::ServiceCoreSidecarFallback,
Self::DnsOverrideAutoDisabled,
Self::EnhanceDiscardedKeys,
Self::TunModeAutoDisabled,
Self::TunModeAutoDisableFailed,
Self::AppRestartCoreStopFailed,
Self::AppQuitCoreStopFailed,
Self::UpdateWithClashProxy,
Self::UpdateFailedEvenWithClash,
Self::ReactivateProfilesError,
Self::UpdateFailed,
Self::ConfigValidateBootError,
Self::ConfigValidateError,
Self::ConfigValidateProcessTerminated,
Self::ConfigValidateScriptError,
Self::ConfigValidateScriptSyntaxError,
Self::ConfigValidateScriptMissingMain,
Self::ConfigValidateFileNotFound,
Self::ConfigValidateYamlSyntaxError,
Self::ConfigValidateYamlReadError,
Self::ConfigValidateYamlMappingError,
Self::ConfigValidateMergeSyntaxError,
Self::ConfigValidateMergeMappingError,
Self::ConfigCoreChangeSuccess,
Self::ConfigCoreChangeError,
Self::MixedPortFallback,
Self::MixedPortFallbackError,
];
}
#[cfg(test)]
mod tests {
use super::NoticeStatus;
#[test]
fn notice_status_wire_contract() -> Result<(), serde_json::Error> {
let statuses = NoticeStatus::ALL;
let expected = [
"info",
"import_sub_url::ok",
"import_sub_url::error",
"set_config::error",
"core_start::error",
"service_core::repair_required",
"service_core::app_data_not_owned",
"service_core::sidecar_fallback",
"dns_override::auto_disabled",
"enhance::discarded_keys",
"tun_mode::auto_disabled",
"tun_mode::auto_disable_failed",
"app_restart::core_stop_failed",
"app_quit::core_stop_failed",
"update_with_clash_proxy",
"update_failed_even_with_clash",
"reactivate_profiles::error",
"update_failed",
"config_validate::boot_error",
"config_validate::error",
"config_validate::process_terminated",
"config_validate::script_error",
"config_validate::script_syntax_error",
"config_validate::script_missing_main",
"config_validate::file_not_found",
"config_validate::yaml_syntax_error",
"config_validate::yaml_read_error",
"config_validate::yaml_mapping_error",
"config_validate::merge_syntax_error",
"config_validate::merge_mapping_error",
"config_core::change_success",
"config_core::change_error",
"mixed_port::fallback",
"mixed_port::fallback_error",
];
let actual: Vec<_> = statuses
.into_iter()
.map(serde_json::to_value)
.collect::<Result<_, _>>()?;
assert_eq!(actual, expected.map(serde_json::Value::from));
Ok(())
}
}