fix: try to fix install service

This commit is contained in:
MystiPanda 2024-07-16 11:28:04 +08:00
parent d98b3224cf
commit af0cd4a342
No known key found for this signature in database
12 changed files with 219 additions and 103 deletions

View File

@ -389,13 +389,13 @@ pub mod service {
} }
#[tauri::command] #[tauri::command]
pub async fn install_service() -> CmdResult { pub async fn install_service(passwd: String) -> CmdResult {
wrap_err!(service::install_service().await) wrap_err!(service::install_service(passwd).await)
} }
#[tauri::command] #[tauri::command]
pub async fn uninstall_service() -> CmdResult { pub async fn uninstall_service(passwd: String) -> CmdResult {
wrap_err!(service::uninstall_service().await) wrap_err!(service::uninstall_service(passwd).await)
} }
} }

View File

@ -28,11 +28,19 @@ pub struct JsonResponse {
pub data: Option<ResponseBody>, pub data: Option<ResponseBody>,
} }
#[cfg(not(target_os = "windows"))]
pub fn sudo(passwd: &String, cmd: String) -> StdCommand {
let shell = format!("echo {} | sudo -S {}", passwd, cmd);
let mut command = StdCommand::new("bash");
command.arg("-c").arg(shell);
command
}
/// Install the Clash Verge Service /// Install the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
/// ///
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn install_service() -> Result<()> { pub async fn install_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@ -65,30 +73,45 @@ pub async fn install_service() -> Result<()> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn install_service() -> Result<()> { pub async fn install_service(passwd: String) -> Result<()> {
use users::get_effective_uid; use users::get_effective_uid;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service"); let installer_path = binary_path.with_file_name("install-service");
if !installer_path.exists() { if !installer_path.exists() {
bail!("installer not found"); bail!("installer not found");
} }
let elevator = crate::utils::unix_helper::linux_elevator(); let output = match get_effective_uid() {
let status = match get_effective_uid() { 0 => {
0 => StdCommand::new(installer_path).status()?, StdCommand::new("chmod")
_ => StdCommand::new(elevator) .arg("+x")
.arg("sh") .arg(installer_path.clone())
.arg("-c") .output()?;
.arg(installer_path) StdCommand::new("chmod")
.status()?, .arg("+x")
.arg(binary_path)
.output()?;
StdCommand::new(installer_path.clone()).output()?
}
_ => {
sudo(
&passwd,
format!("chmod +x {}", installer_path.to_string_lossy()),
)
.output()?;
sudo(
&passwd,
format!("chmod +x {}", binary_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", installer_path.to_string_lossy())).output()?
}
}; };
if output.stderr.len() > 0 {
if !status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@ -96,7 +119,7 @@ pub async fn install_service() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub async fn install_service() -> Result<()> { pub async fn install_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service"); let installer_path = binary_path.with_file_name("install-service");
@ -104,22 +127,24 @@ pub async fn install_service() -> Result<()> {
bail!("installer not found"); bail!("installer not found");
} }
let _ = StdCommand::new("chmod") sudo(
.arg("+x") &passwd,
.arg(installer_path.to_string_lossy().replace(" ", "\\ ")) format!(
.output(); "chmod +x {}",
installer_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", installer_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
let shell = installer_path.to_string_lossy().replace(" ", "\\\\ "); if output.stderr.len() > 0 {
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!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@ -128,7 +153,7 @@ pub async fn install_service() -> Result<()> {
/// Uninstall the Clash Verge Service /// Uninstall the Clash Verge Service
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程 /// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(_passwd: String) -> Result<()> {
use deelevate::{PrivilegeLevel, Token}; use deelevate::{PrivilegeLevel, Token};
use runas::Command as RunasCommand; use runas::Command as RunasCommand;
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
@ -161,7 +186,7 @@ pub async fn uninstall_service() -> Result<()> {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(passwd: String) -> Result<()> {
use users::get_effective_uid; use users::get_effective_uid;
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
@ -171,20 +196,28 @@ pub async fn uninstall_service() -> Result<()> {
bail!("uninstaller not found"); bail!("uninstaller not found");
} }
let elevator = crate::utils::unix_helper::linux_elevator(); let output = match get_effective_uid() {
let status = match get_effective_uid() { 0 => {
0 => StdCommand::new(uninstaller_path).status()?, StdCommand::new("chmod")
_ => StdCommand::new(elevator) .arg("+x")
.arg("sh") .arg(uninstaller_path.clone())
.arg("-c") .output()?;
.arg(uninstaller_path) StdCommand::new(uninstaller_path.clone()).output()?
.status()?, }
}; _ => {
sudo(
&passwd,
format!("chmod +x {}", uninstaller_path.to_string_lossy()),
)
.output()?;
if !status.success() { sudo(&passwd, format!("{}", uninstaller_path.to_string_lossy())).output()?
}
};
if output.stderr.len() > 0 {
bail!( bail!(
"failed to install service with status {}", "failed to install service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }
@ -192,7 +225,7 @@ pub async fn uninstall_service() -> Result<()> {
} }
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
pub async fn uninstall_service() -> Result<()> { pub async fn uninstall_service(passwd: String) -> Result<()> {
let binary_path = dirs::service_path()?; let binary_path = dirs::service_path()?;
let uninstaller_path = binary_path.with_file_name("uninstall-service"); let uninstaller_path = binary_path.with_file_name("uninstall-service");
@ -200,17 +233,24 @@ pub async fn uninstall_service() -> Result<()> {
bail!("uninstaller not found"); bail!("uninstaller not found");
} }
let shell = uninstaller_path.to_string_lossy().replace(" ", "\\\\ "); sudo(
let command = format!(r#"do shell script "{shell}" with administrator privileges"#); &passwd,
format!(
"chmod +x {}",
uninstaller_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
format!("{}", uninstaller_path.to_string_lossy().replace(" ", "\\ ")),
)
.output()?;
let status = StdCommand::new("osascript") if output.stderr.len() > 0 {
.args(vec!["-e", &command])
.status()?;
if !status.success() {
bail!( bail!(
"failed to install service with status {}", "failed to uninstall service with error: {}",
status.code().unwrap() String::from_utf8_lossy(&output.stderr)
); );
} }

View File

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

View File

@ -8,6 +8,7 @@ use serde_yaml::Mapping;
use std::net::TcpListener; use std::net::TcpListener;
use tauri::api::notification; use tauri::api::notification;
use tauri::{App, AppHandle, Manager}; use tauri::{App, AppHandle, Manager};
#[cfg(not(target_os = "linux"))]
use window_shadows::set_shadow; use window_shadows::set_shadow;
pub static VERSION: OnceCell<String> = OnceCell::new(); pub static VERSION: OnceCell<String> = OnceCell::new();

View File

@ -1,14 +0,0 @@
#[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() {
"sudo"
} else {
"pkexec"
}
}
Err(_) => "sudo",
}
}

View File

@ -0,0 +1,54 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogTitle,
TextField,
} from "@mui/material";
interface Props {
onConfirm: (passwd: string) => Promise<void>;
}
export const PasswordInput = (props: Props) => {
const { onConfirm } = props;
const { t } = useTranslation();
const [passwd, setPasswd] = useState("");
useEffect(() => {
if (!open) return;
}, [open]);
return (
<Dialog open={true} maxWidth="xs" fullWidth>
<DialogTitle>{t("Please enter your root password")}</DialogTitle>
<DialogContent>
<TextField
sx={{ mt: 1 }}
autoFocus
label={t("Password")}
fullWidth
size="small"
type="password"
value={passwd}
onKeyDown={(e) => e.key === "Enter" && onConfirm(passwd)}
onChange={(e) => setPasswd(e.target.value)}
></TextField>
</DialogContent>
<DialogActions>
<Button
onClick={async () => await onConfirm(passwd)}
variant="contained"
>
{t("Confirm")}
</Button>
</DialogActions>
</Dialog>
);
};

View File

@ -5,6 +5,8 @@ import { useTranslation } from "react-i18next";
import { installService, uninstallService } from "@/services/cmds"; import { installService, uninstallService } from "@/services/cmds";
import { Notice } from "@/components/base"; import { Notice } from "@/components/base";
import { LoadingButton } from "@mui/lab"; import { LoadingButton } from "@mui/lab";
import { PasswordInput } from "./password-input";
import getSystem from "@/utils/get-system";
interface Props { interface Props {
status: "active" | "installed" | "unknown" | "uninstall"; status: "active" | "installed" | "unknown" | "uninstall";
@ -15,7 +17,7 @@ interface Props {
export const ServiceSwitcher = (props: Props) => { export const ServiceSwitcher = (props: Props) => {
const { status, mutate, patchVerge, onChangeData } = props; const { status, mutate, patchVerge, onChangeData } = props;
const isWindows = getSystem() === "windows";
const isActive = status === "active"; const isActive = status === "active";
const isInstalled = status === "installed"; const isInstalled = status === "installed";
const isUninstall = status === "uninstall" || status === "unknown"; const isUninstall = status === "uninstall" || status === "unknown";
@ -23,40 +25,30 @@ export const ServiceSwitcher = (props: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [serviceLoading, setServiceLoading] = useState(false); const [serviceLoading, setServiceLoading] = useState(false);
const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false); const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
const [openInstall, setOpenInstall] = useState(false);
const [openUninstall, setOpenUninstall] = useState(false);
const onInstallOrEnableService = useLockFn(async () => { async function install(passwd: string) {
setServiceLoading(true);
try { try {
if (isUninstall) { setOpenInstall(false);
// install service await installService(passwd);
await installService();
await mutate(); await mutate();
setTimeout(() => { setTimeout(() => {
mutate(); mutate();
}, 2000); }, 2000);
Notice.success(t("Service Installed Successfully")); Notice.success(t("Service Installed Successfully"));
setServiceLoading(false); setServiceLoading(false);
} else {
// enable or disable service
await patchVerge({ enable_service_mode: !isActive });
onChangeData({ enable_service_mode: !isActive });
await mutate();
setTimeout(() => {
mutate();
}, 2000);
setServiceLoading(false);
}
} catch (err: any) { } catch (err: any) {
await mutate(); await mutate();
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
setServiceLoading(false); setServiceLoading(false);
} }
}); }
const onUninstallService = useLockFn(async () => { async function uninstall(passwd: string) {
setUninstallServiceLoading(true);
try { try {
await uninstallService(); setOpenUninstall(false);
await uninstallService(passwd);
await mutate(); await mutate();
setTimeout(() => { setTimeout(() => {
mutate(); mutate();
@ -68,10 +60,49 @@ export const ServiceSwitcher = (props: Props) => {
Notice.error(err.message || err.toString()); Notice.error(err.message || err.toString());
setUninstallServiceLoading(false); setUninstallServiceLoading(false);
} }
}
const onInstallOrEnableService = useLockFn(async () => {
setServiceLoading(true);
if (isUninstall) {
// install service
if (isWindows) {
await install("");
} else {
setOpenInstall(true);
}
} else {
try {
// enable or disable service
await patchVerge({ enable_service_mode: !isActive });
onChangeData({ enable_service_mode: !isActive });
await mutate();
setTimeout(() => {
mutate();
}, 2000);
setServiceLoading(false);
} catch (err: any) {
await mutate();
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
});
const onUninstallService = useLockFn(async () => {
setUninstallServiceLoading(true);
if (isWindows) {
await uninstall("");
} else {
setOpenUninstall(true);
}
}); });
return ( return (
<> <>
{openInstall && <PasswordInput onConfirm={install} />}
{openUninstall && <PasswordInput onConfirm={uninstall} />}
<LoadingButton <LoadingButton
size="small" size="small"
variant={isUninstall ? "outlined" : "contained"} variant={isUninstall ? "outlined" : "contained"}

View File

@ -267,6 +267,7 @@
"Release Version": "Release Version", "Release Version": "Release Version",
"Alpha Version": "Alpha Version", "Alpha Version": "Alpha Version",
"Please Enable Service Mode": "Please Install and Enable Service Mode First", "Please Enable Service Mode": "Please Install and Enable Service Mode First",
"Please enter your root password": "Please enter your root password",
"Grant": "Grant", "Grant": "Grant",
"Open UWP tool": "Open UWP tool", "Open UWP tool": "Open UWP tool",
"Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction", "Open UWP tool Info": "Since Windows 8, UWP apps (such as Microsoft Store) are restricted from directly accessing local host network services, and this tool can be used to bypass this restriction",

View File

@ -262,6 +262,7 @@
"Release Version": "نسخه نهایی", "Release Version": "نسخه نهایی",
"Alpha Version": "نسخه آلفا", "Alpha Version": "نسخه آلفا",
"Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید", "Please Install and Enable Service Mode First": "لطفاً ابتدا حالت سرویس را نصب و فعال کنید",
"Please enter your root password": "لطفاً رمز ریشه خود را وارد کنید",
"Grant": "اعطا", "Grant": "اعطا",
"Open UWP tool": "باز کردن ابزار UWP", "Open UWP tool": "باز کردن ابزار UWP",
"Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود", "Open UWP tool Info": "از ویندوز 8 به بعد، برنامه‌های UWP (مانند Microsoft Store) از دسترسی مستقیم به خدمات شبکه محلی محدود شده‌اند و این ابزار می‌تواند برای دور زدن این محدودیت استفاده شود",

View File

@ -265,6 +265,7 @@
"Release Version": "Официальная версия", "Release Version": "Официальная версия",
"Alpha Version": "Альфа-версия", "Alpha Version": "Альфа-версия",
"Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания", "Please Enable Service Mode": "Пожалуйста, сначала установите и включите режим обслуживания",
"Please enter your root password": "Пожалуйста, введите ваш пароль root",
"Grant": "Предоставить", "Grant": "Предоставить",
"Open UWP tool": "Открыть UWP инструмент", "Open UWP tool": "Открыть UWP инструмент",
"Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение", "Open UWP tool Info": "С Windows 8 приложения UWP (такие как Microsoft Store) ограничены в прямом доступе к сетевым службам локального хоста, и этот инструмент позволяет обойти это ограничение",

View File

@ -267,6 +267,7 @@
"Release Version": "正式版", "Release Version": "正式版",
"Alpha Version": "预览版", "Alpha Version": "预览版",
"Please Enable Service Mode": "请先安装并启用服务模式", "Please Enable Service Mode": "请先安装并启用服务模式",
"Please enter your root password": "请输入您的 root 密码",
"Grant": "授权", "Grant": "授权",
"Open UWP tool": "UWP 工具", "Open UWP tool": "UWP 工具",
"Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制", "Open UWP tool Info": "Windows 8开始限制 UWP 应用(如微软商店)直接访问本地主机的网络服务,使用此工具可绕过该限制",

View File

@ -201,12 +201,13 @@ export async function checkService() {
} }
} }
export async function installService() { export async function installService(passwd: string) {
return invoke<void>("install_service"); console.log(passwd);
return invoke<void>("install_service", { passwd });
} }
export async function uninstallService() { export async function uninstallService(passwd: string) {
return invoke<void>("uninstall_service"); return invoke<void>("uninstall_service", { passwd });
} }
export async function invoke_uwp_tool() { export async function invoke_uwp_tool() {