diff --git a/.gitignore b/.gitignore index 71b24cb..0c1f955 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules +.pnpm-store .DS_Store dist dist-ssr diff --git a/scripts/check.mjs b/scripts/check.mjs index 5c5a294..e1689f5 100644 --- a/scripts/check.mjs +++ b/scripts/check.mjs @@ -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 { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 8010ddc..350a237 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,6 +45,7 @@ deelevate = "0.2.0" winreg = "0.52.0" [target.'cfg(target_os = "linux")'.dependencies] +users = "0.11.0" #openssl [features] diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 6ae2287..b61aa9f 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -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 { - wrap_err!(win_service::check_service().await) + pub async fn check_service() -> CmdResult { + 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::*; diff --git a/src-tauri/src/core/core.rs b/src-tauri/src/core/core.rs index cf0cd84..b58d0af 100644 --- a/src-tauri/src/core/core.rs +++ b/src-tauri/src/core/core.rs @@ -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(()); } diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 80378f5..55067ec 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -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::*; diff --git a/src-tauri/src/core/win_service.rs b/src-tauri/src/core/service.rs similarity index 70% rename from src-tauri/src/core/win_service.rs rename to src-tauri/src/core/service.rs index 865895b..3027a95 100644 --- a/src-tauri/src/core/win_service.rs +++ b/src-tauri/src/core/service.rs @@ -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 { 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)?; diff --git a/src-tauri/src/feat.rs b/src-tauri/src/feat.rs index 5fa9a50..4e12f28 100644 --- a/src-tauri/src/feat.rs +++ b/src-tauri/src/feat.rs @@ -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; diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index 8930e15..8a38bd1 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -92,12 +92,17 @@ pub fn clash_pid_path() -> Result { Ok(app_home_dir()?.join("clash.pid")) } +#[cfg(target_os = "linux")] +pub fn service_path() -> Result { + Ok(app_resources_dir()?.join("clash-verge-service")) +} + #[cfg(windows)] pub fn service_path() -> Result { Ok(app_resources_dir()?.join("clash-verge-service.exe")) } -#[cfg(windows)] +#[cfg(any(windows, target_os = "linux"))] pub fn service_log_file() -> Result { use chrono::Local; diff --git a/src-tauri/src/utils/mod.rs b/src-tauri/src/utils/mod.rs index 28eacc3..d0037b3 100644 --- a/src-tauri/src/utils/mod.rs +++ b/src-tauri/src/utils/mod.rs @@ -4,3 +4,4 @@ pub mod init; pub mod resolve; pub mod server; pub mod tmpl; +pub mod unix_helper; diff --git a/src-tauri/src/utils/unix_helper.rs b/src-tauri/src/utils/unix_helper.rs new file mode 100644 index 0000000..d16fc67 --- /dev/null +++ b/src-tauri/src/utils/unix_helper.rs @@ -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", + } +} diff --git a/src/components/setting/setting-system.tsx b/src/components/setting/setting-system.tsx index 5782ad1..fda1898 100644 --- a/src/components/setting/setting-system.tsx +++ b/src/components/setting/setting-system.tsx @@ -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) => { - {isWIN && ( + {isServiceModeAvailable && ( )} @@ -66,7 +67,9 @@ const SettingSystem = ({ onError }: Props) => { <> @@ -102,7 +105,7 @@ const SettingSystem = ({ onError }: Props) => { - {isWIN && ( + {isServiceModeAvailable && (