From 34e941c8cbc7605e5aece219bcd318e60705408b Mon Sep 17 00:00:00 2001 From: GyDi Date: Sun, 24 Apr 2022 21:00:17 +0800 Subject: [PATCH] feat: windows service mode --- src-tauri/src/cmds.rs | 51 ++++++ src-tauri/src/core/mod.rs | 38 ++++- src-tauri/src/core/service.rs | 293 ++++++++++++++++++++++++++-------- src-tauri/src/core/verge.rs | 7 + src-tauri/src/main.rs | 7 +- src-tauri/src/utils/dirs.rs | 15 ++ 6 files changed, 339 insertions(+), 72 deletions(-) diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 5edc60c..55d37f3 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -239,3 +239,54 @@ pub fn open_logs_dir() -> Result<(), String> { let log_dir = dirs::app_logs_dir(); wrap_err!(open::that(log_dir)) } + +/// service mode +#[cfg(windows)] +pub mod service { + use super::*; + use crate::core::win_service::JsonResponse; + + #[tauri::command] + pub async fn start_service() -> Result<(), String> { + wrap_err!(crate::core::Service::start_service().await) + } + + #[tauri::command] + pub async fn check_service() -> Result { + wrap_err!(crate::core::Service::check_service().await) + } + + #[tauri::command] + pub async fn install_service() -> Result<(), String> { + wrap_err!(crate::core::Service::install_service().await) + } + + #[tauri::command] + pub async fn uninstall_service() -> Result<(), String> { + wrap_err!(crate::core::Service::uninstall_service().await) + } +} + +#[cfg(not(windows))] +pub mod service { + use super::*; + + #[tauri::command] + pub async fn start_service() -> Result<(), String> { + Ok(()) + } + + #[tauri::command] + pub async fn check_service() -> Result<(), String> { + Ok(()) + } + + #[tauri::command] + pub async fn install_service() -> Result<(), String> { + Ok(()) + } + #[tauri::command] + pub async fn uninstall_service() -> Result<(), String> { + Ok(()) + } +} diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 37daa47..e7be3bb 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1,5 +1,4 @@ use self::notice::Notice; -use self::service::Service; use self::sysopt::Sysopt; use self::timer::Timer; use crate::core::enhance::PrfEnhancedResult; @@ -26,6 +25,7 @@ mod verge; pub use self::clash::*; pub use self::prfitem::*; pub use self::profiles::*; +pub use self::service::*; pub use self::verge::*; #[derive(Clone)] @@ -65,9 +65,22 @@ impl Core { /// initialize the core state pub fn init(&self, app_handle: tauri::AppHandle) { - let mut service = self.service.lock(); - log_if_err!(service.start()); - drop(service); + #[cfg(windows)] + { + let verge = self.verge.lock(); + let enable = verge.enable_service_mode.clone(); + + let mut service = self.service.lock(); + service.set_mode(enable.unwrap_or(false)); + + log_if_err!(service.start()); + } + + #[cfg(not(windows))] + { + let mut service = self.service.lock(); + log_if_err!(service.start()); + } log_if_err!(self.activate()); @@ -159,6 +172,23 @@ impl Core { let proxy_bypass = patch.system_proxy_bypass.clone(); let proxy_guard = patch.enable_proxy_guard.clone(); + #[cfg(windows)] + { + let service_mode = patch.enable_service_mode.clone(); + + if service_mode.is_some() { + let service_mode = service_mode.unwrap(); + + let mut service = self.service.lock(); + service.stop()?; + service.set_mode(service_mode); + service.start()?; + drop(service); + + self.activate_enhanced(false)?; + } + } + if auto_launch.is_some() { let mut sysopt = self.sysopt.lock(); sysopt.update_launch(auto_launch)?; diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 1b711e7..798bee7 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -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, + + #[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, + } + 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 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(), - }; + 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}"), - } - }); + if status.success() { + return Ok(()); + } + + if status.code() == Some(1073i32) { + bail!("clash verge service is installed"); + } + + bail!( + "failed to install service with status {}", + status.code().unwrap() + ) + } + + /// Uninstall the Clash Verge Service + /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程 + pub async fn uninstall_service() -> Result<()> { + let token = Token::with_current_process()?; + let level = token.privilege_level()?; + + 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 { + let url = format!("{SERVICE_URL}/get_clash"); + let response = reqwest::get(url) + .await + .context("failed to connect to the Clash Verge Service")? + .json::() + .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::() + .await + .context("failed to parse the Clash Verge Service response")?; + + if res.code != 0 { + bail!(res.msg); + } Ok(()) } - /// uninstall - pub fn uninstall_service(&mut self) -> Result<()> { - let token = Token::with_current_process()?; - let level = token.privilege_level()?; + /// 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::() + .await + .context("failed to parse the Clash Verge Service response")?; - 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}"), - } - }); + if res.code != 0 { + bail!(res.msg); + } Ok(()) } diff --git a/src-tauri/src/core/verge.rs b/src-tauri/src/core/verge.rs index 1826590..a5bf19d 100644 --- a/src-tauri/src/core/verge.rs +++ b/src-tauri/src/core/verge.rs @@ -21,6 +21,10 @@ pub struct Verge { /// clash tun mode pub enable_tun_mode: Option, + /// windows service mode + #[serde(skip_serializing_if = "Option::is_none")] + pub enable_service_mode: Option, + /// can the app auto startup pub enable_auto_launch: Option, @@ -119,6 +123,9 @@ impl Verge { if patch.enable_tun_mode.is_some() { self.enable_tun_mode = patch.enable_tun_mode; } + if patch.enable_service_mode.is_some() { + self.enable_service_mode = patch.enable_service_mode; + } self.save_file() } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 906c5f0..11b9b54 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -126,7 +126,12 @@ fn main() -> std::io::Result<()> { cmds::change_profile_chain, cmds::change_profile_valid, cmds::read_profile_file, - cmds::save_profile_file + cmds::save_profile_file, + // service mode + cmds::service::start_service, + cmds::service::check_service, + cmds::service::install_service, + cmds::service::uninstall_service, ]); #[cfg(target_os = "macos")] diff --git a/src-tauri/src/utils/dirs.rs b/src-tauri/src/utils/dirs.rs index cfb3541..90bd2a2 100644 --- a/src-tauri/src/utils/dirs.rs +++ b/src-tauri/src/utils/dirs.rs @@ -113,3 +113,18 @@ pub fn service_path() -> PathBuf { res_dir.join(SERVICE_PATH) } } + +#[cfg(windows)] +pub fn service_log_file() -> PathBuf { + use chrono::Local; + + let log_dir = app_logs_dir().join("service"); + + let local_time = Local::now().format("%Y-%m-%d-%H%M%S").to_string(); + let log_file = format!("{}.log", local_time); + let log_file = log_dir.join(log_file); + + std::fs::create_dir_all(&log_dir).unwrap(); + + log_file +}