Compare commits

..
Author SHA1 Message Date
wonfen a7981195c5 refactor(profile): simplify REALITY short-id quoting
Quote short-ids with a plain text pass and keep the result only when nothing but numeric short-ids changed. Drop the editor-save and seq-file paths.
2026-11-09 19:14:17 +08:00
35 changed files with 87 additions and 739 deletions
-4
View File
@@ -52,8 +52,4 @@
- 优化 macOS TUN 模式的系统 DNS,增加阿里和 Cloudflare 地址
**🖥️/🐧 Windows/Linux**
- 优化 Windows/Linux 升级安装时自动重装已安装的服务并保留启停设置,重装失败时提示
</details>
-23
View File
@@ -550,16 +550,9 @@ 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}`,
@@ -580,22 +573,6 @@ 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 {
@@ -3,84 +3,8 @@ 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"
write_upgrade_marker() {
(umask 022 && printf 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$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
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 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 "$gid"
fi
fi
# The enforcement file exists in both Enforcing and Permissive mode.
if [ ! -e /sys/fs/selinux/enforce ]; then
exit 0
-62
View File
@@ -1,70 +1,8 @@
#!/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"
write_upgrade_marker() {
(umask 022 && printf 'gid=%s\nid=%s\n' "$1" "$(date +%s%N)" >"$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
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 ] &&
[ -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
fi
. /etc/os-release
if [ "$ID" = "deepin" ]; then
@@ -1,28 +0,0 @@
#!/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" "$@"
+1 -69
View File
@@ -77,9 +77,6 @@ Var VC_REDIST_URL
Var VC_REDIST_EXE
Var VC_RUNTIME_READY
Var VC_RUNTIME_NEEDED
Var ServiceExisted
Var ServiceStartType
Var ServiceWasRunning
Name "${PRODUCTNAME}"
BrandingText "${COPYRIGHT}"
@@ -469,34 +466,6 @@ FunctionEnd
{{#each language_files}}
!include "{{this}}"
{{/each}}
!ifdef LANG_ENGLISH
LangString serviceReinstallFailed ${LANG_ENGLISH} "The ${PRODUCTNAME} Service could not be reinstalled (code $0). Open ${PRODUCTNAME} and reinstall the Service to restore Service mode."
!endif
!ifdef LANG_SIMPCHINESE
LangString serviceReinstallFailed ${LANG_SIMPCHINESE} "${PRODUCTNAME} 服务重装失败(代码 $0)。请打开 ${PRODUCTNAME} 重新安装服务以恢复服务模式。"
!endif
!ifdef LANG_RUSSIAN
LangString serviceReinstallFailed ${LANG_RUSSIAN} "Не удалось переустановить службу ${PRODUCTNAME} (код $0). Откройте ${PRODUCTNAME} и переустановите службу, чтобы восстановить режим службы."
!endif
!macro RecordVergeService
StrCpy $ServiceExisted 0
StrCpy $ServiceStartType ""
StrCpy $ServiceWasRunning 1
SimpleSC::ExistsService "clash_verge_service"
Pop $0
${If} $0 == 0
StrCpy $ServiceExisted 1
SimpleSC::GetServiceStartType "clash_verge_service"
Pop $0
Pop $1
${IfThen} $0 == 0 ${|} StrCpy $ServiceStartType $1 ${|}
SimpleSC::ServiceIsRunning "clash_verge_service"
Pop $0
Pop $1
${IfThen} $0 == 0 ${|} StrCpy $ServiceWasRunning $1 ${|}
${EndIf}
!macroend
Function .onInit
${GetOptions} $CMDLINE "/P" $PassiveMode
@@ -514,9 +483,6 @@ Function .onInit
StrCpy $UpdateMode 1
${EndIf}
; Recorded before the reinstall page can run the previous uninstaller, which removes the Service.
!insertmacro RecordVergeService
!if "${DISPLAYLANGUAGESELECTOR}" == "true"
; Auto-update forwards the app's UI language as `/LANG=<NSIS-lang-id>` so
; the installer uses it directly and skips the interactive language
@@ -697,36 +663,6 @@ FunctionEnd
${EndIf}
!macroend
; Reinstalls the Service with the bundled cores, then restores the recorded start type and stopped state.
!macro ReinstallVergeService
StrCpy $R0 '"$INSTDIR\resources\clash-verge-service-install.exe" --install-service'
!ifdef MIHOMO_SHA256
StrCpy $R0 '$R0 --install-core "$INSTDIR\verge-mihomo.exe" --sha256 "${MIHOMO_SHA256}"'
!endif
!ifdef MIHOMO_ALPHA_SHA256
StrCpy $R0 '$R0 --install-core "$INSTDIR\verge-mihomo-alpha.exe" --sha256 "${MIHOMO_ALPHA_SHA256}"'
!endif
DetailPrint "Reinstalling ${PRODUCTNAME} Service..."
nsExec::ExecToLog $R0
Pop $0
${If} $0 == 0
${AndIf} $ServiceStartType != ""
${AndIf} $ServiceStartType != 2
SimpleSC::SetServiceStartType "clash_verge_service" $ServiceStartType
Pop $0
${EndIf}
${If} $0 == 0
${AndIf} $ServiceWasRunning == 0
SimpleSC::StopService "clash_verge_service" 1 30
Pop $0
${EndIf}
${If} $0 != 0
DetailPrint "Reinstalling ${PRODUCTNAME} Service failed ($0)"
MessageBox MB_OK|MB_ICONEXCLAMATION "$(serviceReinstallFailed)" /SD IDOK
SetErrorLevel 2
${EndIf}
!macroend
!macro RemoveVergeService
; Check if the service exists
SimpleSC::ExistsService "clash_verge_service"
@@ -1049,11 +985,7 @@ Section Install
${IfThen} $0 != 0 ${|} DetailPrint "Staging verge-mihomo-alpha returned $0" ${|}
!endif
${If} $ServiceExisted = 1
!insertmacro ReinstallVergeService
${Else}
!insertmacro StartVergeService
${EndIf}
!insertmacro StartVergeService
; Create file associations
{{#each file_associations as |association| ~}}
-14
View File
@@ -750,20 +750,6 @@ 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
}
+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(any(target_os = "windows", target_os = "linux")), allow(dead_code))]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
ServiceCoreSidecarFallback,
#[serde(rename = "dns_override::auto_disabled")]
DnsOverrideAutoDisabled,
+1 -39
View File
@@ -50,7 +50,6 @@ pub enum ServiceFallbackNotice {
Unavailable,
CoreRejected(String),
NotAutoStarted,
UpgradeIncomplete,
}
/// The release installer registers AutoStart.
@@ -67,46 +66,9 @@ 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 == SERVICE_UPGRADE_INCOMPLETE {
ServiceFallbackNotice::UpgradeIncomplete
} else if reason.starts_with(CORE_REJECTED_PREFIX) {
Some(if reason.starts_with(CORE_REJECTED_PREFIX) {
ServiceFallbackNotice::CoreRejected(reason)
} else if is_not_auto_started(&reason) {
ServiceFallbackNotice::NotAutoStarted
+8 -57
View File
@@ -292,61 +292,8 @@ impl SilentUpdater {
Ok(updater_builder.build()?.check().await?)
}
async fn install(update: Update, bytes: Vec<u8>, timeout: Option<Duration>) -> 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<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
}))
async fn install(update: Update, bytes: Vec<u8>) -> Result<()> {
Ok(tokio::task::spawn_blocking(move || update.install(&bytes)).await??)
}
/// Installs a newer cached update before normal startup, if the user confirms.
@@ -422,7 +369,11 @@ impl SilentUpdater {
Self::show_update_splash(app_handle, &version);
// `install()` may hang (#2558); on Windows NSIS can take over without returning.
match Self::install(update, bytes, Some(Duration::from_secs(30))).await {
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)")))
{
Ok(()) => {
logging!(info, Type::System, "Update v{version} install triggered at startup");
Self::delete_cache();
@@ -466,7 +417,7 @@ impl SilentUpdater {
};
logging!(info, Type::System, "Installing update v{version}...");
Self::install(update, bytes, None).await?;
Self::install(update, bytes).await?;
Self::delete_cache();
Ok(InstallUpdateOutcome::Installed)
}
+2 -31
View File
@@ -1,13 +1,10 @@
use anyhow::Result;
use nanoid::nanoid;
use scopeguard::defer;
use serde::Serialize;
use smartstring::alias::String;
use std::{
borrow::Cow,
fmt,
hash::{DefaultHasher, Hash as _, Hasher as _},
path::Path,
sync::LazyLock,
sync::atomic::{AtomicBool, Ordering},
};
@@ -332,7 +329,7 @@ impl CoreConfigValidator {
Ok(result) => result,
Err(err) => {
logging!(warn, Type::Validate, "无法确定文件类型: {}, 错误: {}", config_path, err);
return Self::validate_profile_yaml_outcome(config_path).await;
return Self::validate_config_internal_outcome(config_path).await;
}
};
@@ -347,33 +344,7 @@ impl CoreConfigValidator {
}
logging!(info, Type::Validate, "使用Clash内核验证配置文件: {}", config_path);
Self::validate_profile_yaml_outcome(config_path).await
}
/// The user file is saved as written, so the core checks a copy carrying the tolerated short-ids.
async fn validate_profile_yaml_outcome(config_path: &str) -> Result<ValidationOutcome> {
let Some(tolerant) = Self::short_id_tolerant_text(config_path).await else {
return Self::validate_config_internal_outcome(config_path).await;
};
let Ok(app_dir) = dirs::app_home_dir() else {
return Self::validate_config_internal_outcome(config_path).await;
};
let check_path = app_dir.join(format!("short-id-check-{}.yaml", nanoid!()));
defer! {
let _ = std::fs::remove_file(&check_path);
}
if help::save_yaml_str(&check_path, &tolerant).await.is_err() {
return Self::validate_config_internal_outcome(config_path).await;
}
Self::validate_config_internal_outcome(dirs::path_to_str(&check_path)?).await
}
async fn short_id_tolerant_text(config_path: &str) -> Option<std::string::String> {
let yaml = fs::read_to_string(config_path).await.ok()?;
match help::short_id_tolerant(&yaml, Path::new(config_path)) {
Ok((Cow::Owned(tolerant), _)) => Some(tolerant),
_ => None,
}
Self::validate_config_internal_outcome(config_path).await
}
async fn validate_config_internal_outcome(config_path: &str) -> Result<ValidationOutcome> {
+1 -52
View File
@@ -3,10 +3,8 @@ use crate::{
config::PrfItem,
utils::{dirs, help},
};
use clash_verge_logging::{Type, logging};
use serde_yaml_ng::Mapping;
use smartstring::alias::String;
use std::{borrow::Cow, path::Path};
use tokio::fs;
#[derive(Debug, Clone)]
@@ -50,8 +48,7 @@ impl AsyncChainItemFrom for Option<ChainItem> {
data: ChainType::Merge(help::read_mapping(&path).await.ok()?),
}),
"rules" | "proxies" | "groups" => {
let yaml = fs::read_to_string(&path).await.ok()?;
let seq_map = parse_seq_map(&yaml, &path)?;
let seq_map = help::read_yaml::<SeqMap>(&path).await.ok()?;
let data = match itype {
"rules" => ChainType::Rules(seq_map),
"proxies" => ChainType::Proxies(seq_map),
@@ -63,51 +60,3 @@ impl AsyncChainItemFrom for Option<ChainItem> {
}
}
}
/// Typed parsing reads `delete: [1234]` as strings where a `Value` round trip would reject it.
fn parse_seq_map(yaml: &str, path: &Path) -> Option<SeqMap> {
let original = serde_yaml_ng::from_str::<SeqMap>(yaml).ok()?;
let Ok((Cow::Owned(tolerant), _)) = help::short_id_tolerant(yaml, path) else {
return Some(original);
};
match serde_yaml_ng::from_str::<SeqMap>(&tolerant) {
Ok(seq_map) => Some(seq_map),
Err(_) => {
logging!(
warn,
Type::Config,
"kept numeric short-id in {}: quoted text does not load",
path.display()
);
Some(original)
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
mod tests {
use super::parse_seq_map;
use serde_yaml_ng::Value;
use std::path::Path;
#[test]
fn seq_short_id_keeps_typed_parse() {
for (input, delete) in [
("prepend: []\nappend: []\ndelete: [1234]\n", "1234"),
("prepend: []\nappend: []\ndelete: [true]\n", "true"),
] {
let seq_map = parse_seq_map(input, Path::new("b8.yaml")).expect("seq file should load");
assert_eq!(seq_map.delete, [delete]);
}
let input = "prepend:\n - name: r\n reality-opts:\n short-id: 1234e123\nappend: []\ndelete: []\n";
let seq_map = parse_seq_map(input, Path::new("seq.yaml")).expect("seq file should load");
let short_id = seq_map
.prepend
.first()
.and_then(|proxy| proxy.get("reality-opts"))
.and_then(|opts| opts.get("short-id"));
assert_eq!(short_id, Some(&Value::String("1234e123".into())));
}
}
+70 -247
View File
@@ -7,8 +7,6 @@ use scopeguard::{ScopeGuard, guard};
use serde::{Serialize, de::DeserializeOwned};
use serde_yaml_ng::{Mapping, Value};
use std::{
borrow::Cow,
ops::Range,
path::{Path, PathBuf},
str::FromStr,
};
@@ -47,8 +45,8 @@ pub async fn read_mapping(path: &Path) -> Result<Mapping> {
}
fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
match short_id_tolerant(&yaml_str, &path) {
Ok((_, mut val)) => {
match parse_short_id_text(&yaml_str, &path) {
Ok(mut val) => {
val.apply_merge()
.with_context(|| format!("failed to apply merge \"{}\"", path.display()))?;
@@ -68,150 +66,74 @@ fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
}
}
const SHORT_ID_KEY: &str = "short-id:";
/// A float like `1234e123` loses its hex text, so quote short-ids before parsing and keep that only if nothing else changed.
pub(crate) fn short_id_tolerant<'a>(yaml: &'a str, path: &Path) -> serde_yaml_ng::Result<(Cow<'a, str>, Value)> {
let original = serde_yaml_ng::from_str::<Value>(yaml)?;
if !has_numeric_short_id(&original) {
return Ok((Cow::Borrowed(yaml), original));
/// A float like `1234e123` loses its hex text, so retry with short-ids quoted and keep that only if nothing else changed.
fn parse_short_id_text(yaml: &str, path: &Path) -> serde_yaml_ng::Result<Value> {
let value = serde_yaml_ng::from_str::<Value>(yaml)?;
if !has_numeric_short_id(&value) {
return Ok(value);
}
let quoted = quote_plain_short_ids(yaml);
if matches!(quoted, Cow::Borrowed(_)) {
return Ok((quoted, original));
}
match serde_yaml_ng::from_str::<Value>(&quoted) {
Ok(tolerant) if same_except_short_id_text(&original, &tolerant) => Ok((quoted, tolerant)),
match serde_yaml_ng::from_str::<Value>(&quote_short_ids(yaml)) {
Ok(quoted) if same_except_short_id(&value, &quoted) => Ok(quoted),
_ => {
logging!(
warn,
Type::Config,
"kept numeric short-id in {}: quoting it would change other values",
path.display()
);
Ok((Cow::Borrowed(yaml), original))
logging!(warn, Type::Config, "kept numeric short-id in {}", path.display());
Ok(value)
}
}
}
fn quote_short_ids(yaml: &str) -> String {
let mut quoted = String::with_capacity(yaml.len());
let mut rest = yaml;
while let Some((head, tail)) = rest.split_once("short-id:") {
let (gap, value) = tail.split_at(tail.len() - tail.trim_start_matches([' ', '\t']).len());
let (id, after) = value.split_at(value.bytes().take_while(u8::is_ascii_hexdigit).count());
quoted.push_str(head);
quoted.push_str("short-id:");
quoted.push_str(gap);
if !id.is_empty() && (after.is_empty() || after.starts_with([' ', '\t', '\r', '\n', ',', '}'])) {
quoted.push('"');
quoted.push_str(id);
quoted.push('"');
} else {
quoted.push_str(id);
}
rest = after;
}
quoted.push_str(rest);
quoted
}
fn has_numeric_short_id(value: &Value) -> bool {
match value {
Value::Mapping(map) => map
.iter()
.any(|(key, value)| (is_short_id_key(key) && value.is_number()) || has_numeric_short_id(value)),
.any(|(key, value)| (is_short_id(key) && matches!(value, Value::Number(_))) || has_numeric_short_id(value)),
Value::Sequence(seq) => seq.iter().any(has_numeric_short_id),
Value::Tagged(tagged) => has_numeric_short_id(&tagged.value),
_ => false,
}
}
fn same_except_short_id_text(original: &Value, tolerant: &Value) -> bool {
match (original, tolerant) {
(Value::Mapping(original), Value::Mapping(tolerant)) => {
original.len() == tolerant.len()
&& original
.iter()
.zip(tolerant.iter())
.all(|((key, old), (new_key, new))| {
key == new_key && (same_except_short_id_text(old, new) || is_short_id_text(key, old, new))
})
fn same_except_short_id(old: &Value, new: &Value) -> bool {
match (old, new) {
(Value::Mapping(old), Value::Mapping(new)) => {
old.len() == new.len()
&& old.iter().zip(new).all(|((key, old), (new_key, new))| {
key == new_key
&& (same_except_short_id(old, new)
|| (is_short_id(key) && matches!((old, new), (Value::Number(_), Value::String(_)))))
})
}
(Value::Sequence(original), Value::Sequence(tolerant)) => {
original.len() == tolerant.len()
&& original
.iter()
.zip(tolerant.iter())
.all(|(old, new)| same_except_short_id_text(old, new))
(Value::Sequence(old), Value::Sequence(new)) => {
old.len() == new.len() && old.iter().zip(new).all(|(old, new)| same_except_short_id(old, new))
}
(Value::Tagged(original), Value::Tagged(tolerant)) => {
original.tag == tolerant.tag && same_except_short_id_text(&original.value, &tolerant.value)
}
_ => original == tolerant,
_ => old == new,
}
}
fn is_short_id_text(key: &Value, old: &Value, new: &Value) -> bool {
is_short_id_key(key)
&& old.is_number()
&& matches!(new, Value::String(text) if serde_yaml_ng::from_str::<Value>(text).ok().as_ref() == Some(old))
}
fn is_short_id_key(key: &Value) -> bool {
fn is_short_id(key: &Value) -> bool {
key.as_str() == Some("short-id")
}
fn quote_plain_short_ids(yaml: &str) -> Cow<'_, str> {
let mut quoted = String::new();
let mut copied = 0;
for (key, _) in yaml.match_indices(SHORT_ID_KEY) {
let Some(value) = plain_short_id_at(yaml, key) else {
continue;
};
let (Some(head), Some(id)) = (yaml.get(copied..value.start), yaml.get(value.clone())) else {
continue;
};
quoted.push_str(head);
quoted.push('"');
quoted.push_str(id);
quoted.push('"');
copied = value.end;
}
if copied == 0 {
return Cow::Borrowed(yaml);
}
quoted.push_str(yaml.get(copied..).unwrap_or_default());
Cow::Owned(quoted)
}
fn plain_short_id_at(yaml: &str, key: usize) -> Option<Range<usize>> {
let bytes = yaml.as_bytes();
let before = bytes.get(..key)?;
let flow = matches!(
before.iter().rev().find(|byte| !byte.is_ascii_whitespace()),
Some(b'{' | b',')
);
let line_start = before
.iter()
.rev()
.take_while(|&&byte| byte != b'\n')
.all(|&byte| is_blank(byte));
if !flow && !line_start {
return None;
}
let start = key + SHORT_ID_KEY.len();
let gap = bytes.get(start..)?.iter().take_while(|&&byte| is_blank(byte)).count();
if gap == 0 {
return None;
}
let start = start + gap;
let len = bytes
.get(start..)?
.iter()
.take_while(|byte| byte.is_ascii_hexdigit())
.count();
if len == 0 || !ends_plain_short_id(bytes.get(start + len..)?, flow) {
return None;
}
Some(start..start + len)
}
fn ends_plain_short_id(rest: &[u8], flow: bool) -> bool {
let blanks = rest.iter().take_while(|&&byte| is_blank(byte)).count();
match rest.get(blanks..) {
Some([] | [b'\n', ..] | [b'\r', b'\n', ..]) => true,
Some([b'#', ..]) => blanks > 0,
Some([b',' | b'}', ..]) => flow,
_ => false,
}
}
const fn is_blank(byte: u8) -> bool {
matches!(byte, b' ' | b'\t')
}
pub async fn save_yaml<T: Serialize + Sync>(path: &Path, data: &T, prefix: Option<&str>) -> Result<()> {
let data_str = with_encryption(|| async { serde_yaml_ng::to_string(data) }).await?;
@@ -469,136 +391,37 @@ pub fn snapshot_path(original_path: &Path) -> Result<PathBuf> {
#[cfg(test)]
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
mod tests {
use super::{has_numeric_short_id, parse_mapping, quote_plain_short_ids, short_id_tolerant};
use super::{parse_mapping, parse_short_id_text};
use serde_yaml_ng::Value;
use std::{
borrow::Cow,
path::{Path, PathBuf},
};
use std::path::{Path, PathBuf};
#[test]
fn quotes_plain_short_ids() {
let cases = [
("short-id: 1234e123", "short-id: \"1234e123\""),
("short-id: 0e5", "short-id: \"0e5\""),
("short-id: 12345678", "short-id: \"12345678\""),
("short-id: 6ba85179", "short-id: \"6ba85179\""),
("short-id: \"1234e123\"", "short-id: \"1234e123\""),
("short-id: '1234e123'", "short-id: '1234e123'"),
("short-id: \"\"", "short-id: \"\""),
("short-id:\npublic-key: k", "short-id:\npublic-key: k"),
("short-id: null", "short-id: null"),
("short-id: ~", "short-id: ~"),
("short-id: 1234e123 # note", "short-id: \"1234e123\" # note"),
("# short-id: 1234e123", "# short-id: 1234e123"),
(
"reality-opts: {public-key: k, short-id: 1234e123}",
"reality-opts: {public-key: k, short-id: \"1234e123\"}",
),
(
"reality-opts: {short-id: 1234e123, public-key: k}",
"reality-opts: {short-id: \"1234e123\", public-key: k}",
),
(
"{\n public-key: k,\n short-id: 1234e123\n}",
"{\n public-key: k,\n short-id: \"1234e123\"\n}",
),
("short-id: &sid 1234e123", "short-id: &sid 1234e123"),
("short-id: !!str 1234e123", "short-id: !!str 1234e123"),
("short-id: abc,def", "short-id: abc,def"),
("short-id: 1234e123\r\n", "short-id: \"1234e123\"\r\n"),
("my-short-id: 1234e123", "my-short-id: 1234e123"),
("xshort-id: 1", "xshort-id: 1"),
("short-id:1234e123", "short-id:1234e123"),
("short-id: 1.5e3", "short-id: 1.5e3"),
];
for (input, expected) in cases {
let output = quote_plain_short_ids(input);
assert_eq!(output, expected, "input: {input:?}");
assert_eq!(
matches!(output, Cow::Borrowed(_)),
input == expected,
"input: {input:?}"
);
assert_eq!(quote_plain_short_ids(&output), expected, "rerun: {input:?}");
}
}
#[test]
fn plain_short_id_survives_profile_parse() {
fn plain_short_id_stays_text() {
let profile = "\
proxies:
- name: r
type: vless
reality-opts:
public-key: SFv3pZQdZ1fpnNg9yI2RwXKqAOz4mZHRbT2Wq4kz8Ws
short-id: 1234e123
";
let mapping = parse_mapping(profile.into(), PathBuf::from("profile.yaml")).expect("profile should parse");
let short_id = mapping
.get("proxies")
.and_then(|proxies| proxies.get(0))
.and_then(|proxy| proxy.get("reality-opts"))
.and_then(|opts| opts.get("short-id"));
assert_eq!(short_id, Some(&Value::String("1234e123".into())));
let output = serde_yaml_ng::to_string(&mapping).expect("mapping should serialize");
assert!(output.contains("short-id: '1234e123'"), "{output}");
}
#[test]
fn short_id_rewrite_falls_back_to_original_parse() {
// (case, input, has a numeric short-id today)
let cases = [
("B2", "proxies:\n - name: \"node, short-id: 1234e123}\"\n", false),
(
"B3",
"proxies:\n - name: \"node, short-id: 1234e123}\"\n - name: r\n reality-opts:\n short-id: 1234e123\n",
true,
),
(
"B4",
"payload: |\n short-id: 1234e123\nreality-opts:\n short-id: 1234e123\n",
true,
),
("B5", "reality-opts:\n short-id: 1234e123\n more\n", false),
("B6", "reality-opts: {public-key: k, # c\n short-id: 1234e123}\n", true),
];
for (case, input, numeric) in cases {
let today = serde_yaml_ng::from_str::<Value>(input).expect("input should parse");
assert_eq!(has_numeric_short_id(&today), numeric, "{case}");
let (text, value) = short_id_tolerant(input, Path::new("b.yaml")).expect("input should parse");
assert!(matches!(text, Cow::Borrowed(_)), "{case}");
assert_eq!(value, today, "{case}");
}
}
#[test]
fn short_id_rewrite_keeps_every_numeric_short_id() {
let input = "\
proxies:
- name: a
reality-opts:
short-id: 1234e123
- {name: b, reality-opts: {short-id: 0e5}}
- {name: b, reality-opts: {public-key: k, short-id: 0e5}}
";
let (text, value) = short_id_tolerant(input, Path::new("b7.yaml")).expect("input should parse");
assert!(matches!(text, Cow::Owned(_)));
let short_ids: Vec<_> = value
.get("proxies")
.and_then(Value::as_sequence)
.expect("proxies should be a sequence")
.iter()
.map(|proxy| proxy.get("reality-opts").and_then(|opts| opts.get("short-id")).cloned())
.collect();
assert_eq!(
short_ids,
[
Some(Value::String("1234e123".into())),
Some(Value::String("0e5".into()))
]
);
let mapping = parse_mapping(profile.into(), PathBuf::from("profile.yaml")).expect("profile should parse");
let output = serde_yaml_ng::to_string(&mapping).expect("mapping should serialize");
assert!(output.contains("short-id: '1234e123'"), "{output}");
assert!(output.contains("short-id: '0e5'"), "{output}");
}
#[test]
fn short_id_quoting_changing_other_values_is_dropped() {
let profile = "\
proxies:
- name: a
reality-opts:
short-id: 1234e123
note: |
short-id: 1234e123
";
let today = serde_yaml_ng::from_str::<Value>(profile).expect("profile should parse");
let value = parse_short_id_text(profile, Path::new("profile.yaml")).expect("profile should parse");
assert_eq!(value, today);
}
}
-2
View File
@@ -78,8 +78,6 @@ 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!(
+2 -8
View File
@@ -11,10 +11,7 @@
"conflicts": ["clash-verge"],
"replaces": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh",
"files": {
"/usr/lib/clash-verge-rev/clash-verge-service-uninstall": "./sidecar/clash-verge-service-uninstall-bin"
}
"preRemoveScript": "./packages/linux/pre-remove.sh"
},
"rpm": {
"depends": ["openssl", "libayatana-appindicator-gtk3"],
@@ -24,10 +21,7 @@
"conflicts": ["clash-verge"],
"obsoletes": ["clash-verge"],
"postInstallScript": "./packages/linux/post-install-rpm.sh",
"preRemoveScript": "./packages/linux/pre-remove.sh",
"files": {
"/usr/lib/clash-verge-rev/clash-verge-service-uninstall": "./sidecar/clash-verge-service-uninstall-bin"
}
"preRemoveScript": "./packages/linux/pre-remove.sh"
}
},
"externalBin": [
@@ -28,9 +28,7 @@ import getSystem from '@/utils/get-system'
type Remedy = 'installAndRestart' | 'reinstallAndRestart' | 'restartOnly'
const remedyFor = (reason: ServiceRequestReason): Remedy =>
reason === 'serviceLocationRefused' ||
reason === 'serviceNotAutoStarted' ||
reason === 'serviceUpgradeIncomplete'
reason === 'serviceLocationRefused' || reason === 'serviceNotAutoStarted'
? 'reinstallAndRestart'
: reason === 'sysproxySidecarReady'
? 'restartOnly'
@@ -45,8 +43,6 @@ const EXPLANATION = {
'layout.components.serviceMigration.locationRefusedMessage',
serviceNotAutoStarted:
'layout.components.serviceMigration.notAutoStartedMessage',
serviceUpgradeIncomplete:
'layout.components.serviceMigration.upgradeIncompleteMessage',
} as const
const TITLE = {
@@ -55,7 +51,6 @@ 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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"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,7 +23,6 @@
"serviceMigration": {
"locationRefusedMessage": "系统服务的安装路径或目录权限未通过安全检查,无法启动内核。请重新安装服务以恢复服务文件和权限,随后重试。Windows 可能会请求管理员授权。",
"notAutoStartedMessage": "Clash Verge 系统服务的启动类型已被改为手动或禁用(常见于优化软件的开机加速功能),开机后不会自动运行,本次已改用 Sidecar。请重新安装服务以恢复自动启动。Windows 可能会请求管理员授权。",
"upgradeIncompleteMessage": "Clash Verge 已升级,但原先安装的 Clash Verge 服务未就绪。请重装服务以继续使用,系统可能要求管理员认证。",
"title": "需要更新系统服务",
"message": "Clash Verge 系统服务版本与当前应用要求不符。请选择「重新安装服务」卸载现有服务并安装所需版本,也可以仅在本次会话继续使用 Sidecar。",
"checkingMessage": "正在确认系统服务状态及当前运行模式,请稍候……",
-1
View File
@@ -23,7 +23,6 @@
"serviceMigration": {
"locationRefusedMessage": "系統服務的安裝路徑或目錄權限未通過安全檢查,無法啟動核心。請重新安裝服務以還原服務檔案和權限,然後重試。Windows 可能會要求管理員授權。",
"notAutoStartedMessage": "Clash Verge 系統服務的啟動類型已被改為手動或停用(常見於最佳化軟體的開機加速功能),開機後不會自動執行,本次已改用 Sidecar。請重新安裝服務以恢復自動啟動。Windows 可能會要求管理員授權。",
"upgradeIncompleteMessage": "Clash Verge 已升級,但原先安裝的 Clash Verge 服務未就緒。請重新安裝服務以繼續使用,系統可能要求管理員驗證。",
"title": "需要更新系統服務",
"message": "Clash Verge 系統服務版本與目前應用程式要求不符。請選擇「重新安裝服務」移除現有服務並安裝所需版本,也可以僅在本次工作階段繼續使用 Sidecar。",
"checkingMessage": "正在確認系統服務狀態及目前執行模式,請稍候……",
-1
View File
@@ -1035,7 +1035,6 @@ export type ServiceFallbackNotice =
| { kind: 'unavailable' }
| { kind: 'coreRejected'; reason: string }
| { kind: 'notAutoStarted' }
| { kind: 'upgradeIncomplete' }
export type ServiceHealthKind =
| 'unknown'
-2
View File
@@ -125,8 +125,6 @@ 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,7 +5,6 @@ export type ServiceRequestReason =
| 'tunNeedsService'
| 'serviceLocationRefused'
| 'serviceNotAutoStarted'
| 'serviceUpgradeIncomplete'
export interface ServiceRequest {
readonly reason: ServiceRequestReason
-1
View File
@@ -128,7 +128,6 @@ 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,7 +233,6 @@ export interface TranslationResources {
success: string
title: string
unavailableMessage: string
upgradeIncompleteMessage: string
}
sysproxyPrivilege: {
alternative: string