feat(linux): preserve Service state across upgrades and surface failures

Reinstall an existing Service while keeping its enabled, runtime-masked
and active state. Detect, from the journal, a running Service removed by
a previous deb prerm and reinstall it with its recorded group. Package
script output never reaches the app, so leave a /run marker that makes
the app offer a reinstall once when the Service is not ready.

Hold the core lifecycle until the package manager exits, even after the
startup install times out, so no core start overlaps the Service
reinstall; restart the app once a late install succeeds.

Refs #8515

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Tunglies
2026-11-09 18:15:28 +08:00
co-authored by Copilot
parent cdd704f980
commit 63a16a8272
29 changed files with 282 additions and 99 deletions
+55 -34
View File
@@ -5,42 +5,63 @@ chmod +x /usr/bin/clash-verge-service-install
chmod +x /usr/bin/clash-verge-service-uninstall
chmod +x /usr/bin/clash-verge-service
# Failures only warn: dpkg and rpm cannot restore the previous Service from a failed postinst,
# and a failing script would leave the upgraded package unconfigured or report the update as failed.
reinstall_existing_service() {
local unit="/etc/systemd/system/clash-verge-service.service"
# Only a unit kept by an upgrade-aware removal script shows the Service existed before this upgrade.
if [ ! -f "$unit" ] || [ -L "$unit" ]; then
return 0
fi
if [ ! -d /run/systemd/system ]; then
echo "Warning: systemd is not running, so Clash Verge Service was not reinstalled; reinstall it from Clash Verge." >&2
return 0
fi
if ! systemctl is-enabled --quiet clash-verge-service.service; then
echo "Clash Verge Service is disabled; leaving it unchanged." >&2
return 0
fi
# Package managers run without the invoking user's identity, so keep the group that may use the Service.
local group gid
group="$(sed -n 's/^Group=//p' "$unit" | head -n 1)"
gid=""
if [ -n "$group" ]; then
gid="$(getent group "$group" | cut -d: -f3)"
fi
if [ -z "$gid" ]; then
echo "Warning: the Clash Verge Service group '$group' could not be resolved, so the Service was not reinstalled; reinstall it from Clash Verge." >&2
return 0
fi
if ! CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then
echo "Warning: reinstalling Clash Verge Service failed; reinstall it from Clash Verge." >&2
fi
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"
write_upgrade_marker() {
(umask 022 && printf 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$UPGRADE_MARKER")
}
# Upgrades keep an existing Service current; first installs never add one. No success is reported:
# a previous package whose %preun uninstalls unconditionally still removes the Service afterwards.
if [ "$1" -ge 2 ]; then
reinstall_existing_service
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
if [ "$enabled" = "masked-runtime" ]; then
systemctl unmask --runtime --quiet "$SERVICE_UNIT"
fi
local status=0
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" ;;
masked-runtime) systemctl mask --runtime --quiet "$SERVICE_UNIT" ;;
esac
if [ "$active" = "no" ]; then
systemctl stop "$SERVICE_UNIT"
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 marker is written first: a previous package whose %preun uninstalls unconditionally
# still removes the Service after this script, and the app only acts on it if the Service is not ready.
if [ "$1" -ge 2 ] && [ -d /run/systemd/system ] && [ -f "$SERVICE_UNIT_FILE" ] && [ ! -L "$SERVICE_UNIT_FILE" ]; then
gid="$(existing_service_gid)"
write_upgrade_marker "$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
else
reinstall_existing_service "$gid" || true
fi
fi
# The enforcement file exists in both Enforcing and Permissive mode.
+67 -33
View File
@@ -3,41 +3,75 @@ chmod +x /usr/bin/clash-verge-service-install
chmod +x /usr/bin/clash-verge-service-uninstall
chmod +x /usr/bin/clash-verge-service
# Failures only warn: dpkg and rpm cannot restore the previous Service from a failed postinst,
# and a failing script would leave the upgraded package unconfigured or report the update as failed.
reinstall_existing_service() {
local unit="/etc/systemd/system/clash-verge-service.service"
# Only a unit kept by an upgrade-aware removal script shows the Service existed before this upgrade.
if [ ! -f "$unit" ] || [ -L "$unit" ]; then
return 0
fi
if [ ! -d /run/systemd/system ]; then
echo "Warning: systemd is not running, so Clash Verge Service was not reinstalled; reinstall it from Clash Verge." >&2
return 0
fi
if ! systemctl is-enabled --quiet clash-verge-service.service; then
echo "Clash Verge Service is disabled; leaving it unchanged." >&2
return 0
fi
# Package managers run without the invoking user's identity, so keep the group that may use the Service.
local group gid
group="$(sed -n 's/^Group=//p' "$unit" | head -n 1)"
gid=""
if [ -n "$group" ]; then
gid="$(getent group "$group" | cut -d: -f3)"
fi
if [ -z "$gid" ]; then
echo "Warning: the Clash Verge Service group '$group' could not be resolved, so the Service was not reinstalled; reinstall it from Clash Verge." >&2
return 0
fi
if ! CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then
echo "Warning: reinstalling Clash Verge Service failed; reinstall it from Clash Verge." >&2
fi
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"
write_upgrade_marker() {
(umask 022 && printf 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$UPGRADE_MARKER")
}
# Upgrades keep an existing Service current; first installs never add one.
if [ "$1" = "configure" ] && [ -n "$2" ]; then
reinstall_existing_service
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
if [ "$enabled" = "masked-runtime" ]; then
systemctl unmask --runtime --quiet "$SERVICE_UNIT"
fi
local status=0
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" ;;
masked-runtime) systemctl mask --runtime --quiet "$SERVICE_UNIT" ;;
esac
if [ "$active" = "no" ]; then
systemctl stop "$SERVICE_UNIT"
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 ]; then
if [ -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 "$gid"
fi
elif [ -f "$UPGRADE_MARKER" ]; then
# The preinst found that the previous package's prerm removed a running Service.
gid="$(sed -n 's/^gid=//p' "$UPGRADE_MARKER")"
if [ -z "$gid" ]; then
echo "Warning: the previous package removed Clash Verge Service and its group is unknown; Clash Verge will offer to reinstall it." >&2
elif CLASH_VERGE_SERVICE_GID="$gid" /usr/bin/clash-verge-service-install --install-service; then
rm -f "$UPGRADE_MARKER"
else
echo "Warning: reinstalling Clash Verge Service failed; Clash Verge will offer to reinstall it." >&2
fi
fi
fi
. /etc/os-release
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# Previous packages' prerm uninstalls the Service before this script runs, deleting the unit that names
# its group. The journal still shows whether this dpkg run stopped a running Service and which group it ran with;
# the postinst reinstalls from that evidence, and without it nothing is installed.
SERVICE_UNIT="clash-verge-service.service"
UPGRADE_MARKER="/run/clash-verge-service-upgrade"
# systemd's "unit stopped" catalog entry.
UNIT_STOPPED_MESSAGE_ID="9d1aaa27d60140bd96365438aad20286"
dpkg_start_epoch() {
local stat start btime ticks
read -r stat <"/proc/$PPID/stat" || return 1
start="$(printf '%s\n' "${stat##*) }" | cut -d' ' -f20)"
btime="$(sed -n 's/^btime //p' /proc/stat)"
ticks="$(getconf CLK_TCK)"
[ -n "$start" ] && [ -n "$btime" ] && [ -n "$ticks" ] || return 1
echo $((btime + start / ticks))
}
if [ "$1" = "upgrade" ] && [ ! -e "/etc/systemd/system/$SERVICE_UNIT" ] && [ -d /run/systemd/system ] &&
[ "$(cat "/proc/$PPID/comm" 2>/dev/null)" = "dpkg" ] && command -v journalctl >/dev/null; then
since="$(dpkg_start_epoch)"
if [ -n "$since" ] &&
[ -n "$(journalctl -q --no-pager --since "@$since" _PID=1 UNIT="$SERVICE_UNIT" MESSAGE_ID="$UNIT_STOPPED_MESSAGE_ID" -o cat 2>/dev/null)" ]; then
# The truncated process name excludes cores the Service spawned in its cgroup.
gid="$(journalctl -q --no-pager -b _SYSTEMD_UNIT="$SERVICE_UNIT" _COMM=clash-verge-ser -n 1 -o export 2>/dev/null |
sed -n 's/^_GID=//p')"
if [ -z "$gid" ] || [ "$gid" = "0" ] || ! getent group "$gid" >/dev/null; then
gid=""
fi
(umask 022 && printf 'gid=%s\nid=%s\n' "$gid" "$(date +%s%N)" >"$UPGRADE_MARKER")
fi
fi
exit 0
+8 -5
View File
@@ -750,15 +750,18 @@ impl CoreManager {
self.controlled_stop_core_inner().await
}
/// Stops the core only when the Service runs it, returning whether it did. Waits for an in-flight start.
/// 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 stop_service_core(&self) -> Result<bool> {
let _life = self.lifecycle_lock.lock().await;
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(false);
return Ok((life, false));
}
self.controlled_stop_core_inner().await?;
Ok(true)
Ok((life, true))
}
pub(crate) async fn controlled_stop_core_inner(&self) -> Result<()> {
+1 -1
View File
@@ -22,7 +22,7 @@ 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 = "dns_override::auto_disabled")]
DnsOverrideAutoDisabled,
+39 -1
View File
@@ -50,6 +50,7 @@ pub enum ServiceFallbackNotice {
Unavailable,
CoreRejected(String),
NotAutoStarted,
UpgradeIncomplete,
}
/// The release installer registers AutoStart.
@@ -66,9 +67,46 @@ 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;
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, "");
}
if let Err(error) = std::fs::write(&ack, id) {
logging!(warn, Type::Service, "无法记录服务升级确认: {error}");
}
}
pub(crate) fn take_service_fallback_notice() -> Option<ServiceFallbackNotice> {
let reason = PENDING_SERVICE_FALLBACK_NOTICE.lock().take()?;
Some(if reason.starts_with(CORE_REJECTED_PREFIX) {
Some(if reason == SERVICE_UPGRADE_INCOMPLETE {
ServiceFallbackNotice::UpgradeIncomplete
} else if reason.starts_with(CORE_REJECTED_PREFIX) {
ServiceFallbackNotice::CoreRejected(reason)
} else if is_not_auto_started(&reason) {
ServiceFallbackNotice::NotAutoStarted
+49 -24
View File
@@ -293,35 +293,60 @@ impl SilentUpdater {
}
async fn install(update: Update, bytes: Vec<u8>, timeout: Option<Duration>) -> Result<()> {
// The package scripts reinstall an existing Service, killing the core it runs and its session;
// stopping it first keeps the post-update restart from failing on that lost session.
#[cfg(target_os = "linux")]
let stopped_service_core = crate::core::CoreManager::global()
.stop_service_core()
.await
.context("failed to stop the Service core before installing the update")?;
let install = tokio::task::spawn_blocking(move || update.install(&bytes));
let 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) => tokio::time::timeout(limit, install)
.await
.map_err(|_| anyhow::anyhow!("install timed out ({}s)", limit.as_secs())),
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),
};
let result = joined.and_then(|joined| Ok(joined??));
joined.and_then(|joined| {
joined??;
Ok(())
})
}
#[cfg(target_os = "linux")]
if result.is_err()
&& stopped_service_core
&& let Err(error) = crate::core::CoreManager::global().start_core().await
{
logging!(
error,
Type::System,
"Failed to restart the core after the update install failed: {error:#}"
);
}
result
/// 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<u8>) -> Result<tokio::task::JoinHandle<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.
+2
View File
@@ -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!(
+1
View File
@@ -10,6 +10,7 @@
"provides": ["clash-verge"],
"conflicts": ["clash-verge"],
"replaces": ["clash-verge"],
"preInstallScript": "./packages/linux/pre-install.sh",
"postInstallScript": "./packages/linux/post-install.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh"
},
@@ -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 = (
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -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…",
+1
View File
@@ -23,6 +23,7 @@
"serviceMigration": {
"locationRefusedMessage": "系统服务的安装路径或目录权限未通过安全检查,无法启动内核。请重新安装服务以恢复服务文件和权限,随后重试。Windows 可能会请求管理员授权。",
"notAutoStartedMessage": "Clash Verge 系统服务的启动类型已被改为手动或禁用(常见于优化软件的开机加速功能),开机后不会自动运行,本次已改用 Sidecar。请重新安装服务以恢复自动启动。Windows 可能会请求管理员授权。",
"upgradeIncompleteMessage": "Clash Verge 已升级,但原先安装的 Clash Verge 服务未就绪。请重装服务以继续使用,系统可能要求管理员认证。",
"title": "需要更新系统服务",
"message": "Clash Verge 系统服务版本与当前应用要求不符。请选择「重新安装服务」卸载现有服务并安装所需版本,也可以仅在本次会话继续使用 Sidecar。",
"checkingMessage": "正在确认系统服务状态及当前运行模式,请稍候……",
+1
View File
@@ -23,6 +23,7 @@
"serviceMigration": {
"locationRefusedMessage": "系統服務的安裝路徑或目錄權限未通過安全檢查,無法啟動核心。請重新安裝服務以還原服務檔案和權限,然後重試。Windows 可能會要求管理員授權。",
"notAutoStartedMessage": "Clash Verge 系統服務的啟動類型已被改為手動或停用(常見於最佳化軟體的開機加速功能),開機後不會自動執行,本次已改用 Sidecar。請重新安裝服務以恢復自動啟動。Windows 可能會要求管理員授權。",
"upgradeIncompleteMessage": "Clash Verge 已升級,但原先安裝的 Clash Verge 服務未就緒。請重新安裝服務以繼續使用,系統可能要求管理員驗證。",
"title": "需要更新系統服務",
"message": "Clash Verge 系統服務版本與目前應用程式要求不符。請選擇「重新安裝服務」移除現有服務並安裝所需版本,也可以僅在本次工作階段繼續使用 Sidecar。",
"checkingMessage": "正在確認系統服務狀態及目前執行模式,請稍候……",
+1
View File
@@ -1035,6 +1035,7 @@ export type ServiceFallbackNotice =
| { kind: 'unavailable' }
| { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
| { kind: 'upgradeIncomplete' }
export type ServiceHealthKind =
| 'unknown'
+2
View File
@@ -125,6 +125,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',
+1
View File
@@ -5,6 +5,7 @@ export type ServiceRequestReason =
| 'tunNeedsService'
| 'serviceLocationRefused'
| 'serviceNotAutoStarted'
| 'serviceUpgradeIncomplete'
export interface ServiceRequest {
readonly reason: ServiceRequestReason
+1
View File
@@ -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',
+1
View File
@@ -233,6 +233,7 @@ export interface TranslationResources {
success: string
title: string
unavailableMessage: string
upgradeIncompleteMessage: string
}
sysproxyPrivilege: {
alternative: string