feat: Try support service mode for MacOS

This commit is contained in:
MystiPanda 2024-03-31 21:44:34 +08:00
parent 9fc819a410
commit e95808e6be
11 changed files with 158 additions and 177 deletions

2
.gitignore vendored
View File

@ -7,4 +7,4 @@ dist-ssr
update.json update.json
scripts/_env.sh scripts/_env.sh
.vscode .vscode
.tool-version .tool-versions

View File

@ -353,8 +353,8 @@ const resolvePlugin = async () => {
} }
}; };
// Linux service chmod // service chmod
const resolveLinuxServicePermission = async () => { const resolveServicePermission = async () => {
const serviceExecutables = [ const serviceExecutables = [
"clash-verge-service", "clash-verge-service",
"install-service", "install-service",
@ -376,37 +376,30 @@ const resolveLinuxServicePermission = async () => {
const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`; const SERVICE_URL = `https://github.com/clash-verge-rev/clash-verge-service/releases/download/${SIDECAR_HOST}`;
const resolveService = () => const resolveService = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({ resolveResource({
file: "clash-verge-service.exe", file: "clash-verge-service" + ext,
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`, downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
});
const resolveLinuxService = () => {
resolveResource({
file: "clash-verge-service",
downloadURL: `${SERVICE_URL}/clash-verge-service`,
}); });
}; };
const resolveInstall = () =>
const resolveInstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({ resolveResource({
file: "install-service.exe", file: "install-service" + ext,
downloadURL: `${SERVICE_URL}/install-service.exe`, downloadURL: `${SERVICE_URL}/install-service.exe${ext}`,
}); });
const resolveLinuxInstall = () => };
const resolveUninstall = () => {
let ext = platform === "win32" ? ".exe" : "";
resolveResource({ resolveResource({
file: "install-service", file: "uninstall-service" + ext,
downloadURL: `${SERVICE_URL}/install-service`, downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
});
const resolveUninstall = () =>
resolveResource({
file: "uninstall-service.exe",
downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
});
const resolveLinuxUninstall = () =>
resolveResource({
file: "uninstall-service",
downloadURL: `${SERVICE_URL}/uninstall-service`,
}); });
};
const resolveSetDnsScript = () => const resolveSetDnsScript = () =>
resolveResource({ resolveResource({
file: "set_dns.sh", file: "set_dns.sh",
@ -453,27 +446,9 @@ const tasks = [
retry: 5, retry: 5,
}, },
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true }, { name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true }, { name: "service", func: resolveService, retry: 5 },
{ { name: "install", func: resolveInstall, retry: 5 },
name: "linux_service", { name: "uninstall", func: resolveUninstall, retry: 5 },
func: resolveLinuxService,
retry: 5,
linuxOnly: true,
},
{ name: "install", func: resolveInstall, retry: 5, winOnly: true },
{
name: "linux_install",
func: resolveLinuxInstall,
retry: 5,
linuxOnly: true,
},
{ name: "uninstall", func: resolveUninstall, retry: 5, winOnly: true },
{
name: "linux_uninstall",
func: resolveLinuxUninstall,
retry: 5,
linuxOnly: true,
},
{ name: "set_dns_script", func: resolveSetDnsScript, retry: 5 }, { name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
{ name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 }, { name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 }, { name: "mmdb", func: resolveMmdb, retry: 5 },
@ -486,18 +461,19 @@ const tasks = [
winOnly: true, winOnly: true,
}, },
{ {
name: "linux_service_chmod", name: "service_chmod",
func: resolveLinuxServicePermission, func: resolveServicePermission,
retry: 1, retry: 1,
linuxOnly: true, unixOnly: true,
}, },
]; ];
async function runTask() { async function runTask() {
const task = tasks.shift(); const task = tasks.shift();
if (!task) return; if (!task) return;
if (task.winOnly && process.platform !== "win32") return runTask(); if (task.winOnly && platform !== "win32") return runTask();
if (task.linuxOnly && process.platform !== "linux") return runTask(); if (task.linuxOnly && platform !== "linux") return runTask();
if (task.unixOnly && platform === "win32") return runTask();
for (let i = 0; i < task.retry; i++) { for (let i = 0; i < task.retry; i++) {
try { try {

View File

@ -331,7 +331,6 @@ pub fn exit_app(app_handle: tauri::AppHandle) {
std::process::exit(0); std::process::exit(0);
} }
#[cfg(any(windows, target_os = "linux"))]
pub mod service { pub mod service {
use super::*; use super::*;
use crate::core::service; use crate::core::service;
@ -352,24 +351,6 @@ pub mod service {
} }
} }
#[cfg(not(any(windows, target_os = "linux")))]
pub mod service {
use super::*;
#[tauri::command]
pub async fn check_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
Ok(())
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
Ok(())
}
}
#[cfg(not(windows))] #[cfg(not(windows))]
pub mod uwp { pub mod uwp {
use super::*; use super::*;

View File

@ -1,3 +1,4 @@
use super::service;
use super::{clash_api, logger::Logger}; use super::{clash_api, logger::Logger};
use crate::log_err; use crate::log_err;
use crate::{config::*, utils::dirs}; use crate::{config::*, utils::dirs};
@ -93,10 +94,9 @@ impl CoreManager {
None => false, None => false,
}; };
#[cfg(any(target_os = "windows", target_os = "linux"))]
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service"); log::debug!(target: "app", "stop the core by service");
log_err!(super::service::stop_core_by_service().await); log_err!(service::stop_core_by_service().await);
should_kill = true; should_kill = true;
} }
@ -105,32 +105,27 @@ impl CoreManager {
sleep(Duration::from_millis(500)).await; sleep(Duration::from_millis(500)).await;
} }
#[cfg(any(target_os = "windows", target_os = "linux"))] // 服务模式
{ let enable = { Config::verge().latest().enable_service_mode };
use super::service; let enable = enable.unwrap_or(false);
// 服务模式 *self.use_service_mode.lock() = enable;
let enable = { Config::verge().latest().enable_service_mode };
let enable = enable.unwrap_or(false);
*self.use_service_mode.lock() = enable; if enable {
// 服务模式启动失败就直接运行sidecar
log::debug!(target: "app", "try to run core in service mode");
if enable { match (|| async {
// 服务模式启动失败就直接运行sidecar service::check_service().await?;
log::debug!(target: "app", "try to run core in service mode"); service::run_core_by_service(&config_path).await
})()
match (|| async { .await
service::check_service().await?; {
service::run_core_by_service(&config_path).await Ok(_) => return Ok(()),
})() Err(err) => {
.await // 修改这个值免得stop出错
{ *self.use_service_mode.lock() = false;
Ok(_) => return Ok(()), log::error!(target: "app", "{err}");
Err(err) => {
// 修改这个值免得stop出错
*self.use_service_mode.lock() = false;
log::error!(target: "app", "{err}");
}
} }
} }
} }
@ -205,7 +200,6 @@ impl CoreManager {
/// 重启内核 /// 重启内核
pub fn recover_core(&'static self) -> Result<()> { pub fn recover_core(&'static self) -> Result<()> {
// 服务模式不管 // 服务模式不管
#[cfg(any(target_os = "windows", target_os = "linux"))]
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
return Ok(()); return Ok(());
} }
@ -238,11 +232,10 @@ impl CoreManager {
/// 停止核心运行 /// 停止核心运行
pub fn stop_core(&self) -> Result<()> { pub fn stop_core(&self) -> Result<()> {
#[cfg(any(target_os = "windows", target_os = "linux"))]
if *self.use_service_mode.lock() { if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service"); log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
log_err!(super::service::stop_core_by_service().await); log_err!(service::stop_core_by_service().await);
}); });
return Ok(()); return Ok(());
} }

View File

@ -1,5 +1,3 @@
#![cfg(any(target_os = "windows", target_os = "linux"))]
use crate::config::Config; use crate::config::Config;
use crate::utils::dirs; use crate::utils::dirs;
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
@ -31,14 +29,13 @@ pub struct JsonResponse {
/// Install the Clash Verge Service /// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
/// ///
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> { pub async fn install_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe"); let install_path = binary_path.with_file_name("install-service.exe");
@ -80,7 +77,11 @@ pub async fn install_service() -> Result<()> {
let elevator = crate::utils::unix_helper::linux_elevator(); let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() { let status = match get_effective_uid() {
0 => StdCommand::new(installer_path).status()?, 0 => StdCommand::new(installer_path).status()?,
_ => StdCommand::new(elevator).arg("sh").arg("-c").arg(installer_path).status()?, _ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(installer_path)
.status()?,
}; };
if !status.success() { if !status.success() {
@ -93,6 +94,30 @@ pub async fn install_service() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() {
bail!("installer not found");
}
let shell = installer_path.to_string_lossy();
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// Uninstall the Clash Verge Service /// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@ -101,7 +126,6 @@ pub async fn uninstall_service() -> Result<()> {
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe"); let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
@ -143,7 +167,11 @@ pub async fn uninstall_service() -> Result<()> {
let elevator = crate::utils::unix_helper::linux_elevator(); let elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() { let status = match get_effective_uid() {
0 => StdCommand::new(uninstaller_path).status()?, 0 => StdCommand::new(uninstaller_path).status()?,
_ => StdCommand::new(elevator).arg("sh").arg("-c").arg(uninstaller_path).status()?, _ => StdCommand::new(elevator)
.arg("sh")
.arg("-c")
.arg(uninstaller_path)
.status()?,
}; };
if !status.success() { if !status.success() {
@ -156,6 +184,32 @@ pub async fn uninstall_service() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let shell = uninstaller_path.to_string_lossy();
let command = format!(r#"do shell script "{shell}" with administrator privileges"#);
let status = StdCommand::new("osascript")
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// check the windows service status /// check the windows service status
pub async fn check_service() -> Result<JsonResponse> { pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash"); let url = format!("{SERVICE_URL}/get_clash");
@ -185,7 +239,7 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let clash_core = { Config::verge().latest().clash_core.clone() }; let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into()); let clash_core = clash_core.unwrap_or("clash".into());
let bin_ext = if cfg!(windows) {".exe"} else {""}; let bin_ext = if cfg!(windows) { ".exe" } else { "" };
let clash_bin = format!("{clash_core}{bin_ext}"); let clash_bin = format!("{clash_core}{bin_ext}");
let bin_path = current_exe()?.with_file_name(clash_bin); let bin_path = current_exe()?.with_file_name(clash_bin);
let bin_path = dirs::path_to_str(&bin_path)?; let bin_path = dirs::path_to_str(&bin_path)?;

View File

@ -92,17 +92,16 @@ pub fn clash_pid_path() -> Result<PathBuf> {
Ok(app_home_dir()?.join("clash.pid")) Ok(app_home_dir()?.join("clash.pid"))
} }
#[cfg(target_os = "linux")] #[cfg(not(target_os = "windows"))]
pub fn service_path() -> Result<PathBuf> { pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service")) Ok(app_resources_dir()?.join("clash-verge-service"))
} }
#[cfg(windows)] #[cfg(windows)]
pub fn service_path() -> Result<PathBuf> { pub fn service_path() -> Result<PathBuf> {
Ok(app_resources_dir()?.join("clash-verge-service.exe")) Ok(app_resources_dir()?.join("clash-verge-service.exe"))
} }
#[cfg(any(windows, target_os = "linux"))]
pub fn service_log_file() -> Result<PathBuf> { pub fn service_log_file() -> Result<PathBuf> {
use chrono::Local; use chrono::Local;

View File

@ -135,13 +135,12 @@ pub fn delete_log() -> Result<()> {
for file in fs::read_dir(&log_dir)?.flatten() { for file in fs::read_dir(&log_dir)?.flatten() {
let _ = process_file(file); let _ = process_file(file);
} }
#[cfg(target_os = "windows")]
{ let service_log_dir = log_dir.join("service");
let service_log_dir = log_dir.join("service"); for file in fs::read_dir(&service_log_dir)?.flatten() {
for file in fs::read_dir(&service_log_dir)?.flatten() { let _ = process_file(file);
let _ = process_file(file);
}
} }
Ok(()) Ok(())
} }

View File

@ -1,7 +1,6 @@
use std::process::Command;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub fn linux_elevator() -> &'static str { pub fn linux_elevator() -> &'static str {
use std::process::Command;
match Command::new("which").arg("pkexec").output() { match Command::new("which").arg("pkexec").output() {
Ok(output) => { Ok(output) => {
if output.stdout.is_empty() { if output.stdout.is_empty() {

View File

@ -17,24 +17,17 @@ interface Props {
onError?: (err: Error) => void; onError?: (err: Error) => void;
} }
const isServiceModeAvailable =
getSystem() === "windows" || getSystem() === "linux";
const SettingSystem = ({ onError }: Props) => { const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { verge, mutateVerge, patchVerge } = useVerge(); const { verge, mutateVerge, patchVerge } = useVerge();
// service mode // service mode
const { data: serviceStatus } = useSWR( const { data: serviceStatus } = useSWR("checkService", checkService, {
isServiceModeAvailable ? "checkService" : null, revalidateIfStale: false,
checkService, shouldRetryOnError: false,
{ focusThrottleInterval: 36e5, // 1 hour
revalidateIfStale: false, });
shouldRetryOnError: false,
focusThrottleInterval: 36e5, // 1 hour
}
);
const serviceRef = useRef<DialogRef>(null); const serviceRef = useRef<DialogRef>(null);
const sysproxyRef = useRef<DialogRef>(null); const sysproxyRef = useRef<DialogRef>(null);
@ -57,22 +50,13 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}> <SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} /> <SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} /> <TunViewer ref={tunRef} />
{isServiceModeAvailable && ( <ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
)}
<SettingItem <SettingItem
label={t("Tun Mode")} label={t("Tun Mode")}
extra={ extra={
<> <>
<Tooltip <Tooltip title={t("Tun Mode Info")} placement="top">
title={
isServiceModeAvailable
? t("Tun Mode Info Windows")
: t("Tun Mode Info Unix")
}
placement="top"
>
<IconButton color="inherit" size="small"> <IconButton color="inherit" size="small">
<InfoRounded <InfoRounded
fontSize="inherit" fontSize="inherit"
@ -105,39 +89,37 @@ const SettingSystem = ({ onError }: Props) => {
</GuardState> </GuardState>
</SettingItem> </SettingItem>
{isServiceModeAvailable && ( <SettingItem
<SettingItem label={t("Service Mode")}
label={t("Service Mode")} extra={
extra={ <IconButton
<IconButton color="inherit"
color="inherit" size="small"
size="small" onClick={() => serviceRef.current?.open()}
onClick={() => serviceRef.current?.open()}
>
<PrivacyTipRounded
fontSize="inherit"
style={{ cursor: "pointer", opacity: 0.75 }}
/>
</IconButton>
}
>
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
> >
<Switch <PrivacyTipRounded
edge="end" fontSize="inherit"
disabled={ style={{ cursor: "pointer", opacity: 0.75 }}
serviceStatus !== "active" && serviceStatus !== "installed"
}
/> />
</GuardState> </IconButton>
</SettingItem> }
)} >
<GuardState
value={enable_service_mode ?? false}
valueProps="checked"
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => onChangeData({ enable_service_mode: e })}
onGuard={(e) => patchVerge({ enable_service_mode: e })}
>
<Switch
edge="end"
disabled={
serviceStatus !== "active" && serviceStatus !== "installed"
}
/>
</GuardState>
</SettingItem>
<SettingItem <SettingItem
label={t("System Proxy")} label={t("System Proxy")}

View File

@ -169,7 +169,6 @@
"MTU": "Max Transmission Unit", "MTU": "Max Transmission Unit",
"Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it", "Portable Updater Error": "The portable version does not support in-app updates. Please manually download and replace it",
"Tun Mode Info Windows": "The Tun mode requires granting core-related permissions. Please enable service mode before using it", "Tun Mode Info": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
"Tun Mode Info Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings",
"System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode" "System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode"
} }

View File

@ -177,7 +177,6 @@
"Reset to Default": "重置为默认值", "Reset to Default": "重置为默认值",
"Portable Updater Error": "便携版不支持应用内更新,请手动下载替换", "Portable Updater Error": "便携版不支持应用内更新,请手动下载替换",
"Tun Mode Info Windows": "Tun模式需要授予内核相关权限使用前请先开启服务模式", "Tun Mode Info": "Tun模式需要授予内核相关权限使用前请先开启服务模式",
"Tun Mode Info Unix": "Tun模式需要授予内核相关权限使用前请先在内核设置中给内核授权",
"System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用" "System and Mixed Can Only be Used in Service Mode": "System和Mixed只能在服务模式下使用"
} }