123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441 |
- use super::{notice::Notice, ClashInfo};
- use crate::log_if_err;
- use crate::utils::{config, dirs};
- use anyhow::{bail, Result};
- use reqwest::header::HeaderMap;
- use serde_yaml::Mapping;
- use std::{collections::HashMap, time::Duration};
- use tauri::api::process::{Command, CommandChild, CommandEvent};
- use tokio::time::sleep;
- static mut CLASH_CORE: &str = "clash";
- #[derive(Debug)]
- pub struct Service {
- sidecar: Option<CommandChild>,
- #[allow(unused)]
- service_mode: bool,
- }
- impl Service {
- pub fn new() -> Service {
- Service {
- sidecar: None,
- service_mode: false,
- }
- }
- pub fn set_core(&mut self, clash_core: Option<String>) {
- unsafe {
- CLASH_CORE = Box::leak(clash_core.unwrap_or("clash".into()).into_boxed_str());
- }
- }
- #[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");
- }
- let app_dir = dirs::app_home_dir();
- let app_dir = app_dir.as_os_str().to_str().unwrap();
- let clash_core = unsafe { CLASH_CORE };
- let cmd = Command::new_sidecar(clash_core)?;
- let (mut rx, cmd_child) = cmd.args(["-d", app_dir]).spawn()?;
- self.sidecar = Some(cmd_child);
- // clash log
- tauri::async_runtime::spawn(async move {
- while let Some(event) = rx.recv().await {
- match event {
- CommandEvent::Stdout(line) => log::info!("[clash]: {}", line),
- CommandEvent::Stderr(err) => log::error!("[clash]: {}", err),
- _ => {}
- }
- }
- });
- Ok(())
- }
- /// stop the clash sidecar
- fn stop_clash_by_sidecar(&mut self) -> Result<()> {
- if let Some(sidecar) = self.sidecar.take() {
- sidecar.kill()?;
- }
- Ok(())
- }
- /// update clash config
- /// using PUT methods
- pub fn set_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> {
- if !self.service_mode && self.sidecar.is_none() {
- bail!("did not start sidecar");
- }
- let temp_path = dirs::profiles_temp_path();
- config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
- let (server, headers) = Self::clash_client_info(info)?;
- tauri::async_runtime::spawn(async move {
- let mut data = HashMap::new();
- data.insert("path", temp_path.as_os_str().to_str().unwrap());
- // retry 5 times
- for _ in 0..5 {
- match reqwest::ClientBuilder::new().no_proxy().build() {
- Ok(client) => {
- let builder = client.put(&server).headers(headers.clone()).json(&data);
- match builder.send().await {
- Ok(resp) => {
- if resp.status() != 204 {
- log::error!("failed to activate clash with status \"{}\"", resp.status());
- }
- notice.refresh_clash();
- // do not retry
- break;
- }
- Err(err) => log::error!("failed to activate for `{err}`"),
- }
- }
- Err(err) => log::error!("failed to activate for `{err}`"),
- }
- sleep(Duration::from_millis(500)).await;
- }
- });
- Ok(())
- }
- /// patch clash config
- pub fn patch_config(&self, info: ClashInfo, config: Mapping, notice: Notice) -> Result<()> {
- if !self.service_mode && self.sidecar.is_none() {
- bail!("did not start sidecar");
- }
- let (server, headers) = Self::clash_client_info(info)?;
- tauri::async_runtime::spawn(async move {
- if let Ok(client) = reqwest::ClientBuilder::new().no_proxy().build() {
- let builder = client.patch(&server).headers(headers.clone()).json(&config);
- match builder.send().await {
- Ok(_) => notice.refresh_clash(),
- Err(err) => log::error!("{err}"),
- }
- }
- });
- Ok(())
- }
- /// get clash client url and headers from clash info
- fn clash_client_info(info: ClashInfo) -> Result<(String, HeaderMap)> {
- if info.server.is_none() {
- if info.port.is_none() {
- bail!("failed to parse config.yaml file");
- } else {
- bail!("failed to parse the server");
- }
- }
- let server = info.server.unwrap();
- let server = format!("http://{server}/configs");
- let mut headers = HeaderMap::new();
- headers.insert("Content-Type", "application/json".parse().unwrap());
- if let Some(secret) = info.secret.as_ref() {
- let secret = format!("Bearer {}", secret.clone()).parse().unwrap();
- headers.insert("Authorization", secret);
- }
- Ok((server, headers))
- }
- }
- impl Drop for Service {
- fn drop(&mut self) {
- log_if_err!(self.stop());
- }
- }
- /// ### Service Mode
- ///
- #[cfg(windows)]
- pub mod win_service {
- use super::*;
- use anyhow::Context;
- use deelevate::{PrivilegeLevel, Token};
- use runas::Command as RunasCommand;
- use std::os::windows::process::CommandExt;
- use std::{env::current_exe, process::Command as StdCommand};
- 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
- /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
- pub async fn install_service() -> Result<()> {
- let binary_path = dirs::service_path();
- let install_path = binary_path.with_file_name("install-service.exe");
- if !install_path.exists() {
- bail!("installer exe not found");
- }
- let token = Token::with_current_process()?;
- let level = token.privilege_level()?;
- let status = match level {
- PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).status()?,
- _ => StdCommand::new(install_path)
- .creation_flags(0x08000000)
- .status()?,
- };
- if !status.success() {
- bail!(
- "failed to install service with status {}",
- status.code().unwrap()
- );
- }
- Ok(())
- }
- /// Uninstall the Clash Verge Service
- /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
- pub async fn uninstall_service() -> Result<()> {
- let binary_path = dirs::service_path();
- let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
- 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).status()?,
- _ => StdCommand::new(uninstall_path)
- .creation_flags(0x08000000)
- .status()?,
- };
- if !status.success() {
- bail!(
- "failed to uninstall service with status {}",
- status.code().unwrap()
- );
- }
- Ok(())
- }
- /// [deprecated]
- /// 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 => RunasCommand::new("sc").args(&args).status()?,
- _ => StdCommand::new("sc").args(&args).status()?,
- };
- match status.success() {
- true => Ok(()),
- false => bail!(
- "failed to start service with status {}",
- status.code().unwrap()
- ),
- }
- }
- /// stop service
- pub async fn stop_service() -> Result<()> {
- let url = format!("{SERVICE_URL}/stop_service");
- let res = reqwest::ClientBuilder::new()
- .no_proxy()
- .build()?
- .post(url)
- .send()
- .await?
- .json::<JsonResponse>()
- .await
- .context("failed to connect to the Clash Verge Service")?;
- if res.code != 0 {
- bail!(res.msg);
- }
- Ok(())
- }
- /// check the windows service status
- pub async fn check_service() -> Result<JsonResponse> {
- let url = format!("{SERVICE_URL}/get_clash");
- let response = reqwest::ClientBuilder::new()
- .no_proxy()
- .build()?
- .get(url)
- .send()
- .await?
- .json::<JsonResponse>()
- .await
- .context("failed to connect to the Clash Verge Service")?;
- 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 clash_core = unsafe { CLASH_CORE };
- let clash_bin = format!("{clash_core}.exe");
- let bin_path = current_exe().unwrap().with_file_name(clash_bin);
- 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::ClientBuilder::new()
- .no_proxy()
- .build()?
- .post(url)
- .json(&map)
- .send()
- .await?
- .json::<JsonResponse>()
- .await
- .context("failed to connect to the Clash Verge Service")?;
- 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::ClientBuilder::new()
- .no_proxy()
- .build()?
- .post(url)
- .send()
- .await?
- .json::<JsonResponse>()
- .await
- .context("failed to connect to the Clash Verge Service")?;
- if res.code != 0 {
- bail!(res.msg);
- }
- Ok(())
- }
- }
- }
|