feat: Modify startup logic and install services by default

This commit is contained in:
huzibaca 2024-10-10 00:34:36 +08:00
parent 30c77b891d
commit 3f3fad0db7
No known key found for this signature in database
GPG Key ID: D4364EE4851DC302
12 changed files with 126 additions and 436 deletions

View File

@ -383,16 +383,6 @@ pub mod service {
pub async fn check_service() -> CmdResult<service::JsonResponse> {
wrap_err!(service::check_service().await)
}
#[tauri::command]
pub async fn install_service(passwd: String) -> CmdResult {
wrap_err!(service::install_service(passwd).await)
}
#[tauri::command]
pub async fn uninstall_service(passwd: String) -> CmdResult {
wrap_err!(service::uninstall_service(passwd).await)
}
}
#[cfg(not(windows))]

View File

@ -56,10 +56,6 @@ pub struct IVerge {
/// clash tun mode
pub enable_tun_mode: Option<bool>,
/// windows service mode
#[serde(skip_serializing_if = "Option::is_none")]
pub enable_service_mode: Option<bool>,
/// can the app auto startup
pub enable_auto_launch: Option<bool>,
@ -279,7 +275,6 @@ impl IVerge {
patch!(tun_tray_icon);
patch!(enable_tun_mode);
patch!(enable_service_mode);
patch!(enable_auto_launch);
patch!(enable_silent_start);
patch!(enable_random_port);

View File

@ -6,7 +6,6 @@ use anyhow::{bail, Result};
use once_cell::sync::OnceCell;
use serde_yaml::Mapping;
use std::{sync::Arc, time::Duration};
use tauri_plugin_shell::process::{CommandChild, CommandEvent};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use tokio::time::sleep;
@ -14,7 +13,6 @@ use tokio::time::sleep;
#[derive(Debug)]
pub struct CoreManager {
running: Arc<Mutex<bool>>,
sidecar: Arc<Mutex<Option<CommandChild>>>,
}
impl CoreManager {
@ -22,7 +20,6 @@ impl CoreManager {
static CORE_MANAGER: OnceCell<CoreManager> = OnceCell::new();
CORE_MANAGER.get_or_init(|| CoreManager {
running: Arc::new(Mutex::new(false)),
sidecar: Arc::new(Mutex::new(None)),
})
}
@ -75,6 +72,7 @@ impl CoreManager {
log::debug!("core is not running");
return Ok(());
}
println!("stop core");
// 关闭tun模式
let mut disable = Mapping::new();
@ -84,22 +82,18 @@ impl CoreManager {
log::debug!(target: "app", "disable tun mode");
log_err!(clash_api::patch_configs(&disable).await);
if let Some(sidecar) = self.sidecar.lock().await.take() {
let _ = sidecar.kill();
} else {
// 服务模式
if service::check_service().await.is_ok() {
log::debug!(target: "app", "stop the core by service");
log_err!(service::stop_core_by_service().await);
}
service::stop_core_by_service().await?;
}
*running = false;
Ok(())
}
/// 启动核心
pub async fn start_core(&self) -> Result<()> {
println!("start core");
let mut running = self.running.lock().await;
if *running {
log::debug!("core is running");
@ -107,62 +101,13 @@ impl CoreManager {
}
let config_path = Config::generate_file(ConfigType::Run)?;
let clash_core = { Config::verge().latest().clash_core.clone() };
let clash_core = clash_core.unwrap_or("verge-mihomo".into());
// 服务模式
let service_enable = { Config::verge().latest().enable_service_mode };
let service_enable = service_enable.unwrap_or(false);
// 服务模式
if service::check_service().await.is_ok() {
log::debug!(target: "app", "try to run core in service mode");
if service_enable {
service::run_core_by_service(&config_path).await?;
let mut sidecar = self.sidecar.lock().await;
if sidecar.is_some() {
sidecar.take();
}
*running = true;
return Ok(());
}
}
let app_dir = dirs::app_home_dir()?;
let app_dir = dirs::path_to_str(&app_dir)?;
let config_path = dirs::path_to_str(&config_path)?;
let args = vec!["-d", app_dir, "-f", config_path];
let app_handle = handle::Handle::global().app_handle().unwrap();
let cmd = app_handle.shell().sidecar(clash_core)?;
let (mut rx, cmd_child) = cmd.args(args).spawn()?;
let mut sidecar = self.sidecar.lock().await;
*sidecar = Some(cmd_child);
tauri::async_runtime::spawn(async move {
while let Some(event) = rx.recv().await {
match event {
CommandEvent::Stdout(line) => {
let line = String::from_utf8(line).unwrap_or_default();
log::info!(target: "app", "[mihomo]: {line}");
Logger::global().set_log(line);
}
CommandEvent::Stderr(err) => {
let err = String::from_utf8(err).unwrap_or_default();
log::error!(target: "app", "[mihomo]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Error(err) => {
log::error!(target: "app", "[mihomo]: {err}");
Logger::global().set_log(err);
}
CommandEvent::Terminated(_) => {
log::info!(target: "app", "mihomo core terminated");
break;
}
_ => {}
}
}
});
*running = true;
Ok(())
}

View File

@ -26,32 +26,42 @@ pub struct JsonResponse {
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
/// 该函数应该在协程或者线程中执行避免UAC弹窗阻塞主线程
///
#[cfg(target_os = "windows")]
pub async fn install_service(_passwd: String) -> Result<()> {
pub async fn reinstall_service(_passwd: String) -> 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");
let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
if !install_path.exists() {
bail!("installer exe not found");
}
if !uninstall_path.exists() {
bail!("uninstaller exe not found");
}
let token = Token::with_current_process()?;
let level = token.privilege_level()?;
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
_ => StdCommand::new(uninstall_path)
.creation_flags(0x08000000)
.status()?,
};
if !status.success() {
bail!(
"failed to uninstall service with status {}",
status.code().unwrap()
);
}
let status = match level {
PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
@ -71,45 +81,52 @@ pub async fn install_service(_passwd: String) -> Result<()> {
}
#[cfg(target_os = "linux")]
pub async fn install_service(passwd: String) -> Result<()> {
pub async fn reinstall_service(passwd: String) -> Result<()> {
use users::get_effective_uid;
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
let uninstaller_path = binary_path.with_file_name("uninstall-service");
if !installer_path.exists() {
bail!("installer not found");
}
let output = match get_effective_uid() {
0 => {
StdCommand::new("chmod")
.arg("+x")
.arg(installer_path.clone())
.output()?;
StdCommand::new("chmod")
.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 !uninstaller_path.exists() {
bail!("uninstaller not found");
}
let elevator = crate::utils::help::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 !output.status.success() {
if !status.success() {
bail!(
"failed to install service with error: {}",
String::from_utf8_lossy(&output.stderr)
"failed to install service with status {}",
status.code().unwrap()
);
}
let elevator = crate::utils::help::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()
);
}
@ -117,38 +134,35 @@ pub async fn install_service(passwd: String) -> Result<()> {
}
#[cfg(target_os = "macos")]
pub async fn install_service(passwd: String) -> Result<()> {
pub async fn reinstall_service() -> Result<()> {
let binary_path = dirs::service_path()?;
let installer_path = binary_path.with_file_name("install-service");
let uninstall_path = binary_path.with_file_name("uninstall-service");
if !installer_path.exists() {
bail!("installer not found");
}
sudo(
&passwd,
format!(
"chmod +x {}",
installer_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
installer_path
.to_string_lossy()
.replace(" ", "\\ ")
.to_string(),
)
.output()?;
if !output.status.success() {
bail!(
"failed to install service with error: {}",
String::from_utf8_lossy(&output.stderr)
);
if !uninstall_path.exists() {
bail!("uninstaller not found");
}
let install_shell: String = installer_path.to_string_lossy().replace(" ", "\\\\ ");
let uninstall_shell: String = uninstall_path.to_string_lossy().replace(" ", "\\\\ ");
let command = format!(
r#"do shell script "{uninstall_shell} && {install_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
@ -186,82 +200,6 @@ pub async fn uninstall_service(_passwd: String) -> Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
pub async fn uninstall_service(passwd: String) -> 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 output = match get_effective_uid() {
0 => {
StdCommand::new("chmod")
.arg("+x")
.arg(uninstaller_path.clone())
.output()?;
StdCommand::new(uninstaller_path.clone()).output()?
}
_ => {
sudo(
&passwd,
format!("chmod +x {}", uninstaller_path.to_string_lossy()),
)
.output()?;
sudo(&passwd, format!("{}", uninstaller_path.to_string_lossy())).output()?
}
};
if !output.status.success() {
bail!(
"failed to install service with error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
#[cfg(target_os = "macos")]
pub async fn uninstall_service(passwd: String) -> 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");
}
sudo(
&passwd,
format!(
"chmod +x {}",
uninstaller_path.to_string_lossy().replace(" ", "\\ ")
),
)
.output()?;
let output = sudo(
&passwd,
uninstaller_path
.to_string_lossy()
.replace(" ", "\\ ")
.to_string(),
)
.output()?;
if !output.status.success() {
bail!(
"failed to uninstall service with error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
Ok(())
}
/// check the windows service status
pub async fn check_service() -> Result<JsonResponse> {
let url = format!("{SERVICE_URL}/get_clash");

View File

@ -364,15 +364,8 @@ fn create_tray_menu(
.unwrap();
let separator = &PredefinedMenuItem::separator(app_handle).unwrap();
let enable = {
Config::verge()
.latest()
.enable_service_mode
.unwrap_or(false)
};
let menu = if enable {
tauri::menu::MenuBuilder::new(app_handle)
let menu = tauri::menu::MenuBuilder::new(app_handle)
.items(&[
open_window,
separator,
@ -389,27 +382,7 @@ fn create_tray_menu(
quit,
])
.build()
.unwrap()
} else {
tauri::menu::MenuBuilder::new(app_handle)
.items(&[
open_window,
separator,
rule_mode,
global_mode,
direct_mode,
separator,
system_proxy,
copy_env,
open_dir,
more,
separator,
quit,
])
.build()
.unwrap()
};
.unwrap();
Ok(menu)
}

View File

@ -177,11 +177,6 @@ pub async fn patch_verge(patch: IVerge) -> Result<()> {
let mut should_update_sysproxy = false;
let mut should_update_systray_part = false;
let service_mode = patch.enable_service_mode;
if service_mode.is_some() {
should_restart_core = true;
}
if tun_mode.is_some() {
should_update_clash_config = true;
}

View File

@ -118,8 +118,6 @@ pub fn run() {
cmds::save_profile_file,
// service mode
cmds::service::check_service,
cmds::service::install_service,
cmds::service::uninstall_service,
// clash api
cmds::clash_api_get_proxy_delay
]);

View File

@ -105,6 +105,21 @@ pub fn open_file(app: tauri::AppHandle, path: PathBuf) -> Result<()> {
Ok(())
}
#[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",
}
}
#[macro_export]
macro_rules! error {
($result: expr) => {

View File

@ -42,6 +42,13 @@ pub async fn resolve_setup(app: &mut App) {
handle::Handle::global().init(app.app_handle());
VERSION.get_or_init(|| version.clone());
if service::check_service().await.is_err() {
log_err!(service::reinstall_service().await);
//延迟启动,避免闪屏
std::thread::sleep(std::time::Duration::from_millis(1000));
}
log_err!(init::init_config());
log_err!(init::init_resources());
log_err!(init::init_scheme());

View File

@ -1,134 +0,0 @@
import { KeyedMutator } from "swr";
import { useState } from "react";
import { useLockFn } from "ahooks";
import { useTranslation } from "react-i18next";
import { installService, uninstallService } from "@/services/cmds";
import { Notice } from "@/components/base";
import { LoadingButton } from "@mui/lab";
import { PasswordInput } from "./password-input";
import getSystem from "@/utils/get-system";
interface Props {
status: "active" | "installed" | "unknown" | "uninstall";
mutate: KeyedMutator<"active" | "installed" | "unknown" | "uninstall">;
patchVerge: (value: Partial<IVergeConfig>) => Promise<void>;
onChangeData: (patch: Partial<IVergeConfig>) => void;
}
export const ServiceSwitcher = (props: Props) => {
const { status, mutate, patchVerge, onChangeData } = props;
const isWindows = getSystem() === "windows";
const isActive = status === "active";
const isInstalled = status === "installed";
const isUninstall = status === "uninstall" || status === "unknown";
const { t } = useTranslation();
const [serviceLoading, setServiceLoading] = useState(false);
const [uninstallServiceLoaing, setUninstallServiceLoading] = useState(false);
const [openInstall, setOpenInstall] = useState(false);
const [openUninstall, setOpenUninstall] = useState(false);
async function install(passwd: string) {
try {
setOpenInstall(false);
await installService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Installed Successfully"));
setServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
setServiceLoading(false);
}
}
async function uninstall(passwd: string) {
try {
setOpenUninstall(false);
await uninstallService(passwd);
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.success(t("Service Uninstalled Successfully"));
setUninstallServiceLoading(false);
} catch (err: any) {
await mutate();
setTimeout(() => {
mutate();
}, 2000);
Notice.error(err.message || err.toString());
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 (
<>
{openInstall && <PasswordInput onConfirm={install} />}
{openUninstall && <PasswordInput onConfirm={uninstall} />}
<LoadingButton
size="small"
variant={isUninstall ? "outlined" : "contained"}
onClick={onInstallOrEnableService}
loading={serviceLoading}
>
{isActive ? t("Disable") : isInstalled ? t("Enable") : t("Install")}
</LoadingButton>
{isInstalled && (
<LoadingButton
size="small"
variant="outlined"
color="error"
sx={{ ml: 1 }}
onClick={onUninstallService}
loading={uninstallServiceLoaing}
>
{t("Uninstall")}
</LoadingButton>
)}
</>
);
};

View File

@ -7,7 +7,6 @@ import { useVerge } from "@/hooks/use-verge";
import { DialogRef, Notice, Switch } from "@/components/base";
import { SettingList, SettingItem } from "./mods/setting-comp";
import { GuardState } from "./mods/guard-state";
import { ServiceSwitcher } from "./mods/service-switcher";
import { SysproxyViewer } from "./mods/sysproxy-viewer";
import { TunViewer } from "./mods/tun-viewer";
import { TooltipIcon } from "@/components/base/base-tooltip-icon";
@ -67,37 +66,15 @@ const SettingSystem = ({ onError }: Props) => {
onCatch={onError}
onFormat={onSwitchFormat}
onChange={(e) => {
if (serviceStatus !== "active") {
onChangeData({ enable_tun_mode: false });
} else {
onChangeData({ enable_tun_mode: e });
}
}}
onGuard={(e) => {
if (serviceStatus !== "active" && e) {
Notice.error(t("Please Enable Service Mode"));
return Promise.resolve();
} else {
return patchVerge({ enable_tun_mode: e });
}
}}
>
<Switch edge="end" />
</GuardState>
</SettingItem>
<SettingItem
label={t("Service Mode")}
extra={<TooltipIcon title={t("Service Mode Info")} />}
>
<ServiceSwitcher
status={serviceStatus ?? "unknown"}
mutate={mutateServiceStatus}
patchVerge={patchVerge}
onChangeData={onChangeData}
/>
</SettingItem>
<SettingItem
label={t("System Proxy")}
extra={

View File

@ -200,15 +200,6 @@ export async function checkService() {
return "uninstall";
}
}
export async function installService(passwd: string) {
return invoke<void>("install_service", { passwd });
}
export async function uninstallService(passwd: string) {
return invoke<void>("uninstall_service", { passwd });
}
export async function invoke_uwp_tool() {
return invoke<void>("invoke_uwp_tool").catch((err) =>
Notice.error(err?.message || err.toString(), 1500)