123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- use crate::data::{ClashInfo, Data};
- use crate::log_if_err;
- use crate::utils::{config, dirs};
- use anyhow::{bail, Result};
- use parking_lot::RwLock;
- use reqwest::header::HeaderMap;
- use serde_yaml::Mapping;
- use std::fs;
- use std::io::Write;
- use std::sync::Arc;
- use std::{
- collections::{HashMap, VecDeque},
- time::Duration,
- };
- use tauri::api::process::{Command, CommandChild, CommandEvent};
- use tokio::time::sleep;
- const LOGS_QUEUE_LEN: usize = 100;
- #[derive(Debug)]
- pub struct Service {
- sidecar: Option<CommandChild>,
- logs: Arc<RwLock<VecDeque<String>>>,
- #[allow(unused)]
- use_service_mode: bool,
- }
- impl Service {
- pub fn new() -> Service {
- let queue = VecDeque::with_capacity(LOGS_QUEUE_LEN + 10);
- Service {
- sidecar: None,
- logs: Arc::new(RwLock::new(queue)),
- use_service_mode: false,
- }
- }
- pub fn start(&mut self) -> Result<()> {
- #[cfg(not(target_os = "windows"))]
- self.start_clash_by_sidecar()?;
- #[cfg(target_os = "windows")]
- {
- let enable = {
- let data = Data::global();
- let verge = data.verge.lock();
- verge.enable_service_mode.clone().unwrap_or(false)
- };
- self.use_service_mode = enable;
- if !enable {
- return self.start_clash_by_sidecar();
- }
- tauri::async_runtime::spawn(async move {
- match Self::check_service().await {
- Ok(status) => {
-
- if status.code != 0 {
- log_if_err!(Self::start_clash_by_service().await);
- }
- }
- Err(err) => log::error!(target: "app", "{err}"),
- }
- });
- }
- Ok(())
- }
- pub fn stop(&mut self) -> Result<()> {
- #[cfg(not(target_os = "windows"))]
- self.stop_clash_by_sidecar()?;
- #[cfg(target_os = "windows")]
- {
- let _ = self.stop_clash_by_sidecar();
- if self.use_service_mode {
- tauri::async_runtime::block_on(async move {
- log_if_err!(Self::stop_clash_by_service().await);
- });
- }
- }
- Ok(())
- }
- pub fn restart(&mut self) -> Result<()> {
- self.stop()?;
- self.start()
- }
- pub fn get_logs(&self) -> VecDeque<String> {
- self.logs.read().clone()
- }
- #[allow(unused)]
- pub fn set_logs(&self, text: String) {
- let mut logs = self.logs.write();
- if logs.len() > LOGS_QUEUE_LEN {
- (*logs).pop_front();
- }
- (*logs).push_back(text);
- }
- pub fn clear_logs(&self) {
- let mut logs = self.logs.write();
- (*logs).clear();
- }
-
- fn start_clash_by_sidecar(&mut self) -> Result<()> {
- if self.sidecar.is_some() {
- let sidecar = self.sidecar.take().unwrap();
- let _ = sidecar.kill();
- }
- let clash_core: String = {
- let global = Data::global();
- let verge = global.verge.lock();
- verge.clash_core.clone().unwrap_or("clash".into())
- };
- let app_dir = dirs::app_home_dir();
- let app_dir = app_dir.as_os_str().to_str().unwrap();
-
- let args = match clash_core.as_str() {
- "clash-meta" => vec!["-m", "-d", app_dir],
- _ => vec!["-d", app_dir],
- };
- let cmd = Command::new_sidecar(clash_core)?;
- let (mut rx, cmd_child) = cmd.args(args).spawn()?;
-
- let pid = cmd_child.pid();
- log_if_err!(|| -> Result<()> {
- let path = dirs::clash_pid_path();
- fs::File::create(path)?.write(format!("{pid}").as_bytes())?;
- Ok(())
- }());
- self.sidecar = Some(cmd_child);
-
- let logs = self.logs.clone();
- tauri::async_runtime::spawn(async move {
- let write_log = |text: String| {
- let mut logs = logs.write();
- if logs.len() >= LOGS_QUEUE_LEN {
- (*logs).pop_front();
- }
- (*logs).push_back(text);
- };
- while let Some(event) = rx.recv().await {
- match event {
- CommandEvent::Stdout(line) => {
- let can_short = line.starts_with("time=") && line.len() > 33;
- let stdout = if can_short { &line[33..] } else { &line };
- log::info!(target: "app" ,"[clash]: {}", stdout);
- write_log(line);
- }
- CommandEvent::Stderr(err) => {
- log::error!(target: "app" ,"[clash error]: {}", err);
- write_log(err);
- }
- CommandEvent::Error(err) => log::error!(target: "app" ,"{err}"),
- CommandEvent::Terminated(_) => break,
- _ => {}
- }
- }
- });
- Ok(())
- }
-
- fn stop_clash_by_sidecar(&mut self) -> Result<()> {
- if let Some(sidecar) = self.sidecar.take() {
- sidecar.kill()?;
- }
- Ok(())
- }
- pub fn check_start(&mut self) -> Result<()> {
- #[cfg(target_os = "windows")]
- {
- let global = Data::global();
- let verge = global.verge.lock();
- let service_mode = verge.enable_service_mode.unwrap_or(false);
- if !service_mode && self.sidecar.is_none() {
- self.start()?;
- }
- }
- #[cfg(not(target_os = "windows"))]
- if self.sidecar.is_none() {
- self.start()?;
- }
- Ok(())
- }
-
-
- pub async fn set_config(info: ClashInfo, config: Mapping) -> Result<()> {
- let temp_path = dirs::clash_runtime_yaml();
- config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
- let (server, headers) = Self::clash_client_info(info)?;
- let mut data = HashMap::new();
- data.insert("path", temp_path.as_os_str().to_str().unwrap());
- macro_rules! report_err {
- ($i: expr, $e: expr) => {
- match $i {
- 4 => bail!($e),
- _ => log::error!(target: "app", $e),
- }
- };
- }
-
- for i in 0..5 {
- let headers = headers.clone();
- match reqwest::ClientBuilder::new().no_proxy().build() {
- Ok(client) => {
- let builder = client.put(&server).headers(headers).json(&data);
- match builder.send().await {
- Ok(resp) => match resp.status().as_u16() {
- 204 => break,
-
- 400 => bail!("failed to update clash config with status 400"),
- status @ _ => {
- report_err!(i, "failed to activate clash with status \"{status}\"")
- }
- },
- Err(err) => report_err!(i, "{err}"),
- }
- }
- Err(err) => report_err!(i, "{err}"),
- }
- sleep(Duration::from_millis(500)).await;
- }
- Ok(())
- }
-
- pub async fn patch_config(info: ClashInfo, config: Mapping) -> Result<()> {
- let (server, headers) = Self::clash_client_info(info)?;
- let client = reqwest::ClientBuilder::new().no_proxy().build()?;
- let builder = client.patch(&server).headers(headers.clone()).json(&config);
- builder.send().await?;
- Ok(())
- }
-
- fn clash_client_info(info: ClashInfo) -> Result<(String, HeaderMap)> {
- if info.server.is_none() {
- let status = &info.status;
- if info.port.is_none() {
- bail!("failed to parse config.yaml file with status {status}");
- } else {
- bail!("failed to parse the server with status {status}");
- }
- }
- 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))
- }
-
- pub fn kill_old_clash() {
- use sysinfo::{Pid, PidExt, ProcessExt, System, SystemExt};
- if let Ok(pid) = fs::read(dirs::clash_pid_path()) {
- if let Ok(pid) = String::from_utf8_lossy(&pid).parse() {
- let mut system = System::new();
- system.refresh_all();
- let proc = system.process(Pid::from_u32(pid));
- if let Some(proc) = proc {
- proc.kill();
- }
- }
- }
- }
- }
- impl Drop for Service {
- fn drop(&mut self) {
- log_if_err!(self.stop());
- }
- }
- #[cfg(target_os = "windows")]
- pub mod win_service {
- use super::*;
- use anyhow::Context;
- use deelevate::{PrivilegeLevel, Token};
- use runas::Command as RunasCommand;
- use serde::{Deserialize, Serialize};
- 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 {
-
-
- 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(())
- }
-
-
- 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(())
- }
-
-
-
- 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()
- ),
- }
- }
-
- 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(())
- }
-
- 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)
- }
-
- 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 = {
- let global = Data::global();
- let verge = global.verge.lock();
- verge.clash_core.clone().unwrap_or("clash".into())
- };
- 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(())
- }
-
- 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(())
- }
- }
- }
|