|
@@ -3,6 +3,7 @@ use crate::log_if_err;
|
|
|
use crate::utils::{config, dirs};
|
|
|
use anyhow::{bail, Result};
|
|
|
use reqwest::header::HeaderMap;
|
|
|
+use serde::{Deserialize, Serialize};
|
|
|
use serde_yaml::Mapping;
|
|
|
use std::{collections::HashMap, time::Duration};
|
|
|
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
|
@@ -11,14 +12,79 @@ use tokio::time::sleep;
|
|
|
#[derive(Debug)]
|
|
|
pub struct Service {
|
|
|
sidecar: Option<CommandChild>,
|
|
|
+
|
|
|
+ #[allow(unused)]
|
|
|
+ service_mode: bool,
|
|
|
}
|
|
|
|
|
|
impl Service {
|
|
|
pub fn new() -> Service {
|
|
|
- Service { sidecar: None }
|
|
|
+ Service {
|
|
|
+ sidecar: None,
|
|
|
+ service_mode: false,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ #[allow(unused)]
|
|
|
+ pub fn set_mode(&mut self, enable: bool) {
|
|
|
+ self.service_mode = enable;
|
|
|
}
|
|
|
|
|
|
+ #[cfg(not(windows))]
|
|
|
pub fn start(&mut self) -> Result<()> {
|
|
|
+ self.start_clash_by_sidecar()
|
|
|
+ }
|
|
|
+
|
|
|
+ #[cfg(windows)]
|
|
|
+ pub fn start(&mut self) -> Result<()> {
|
|
|
+ if !self.service_mode {
|
|
|
+ return self.start_clash_by_sidecar();
|
|
|
+ }
|
|
|
+
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
+ match Self::check_service().await {
|
|
|
+ Ok(status) => {
|
|
|
+ // 未启动clash
|
|
|
+ if status.code != 0 {
|
|
|
+ if let Err(err) = Self::start_clash_by_service().await {
|
|
|
+ log::error!("{err}");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ Err(err) => log::error!("{err}"),
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ #[cfg(not(windows))]
|
|
|
+ pub fn stop(&mut self) -> Result<()> {
|
|
|
+ self.stop_clash_by_sidecar()
|
|
|
+ }
|
|
|
+
|
|
|
+ #[cfg(windows)]
|
|
|
+ pub fn stop(&mut self) -> Result<()> {
|
|
|
+ if !self.service_mode {
|
|
|
+ return self.stop_clash_by_sidecar();
|
|
|
+ }
|
|
|
+
|
|
|
+ tauri::async_runtime::spawn(async move {
|
|
|
+ if let Err(err) = Self::stop_clash_by_service().await {
|
|
|
+ log::error!("{err}");
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn restart(&mut self) -> Result<()> {
|
|
|
+ self.stop()?;
|
|
|
+ self.start()
|
|
|
+ }
|
|
|
+
|
|
|
+ /// start the clash sidecar
|
|
|
+ fn start_clash_by_sidecar(&mut self) -> Result<()> {
|
|
|
if self.sidecar.is_some() {
|
|
|
bail!("could not run clash sidecar twice");
|
|
|
}
|
|
@@ -45,22 +111,18 @@ impl Service {
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- pub fn stop(&mut self) -> Result<()> {
|
|
|
+ /// stop the clash sidecar
|
|
|
+ fn stop_clash_by_sidecar(&mut self) -> Result<()> {
|
|
|
if let Some(sidecar) = self.sidecar.take() {
|
|
|
sidecar.kill()?;
|
|
|
}
|
|
|
Ok(())
|
|
|
}
|
|
|
|
|
|
- pub fn restart(&mut self) -> Result<()> {
|
|
|
- self.stop()?;
|
|
|
- self.start()
|
|
|
- }
|
|
|
-
|
|
|
/// update clash config
|
|
|
/// using PUT methods
|
|
|
pub fn set_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> {
|
|
|
- if self.sidecar.is_none() {
|
|
|
+ if !self.service_mode && self.sidecar.is_none() {
|
|
|
bail!("did not start sidecar");
|
|
|
}
|
|
|
|
|
@@ -125,85 +187,182 @@ impl Drop for Service {
|
|
|
/// ### Service Mode
|
|
|
///
|
|
|
#[cfg(windows)]
|
|
|
-mod win_service {
|
|
|
+pub mod win_service {
|
|
|
use super::*;
|
|
|
+ use anyhow::Context;
|
|
|
use deelevate::{PrivilegeLevel, Token};
|
|
|
use runas::Command as RunasCommond;
|
|
|
- use std::process::Command as StdCommond;
|
|
|
+ use std::{env::current_exe, process::Command as StdCommond};
|
|
|
|
|
|
const SERVICE_NAME: &str = "clash_verge_service";
|
|
|
|
|
|
+ const SERVICE_URL: &str = "http://127.0.0.1:33211";
|
|
|
+
|
|
|
+ #[derive(Debug, Deserialize, Serialize, Clone)]
|
|
|
+ pub struct ResponseBody {
|
|
|
+ pub bin_path: String,
|
|
|
+ pub config_dir: String,
|
|
|
+ pub log_file: String,
|
|
|
+ }
|
|
|
+
|
|
|
+ #[derive(Debug, Deserialize, Serialize, Clone)]
|
|
|
+ pub struct JsonResponse {
|
|
|
+ pub code: u64,
|
|
|
+ pub msg: String,
|
|
|
+ pub data: Option<ResponseBody>,
|
|
|
+ }
|
|
|
+
|
|
|
impl Service {
|
|
|
- /// install the Clash Verge Service (windows only)
|
|
|
- pub fn install_service(&mut self) -> Result<()> {
|
|
|
+ /// Install the Clash Verge Service
|
|
|
+ /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
|
|
+ pub async fn install_service() -> Result<()> {
|
|
|
let binary_path = dirs::service_path();
|
|
|
let arg = format!("binpath={}", binary_path.as_os_str().to_string_lossy());
|
|
|
|
|
|
let token = Token::with_current_process()?;
|
|
|
let level = token.privilege_level()?;
|
|
|
|
|
|
- tauri::async_runtime::spawn(async move {
|
|
|
- let args = [
|
|
|
- "create",
|
|
|
- SERVICE_NAME,
|
|
|
- arg.as_str(),
|
|
|
- "type=own",
|
|
|
- "start=AUTO",
|
|
|
- "displayname=Clash Verge Service",
|
|
|
- ];
|
|
|
-
|
|
|
- let status = match level {
|
|
|
- PrivilegeLevel::NotPrivileged => RunasCommond::new("sc").args(&args).status(),
|
|
|
- _ => StdCommond::new("sc").args(&args).status(),
|
|
|
- };
|
|
|
-
|
|
|
- match status {
|
|
|
- Ok(status) => {
|
|
|
- if status.success() {
|
|
|
- log::info!("install clash verge service successfully");
|
|
|
- } else if status.code() == Some(1073i32) {
|
|
|
- log::info!("clash verge service is installed");
|
|
|
- } else {
|
|
|
- log::error!(
|
|
|
- "failed to install service with status {}",
|
|
|
- status.code().unwrap()
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- Err(err) => log::error!("failed to install service for {err}"),
|
|
|
- }
|
|
|
- });
|
|
|
+ let args = [
|
|
|
+ "create",
|
|
|
+ SERVICE_NAME,
|
|
|
+ arg.as_str(),
|
|
|
+ "type=own",
|
|
|
+ "start=AUTO",
|
|
|
+ "displayname=Clash Verge Service",
|
|
|
+ ];
|
|
|
+
|
|
|
+ let status = match level {
|
|
|
+ PrivilegeLevel::NotPrivileged => RunasCommond::new("sc").args(&args).status()?,
|
|
|
+ _ => StdCommond::new("sc").args(&args).status()?,
|
|
|
+ };
|
|
|
+
|
|
|
+ if status.success() {
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
|
|
|
- Ok(())
|
|
|
+ if status.code() == Some(1073i32) {
|
|
|
+ bail!("clash verge service is installed");
|
|
|
+ }
|
|
|
+
|
|
|
+ bail!(
|
|
|
+ "failed to install service with status {}",
|
|
|
+ status.code().unwrap()
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
- /// uninstall
|
|
|
- pub fn uninstall_service(&mut self) -> Result<()> {
|
|
|
+ /// Uninstall the Clash Verge Service
|
|
|
+ /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
|
|
+ pub async fn uninstall_service() -> Result<()> {
|
|
|
let token = Token::with_current_process()?;
|
|
|
let level = token.privilege_level()?;
|
|
|
|
|
|
- tauri::async_runtime::spawn(async move {
|
|
|
- let args = ["delete", SERVICE_NAME];
|
|
|
-
|
|
|
- let status = match level {
|
|
|
- PrivilegeLevel::NotPrivileged => RunasCommond::new("sc").args(&args).status(),
|
|
|
- _ => StdCommond::new("sc").args(&args).status(),
|
|
|
- };
|
|
|
-
|
|
|
- match status {
|
|
|
- Ok(status) => {
|
|
|
- if status.success() {
|
|
|
- log::info!("uninstall clash verge service successfully");
|
|
|
- } else {
|
|
|
- log::error!(
|
|
|
- "failed to uninstall service with status {}",
|
|
|
- status.code().unwrap()
|
|
|
- );
|
|
|
- }
|
|
|
- }
|
|
|
- Err(err) => log::error!("failed to uninstall service for {err}"),
|
|
|
- }
|
|
|
- });
|
|
|
+ let args = ["delete", SERVICE_NAME];
|
|
|
+
|
|
|
+ let status = match level {
|
|
|
+ PrivilegeLevel::NotPrivileged => RunasCommond::new("sc").args(&args).status()?,
|
|
|
+ _ => StdCommond::new("sc").args(&args).status()?,
|
|
|
+ };
|
|
|
+
|
|
|
+ match status.success() {
|
|
|
+ true => Ok(()),
|
|
|
+ false => bail!(
|
|
|
+ "failed to uninstall service with status {}",
|
|
|
+ status.code().unwrap()
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// start service
|
|
|
+ /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
|
|
|
+ pub async fn start_service() -> Result<()> {
|
|
|
+ let token = Token::with_current_process()?;
|
|
|
+ let level = token.privilege_level()?;
|
|
|
+
|
|
|
+ let args = ["start", SERVICE_NAME];
|
|
|
+
|
|
|
+ let status = match level {
|
|
|
+ PrivilegeLevel::NotPrivileged => RunasCommond::new("sc").args(&args).status()?,
|
|
|
+ _ => StdCommond::new("sc").args(&args).status()?,
|
|
|
+ };
|
|
|
+
|
|
|
+ match status.success() {
|
|
|
+ true => Ok(()),
|
|
|
+ false => bail!(
|
|
|
+ "failed to start service with status {}",
|
|
|
+ status.code().unwrap()
|
|
|
+ ),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// check the windows service status
|
|
|
+ pub async fn check_service() -> Result<JsonResponse> {
|
|
|
+ let url = format!("{SERVICE_URL}/get_clash");
|
|
|
+ let response = reqwest::get(url)
|
|
|
+ .await
|
|
|
+ .context("failed to connect to the Clash Verge Service")?
|
|
|
+ .json::<JsonResponse>()
|
|
|
+ .await
|
|
|
+ .context("failed to parse the Clash Verge Service response")?;
|
|
|
+
|
|
|
+ Ok(response)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// start the clash by service
|
|
|
+ pub(super) async fn start_clash_by_service() -> Result<()> {
|
|
|
+ let status = Self::check_service().await?;
|
|
|
+
|
|
|
+ if status.code == 0 {
|
|
|
+ Self::stop_clash_by_service().await?;
|
|
|
+ sleep(Duration::from_secs(1)).await;
|
|
|
+ }
|
|
|
+
|
|
|
+ let bin_path = current_exe().unwrap().with_file_name("clash.exe");
|
|
|
+ let bin_path = bin_path.as_os_str().to_str().unwrap();
|
|
|
+
|
|
|
+ let config_dir = dirs::app_home_dir();
|
|
|
+ let config_dir = config_dir.as_os_str().to_str().unwrap();
|
|
|
+
|
|
|
+ let log_path = dirs::service_log_file();
|
|
|
+ let log_path = log_path.as_os_str().to_str().unwrap();
|
|
|
+
|
|
|
+ let mut map = HashMap::new();
|
|
|
+ map.insert("bin_path", bin_path);
|
|
|
+ map.insert("config_dir", config_dir);
|
|
|
+ map.insert("log_file", log_path);
|
|
|
+
|
|
|
+ let url = format!("{SERVICE_URL}/start_clash");
|
|
|
+ let res = reqwest::Client::new()
|
|
|
+ .post(url)
|
|
|
+ .json(&map)
|
|
|
+ .send()
|
|
|
+ .await
|
|
|
+ .context("failed to connect to the Clash Verge Service")?
|
|
|
+ .json::<JsonResponse>()
|
|
|
+ .await
|
|
|
+ .context("failed to parse the Clash Verge Service response")?;
|
|
|
+
|
|
|
+ if res.code != 0 {
|
|
|
+ bail!(res.msg);
|
|
|
+ }
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ /// stop the clash by service
|
|
|
+ pub(super) async fn stop_clash_by_service() -> Result<()> {
|
|
|
+ let url = format!("{SERVICE_URL}/stop_clash");
|
|
|
+ let res = reqwest::Client::new()
|
|
|
+ .post(url)
|
|
|
+ .send()
|
|
|
+ .await
|
|
|
+ .context("failed to connect to the Clash Verge Service")?
|
|
|
+ .json::<JsonResponse>()
|
|
|
+ .await
|
|
|
+ .context("failed to parse the Clash Verge Service response")?;
|
|
|
+
|
|
|
+ if res.code != 0 {
|
|
|
+ bail!(res.msg);
|
|
|
+ }
|
|
|
|
|
|
Ok(())
|
|
|
}
|