Browse Source

feat: Try support service mode for MacOS

MystiPanda 1 year ago
parent
commit
1c583d2ea9

+ 1 - 1
.gitignore

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

+ 28 - 52
scripts/check.mjs

@@ -353,8 +353,8 @@ const resolvePlugin = async () => {
   }
 };
 
-// Linux service chmod
-const resolveLinuxServicePermission = async () => {
+// service chmod
+const resolveServicePermission = async () => {
   const serviceExecutables = [
     "clash-verge-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 resolveService = () =>
+const resolveService = () => {
+  let ext = platform === "win32" ? ".exe" : "";
   resolveResource({
-    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`,
+    file: "clash-verge-service" + ext,
+    downloadURL: `${SERVICE_URL}/clash-verge-service${ext}`,
   });
 };
-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 = () =>
+
+const resolveInstall = () => {
+  let ext = platform === "win32" ? ".exe" : "";
   resolveResource({
-    file: "uninstall-service.exe",
-    downloadURL: `${SERVICE_URL}/uninstall-service.exe`,
+    file: "install-service" + ext,
+    downloadURL: `${SERVICE_URL}/install-service.exe${ext}`,
   });
-const resolveLinuxUninstall = () =>
+};
+
+const resolveUninstall = () => {
+  let ext = platform === "win32" ? ".exe" : "";
   resolveResource({
-    file: "uninstall-service",
-    downloadURL: `${SERVICE_URL}/uninstall-service`,
+    file: "uninstall-service" + ext,
+    downloadURL: `${SERVICE_URL}/uninstall-service${ext}`,
   });
+};
+
 const resolveSetDnsScript = () =>
   resolveResource({
     file: "set_dns.sh",
@@ -453,27 +446,9 @@ const tasks = [
     retry: 5,
   },
   { 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: "service", func: resolveService, retry: 5 },
+  { name: "install", func: resolveInstall, retry: 5 },
+  { name: "uninstall", func: resolveUninstall, retry: 5 },
   { name: "set_dns_script", func: resolveSetDnsScript, retry: 5 },
   { name: "unset_dns_script", func: resolveUnSetDnsScript, retry: 5 },
   { name: "mmdb", func: resolveMmdb, retry: 5 },
@@ -486,18 +461,19 @@ const tasks = [
     winOnly: true,
   },
   {
-    name: "linux_service_chmod",
-    func: resolveLinuxServicePermission,
+    name: "service_chmod",
+    func: resolveServicePermission,
     retry: 1,
-    linuxOnly: true,
+    unixOnly: 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();
+  if (task.winOnly && platform !== "win32") return runTask();
+  if (task.linuxOnly && platform !== "linux") return runTask();
+  if (task.unixOnly && platform === "win32") return runTask();
 
   for (let i = 0; i < task.retry; i++) {
     try {

+ 0 - 19
src-tauri/src/cmds.rs

@@ -331,7 +331,6 @@ pub fn exit_app(app_handle: tauri::AppHandle) {
     std::process::exit(0);
 }
 
-#[cfg(any(windows, target_os = "linux"))]
 pub mod service {
     use super::*;
     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))]
 pub mod uwp {
     use super::*;

+ 24 - 31
src-tauri/src/core/core.rs

@@ -1,3 +1,4 @@
+use super::service;
 use super::{clash_api, logger::Logger};
 use crate::log_err;
 use crate::{config::*, utils::dirs};
@@ -93,10 +94,9 @@ impl CoreManager {
             None => false,
         };
 
-        #[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::service::stop_core_by_service().await);
+            log_err!(service::stop_core_by_service().await);
             should_kill = true;
         }
 
@@ -105,32 +105,27 @@ impl CoreManager {
             sleep(Duration::from_millis(500)).await;
         }
 
-        #[cfg(any(target_os = "windows", target_os = "linux"))]
-        {
-            use super::service;
-
-            // 服务模式
-            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");
-
-                match (|| async {
-                    service::check_service().await?;
-                    service::run_core_by_service(&config_path).await
-                })()
-                .await
-                {
-                    Ok(_) => return Ok(()),
-                    Err(err) => {
-                        // 修改这个值,免得stop出错
-                        *self.use_service_mode.lock() = false;
-                        log::error!(target: "app", "{err}");
-                    }
+        // 服务模式
+        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");
+
+            match (|| async {
+                service::check_service().await?;
+                service::run_core_by_service(&config_path).await
+            })()
+            .await
+            {
+                Ok(_) => return Ok(()),
+                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<()> {
         // 服务模式不管
-        #[cfg(any(target_os = "windows", target_os = "linux"))]
         if *self.use_service_mode.lock() {
             return Ok(());
         }
@@ -238,11 +232,10 @@ impl CoreManager {
 
     /// 停止核心运行
     pub fn stop_core(&self) -> Result<()> {
-        #[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::service::stop_core_by_service().await);
+                log_err!(service::stop_core_by_service().await);
             });
             return Ok(());
         }

+ 62 - 8
src-tauri/src/core/service.rs

@@ -1,5 +1,3 @@
-#![cfg(any(target_os = "windows", target_os = "linux"))]
-
 use crate::config::Config;
 use crate::utils::dirs;
 use anyhow::{bail, Context, Result};
@@ -31,14 +29,13 @@ 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");
 
@@ -80,7 +77,11 @@ pub async fn install_service() -> Result<()> {
     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()?,
+        _ => StdCommand::new(elevator)
+            .arg("sh")
+            .arg("-c")
+            .arg(installer_path)
+            .status()?,
     };
 
     if !status.success() {
@@ -93,6 +94,30 @@ pub async fn install_service() -> Result<()> {
     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
 /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
 #[cfg(target_os = "windows")]
@@ -101,7 +126,6 @@ pub async fn uninstall_service() -> Result<()> {
     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");
 
@@ -143,7 +167,11 @@ pub async fn uninstall_service() -> Result<()> {
     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()?,
+        _ => StdCommand::new(elevator)
+            .arg("sh")
+            .arg("-c")
+            .arg(uninstaller_path)
+            .status()?,
     };
 
     if !status.success() {
@@ -156,6 +184,32 @@ pub async fn uninstall_service() -> Result<()> {
     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
 pub async fn check_service() -> Result<JsonResponse> {
     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 = 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 bin_path = current_exe()?.with_file_name(clash_bin);
     let bin_path = dirs::path_to_str(&bin_path)?;

+ 2 - 3
src-tauri/src/utils/dirs.rs

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

+ 5 - 6
src-tauri/src/utils/init.rs

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

+ 1 - 2
src-tauri/src/utils/unix_helper.rs

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

+ 36 - 54
src/components/setting/setting-system.tsx

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

+ 1 - 2
src/locales/en.json

@@ -169,7 +169,6 @@
   "MTU": "Max Transmission Unit",
 
   "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 Unix": "The Tun mode requires granting core-related permissions. Before using it, please authorize the core in the core settings",
+  "Tun Mode Info": "The Tun mode requires granting core-related permissions. Please enable service mode before using it",
   "System and Mixed Can Only be Used in Service Mode": "System and Mixed Can Only be Used in Service Mode"
 }

+ 1 - 2
src/locales/zh.json

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