From da7c4b958d3187697a6c7fb936c80e30c4a5da12 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Tue, 25 Nov 2025 21:19:36 +0800 Subject: [PATCH] feat: use rust impl checkHost --- lib/common/conf/url_conf.dart | 56 +++-------------- lib/common/rust/api/http_api.dart | 15 +++++ lib/common/rust/api/win32_api.dart | 1 - lib/common/rust/frb_generated.dart | 40 +++++++++++- lib/common/rust/frb_generated.io.dart | 46 +++++++++++--- lib/ui/splash_ui.dart | 71 ++++++++++----------- rust/src/api/http_api.rs | 14 +++++ rust/src/frb_generated.rs | 38 +++++++++++- rust/src/http_package/mod.rs | 89 +++++++++++++++++++++++++++ 9 files changed, 276 insertions(+), 94 deletions(-) diff --git a/lib/common/conf/url_conf.dart b/lib/common/conf/url_conf.dart index 2edad62..2b74821 100644 --- a/lib/common/conf/url_conf.dart +++ b/lib/common/conf/url_conf.dart @@ -1,7 +1,6 @@ import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/common/io/doh_client.dart'; -import 'package:starcitizen_doctor/common/io/rs_http.dart'; -import 'package:starcitizen_doctor/common/rust/http_package.dart'; +import 'package:starcitizen_doctor/common/rust/api/http_api.dart' as rust_http; import 'package:starcitizen_doctor/common/utils/log.dart'; class URLConf { @@ -43,13 +42,19 @@ class URLConf { // 使用 DNS 获取可用列表 final gitApiList = _genFinalList(await dnsLookupTxt("git.dns.scbox.org")); dPrint("DNS gitApiList ==== $gitApiList"); - final fasterGit = await getFasterUrl(gitApiList, "git"); + final fasterGit = await rust_http.getFasterUrl( + urls: gitApiList, + pathSuffix: "/SCToolBox/Api/raw/branch/main/sc_doctor/version.json", + ); dPrint("gitApiList.Faster ==== $fasterGit"); if (fasterGit != null) { gitApiHome = fasterGit; } final newsApiList = _genFinalList(await dnsLookupTxt("news.dns.scbox.org")); - final fasterNews = await getFasterUrl(newsApiList, "news"); + final fasterNews = await rust_http.getFasterUrl( + urls: newsApiList, + pathSuffix: "/api/latest", + ); dPrint("DNS newsApiList ==== $newsApiList"); dPrint("newsApiList.Faster ==== $fasterNews"); if (fasterNews != null) { @@ -62,53 +67,12 @@ class URLConf { static Future> dnsLookupTxt(String host) async { if (await Api.isUseInternalDNS()) { dPrint("[URLConf] use internal DNS LookupTxt $host"); - return RSHttp.dnsLookupTxt(host); + return rust_http.dnsLookupTxt(host: host); } dPrint("[URLConf] use DOH LookupTxt $host"); return (await DohClient.resolveTXT(host)) ?? []; } - static Future getFasterUrl(List urls, String mode) async { - String firstUrl = ""; - int callLen = 0; - - void onCall(RustHttpResponse? response, String url) { - callLen++; - if (response != null && response.statusCode == 200 && firstUrl.isEmpty) { - firstUrl = url; - } - } - - for (var url in urls) { - var reqUrl = url; - switch (mode) { - case "git": - reqUrl = "$url/SCToolBox/Api/raw/branch/main/sc_doctor/version.json"; - break; - case "news": - reqUrl = "$url/api/latest"; - break; - } - RSHttp.head(reqUrl).then( - (resp) => onCall(resp, url), - onError: (err) { - callLen++; - dPrint("RSHttp.head error $err"); - }, - ); - } - - while (true) { - await Future.delayed(const Duration(milliseconds: 16)); - if (firstUrl.isNotEmpty) { - return firstUrl; - } - if (callLen == urls.length && firstUrl.isEmpty) { - return null; - } - } - } - static List _genFinalList(List sList) { List list = []; for (var ll in sList) { diff --git a/lib/common/rust/api/http_api.dart b/lib/common/rust/api/http_api.dart index 15c3f36..6b49200 100644 --- a/lib/common/rust/api/http_api.dart +++ b/lib/common/rust/api/http_api.dart @@ -34,4 +34,19 @@ Future> dnsLookupTxt({required String host}) => Future> dnsLookupIps({required String host}) => RustLib.instance.api.crateApiHttpApiDnsLookupIps(host: host); +/// Get the fastest URL from a list of URLs by testing them concurrently. +/// Returns the first URL that responds successfully, canceling other requests. +/// +/// # Arguments +/// * `urls` - List of base URLs to test +/// * `path_suffix` - Optional path suffix to append to each URL (e.g., "/api/version") +/// If None, tests the base URL directly +Future getFasterUrl({ + required List urls, + String? pathSuffix, +}) => RustLib.instance.api.crateApiHttpApiGetFasterUrl( + urls: urls, + pathSuffix: pathSuffix, +); + enum MyMethod { options, gets, post, put, delete, head, trace, connect, patch } diff --git a/lib/common/rust/api/win32_api.dart b/lib/common/rust/api/win32_api.dart index 78d9de7..7b4de6b 100644 --- a/lib/common/rust/api/win32_api.dart +++ b/lib/common/rust/api/win32_api.dart @@ -6,7 +6,6 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `get_process_path` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt` Future sendNotify({ diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index 3bdc0d7..3f5a045 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 1227557070; + int get rustContentHash => -518970253; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -95,6 +95,11 @@ abstract class RustLibApi extends BaseApi { bool? withCustomDns, }); + Future crateApiHttpApiGetFasterUrl({ + required List urls, + String? pathSuffix, + }); + Future> crateApiWin32ApiGetProcessListByName({ required String processName, }); @@ -288,6 +293,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ], ); + @override + Future crateApiHttpApiGetFasterUrl({ + required List urls, + String? pathSuffix, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_list_String(urls); + var arg1 = cst_encode_opt_String(pathSuffix); + return wire.wire__crate__api__http_api__get_faster_url( + port_, + arg0, + arg1, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_opt_String, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiHttpApiGetFasterUrlConstMeta, + argValues: [urls, pathSuffix], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiHttpApiGetFasterUrlConstMeta => + const TaskConstMeta( + debugName: "get_faster_url", + argNames: ["urls", "pathSuffix"], + ); + @override Future> crateApiWin32ApiGetProcessListByName({ required String processName, diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index 49e40fa..65264a6 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -762,6 +762,38 @@ class RustLibWire implements BaseWire { ) >(); + void wire__crate__api__http_api__get_faster_url( + int port_, + ffi.Pointer urls, + ffi.Pointer path_suffix, + ) { + return _wire__crate__api__http_api__get_faster_url( + port_, + urls, + path_suffix, + ); + } + + late final _wire__crate__api__http_api__get_faster_urlPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ) + > + >('frbgen_starcitizen_doctor_wire__crate__api__http_api__get_faster_url'); + late final _wire__crate__api__http_api__get_faster_url = + _wire__crate__api__http_api__get_faster_urlPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ) + >(); + void wire__crate__api__win32_api__get_process_list_by_name( int port_, ffi.Pointer process_name, @@ -1312,6 +1344,13 @@ final class wire_cst_list_record_string_string extends ffi.Struct { external int len; } +final class wire_cst_list_String extends ffi.Struct { + external ffi.Pointer> ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_rsi_launcher_asar_data extends ffi.Struct { external ffi.Pointer asar_path; @@ -1327,13 +1366,6 @@ final class wire_cst_list_prim_u_8_loose extends ffi.Struct { external int len; } -final class wire_cst_list_String extends ffi.Struct { - external ffi.Pointer> ptr; - - @ffi.Int32() - external int len; -} - final class wire_cst_process_info extends ffi.Struct { @ffi.Uint32() external int pid; diff --git a/lib/ui/splash_ui.dart b/lib/ui/splash_ui.dart index e88c0f6..0f5b336 100644 --- a/lib/ui/splash_ui.dart +++ b/lib/ui/splash_ui.dart @@ -30,42 +30,37 @@ class SplashUI extends HookConsumerWidget { return null; }, []); - return makeDefaultPage(context, - content: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset("assets/app_logo.png", width: 192, height: 192), - const SizedBox(height: 32), - const ProgressRing(), - const SizedBox(height: 32), - if (step == 0) Text(S.current.app_splash_checking_availability), - if (step == 1) Text(S.current.app_splash_checking_for_updates), - if (step == 2) Text(S.current.app_splash_almost_done), - ], - ), + return makeDefaultPage( + context, + content: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset("assets/app_logo.png", width: 192, height: 192), + const SizedBox(height: 32), + const ProgressRing(), + const SizedBox(height: 32), + if (step == 0) Text(S.current.app_splash_checking_availability), + if (step == 1) Text(S.current.app_splash_checking_for_updates), + if (step == 2) Text(S.current.app_splash_almost_done), + ], ), - automaticallyImplyLeading: false, - titleRow: Align( - alignment: AlignmentDirectional.centerStart, - child: Row( - children: [ - Image.asset( - "assets/app_logo_mini.png", - width: 20, - height: 20, - fit: BoxFit.cover, - ), - const SizedBox(width: 12), - Text(S.current.app_index_version_info( - ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")) - ], - ), - )); + ), + automaticallyImplyLeading: false, + titleRow: Align( + alignment: AlignmentDirectional.centerStart, + child: Row( + children: [ + Image.asset("assets/app_logo_mini.png", width: 20, height: 20, fit: BoxFit.cover), + const SizedBox(width: 12), + Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")), + ], + ), + ), + ); } - void _initApp(BuildContext context, AppGlobalModel appModel, - ValueNotifier stepState, WidgetRef ref) async { + void _initApp(BuildContext context, AppGlobalModel appModel, ValueNotifier stepState, WidgetRef ref) async { await appModel.initApp(); final appConf = await Hive.openBox("app_conf"); final v = appConf.get("splash_alert_info_version", defaultValue: 0); @@ -92,11 +87,11 @@ class SplashUI extends HookConsumerWidget { Future _showAlert(BuildContext context, Box appConf) async { final userOk = await showConfirmDialogs( - context, - S.current.app_splash_dialog_u_a_p_p, - MarkdownWidget(data: S.current.app_splash_dialog_u_a_p_p_content), - constraints: - BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .5)); + context, + S.current.app_splash_dialog_u_a_p_p, + MarkdownWidget(data: S.current.app_splash_dialog_u_a_p_p_content), + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .5), + ); if (userOk) { await appConf.put("splash_alert_info_version", _alertInfoVersion); } else { diff --git a/rust/src/api/http_api.rs b/rust/src/api/http_api.rs index ac2e3fa..f170d8f 100644 --- a/rust/src/api/http_api.rs +++ b/rust/src/api/http_api.rs @@ -59,3 +59,17 @@ pub async fn dns_lookup_txt(host: String) -> anyhow::Result> { pub async fn dns_lookup_ips(host: String) -> anyhow::Result> { http_package::dns_lookup_ips(host).await } + +/// Get the fastest URL from a list of URLs by testing them concurrently. +/// Returns the first URL that responds successfully, canceling other requests. +/// +/// # Arguments +/// * `urls` - List of base URLs to test +/// * `path_suffix` - Optional path suffix to append to each URL (e.g., "/api/version") +/// If None, tests the base URL directly +pub async fn get_faster_url( + urls: Vec, + path_suffix: Option, +) -> anyhow::Result> { + http_package::get_faster_url(urls, path_suffix).await +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 3c763f0..3af29a5 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1227557070; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -518970253; // Section: executor @@ -156,6 +156,33 @@ fn wire__crate__api__http_api__fetch_impl( }, ) } +fn wire__crate__api__http_api__get_faster_url_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + urls: impl CstDecode>, + path_suffix: impl CstDecode>, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "get_faster_url", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_urls = urls.cst_decode(); + let api_path_suffix = path_suffix.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::http_api::get_faster_url(api_urls, api_path_suffix).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__win32_api__get_process_list_by_name_impl( port_: flutter_rust_bridge::for_generated::MessagePort, process_name: impl CstDecode, @@ -1684,6 +1711,15 @@ mod io { ) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__http_api__get_faster_url( + port_: i64, + urls: *mut wire_cst_list_String, + path_suffix: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__http_api__get_faster_url_impl(port_, urls, path_suffix) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_list_by_name( port_: i64, diff --git a/rust/src/http_package/mod.rs b/rust/src/http_package/mod.rs index 3ee52b9..440aa44 100644 --- a/rust/src/http_package/mod.rs +++ b/rust/src/http_package/mod.rs @@ -9,6 +9,8 @@ use std::str::FromStr; use std::sync::{Arc, RwLock}; use std::time::Duration; use url::Url; +use tokio::sync::mpsc; +use tokio::task::JoinHandle; #[derive(Debug)] #[allow(non_camel_case_types)] @@ -183,3 +185,90 @@ fn _mix_header( req = req.headers(dh.clone()); req } + +/// Get the fastest URL from a list of URLs by testing them concurrently. +/// Returns the first URL that responds successfully (HTTP 200), and cancels all other pending requests. +/// +/// # Arguments +/// * `urls` - List of base URLs to test +/// * `path_suffix` - Optional path suffix to append to each URL for testing +/// If None, tests the base URL directly +/// +/// # Returns +/// * `Ok(Some(url))` - The first base URL that responded successfully +/// * `Ok(None)` - All URLs failed or the list was empty +pub async fn get_faster_url( + urls: Vec, + path_suffix: Option, +) -> anyhow::Result> { + if urls.is_empty() { + return Ok(None); + } + + let (tx, mut rx) = mpsc::channel(urls.len()); + let mut handles: Vec> = Vec::new(); + + // Spawn a task for each URL + for url in urls.iter() { + let url_clone = url.clone(); + let tx_clone = tx.clone(); + let path_suffix_clone = path_suffix.clone(); + + let handle = tokio::spawn(async move { + // Build request URL + let req_url = if let Some(suffix) = path_suffix_clone { + format!("{}{}", url_clone, suffix) + } else { + url_clone.clone() + }; + + // Perform HEAD request + let result = fetch( + Method::HEAD, + req_url, + None, + None, + None, + Some(false), + ).await; + + // Send result back through channel + if let Ok(response) = result { + if response.status_code == 200 { + let _ = tx_clone.send(Some(url_clone)).await; + return; + } + } + + // Send None if request failed + let _ = tx_clone.send(None).await; + }); + + handles.push(handle); + } + + // Drop the original sender so the channel closes when all tasks complete + drop(tx); + + // Wait for the first successful response + let mut completed = 0; + let total = urls.len(); + + while let Some(result) = rx.recv().await { + if let Some(url) = result { + // Found a successful URL - abort all other tasks + for handle in handles { + handle.abort(); + } + return Ok(Some(url)); + } + + completed += 1; + if completed >= total { + // All requests completed without success + break; + } + } + + Ok(None) +}