feat: Service Mode for Linux (#804)

This commit is contained in:
HZ is not Chatty 2024-03-31 16:16:23 +08:00 committed by GitHub
parent c5d7c29f3d
commit 3b06cf8a2a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 180 additions and 29 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
node_modules
.pnpm-store
.DS_Store
dist
dist-ssr

View File

@ -353,6 +353,23 @@ const resolvePlugin = async () => {
}
};
// Linux service chmod
const resolveLinuxServicePermission = async () => {
const serviceExecutables = [
"clash-verge-service",
"install-service",
"uninstall-service",
];
const resDir = path.join(cwd, "src-tauri/resources");
for (f of serviceExecutables) {
const targetPath = path.join(resDir, f);
if (await fs.pathExists(targetPath)) {
execSync(`chmod 755 ${targetPath}`);
console.log(`[INFO]: "${targetPath}" chmod finished`);
}
}
};
/**
* main
*/
@ -364,16 +381,32 @@ const resolveService = () =>
file: "clash-verge-service.exe",
downloadURL: `${SERVICE_URL}/clash-verge-service.exe`,
});
const resolveLinuxService = () => {
resolveResource({
file: "clash-verge-service",
downloadURL: `${SERVICE_URL}/clash-verge-service`,
});
};
const resolveInstall = () =>
resolveResource({
file: "install-service.exe",
downloadURL: `${SERVICE_URL}/install-service.exe`,
});
const resolveLinuxInstall = () =>
resolveResource({
file: "install-service",
downloadURL: `${SERVICE_URL}/install-service`,
});
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 = () =>
resolveResource({
file: "set_dns.sh",
@ -421,8 +454,26 @@ const tasks = [
},
{ name: "plugin", func: resolvePlugin, retry: 5, winOnly: true },
{ name: "service", func: resolveService, retry: 5, winOnly: true },
{
name: "linux_service",
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: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
{ name: "mmdb", func: resolveMmdb, retry: 5 },
@ -434,12 +485,19 @@ const tasks = [
retry: 5,
winOnly: true,
},
{
name: "linux_service_chmod",
func: resolveLinuxServicePermission,
retry: 1,
linuxOnly: true,
},
];
async function runTask() {
const task = tasks.shift();
if (!task) return;
if (task.winOnly && process.platform !== "win32") return runTask();
if (task.linuxOnly && process.platform !== "linux") return runTask();
for (let i = 0; i < task.retry; i++) {
try {

View File

@ -45,6 +45,7 @@ deelevate = "0.2.0"
winreg = "0.52.0"
[target.'cfg(target_os = "linux")'.dependencies]
users = "0.11.0"
#openssl
[features]

View File

@ -331,28 +331,28 @@ pub fn exit_app(app_handle: tauri::AppHandle) {
std::process::exit(0);
}
#[cfg(windows)]
#[cfg(any(windows, target_os = "linux"))]
pub mod service {
use super::*;
use crate::core::win_service;
use crate::core::service;
#[tauri::command]
pub async fn check_service() -> CmdResult<win_service::JsonResponse> {
wrap_err!(win_service::check_service().await)
pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(service::check_service().await)
}
#[tauri::command]
pub async fn install_service() -> CmdResult {
wrap_err!(win_service::install_service().await)
wrap_err!(service::install_service().await)
}
#[tauri::command]
pub async fn uninstall_service() -> CmdResult {
wrap_err!(win_service::uninstall_service().await)
wrap_err!(service::uninstall_service().await)
}
}
#[cfg(not(windows))]
#[cfg(not(any(windows, target_os = "linux")))]
pub mod service {
use super::*;

View File

@ -93,10 +93,10 @@ impl CoreManager {
None => false,
};
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "linux"))]
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
log_err!(super::win_service::stop_core_by_service().await);
log_err!(super::service::stop_core_by_service().await);
should_kill = true;
}
@ -105,9 +105,9 @@ impl CoreManager {
sleep(Duration::from_millis(500)).await;
}
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
use super::win_service;
use super::service;
// 服务模式
let enable = { Config::verge().latest().enable_service_mode };
@ -120,8 +120,8 @@ impl CoreManager {
log::debug!(target: "app", "try to run core in service mode");
match (|| async {
win_service::check_service().await?;
win_service::run_core_by_service(&config_path).await
service::check_service().await?;
service::run_core_by_service(&config_path).await
})()
.await
{
@ -205,7 +205,7 @@ impl CoreManager {
/// 重启内核
pub fn recover_core(&'static self) -> Result<()> {
// 服务模式不管
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "linux"))]
if *self.use_service_mode.lock() {
return Ok(());
}
@ -238,11 +238,11 @@ impl CoreManager {
/// 停止核心运行
pub fn stop_core(&self) -> Result<()> {
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "linux"))]
if *self.use_service_mode.lock() {
log::debug!(target: "app", "stop the core by service");
tauri::async_runtime::block_on(async move {
log_err!(super::win_service::stop_core_by_service().await);
log_err!(super::service::stop_core_by_service().await);
});
return Ok(());
}

View File

@ -7,7 +7,7 @@ pub mod manager;
pub mod sysopt;
pub mod timer;
pub mod tray;
pub mod win_service;
pub mod service;
pub mod win_uwp;
pub use self::core::*;

View File

@ -1,18 +1,17 @@
#![cfg(target_os = "windows")]
#![cfg(any(target_os = "windows", target_os = "linux"))]
use crate::config::Config;
use crate::utils::dirs;
use anyhow::{bail, Context, Result};
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::os::windows::process::CommandExt;
use std::path::PathBuf;
use std::time::Duration;
use std::{env::current_exe, process::Command as StdCommand};
use tokio::time::sleep;
// Windows only
const SERVICE_URL: &str = "http://127.0.0.1:33211";
#[derive(Debug, Deserialize, Serialize, Clone)]
@ -32,7 +31,14 @@ pub struct JsonResponse {
/// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
///
#[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let install_path = binary_path.with_file_name("install-service.exe");
@ -60,9 +66,42 @@ pub async fn install_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> {
use users::get_effective_uid;
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 elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(installer_path).status()?,
_ => StdCommand::new(elevator).arg("sh").arg("-c").arg(installer_path).status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> {
use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt;
let binary_path = dirs::service_path()?;
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
@ -90,6 +129,33 @@ pub async fn uninstall_service() -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> {
use users::get_effective_uid;
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 elevator = crate::utils::unix_helper::linux_elevator();
let status = match get_effective_uid() {
0 => StdCommand::new(uninstaller_path).status()?,
_ => StdCommand::new(elevator).arg("sh").arg("-c").arg(uninstaller_path).status()?,
};
if !status.success() {
bail!(
"failed to install service with status {}",
status.code().unwrap()
);
}
Ok(())
}
/// check the windows service status
pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");
@ -119,7 +185,8 @@ pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("clash".into());
let clash_bin = format!("{clash_core}.exe");
let bin_ext = if cfg!(windows) {".exe"} else {""};
let clash_bin = format!("{clash_core}{bin_ext}");
let bin_path = current_exe()?.with_file_name(clash_bin);
let bin_path = dirs::path_to_str(&bin_path)?;

View File

@ -185,7 +185,7 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let tun_tray_icon = patch.tun_tray_icon;
match {
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "linux"))]
{
let service_mode = patch.enable_service_mode;

View File

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

View File

@ -4,3 +4,4 @@ pub mod init;
pub mod resolve;
pub mod server;
pub mod tmpl;
pub mod unix_helper;

View File

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

View File

@ -17,7 +17,8 @@ interface Props {
onError?: (err: Error) => void;
}
const isWIN = getSystem() === "windows";
const isServiceModeAvailable =
getSystem() === "windows" || getSystem() === "linux";
const SettingSystem = ({ onError }: Props) => {
const { t } = useTranslation();
@ -26,7 +27,7 @@ const SettingSystem = ({ onError }: Props) => {
// service mode
const { data: serviceStatus } = useSWR(
isWIN ? "checkService" : null,
isServiceModeAvailable ? "checkService" : null,
checkService,
{
revalidateIfStale: false,
@ -56,7 +57,7 @@ const SettingSystem = ({ onError }: Props) => {
<SettingList title={t("System Setting")}>
<SysproxyViewer ref={sysproxyRef} />
<TunViewer ref={tunRef} />
{isWIN && (
{isServiceModeAvailable && (
<ServiceViewer ref={serviceRef} enable={!!enable_service_mode} />
)}
@ -66,7 +67,9 @@ const SettingSystem = ({ onError }: Props) => {
<>
<Tooltip
title={
isWIN ? t("Tun Mode Info Windows") : t("Tun Mode Info Unix")
isServiceModeAvailable
? t("Tun Mode Info Windows")
: t("Tun Mode Info Unix")
}
placement="top"
>
@ -102,7 +105,7 @@ const SettingSystem = ({ onError }: Props) => {
</GuardState>
</SettingItem>
{isWIN && (
{isServiceModeAvailable && (
<SettingItem
label={t("Service Mode")}
extra={