Explorar o código

feat: Service Mode for Linux (#804)

HZ is not Chatty hai 1 ano
pai
achega
3b06cf8a2a

+ 1 - 0
.gitignore

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

+ 58 - 0
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 {

+ 1 - 0
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]

+ 7 - 7
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<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::*;
 

+ 9 - 9
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(());
         }

+ 1 - 1
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::*;

+ 72 - 5
src-tauri/src/core/win_service.rs → 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<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)?;
 

+ 1 - 1
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;
 

+ 6 - 1
src-tauri/src/utils/dirs.rs

@@ -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;
 

+ 1 - 0
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;

+ 15 - 0
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",
+    }
+}

+ 8 - 5
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) => {
     <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={