diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 33de5e67a1b54947daa884494b272765a0cca39e..3c721a5b56b4f9b5e519ab377fa1f7d6bd3bb5c7 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -175,6 +175,15 @@ pub async fn restart_sidecar() -> CmdResult { wrap_err!(CoreManager::global().run_core().await) } +#[tauri::command] +pub fn grant_permission(core: String) -> CmdResult { + #[cfg(target_os = "macos")] + return wrap_err!(manager::grant_permission(core)); + + #[cfg(not(target_os = "macos"))] + return Err("Unsupported target"); +} + /// get the system proxy #[tauri::command] pub fn get_sys_proxy() -> CmdResult<Mapping> { diff --git a/src-tauri/src/core/manager.rs b/src-tauri/src/core/manager.rs new file mode 100644 index 0000000000000000000000000000000000000000..250dc5cfb2b522848d13f6a533d66836545a0c54 --- /dev/null +++ b/src-tauri/src/core/manager.rs @@ -0,0 +1,37 @@ +/// ç»™clashå†…æ ¸çš„tun模å¼æŽˆæƒ +#[cfg(any(target_os = "macos", target_os = "linux"))] +pub fn grant_permission(core: String) -> anyhow::Result<()> { + use std::process::Command; + use tauri::utils::platform::current_exe; + + let path = current_exe()?.with_file_name(core).canonicalize()?; + let path = path.display(); + + log::debug!("grant_permission path: {path}"); + + #[cfg(target_os = "macos")] + let output = { + let shell = format!("chown root:admin {path}\nchmod +sx {path}"); + let command = format!(r#"do shell script "{shell}" with administrator privileges"#); + Command::new("osascript") + .args(vec!["-e", &command]) + .output()? + }; + + #[cfg(target_os = "linux")] + let output = { + let shell = format!("setcap cap_net_bind_service,cap_net_admin=+ep {path}"); + Command::new("sudo") + .arg("sh") + .arg("-c") + .arg(shell) + .output()? + }; + + if output.status.success() { + Ok(()) + } else { + let stderr = std::str::from_utf8(&output.stderr).unwrap_or(""); + anyhow::bail!("{stderr}"); + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 18655abb8a345a514fb865bcfa06b7d6c6528bcd..4221721f1ee35ae8128c1f84d7881318ad94393f 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -3,6 +3,7 @@ mod core; pub mod handle; pub mod hotkey; pub mod logger; +pub mod manager; pub mod sysopt; pub mod timer; pub mod tray; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a3ff8665d55f2fca3642359303d4ca10b7f0d5f0..6d0f79eb3e4590a0bf8253f7e6bb7fb09969f081 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -36,6 +36,7 @@ fn main() -> std::io::Result<()> { cmds::open_core_dir, // cmds::kill_sidecar, cmds::restart_sidecar, + cmds::grant_permission, // clash cmds::get_clash_info, cmds::get_clash_logs, diff --git a/src/components/setting/mods/clash-core-viewer.tsx b/src/components/setting/mods/clash-core-viewer.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8d05898423e84b031dbf8740c8a4a4ff229e5f98 --- /dev/null +++ b/src/components/setting/mods/clash-core-viewer.tsx @@ -0,0 +1,106 @@ +import { mutate } from "swr"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { BaseDialog, DialogRef, Notice } from "@/components/base"; +import { useTranslation } from "react-i18next"; +import { useVerge } from "@/hooks/use-verge"; +import { useLockFn } from "ahooks"; +import { Lock } from "@mui/icons-material"; +import { IconButton, List, ListItemButton, ListItemText } from "@mui/material"; +import { changeClashCore } from "@/services/cmds"; +import { closeAllConnections } from "@/services/api"; +import { grantPermission } from "@/services/cmds"; +import getSystem from "@/utils/get-system"; + +const VALID_CORE = [ + { name: "Clash", core: "clash" }, + { name: "Clash Meta", core: "clash-meta" }, +]; + +const OS = getSystem(); + +export const ClashCoreViewer = forwardRef<DialogRef>((props, ref) => { + const { t } = useTranslation(); + + const { verge, mutateVerge } = useVerge(); + + const [open, setOpen] = useState(false); + + useImperativeHandle(ref, () => ({ + open: () => setOpen(true), + close: () => setOpen(false), + })); + + const { clash_core = "clash" } = verge ?? {}; + + const onCoreChange = useLockFn(async (core: string) => { + if (core === clash_core) return; + + try { + closeAllConnections(); + await changeClashCore(core); + mutateVerge(); + setTimeout(() => { + mutate("getClashConfig"); + mutate("getVersion"); + }, 100); + Notice.success(`Successfully switch to ${core}`, 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + const onGrant = useLockFn(async (core: string) => { + try { + await grantPermission(core); + Notice.success(`Successfully grant permission to ${core}`, 1000); + } catch (err: any) { + Notice.error(err?.message || err.toString()); + } + }); + + return ( + <BaseDialog + open={open} + title={t("Clash Core")} + contentSx={{ + pb: 0, + width: 320, + height: 200, + overflowY: "auto", + userSelect: "text", + marginTop: "-8px", + }} + disableOk + cancelBtn={t("Back")} + onClose={() => setOpen(false)} + onCancel={() => setOpen(false)} + > + <List component="nav"> + {VALID_CORE.map((each) => ( + <ListItemButton + key={each.core} + selected={each.core === clash_core} + onClick={() => onCoreChange(each.core)} + > + <ListItemText primary={each.name} secondary={`/${each.core}`} /> + + {(OS === "macos" || OS === "linux") && ( + <IconButton + color="inherit" + size="small" + edge="end" + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + onGrant(each.core); + }} + > + <Lock fontSize="inherit" /> + </IconButton> + )} + </ListItemButton> + ))} + </List> + </BaseDialog> + ); +}); diff --git a/src/components/setting/setting-clash.tsx b/src/components/setting/setting-clash.tsx index a3af39797bdf8b89a9ded69cfaf6ff25350ff9c8..19f77ca6120cdadd88467d9862ebfd8c17cc1ca9 100644 --- a/src/components/setting/setting-clash.tsx +++ b/src/components/setting/setting-clash.tsx @@ -8,16 +8,16 @@ import { Typography, IconButton, } from "@mui/material"; -import { ArrowForward } from "@mui/icons-material"; +import { ArrowForward, Settings } from "@mui/icons-material"; import { DialogRef } from "@/components/base"; import { useClash } from "@/hooks/use-clash"; import { GuardState } from "./mods/guard-state"; -import { CoreSwitch } from "./mods/core-switch"; import { WebUIViewer } from "./mods/web-ui-viewer"; import { ClashFieldViewer } from "./mods/clash-field-viewer"; import { ClashPortViewer } from "./mods/clash-port-viewer"; import { ControllerViewer } from "./mods/controller-viewer"; import { SettingList, SettingItem } from "./mods/setting-comp"; +import { ClashCoreViewer } from "./mods/clash-core-viewer"; interface Props { onError: (err: Error) => void; @@ -39,6 +39,7 @@ const SettingClash = ({ onError }: Props) => { const fieldRef = useRef<DialogRef>(null); const portRef = useRef<DialogRef>(null); const ctrlRef = useRef<DialogRef>(null); + const coreRef = useRef<DialogRef>(null); const onSwitchFormat = (_e: any, value: boolean) => value; const onChangeData = (patch: Partial<IConfigData>) => { @@ -51,6 +52,7 @@ const SettingClash = ({ onError }: Props) => { <ClashFieldViewer ref={fieldRef} /> <ClashPortViewer ref={portRef} /> <ControllerViewer ref={ctrlRef} /> + <ClashCoreViewer ref={coreRef} /> <SettingItem label={t("Allow Lan")}> <GuardState @@ -143,7 +145,21 @@ const SettingClash = ({ onError }: Props) => { </IconButton> </SettingItem> - <SettingItem label={t("Clash Core")} extra={<CoreSwitch />}> + <SettingItem + label={t("Clash Core")} + extra={ + <IconButton + color="inherit" + size="small" + onClick={() => coreRef.current?.open()} + > + <Settings + fontSize="inherit" + style={{ cursor: "pointer", opacity: 0.75 }} + /> + </IconButton> + } + > <Typography sx={{ py: "7px", pr: 1 }}>{version}</Typography> </SettingItem> </SettingList> diff --git a/src/services/cmds.ts b/src/services/cmds.ts index d6bbca530b1681c583b1a02b4700cd62feaa836b..2bd857e1643b38946da21e4c2c7310831d3fb792 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -127,6 +127,10 @@ export async function restartSidecar() { return invoke<void>("restart_sidecar"); } +export async function grantPermission(core: string) { + return invoke<void>("grant_permission", { core }); +} + export async function openAppDir() { return invoke<void>("open_app_dir").catch((err) => Notice.error(err?.message || err.toString(), 1500)