From 670333fcc2896b8b7b8c9b5234357a36e6a60ebe Mon Sep 17 00:00:00 2001 From: Tunglies Date: Tue, 15 Apr 2025 17:57:41 +0800 Subject: [PATCH] platform: windows support --- .../src_crates/crate_mihomo_api/Cargo.toml | 14 +- .../crate_mihomo_api/scripts/test_windows.ps1 | 13 ++ .../src_crates/crate_mihomo_api/src/lib.rs | 1 + .../src_crates/crate_mihomo_api/src/model.rs | 11 +- .../crate_mihomo_api/src/platform/mod.rs | 4 + .../crate_mihomo_api/src/platform/unix.rs | 60 ++++++++ .../crate_mihomo_api/src/platform/windows.rs | 129 ++++++++++++++++++ .../src_crates/crate_mihomo_api/src/sock.rs | 47 ++----- .../crate_mihomo_api/tests/test_mihomo_api.rs | 16 ++- 9 files changed, 244 insertions(+), 51 deletions(-) create mode 100644 src-tauri/src_crates/crate_mihomo_api/scripts/test_windows.ps1 create mode 100644 src-tauri/src_crates/crate_mihomo_api/src/platform/mod.rs create mode 100644 src-tauri/src_crates/crate_mihomo_api/src/platform/unix.rs create mode 100644 src-tauri/src_crates/crate_mihomo_api/src/platform/windows.rs diff --git a/src-tauri/src_crates/crate_mihomo_api/Cargo.toml b/src-tauri/src_crates/crate_mihomo_api/Cargo.toml index b1dbfbfe..c7ce1974 100644 --- a/src-tauri/src_crates/crate_mihomo_api/Cargo.toml +++ b/src-tauri/src_crates/crate_mihomo_api/Cargo.toml @@ -4,18 +4,18 @@ edition = "2024" [dependencies] async-trait = "0.1.88" +futures = "0.3.31" http-body-util = "0.1.3" hyper = { version = "1.6.0", features = ["http1", "client"] } hyper-util = "0.1.11" -hyperlocal = "0.9.1" serde_json = "1.0.140" -tokio = { version = "1.44.1", features = [ - "rt", - "macros", - "rt-multi-thread", - "io-std", -] } +time = "0.3.41" +tokio = { version = "1.44.1", features = ["rt", "macros", "rt-multi-thread", "io-std", "net", "io-util", "time"] } +tokio-util = { version = "0.7.14", features = ["codec"] } [dev-dependencies] dotenv = "0.15.0" lazy_static = "1.5.0" + +[target.'cfg(unix)'.dependencies] +hyperlocal = "0.9.1" diff --git a/src-tauri/src_crates/crate_mihomo_api/scripts/test_windows.ps1 b/src-tauri/src_crates/crate_mihomo_api/scripts/test_windows.ps1 new file mode 100644 index 00000000..d4b5e62e --- /dev/null +++ b/src-tauri/src_crates/crate_mihomo_api/scripts/test_windows.ps1 @@ -0,0 +1,13 @@ +$pipeName = "\\.\pipe\mihomo" +$pipe = new-object System.IO.Pipes.NamedPipeClientStream(".", "mihomo", [System.IO.Pipes.PipeDirection]::InOut) +$pipe.Connect(1000) # 尝试连接 1 秒 +if ($pipe.IsConnected) { + Write-Host "成功连接到管道" + # 示例写入或读取可以加上如下内容 + # $writer = new-object System.IO.StreamWriter($pipe) + # $writer.WriteLine("hello pipe") + # $writer.Flush() + $pipe.Close() +} else { + Write-Host "连接失败" +} diff --git a/src-tauri/src_crates/crate_mihomo_api/src/lib.rs b/src-tauri/src_crates/crate_mihomo_api/src/lib.rs index 5aac4768..db3c769a 100644 --- a/src-tauri/src_crates/crate_mihomo_api/src/lib.rs +++ b/src-tauri/src_crates/crate_mihomo_api/src/lib.rs @@ -46,3 +46,4 @@ pub use model::E; pub use model::MihomoData; pub use model::MihomoManager; pub mod sock; +pub mod platform; \ No newline at end of file diff --git a/src-tauri/src_crates/crate_mihomo_api/src/model.rs b/src-tauri/src_crates/crate_mihomo_api/src/model.rs index 3baedb03..1e96594e 100644 --- a/src-tauri/src_crates/crate_mihomo_api/src/model.rs +++ b/src-tauri/src_crates/crate_mihomo_api/src/model.rs @@ -1,8 +1,5 @@ use async_trait::async_trait; -use http_body_util::Full; -use hyper::{Method, body::Bytes}; -use hyper_util::client::legacy::Client; -use hyperlocal::{UnixConnector, Uri}; +use hyper::Method; use serde_json::Value; use std::{error::Error, sync::Arc}; use tokio::sync::Mutex; @@ -29,13 +26,14 @@ pub trait MihomoClient: Sized { async fn set_data_providers_proxies(&self, data: Value); async fn get_data_proxies(&self) -> Value; async fn get_data_providers_proxies(&self) -> Value; - async fn generate_unix_path(&self, path: &str) -> Uri; + // async fn generate_unix_path(&self, path: &str) -> Uri; async fn send_request( &self, path: &str, method: Method, body: Option, ) -> Result; + async fn get_version(&self) -> Result; async fn is_mihomo_running(&self) -> Result<(), E>; async fn put_configs_force(&self, clash_config_path: &str) -> Result<(), E>; async fn patch_configs(&self, config: Value) -> Result<(), E>; @@ -51,8 +49,9 @@ pub trait MihomoClient: Sized { ) -> Result; } +use crate::platform::Client; pub struct MihomoManager { pub(super) socket_path: String, - pub(super) client: Arc>>>, + pub(super) client: Arc>, pub(super) data: Arc>, } diff --git a/src-tauri/src_crates/crate_mihomo_api/src/platform/mod.rs b/src-tauri/src_crates/crate_mihomo_api/src/platform/mod.rs new file mode 100644 index 00000000..01d92805 --- /dev/null +++ b/src-tauri/src_crates/crate_mihomo_api/src/platform/mod.rs @@ -0,0 +1,4 @@ +#[cfg(unix)] pub mod unix; +#[cfg(unix)] pub use unix::UnixClient as Client; +#[cfg(windows)] pub mod windows; +#[cfg(windows)] pub use windows::WindowsClient as Client; \ No newline at end of file diff --git a/src-tauri/src_crates/crate_mihomo_api/src/platform/unix.rs b/src-tauri/src_crates/crate_mihomo_api/src/platform/unix.rs new file mode 100644 index 00000000..819f6ce4 --- /dev/null +++ b/src-tauri/src_crates/crate_mihomo_api/src/platform/unix.rs @@ -0,0 +1,60 @@ +// #[cfg(any(target_os = "linux", target_os = "macos"))] +use crate::model::E; +use http_body_util::{BodyExt, Full}; +use hyper::{ + Method, Request, + body::Bytes, + header::{HeaderName, HeaderValue}, +}; +use hyper_util::client::legacy::Client; +use hyperlocal::{UnixClientExt, Uri}; +use serde_json::Value; +use std::sync::Arc; +use tokio::sync::Mutex; + +pub struct UnixClient { + client: Arc>>>, +} + +impl UnixClient { + pub fn new() -> Self { + let client: Client<_, Full> = Client::unix(); + Self { + client: Arc::new(Mutex::new(client)), + } + } + + pub async fn generate_unix_path(&self, socket_path: &str, path: &str) -> Uri { + Uri::new(socket_path, path).into() + } + + pub async fn send_request( + &self, + socket_path: &str, + path: &str, + method: Method, + body: Option, + ) -> Result { + let uri = self.generate_unix_path(socket_path, path).await; + + let mut request_builder = Request::builder().method(method).uri(uri); + + let body_bytes = if let Some(body) = body { + request_builder = request_builder.header( + HeaderName::from_static("Content-Type"), + HeaderValue::from_static("application/json"), + ); + Bytes::from(serde_json::to_vec(&body)?) + } else { + Bytes::new() + }; + + let request = request_builder.body(Full::new(body_bytes))?; + + let response = self.client.lock().await.request(request).await?; + let body_bytes = response.into_body().collect().await?.to_bytes(); + let json_value = serde_json::from_slice(&body_bytes)?; + + Ok(json_value) + } +} \ No newline at end of file diff --git a/src-tauri/src_crates/crate_mihomo_api/src/platform/windows.rs b/src-tauri/src_crates/crate_mihomo_api/src/platform/windows.rs new file mode 100644 index 00000000..956d5df3 --- /dev/null +++ b/src-tauri/src_crates/crate_mihomo_api/src/platform/windows.rs @@ -0,0 +1,129 @@ +use crate::{model::E, sock}; +use hyper::Method; +use serde_json::Value; +use tokio_util::codec::{Framed, LinesCodec}; +use std::{sync::Arc, time::Duration}; +use tokio::{ + time::timeout, + sync::Mutex, +}; +use futures::{SinkExt, StreamExt}; +use tokio::net::windows::named_pipe::ClientOptions; + +pub struct WindowsClient { + lock: Arc>, +} + +impl WindowsClient { + pub fn new() -> Self { + Self { + lock: Arc::new(Mutex::new(())), + } + } + + pub async fn send_request( + &self, + socket_path: String, + path: &str, + method: Method, + body: Option, + ) -> Result { + // Acquire lock before opening pipe + // let _guard = self.lock.lock().await; + + // Attempt to open the pipe with retry logic + let mut retries = 0; + let pipe = loop { + match ClientOptions::new().open(socket_path.clone()) { + Ok(pipe) => break pipe, + Err(e) if e.raw_os_error() == Some(231) && retries < 5 => { + retries += 1; + let delay = Duration::from_millis(200 * retries); + tokio::time::sleep(delay).await; + continue; + } + Err(e) => return Err(e.into()), + } + }; + + // Use a scope to ensure the pipe is dropped when done + let result = async { + let mut framed = Framed::new(pipe, LinesCodec::new()); + + // Build request + let mut request = format!( + "{} {} HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\n", + method.as_str(), + path + ); + + if let Some(ref json_body) = body { + let body_str = json_body.to_string(); + request += &format!("Content-Length: {}\r\n\r\n{}", body_str.len(), body_str); + } else { + request += "\r\n"; + } + + framed.send(request).await?; + + // Parse headers + let mut headers_done = false; + let mut is_chunked = false; + + while let Ok(Some(Ok(line))) = timeout(Duration::from_secs(5), framed.next()).await { + if line.is_empty() { + headers_done = true; + break; + } + + if line.starts_with("HTTP/1.1 4") || line.starts_with("HTTP/1.1 5") { + return Err(format!("Server error: {}", line).into()); + } + + if line.eq_ignore_ascii_case("Transfer-Encoding: chunked") { + is_chunked = true; + } + } + + if !headers_done { + return Err("Malformed response: no headers end".into()); + } + + let mut response_body = String::new(); + + if is_chunked { + // Handle chunked encoding + loop { + // Read chunk size line + let chunk_size_line = match timeout(Duration::from_secs(5), framed.next()).await { + Ok(Some(Ok(line))) => line, + _ => break, + }; + + let chunk_size = match usize::from_str_radix(chunk_size_line.trim(), 16) { + Ok(0) => break, // End of chunks + Ok(_) => (), // We don't actually need the size with LinesCodec + Err(_) => return Err("Invalid chunk size".into()), + }; + + // Read chunk data line + if let Ok(Some(Ok(data_line))) = timeout(Duration::from_secs(5), framed.next()).await { + response_body.push_str(&data_line); + } + + // Skip trailing CRLF (empty line) + let _ = framed.next().await; + } + } else { + // Handle normal content + while let Ok(Some(Ok(line))) = timeout(Duration::from_secs(5), framed.next()).await { + response_body.push_str(&line); + } + } + + serde_json::from_str(&response_body).map_err(|e| e.into()) + }.await; + + result + } +} \ No newline at end of file diff --git a/src-tauri/src_crates/crate_mihomo_api/src/sock.rs b/src-tauri/src_crates/crate_mihomo_api/src/sock.rs index 00519f81..593c50c6 100644 --- a/src-tauri/src_crates/crate_mihomo_api/src/sock.rs +++ b/src-tauri/src_crates/crate_mihomo_api/src/sock.rs @@ -1,16 +1,10 @@ use crate::model::E; use async_trait::async_trait; -use http_body_util::{BodyExt, Full}; -use hyper::{ - Method, Request, - body::Bytes, - header::{HeaderName, HeaderValue}, -}; -use hyper_util::client::legacy::Client; -use hyperlocal::{UnixClientExt, Uri}; +use hyper::Method; use serde_json::Value; use std::sync::Arc; use tokio::sync::Mutex; +use crate::platform::Client; use crate::{ MihomoData, @@ -19,7 +13,7 @@ use crate::{ impl MihomoManager { pub fn new(socket_path: String) -> Self { - let client: Client<_, Full> = Client::unix(); + let client = Client::new(); Self { socket_path, client: Arc::new(Mutex::new(client)), @@ -46,40 +40,23 @@ impl MihomoClient for MihomoManager { self.data.lock().await.providers_proxies.clone() } - async fn generate_unix_path(&self, path: &str) -> Uri { - Uri::new(self.socket_path.clone(), path).into() - } - async fn send_request( &self, path: &str, method: Method, body: Option, ) -> Result { - let uri = self.generate_unix_path(path).await; - - let mut request_builder = Request::builder().method(method).uri(uri); - - let body_bytes = if let Some(body) = body { - request_builder = request_builder.header( - HeaderName::from_static("Content-Type"), - HeaderValue::from_static("application/json"), - ); - Bytes::from(serde_json::to_vec(&body)?) - } else { - Bytes::new() - }; - - let request = request_builder.body(Full::new(body_bytes))?; - - let response = self.client.lock().await.request(request).await?; - let body_bytes = response.into_body().collect().await?.to_bytes(); - let json_value = serde_json::from_slice(&body_bytes)?; - - Ok(json_value) + let client = self.client.lock().await; + client.send_request(self.socket_path.clone(), path, method, body).await } + + async fn get_version(&self) -> Result { + let data = self.send_request("/version", Method::GET, None).await?; + Ok(data) + } + async fn is_mihomo_running(&self) -> Result<(), E> { - let _ = self.send_request("/version", Method::GET, None).await?; + self.get_version().await?; Ok(()) } diff --git a/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs b/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs index f8d6df7e..50f95dfb 100644 --- a/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs +++ b/src-tauri/src_crates/crate_mihomo_api/tests/test_mihomo_api.rs @@ -4,12 +4,12 @@ use std::env; lazy_static::lazy_static! { static ref LOCAL_SOCK: String = { - // 加载 .env 文件 dotenv().ok(); - // 从 .env 或系统环境变量读取 env::var("LOCAL_SOCK") - .expect("LOCAL_SOCK must be set in .env or environment variables") + .expect("LOCAL_SOCK must be set in .env or environment variables") + .trim_matches('"') + .to_string() }; } @@ -27,6 +27,16 @@ async fn test_mihomo_manager_init() { assert_eq!(providers, serde_json::Value::Null); } +#[tokio::test] +async fn test_get_version() { + let manager = mihomo_api::MihomoManager::new(LOCAL_SOCK.to_string()); + let version = manager.get_version().await; + assert!(version.is_ok()); + if let Ok(version) = version { + assert!(!version.get("version").is_none()); + } +} + #[tokio::test] async fn test_refresh_proxies() { let manager = mihomo_api::MihomoManager::new(LOCAL_SOCK.to_string());