diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 19fb739325c7753ac1b0af62a916c986587d1a0a..e7a3d2b29bd40bae4c517aa61bae0a754a06e0c9 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -5,7 +5,7 @@ use crate::{ use crate::{log_if_err, ret_err, wrap_err}; use anyhow::Result; use serde_yaml::Mapping; -use std::collections::HashMap; +use std::collections::{HashMap, VecDeque}; use tauri::{api, State}; type CmdResult<T = ()> = Result<T, String>; @@ -256,6 +256,12 @@ pub fn get_cur_proxy(core: State<'_, Core>) -> CmdResult<Option<SysProxyConfig>> wrap_err!(sysopt.get_sysproxy()) } +#[tauri::command] +pub fn get_clash_logs(core: State<'_, Core>) -> CmdResult<VecDeque<String>> { + let service = core.service.lock(); + Ok(service.get_logs()) +} + /// open app config dir #[tauri::command] pub fn open_app_dir() -> Result<(), String> { diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 70b0e5c44d162bbd725f4d751d631922d5bfd3f9..f3bd3f9fd3396d573381e179bd1bf6aaa4958504 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -129,6 +129,7 @@ impl Core { let mut service = self.service.lock(); service.stop()?; service.set_core(Some(clash_core)); + service.clear_logs(); service.start()?; drop(service); diff --git a/src-tauri/src/core/service.rs b/src-tauri/src/core/service.rs index 8557dcd74d498ead3eb6fd8abb196a04b1135aa0..7c29e6f3e8526a01aa5993147e257f1d299d5e23 100644 --- a/src-tauri/src/core/service.rs +++ b/src-tauri/src/core/service.rs @@ -2,28 +2,38 @@ use super::{notice::Notice, ClashInfo}; 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::{collections::HashMap, time::Duration}; +use std::sync::Arc; +use std::{ + collections::{HashMap, VecDeque}, + time::Duration, +}; use tauri::api::process::{Command, CommandChild, CommandEvent}; use tokio::time::sleep; static mut CLASH_CORE: &str = "clash"; +const LOGS_QUEUE_LEN: usize = 100; #[derive(Debug)] pub struct Service { sidecar: Option<CommandChild>, + logs: Arc<RwLock<VecDeque<String>>>, + #[allow(unused)] 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)), service_mode: false, } } @@ -39,46 +49,46 @@ impl Service { self.service_mode = enable; } - #[cfg(not(windows))] pub fn start(&mut self) -> Result<()> { - self.start_clash_by_sidecar() - } + #[cfg(not(windows))] + self.start_clash_by_sidecar()?; - #[cfg(windows)] - pub fn start(&mut self) -> Result<()> { - if !self.service_mode { - return self.start_clash_by_sidecar(); - } + #[cfg(windows)] + { + 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 { - log_if_err!(Self::start_clash_by_service().await); + tauri::async_runtime::spawn(async move { + match Self::check_service().await { + Ok(status) => { + // 未å¯åŠ¨clash + if status.code != 0 { + log_if_err!(Self::start_clash_by_service().await); + } } + Err(err) => log::error!(target: "app", "{err}"), } - Err(err) => log::error!(target: "app", "{err}"), - } - }); + }); + } Ok(()) } - #[cfg(not(windows))] pub fn stop(&mut self) -> Result<()> { - self.stop_clash_by_sidecar() - } + #[cfg(not(windows))] + self.stop_clash_by_sidecar()?; - #[cfg(windows)] - pub fn stop(&mut self) -> Result<()> { - if !self.service_mode { - return self.stop_clash_by_sidecar(); - } + #[cfg(windows)] + { + if !self.service_mode { + return self.stop_clash_by_sidecar(); + } - tauri::async_runtime::spawn(async move { - log_if_err!(Self::stop_clash_by_service().await); - }); + tauri::async_runtime::spawn(async move { + log_if_err!(Self::stop_clash_by_service().await); + }); + } Ok(()) } @@ -88,6 +98,24 @@ impl Service { 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(); + } + /// start the clash sidecar fn start_clash_by_sidecar(&mut self) -> Result<()> { if self.sidecar.is_some() { @@ -112,14 +140,28 @@ impl Service { self.sidecar = Some(cmd_child); // clash log + 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 stdout = if line.len() > 33 { &line[33..] } else { &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::Stderr(err) => log::error!(target: "app" ,"[clash error]: {}", err), _ => {} } } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fff8cb1088bcec8edeafc7204715de36bbaf65bf..542eb7a5b2527ebee2693b667ccb5356c36fe2b4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -118,6 +118,7 @@ fn main() -> std::io::Result<()> { cmds::restart_sidecar, // clash cmds::get_clash_info, + cmds::get_clash_logs, cmds::patch_clash_config, cmds::change_clash_core, cmds::get_runtime_config, diff --git a/src/components/layout/use-log-setup.ts b/src/components/layout/use-log-setup.ts index 9c5f0d50c24c36dbd1357a6d5245f2e629d1fa52..2d05477061180171877241099634b337793c4ad1 100644 --- a/src/components/layout/use-log-setup.ts +++ b/src/components/layout/use-log-setup.ts @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import { useSetRecoilState } from "recoil"; import { listen } from "@tauri-apps/api/event"; import { getInformation } from "@/services/api"; +import { getClashLogs } from "@/services/cmds"; import { atomLogData } from "@/services/states"; const MAX_LOG_NUM = 1000; @@ -13,6 +14,8 @@ export default function useLogSetup() { const setLogData = useSetRecoilState(atomLogData); useEffect(() => { + getClashLogs().then(setLogData); + let ws: WebSocket = null!; const handler = (event: MessageEvent<any>) => { diff --git a/src/services/cmds.ts b/src/services/cmds.ts index a5844f4992743a0d02209a9674010ddea791f189..e4f9ebf4082e76ad08140a627a58ca9466c2642c 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -1,6 +1,29 @@ import { invoke } from "@tauri-apps/api/tauri"; import Notice from "@/components/base/base-notice"; +export async function getClashLogs() { + const regex = /time="(.+?)"\s+level=(.+?)\s+msg="(.+?)"/; + const newRegex = /(.+?)\s+(.+?)\s+(.+)/; + const logs = await invoke<string[]>("get_clash_logs"); + + return logs + .map((log) => { + const result = log.match(regex); + if (result) { + const [_, time, type, payload] = result; + return { time, type, payload }; + } + + const result2 = log.match(newRegex); + if (result2) { + const [_, time, type, payload] = result2; + return { time, type, payload }; + } + return null; + }) + .filter(Boolean) as ApiType.LogItem[]; +} + export async function getProfiles() { return invoke<CmdType.ProfilesConfig>("get_profiles"); }