mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-29 18:12:10 +08:00
fix(service): fall back after installation verification rejects cores
This commit is contained in:
Generated
-1
@@ -1224,7 +1224,6 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "clash_verge_service_ipc"
|
name = "clash_verge_service_ipc"
|
||||||
version = "2.7.3"
|
version = "2.7.3"
|
||||||
source = "git+https://github.com/clash-verge-rev/clash-verge-service-ipc.git?tag=v2.7.3#29251d968e1a38fac1de0ebe9d6d69402bea83b4"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"kode-bridge",
|
"kode-bridge",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
**🖥️ Windows**
|
**🖥️ Windows**
|
||||||
|
|
||||||
|
- 修复 Windows 旧版服务状态残留导致无法重装的问题,自动备份可识别的残留并恢复安装
|
||||||
|
- 修复 Windows 服务安装后权限检查不通过时,未说明原因并自动切换为普通模式的问题
|
||||||
- 修复 Windows 服务因路径或目录权限问题无法启动时,缺少重新安装服务的提示
|
- 修复 Windows 服务因路径或目录权限问题无法启动时,缺少重新安装服务的提示
|
||||||
- 修复 Windows 服务回退检查失败后状态未更新、无法重新尝试 Sidecar 的问题
|
- 修复 Windows 服务回退检查失败后状态未更新、无法重新尝试 Sidecar 的问题
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ tracing = { workspace = true }
|
|||||||
tauri-plugin-devtools = { version = "2.2.1", optional = true }
|
tauri-plugin-devtools = { version = "2.2.1", optional = true }
|
||||||
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
|
tauri-plugin-mihomo = { git = "https://github.com/clash-verge-rev/tauri-plugin-mihomo", branch = "main" }
|
||||||
async-trait = "0.1.92"
|
async-trait = "0.1.92"
|
||||||
clash_verge_service_ipc = { git = "https://github.com/clash-verge-rev/clash-verge-service-ipc.git", tag = "v2.7.3", version = "2.7.3", features = [
|
clash_verge_service_ipc = { path = "../../clash-verge-service-ipc", version = "2.7.3", features = [
|
||||||
"client",
|
"client",
|
||||||
] }
|
] }
|
||||||
arc-swap = "1.9.2"
|
arc-swap = "1.9.2"
|
||||||
|
|||||||
+139
-17
@@ -8,26 +8,78 @@ use crate::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn execute_service_operation_sync(status: ServiceStatus, error_code: &str) -> CmdResult {
|
#[derive(Debug, PartialEq, Eq, serde::Serialize)]
|
||||||
|
#[serde(tag = "status", rename_all = "camelCase")]
|
||||||
|
pub enum ServiceInstallOutcome {
|
||||||
|
Installed,
|
||||||
|
Sidecar { reason: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn execute_service_operation_sync(status: ServiceStatus, error_code: &str) -> CmdResult<ServiceInstallOutcome> {
|
||||||
let manager = CoreManager::global();
|
let manager = CoreManager::global();
|
||||||
let _lifecycle = manager.lifecycle_lock.lock().await;
|
let result = {
|
||||||
if matches!(
|
let _lifecycle = manager.lifecycle_lock.lock().await;
|
||||||
&status,
|
if matches!(
|
||||||
ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired
|
&status,
|
||||||
) {
|
ServiceStatus::ReinstallRequired | ServiceStatus::ForceReinstallRequired
|
||||||
manager
|
) {
|
||||||
.controlled_stop_core_inner()
|
manager
|
||||||
.await
|
.controlled_stop_core_inner()
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, error_code))?;
|
.await
|
||||||
}
|
.map_err(|error| proxy_aware_coded_error(&error, error_code))?;
|
||||||
SERVICE_MANAGER
|
}
|
||||||
.handle_service_status(status)
|
SERVICE_MANAGER.handle_service_status(status).await
|
||||||
.await
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
return finish_service_installation(result, error_code, || manager.continue_with_sidecar()).await;
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
result
|
||||||
|
.map(|()| ServiceInstallOutcome::Installed)
|
||||||
.map_err(|error| proxy_aware_coded_error(&error, error_code))
|
.map_err(|error| proxy_aware_coded_error(&error, error_code))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(windows, test))]
|
||||||
|
async fn finish_service_installation<Fallback, FallbackFuture>(
|
||||||
|
installation: anyhow::Result<()>,
|
||||||
|
error_code: &str,
|
||||||
|
fallback: Fallback,
|
||||||
|
) -> CmdResult<ServiceInstallOutcome>
|
||||||
|
where
|
||||||
|
Fallback: FnOnce() -> FallbackFuture,
|
||||||
|
FallbackFuture: std::future::Future<Output = anyhow::Result<()>>,
|
||||||
|
{
|
||||||
|
let Err(error) = installation else {
|
||||||
|
return Ok(ServiceInstallOutcome::Installed);
|
||||||
|
};
|
||||||
|
let Some(failure) = error.downcast_ref::<clash_verge_service_ipc::management::InstallationVerificationError>()
|
||||||
|
else {
|
||||||
|
return Err(proxy_aware_coded_error(&error, error_code));
|
||||||
|
};
|
||||||
|
let reasons = failure
|
||||||
|
.status
|
||||||
|
.cores
|
||||||
|
.iter()
|
||||||
|
.filter_map(|core| match &core.availability {
|
||||||
|
clash_verge_service_ipc::CoreAvailability::Rejected { reason } => Some(format!("{}: {reason}", core.name)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if reasons.is_empty() {
|
||||||
|
return Err(proxy_aware_coded_error(&error, error_code));
|
||||||
|
}
|
||||||
|
let reason = reasons.join("\n");
|
||||||
|
fallback().await.map_err(|error| {
|
||||||
|
proxy_aware_coded_error(
|
||||||
|
&error.context(format!("Service core rejected: {reason}; Sidecar fallback failed")),
|
||||||
|
"SERVICE_SIDECAR_FAILED",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(ServiceInstallOutcome::Sidecar { reason })
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn install_service() -> CmdResult {
|
pub async fn install_service() -> CmdResult<ServiceInstallOutcome> {
|
||||||
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
|
execute_service_operation_sync(ServiceStatus::InstallRequired, "SERVICE_INSTALL_FAILED").await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,12 +92,12 @@ pub async fn uninstall_service() -> CmdResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reinstall_service() -> CmdResult {
|
pub async fn reinstall_service() -> CmdResult<ServiceInstallOutcome> {
|
||||||
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
|
execute_service_operation_sync(ServiceStatus::ReinstallRequired, "SERVICE_REINSTALL_FAILED").await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn repair_service() -> CmdResult {
|
pub async fn repair_service() -> CmdResult<ServiceInstallOutcome> {
|
||||||
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
|
execute_service_operation_sync(ServiceStatus::ForceReinstallRequired, "SERVICE_REPAIR_FAILED").await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,3 +125,73 @@ pub fn sync_runtime_providers() {
|
|||||||
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
|
request_runtime_provider_sync(timing::RUNTIME_PROVIDER_SETTLE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::expect_used, reason = "tests assert by panicking")]
|
||||||
|
mod tests {
|
||||||
|
use super::{ServiceInstallOutcome, finish_service_installation};
|
||||||
|
use clash_verge_service_ipc::{
|
||||||
|
CoreAvailability, CoreInspection, InstallationStatus, ProtocolInfo, management::InstallationVerificationError,
|
||||||
|
};
|
||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn rejected_installation_reports_sidecar_only_after_successful_continuation() {
|
||||||
|
let reason = "core path C:\\ is writable by Everyone";
|
||||||
|
for (availability, fallback_ready) in [
|
||||||
|
(None, false),
|
||||||
|
(Some(CoreAvailability::Missing), false),
|
||||||
|
(Some(CoreAvailability::DigestMismatch), false),
|
||||||
|
(Some(CoreAvailability::Rejected { reason: reason.into() }), false),
|
||||||
|
(Some(CoreAvailability::Rejected { reason: reason.into() }), true),
|
||||||
|
] {
|
||||||
|
let fallback_called = Cell::new(false);
|
||||||
|
let rejected = matches!(&availability, Some(CoreAvailability::Rejected { .. }));
|
||||||
|
let error = match availability {
|
||||||
|
Some(availability) => anyhow::Error::new(InstallationVerificationError {
|
||||||
|
status: InstallationStatus {
|
||||||
|
service_sha256: String::new(),
|
||||||
|
protocol: ProtocolInfo::current(),
|
||||||
|
cores: vec![CoreInspection {
|
||||||
|
name: "verge-mihomo.exe".into(),
|
||||||
|
availability,
|
||||||
|
}],
|
||||||
|
core_busy: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
None => anyhow::anyhow!("{reason}"),
|
||||||
|
}
|
||||||
|
.context("install service failed");
|
||||||
|
let result = finish_service_installation(Err(error), "SERVICE_INSTALL_FAILED", || async {
|
||||||
|
fallback_called.set(true);
|
||||||
|
anyhow::ensure!(fallback_ready, "another core is running");
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(fallback_called.get(), rejected);
|
||||||
|
if rejected && fallback_ready {
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
Ok(ServiceInstallOutcome::Sidecar {
|
||||||
|
reason: format!("verge-mihomo.exe: {reason}")
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let error = result.expect_err("an unavailable fallback must remain an error");
|
||||||
|
assert_eq!(
|
||||||
|
error.code.as_deref(),
|
||||||
|
Some(if rejected {
|
||||||
|
"SERVICE_SIDECAR_FAILED"
|
||||||
|
} else {
|
||||||
|
"SERVICE_INSTALL_FAILED"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
if rejected {
|
||||||
|
assert!(error.detail.contains(reason));
|
||||||
|
assert!(error.detail.contains("another core is running"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ const fn can_allow_sidecar_for_session(running_mode: &RunningMode, service_statu
|
|||||||
| ServiceStatus::NeedsReinstall
|
| ServiceStatus::NeedsReinstall
|
||||||
| ServiceStatus::InstallRequired
|
| ServiceStatus::InstallRequired
|
||||||
| ServiceStatus::Unavailable(_)
|
| ServiceStatus::Unavailable(_)
|
||||||
) | (RunningMode::Sidecar, ServiceStatus::InstallRequired)
|
| ServiceStatus::SidecarAllowed
|
||||||
|
) | (
|
||||||
|
RunningMode::Sidecar,
|
||||||
|
ServiceStatus::InstallRequired | ServiceStatus::Unavailable(_) | ServiceStatus::SidecarAllowed
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +385,9 @@ impl CoreManager {
|
|||||||
if matches!(*mode, RunningMode::NotRunning) {
|
if matches!(*mode, RunningMode::NotRunning) {
|
||||||
clash_verge_service_ipc::execution::check_sidecar_available().await?;
|
clash_verge_service_ipc::execution::check_sidecar_available().await?;
|
||||||
}
|
}
|
||||||
SERVICE_MANAGER.allow_sidecar_for_session()?;
|
if !matches!(status, ServiceStatus::SidecarAllowed) {
|
||||||
|
SERVICE_MANAGER.allow_sidecar_for_session()?;
|
||||||
|
}
|
||||||
// Settling on Sidecar is what makes the verdict final, so ask only once it is recorded.
|
// Settling on Sidecar is what makes the verdict final, so ask only once it is recorded.
|
||||||
// Elevation alone carries TUN on Sidecar; a Sidecar that cannot must write it off.
|
// Elevation alone carries TUN on Sidecar; a Sidecar that cannot must write it off.
|
||||||
let prepared = async {
|
let prepared = async {
|
||||||
@@ -1474,12 +1480,18 @@ mod tests {
|
|||||||
ServiceStatus::NeedsReinstall,
|
ServiceStatus::NeedsReinstall,
|
||||||
ServiceStatus::InstallRequired,
|
ServiceStatus::InstallRequired,
|
||||||
ServiceStatus::Unavailable("offline".into()),
|
ServiceStatus::Unavailable("offline".into()),
|
||||||
|
ServiceStatus::SidecarAllowed,
|
||||||
];
|
];
|
||||||
for status in &allowed_statuses {
|
for status in &allowed_statuses {
|
||||||
assert!(can_allow_sidecar_for_session(&RunningMode::NotRunning, status));
|
assert!(can_allow_sidecar_for_session(&RunningMode::NotRunning, status));
|
||||||
assert!(!can_allow_sidecar_for_session(&RunningMode::Service, status));
|
assert!(!can_allow_sidecar_for_session(&RunningMode::Service, status));
|
||||||
if !matches!(status, ServiceStatus::InstallRequired) {
|
if !matches!(
|
||||||
|
status,
|
||||||
|
ServiceStatus::InstallRequired | ServiceStatus::Unavailable(_) | ServiceStatus::SidecarAllowed
|
||||||
|
) {
|
||||||
assert!(!can_allow_sidecar_for_session(&RunningMode::Sidecar, status));
|
assert!(!can_allow_sidecar_for_session(&RunningMode::Sidecar, status));
|
||||||
|
} else {
|
||||||
|
assert!(can_allow_sidecar_for_session(&RunningMode::Sidecar, status));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,7 +1501,6 @@ mod tests {
|
|||||||
ServiceStatus::UninstallRequired,
|
ServiceStatus::UninstallRequired,
|
||||||
ServiceStatus::ReinstallRequired,
|
ServiceStatus::ReinstallRequired,
|
||||||
ServiceStatus::ForceReinstallRequired,
|
ServiceStatus::ForceReinstallRequired,
|
||||||
ServiceStatus::SidecarAllowed,
|
|
||||||
];
|
];
|
||||||
for status in &rejected_statuses {
|
for status in &rejected_statuses {
|
||||||
assert!(!can_allow_sidecar_for_session(&RunningMode::NotRunning, status));
|
assert!(!can_allow_sidecar_for_session(&RunningMode::NotRunning, status));
|
||||||
|
|||||||
@@ -47,9 +47,7 @@ pub fn classify_service_version_reply(reply: &ServiceVersionReply) -> ServiceVer
|
|||||||
return match &reply.core {
|
return match &reply.core {
|
||||||
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
|
Some(clash_verge_service_ipc::CoreAvailability::Ready) => ServiceVersionCheck::Ready,
|
||||||
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
|
Some(clash_verge_service_ipc::CoreAvailability::Rejected { reason }) => {
|
||||||
ServiceVersionCheck::CoreUnavailable(format!(
|
ServiceVersionCheck::CoreUnavailable(format!("approved core was rejected: {reason}"))
|
||||||
"approved core was rejected: {reason}; reinstall the service to repair it"
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
_ => ServiceVersionCheck::CoreUnavailable(
|
_ => ServiceVersionCheck::CoreUnavailable(
|
||||||
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
|
"the selected approved core is missing or does not match; reinstall the service to repair it".into(),
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { beforeEach, expect, it, vi } from 'vitest'
|
import { beforeEach, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
import type { RunState } from '@/services/cmds'
|
import {
|
||||||
|
getRuntimeState,
|
||||||
|
installService,
|
||||||
|
restartCore,
|
||||||
|
type RunState,
|
||||||
|
} from '@/services/cmds'
|
||||||
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { useQuery } from '@/services/query-client'
|
import { useQuery } from '@/services/query-client'
|
||||||
|
|
||||||
import { ServiceMigrationDialog } from './service-migration-dialog'
|
import { ServiceMigrationDialog } from './service-migration-dialog'
|
||||||
@@ -14,8 +20,14 @@ vi.mock('@mui/material', () => ({ Alert: 'div' }))
|
|||||||
vi.mock('@/components/base', () => ({ BaseDialog: 'dialog' }))
|
vi.mock('@/components/base', () => ({ BaseDialog: 'dialog' }))
|
||||||
vi.mock('@/hooks/use-visibility', () => ({ useVisibility: () => true }))
|
vi.mock('@/hooks/use-visibility', () => ({ useVisibility: () => true }))
|
||||||
vi.mock('@/hooks/use-system-state', () => ({ runStateQueryKey: ['state'] }))
|
vi.mock('@/hooks/use-system-state', () => ({ runStateQueryKey: ['state'] }))
|
||||||
vi.mock('@/services/cmds', () => ({ getRuntimeState: vi.fn() }))
|
vi.mock('@/services/cmds', () => ({
|
||||||
vi.mock('@/services/notice-service', () => ({ showNotice: {} }))
|
getRuntimeState: vi.fn(),
|
||||||
|
installService: vi.fn(),
|
||||||
|
restartCore: vi.fn(),
|
||||||
|
}))
|
||||||
|
vi.mock('@/services/notice-service', () => ({
|
||||||
|
showNotice: { warning: vi.fn(), error: vi.fn(), success: vi.fn() },
|
||||||
|
}))
|
||||||
vi.mock('@/services/query-client', () => ({
|
vi.mock('@/services/query-client', () => ({
|
||||||
useQuery: vi.fn(),
|
useQuery: vi.fn(),
|
||||||
setCacheData: vi.fn(),
|
setCacheData: vi.fn(),
|
||||||
@@ -47,6 +59,36 @@ it.each([
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it('reports the permission refusal and finishes after installation falls back to Sidecar', async () => {
|
||||||
|
vi.mocked(useQuery).mockReturnValue({
|
||||||
|
data: { service: 'notInstalled', mode: 'NotRunning' },
|
||||||
|
} as ReturnType<typeof useQuery>)
|
||||||
|
const reason = 'core path C:\\ is writable by Everyone'
|
||||||
|
vi.mocked(installService).mockResolvedValue({ status: 'sidecar', reason })
|
||||||
|
vi.mocked(getRuntimeState).mockResolvedValue({
|
||||||
|
service: 'unavailable',
|
||||||
|
mode: 'Sidecar',
|
||||||
|
sidecarAllowed: true,
|
||||||
|
serviceNeedsAttention: false,
|
||||||
|
} as RunState)
|
||||||
|
|
||||||
|
const dialog = ServiceMigrationDialog()
|
||||||
|
dialog.props.onOk()
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(showNotice.warning).toHaveBeenCalledWith(
|
||||||
|
'settings.feedback.notifications.clashService.permissionFallback',
|
||||||
|
{ reason },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(restartCore).not.toHaveBeenCalled()
|
||||||
|
expect(showNotice.error).not.toHaveBeenCalled()
|
||||||
|
expect(showNotice.success).not.toHaveBeenCalled()
|
||||||
|
expect(vi.mocked(useState).mock.results[2].value[1]).toHaveBeenLastCalledWith(
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
{ service: 'notInstalled', mode: 'NotRunning' },
|
{ service: 'notInstalled', mode: 'NotRunning' },
|
||||||
{ service: 'versionMismatch', mode: 'NotRunning' },
|
{ service: 'versionMismatch', mode: 'NotRunning' },
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
repairService,
|
repairService,
|
||||||
restartCore,
|
restartCore,
|
||||||
type RunState,
|
type RunState,
|
||||||
|
type ServiceInstallOutcome,
|
||||||
} from '@/services/cmds'
|
} from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
import { setCacheData, useQuery } from '@/services/query-client'
|
import { setCacheData, useQuery } from '@/services/query-client'
|
||||||
@@ -79,16 +80,15 @@ export const ServiceMigrationDialog = () => {
|
|||||||
const handleServiceAction = async () => {
|
const handleServiceAction = async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setWorkflowIncomplete(true)
|
setWorkflowIncomplete(true)
|
||||||
let actionSucceeded = false
|
let outcome: ServiceInstallOutcome | undefined
|
||||||
try {
|
try {
|
||||||
if (remedy === 'install') {
|
if (remedy === 'install') {
|
||||||
await installService()
|
outcome = await installService()
|
||||||
} else if (remedy === 'repair') {
|
} else if (remedy === 'repair') {
|
||||||
await repairService()
|
outcome = await repairService()
|
||||||
} else {
|
} else {
|
||||||
await reinstallService()
|
outcome = await reinstallService()
|
||||||
}
|
}
|
||||||
actionSucceeded = true
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showNotice.error(
|
showNotice.error(
|
||||||
'layout.components.serviceMigration.errors.actionFailed',
|
'layout.components.serviceMigration.errors.actionFailed',
|
||||||
@@ -106,10 +106,19 @@ export const ServiceMigrationDialog = () => {
|
|||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!actionSucceeded || !initialRefreshSucceeded) {
|
if (!outcome || !initialRefreshSucceeded) {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (outcome.status === 'sidecar') {
|
||||||
|
setWorkflowIncomplete(false)
|
||||||
|
setLoading(false)
|
||||||
|
showNotice.warning(
|
||||||
|
'settings.feedback.notifications.clashService.permissionFallback',
|
||||||
|
{ reason: outcome.reason },
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let restartSucceeded = false
|
let restartSucceeded = false
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
restartCore,
|
restartCore,
|
||||||
type FailedOperation,
|
type FailedOperation,
|
||||||
type PendingFailure,
|
type PendingFailure,
|
||||||
|
type ServiceInstallOutcome,
|
||||||
} from '@/services/cmds'
|
} from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
import {
|
import {
|
||||||
@@ -99,12 +100,21 @@ export const SysproxyPrivilegeDialog = () => {
|
|||||||
|
|
||||||
const handleFix = async () => {
|
const handleFix = async () => {
|
||||||
try {
|
try {
|
||||||
|
let outcome: ServiceInstallOutcome | undefined
|
||||||
if (remedy === 'reinstallAndRestart') {
|
if (remedy === 'reinstallAndRestart') {
|
||||||
setStep('installing')
|
setStep('installing')
|
||||||
await reinstallService()
|
outcome = await reinstallService()
|
||||||
} else if (remedy === 'installAndRestart') {
|
} else if (remedy === 'installAndRestart') {
|
||||||
setStep('installing')
|
setStep('installing')
|
||||||
await installService()
|
outcome = await installService()
|
||||||
|
}
|
||||||
|
if (outcome?.status === 'sidecar') {
|
||||||
|
showNotice.warning(
|
||||||
|
'settings.feedback.notifications.clashService.permissionFallback',
|
||||||
|
{ reason: outcome.reason },
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
setStep('restarting')
|
setStep('restarting')
|
||||||
await restartCore()
|
await restartCore()
|
||||||
|
|||||||
@@ -3,17 +3,18 @@ import { useCallback } from 'react'
|
|||||||
import { installService, restartCore } from '@/services/cmds'
|
import { installService, restartCore } from '@/services/cmds'
|
||||||
import { showNotice } from '@/services/notice-service'
|
import { showNotice } from '@/services/notice-service'
|
||||||
|
|
||||||
const executeWithErrorHandling = async (
|
const executeWithErrorHandling = async <T>(
|
||||||
operation: () => Promise<void>,
|
operation: () => Promise<T>,
|
||||||
loadingKey: string,
|
loadingKey: string,
|
||||||
successKey?: string,
|
successKey?: string,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
showNotice.info(loadingKey)
|
showNotice.info(loadingKey)
|
||||||
await operation()
|
const result = await operation()
|
||||||
if (successKey) {
|
if (successKey) {
|
||||||
showNotice.success(successKey)
|
showNotice.success(successKey)
|
||||||
}
|
}
|
||||||
|
return result
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showNotice.error(err)
|
showNotice.error(err)
|
||||||
throw err
|
throw err
|
||||||
@@ -22,9 +23,18 @@ const executeWithErrorHandling = async (
|
|||||||
|
|
||||||
export const useServiceInstaller = () => {
|
export const useServiceInstaller = () => {
|
||||||
const installServiceAndRestartCore = useCallback(async () => {
|
const installServiceAndRestartCore = useCallback(async () => {
|
||||||
await executeWithErrorHandling(
|
const outcome = await executeWithErrorHandling(
|
||||||
() => installService(),
|
() => installService(),
|
||||||
'settings.statuses.clashService.installing',
|
'settings.statuses.clashService.installing',
|
||||||
|
)
|
||||||
|
if (outcome.status === 'sidecar') {
|
||||||
|
showNotice.warning(
|
||||||
|
'settings.feedback.notifications.clashService.permissionFallback',
|
||||||
|
{ reason: outcome.reason },
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
showNotice.success(
|
||||||
'settings.feedback.notifications.clashService.installSuccess',
|
'settings.feedback.notifications.clashService.installSuccess',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "تم تثبيت الخدمة بنجاح",
|
"installSuccess": "تم تثبيت الخدمة بنجاح",
|
||||||
|
"permissionFallback": "لم تجتز ملفات نواة الخدمة أو مجلداتها فحص الأمان. تم التبديل إلى وضع Sidecar؛ ويتطلب TUN صلاحيات المسؤول. السبب: {{reason}}",
|
||||||
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة. تم التبديل إلى وضع Sidecar؛ ولا تزال الخدمة غير متاحة.",
|
"sidecarFallback": "تعذّر على الخدمة تشغيل النواة. تم التبديل إلى وضع Sidecar؛ ولا تزال الخدمة غير متاحة.",
|
||||||
"uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح"
|
"uninstallSuccess": "تم إلغاء تثبيت الخدمة بنجاح"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Service erfolgreich installiert",
|
"installSuccess": "Service erfolgreich installiert",
|
||||||
|
"permissionFallback": "Die Kerndateien oder Verzeichnisse des Dienstes haben die Sicherheitsprüfung nicht bestanden. Die Anwendung läuft im Sidecar-Modus weiter; TUN benötigt Administratorrechte. Grund: {{reason}}",
|
||||||
"sidecarFallback": "Der Dienst konnte den Kern nicht starten. Die Anwendung läuft im Sidecar-Modus weiter; der Dienst bleibt nicht verfügbar.",
|
"sidecarFallback": "Der Dienst konnte den Kern nicht starten. Die Anwendung läuft im Sidecar-Modus weiter; der Dienst bleibt nicht verfügbar.",
|
||||||
"uninstallSuccess": "Service erfolgreich deinstalliert"
|
"uninstallSuccess": "Service erfolgreich deinstalliert"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Service Installed Successfully",
|
"installSuccess": "Service Installed Successfully",
|
||||||
|
"permissionFallback": "The service's core files or directories failed a security check. Switched to Sidecar mode; TUN requires administrator privileges. Reason: {{reason}}",
|
||||||
"sidecarFallback": "The service could not start the core. Switched to Sidecar mode; the service remains unavailable.",
|
"sidecarFallback": "The service could not start the core. Switched to Sidecar mode; the service remains unavailable.",
|
||||||
"uninstallSuccess": "Service Uninstalled Successfully"
|
"uninstallSuccess": "Service Uninstalled Successfully"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Servicio instalado con éxito",
|
"installSuccess": "Servicio instalado con éxito",
|
||||||
|
"permissionFallback": "Los archivos o directorios del núcleo del servicio no superaron la comprobación de seguridad. Se ha cambiado al modo Sidecar; TUN requiere permisos de administrador. Motivo: {{reason}}",
|
||||||
"sidecarFallback": "El servicio no pudo iniciar el núcleo. Se ha cambiado al modo Sidecar; el servicio sigue sin estar disponible.",
|
"sidecarFallback": "El servicio no pudo iniciar el núcleo. Se ha cambiado al modo Sidecar; el servicio sigue sin estar disponible.",
|
||||||
"uninstallSuccess": "Servicio desinstalado con éxito"
|
"uninstallSuccess": "Servicio desinstalado con éxito"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "سرویس با موفقیت نصب شد",
|
"installSuccess": "سرویس با موفقیت نصب شد",
|
||||||
|
"permissionFallback": "فایلها یا پوشههای هستهٔ سرویس از بررسی امنیتی عبور نکردند. به حالت Sidecar تغییر یافت؛ TUN به دسترسی مدیر نیاز دارد. دلیل: {{reason}}",
|
||||||
"sidecarFallback": "سرویس نتوانست هسته را اجرا کند. اجرا در حالت Sidecar ادامه مییابد؛ سرویس همچنان در دسترس نیست.",
|
"sidecarFallback": "سرویس نتوانست هسته را اجرا کند. اجرا در حالت Sidecar ادامه مییابد؛ سرویس همچنان در دسترس نیست.",
|
||||||
"uninstallSuccess": "سرویس با موفقیت حذف نصب شد"
|
"uninstallSuccess": "سرویس با موفقیت حذف نصب شد"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Layanan Berhasil Diinstal",
|
"installSuccess": "Layanan Berhasil Diinstal",
|
||||||
|
"permissionFallback": "Berkas atau direktori inti layanan tidak lolos pemeriksaan keamanan. Beralih ke mode Sidecar; TUN memerlukan hak administrator. Alasan: {{reason}}",
|
||||||
"sidecarFallback": "Layanan tidak dapat memulai inti. Beralih ke mode Sidecar; layanan masih tidak tersedia.",
|
"sidecarFallback": "Layanan tidak dapat memulai inti. Beralih ke mode Sidecar; layanan masih tidak tersedia.",
|
||||||
"uninstallSuccess": "Layanan Berhasil Dicopot"
|
"uninstallSuccess": "Layanan Berhasil Dicopot"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "サービスのインストールに成功しました。",
|
"installSuccess": "サービスのインストールに成功しました。",
|
||||||
|
"permissionFallback": "サービスのコアファイルまたはディレクトリがセキュリティチェックに失敗したため、Sidecar モードに切り替えました。TUN には管理者権限が必要です。理由:{{reason}}",
|
||||||
"sidecarFallback": "サービスでコアを起動できなかったため、Sidecar モードで動作を継続しています。サービスは引き続き利用できません。",
|
"sidecarFallback": "サービスでコアを起動できなかったため、Sidecar モードで動作を継続しています。サービスは引き続き利用できません。",
|
||||||
"uninstallSuccess": "サービスのアンインストールに成功しました。"
|
"uninstallSuccess": "サービスのアンインストールに成功しました。"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "서비스가 설치되었습니다",
|
"installSuccess": "서비스가 설치되었습니다",
|
||||||
|
"permissionFallback": "서비스의 코어 파일 또는 디렉터리가 보안 검사를 통과하지 못해 Sidecar 모드로 전환했습니다. TUN에는 관리자 권한이 필요합니다. 사유: {{reason}}",
|
||||||
"sidecarFallback": "서비스가 코어를 시작하지 못해 Sidecar 모드로 계속 실행합니다. 서비스는 여전히 사용할 수 없습니다.",
|
"sidecarFallback": "서비스가 코어를 시작하지 못해 Sidecar 모드로 계속 실행합니다. 서비스는 여전히 사용할 수 없습니다.",
|
||||||
"uninstallSuccess": "서비스가 제거되었습니다"
|
"uninstallSuccess": "서비스가 제거되었습니다"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Служба успешно установлена",
|
"installSuccess": "Служба успешно установлена",
|
||||||
|
"permissionFallback": "Файлы или каталоги ядра службы не прошли проверку безопасности. Работа продолжена в режиме Sidecar; для TUN нужны права администратора. Причина: {{reason}}",
|
||||||
"sidecarFallback": "Служба не смогла запустить ядро. Работа продолжена в режиме Sidecar; служба по-прежнему недоступна.",
|
"sidecarFallback": "Служба не смогла запустить ядро. Работа продолжена в режиме Sidecar; служба по-прежнему недоступна.",
|
||||||
"uninstallSuccess": "Служба успешно удалена"
|
"uninstallSuccess": "Служба успешно удалена"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Hizmet Başarıyla Kuruldu",
|
"installSuccess": "Hizmet Başarıyla Kuruldu",
|
||||||
|
"permissionFallback": "Hizmetin çekirdek dosyaları veya dizinleri güvenlik denetimini geçemedi. Sidecar moduna geçildi; TUN için yönetici yetkileri gerekir. Neden: {{reason}}",
|
||||||
"sidecarFallback": "Hizmet çekirdeği başlatamadı. Sidecar moduna geçildi; hizmet hâlâ kullanılamıyor.",
|
"sidecarFallback": "Hizmet çekirdeği başlatamadı. Sidecar moduna geçildi; hizmet hâlâ kullanılamıyor.",
|
||||||
"uninstallSuccess": "Hizmet Başarıyla Kaldırıldı"
|
"uninstallSuccess": "Hizmet Başarıyla Kaldırıldı"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "Сервис уңышлы урнаштырылды",
|
"installSuccess": "Сервис уңышлы урнаштырылды",
|
||||||
|
"permissionFallback": "Хезмәт төшенең файллары яки каталоглары иминлек тикшерүен үтмәде. Sidecar режимына күчелде; TUN өчен администратор хокуклары кирәк. Сәбәп: {{reason}}",
|
||||||
"sidecarFallback": "Хезмәт төшне эшләтеп җибәрә алмады. Эш Sidecar режимында дәвам итә; хезмәт һаман да эшләми.",
|
"sidecarFallback": "Хезмәт төшне эшләтеп җибәрә алмады. Эш Sidecar режимында дәвам итә; хезмәт һаман да эшләми.",
|
||||||
"uninstallSuccess": "Сервис уңышлы салдырылды"
|
"uninstallSuccess": "Сервис уңышлы салдырылды"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "已成功安装服务",
|
"installSuccess": "已成功安装服务",
|
||||||
|
"permissionFallback": "服务内核的文件或目录未通过安全检查,已切换到普通模式(Sidecar);普通权限下 TUN 模式不可用。原因:{{reason}}",
|
||||||
"sidecarFallback": "服务无法启动内核,已降级为 Sidecar 模式运行,服务仍不可用。",
|
"sidecarFallback": "服务无法启动内核,已降级为 Sidecar 模式运行,服务仍不可用。",
|
||||||
"uninstallSuccess": "已成功卸载服务"
|
"uninstallSuccess": "已成功卸载服务"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -684,6 +684,7 @@
|
|||||||
},
|
},
|
||||||
"clashService": {
|
"clashService": {
|
||||||
"installSuccess": "已成功安裝服務",
|
"installSuccess": "已成功安裝服務",
|
||||||
|
"permissionFallback": "服務核心的檔案或目錄未通過安全檢查,已切換至一般模式(Sidecar);一般權限下無法使用 TUN 模式。原因:{{reason}}",
|
||||||
"sidecarFallback": "服務無法啟動核心,已降級為 Sidecar 模式執行,服務仍無法使用。",
|
"sidecarFallback": "服務無法啟動核心,已降級為 Sidecar 模式執行,服務仍無法使用。",
|
||||||
"uninstallSuccess": "已成功解除安裝服務"
|
"uninstallSuccess": "已成功解除安裝服務"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -454,8 +454,12 @@ export const getAppUptime = async () => {
|
|||||||
return invoke<number>('get_app_uptime')
|
return invoke<number>('get_app_uptime')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ServiceInstallOutcome =
|
||||||
|
| { status: 'installed' }
|
||||||
|
| { status: 'sidecar'; reason: string }
|
||||||
|
|
||||||
export const installService = async () => {
|
export const installService = async () => {
|
||||||
return invoke<void>('install_service')
|
return invoke<ServiceInstallOutcome>('install_service')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const uninstallService = async () => {
|
export const uninstallService = async () => {
|
||||||
@@ -463,11 +467,11 @@ export const uninstallService = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const reinstallService = async () => {
|
export const reinstallService = async () => {
|
||||||
return invoke<void>('reinstall_service')
|
return invoke<ServiceInstallOutcome>('reinstall_service')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const repairService = async () => {
|
export const repairService = async () => {
|
||||||
return invoke<void>('repair_service')
|
return invoke<ServiceInstallOutcome>('repair_service')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const continueWithSidecar = async () => {
|
export const continueWithSidecar = async () => {
|
||||||
|
|||||||
@@ -804,6 +804,7 @@ export const translationKeys = [
|
|||||||
'settings.feedback.notifications.clash.changeFailed',
|
'settings.feedback.notifications.clash.changeFailed',
|
||||||
'settings.feedback.notifications.clash.geoDataUpdated',
|
'settings.feedback.notifications.clash.geoDataUpdated',
|
||||||
'settings.feedback.notifications.clashService.installSuccess',
|
'settings.feedback.notifications.clashService.installSuccess',
|
||||||
|
'settings.feedback.notifications.clashService.permissionFallback',
|
||||||
'settings.feedback.notifications.clashService.sidecarFallback',
|
'settings.feedback.notifications.clashService.sidecarFallback',
|
||||||
'settings.feedback.notifications.clashService.uninstallSuccess',
|
'settings.feedback.notifications.clashService.uninstallSuccess',
|
||||||
'settings.feedback.notifications.updater.withClashProxySuccess',
|
'settings.feedback.notifications.updater.withClashProxySuccess',
|
||||||
|
|||||||
@@ -845,6 +845,7 @@ export interface TranslationResources {
|
|||||||
}
|
}
|
||||||
clashService: {
|
clashService: {
|
||||||
installSuccess: string
|
installSuccess: string
|
||||||
|
permissionFallback: string
|
||||||
sidecarFallback: string
|
sidecarFallback: string
|
||||||
uninstallSuccess: string
|
uninstallSuccess: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user