feat: use rust impl checkHost

This commit is contained in:
xkeyC 2025-11-25 21:19:36 +08:00
parent 3c07d12ee9
commit da7c4b958d
9 changed files with 276 additions and 94 deletions

View File

@ -1,7 +1,6 @@
import 'package:starcitizen_doctor/api/api.dart'; import 'package:starcitizen_doctor/api/api.dart';
import 'package:starcitizen_doctor/common/io/doh_client.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/api/http_api.dart' as rust_http;
import 'package:starcitizen_doctor/common/rust/http_package.dart';
import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/log.dart';
class URLConf { class URLConf {
@ -43,13 +42,19 @@ class URLConf {
// 使 DNS // 使 DNS
final gitApiList = _genFinalList(await dnsLookupTxt("git.dns.scbox.org")); final gitApiList = _genFinalList(await dnsLookupTxt("git.dns.scbox.org"));
dPrint("DNS gitApiList ==== $gitApiList"); 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"); dPrint("gitApiList.Faster ==== $fasterGit");
if (fasterGit != null) { if (fasterGit != null) {
gitApiHome = fasterGit; gitApiHome = fasterGit;
} }
final newsApiList = _genFinalList(await dnsLookupTxt("news.dns.scbox.org")); 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("DNS newsApiList ==== $newsApiList");
dPrint("newsApiList.Faster ==== $fasterNews"); dPrint("newsApiList.Faster ==== $fasterNews");
if (fasterNews != null) { if (fasterNews != null) {
@ -62,53 +67,12 @@ class URLConf {
static Future<List<String>> dnsLookupTxt(String host) async { static Future<List<String>> dnsLookupTxt(String host) async {
if (await Api.isUseInternalDNS()) { if (await Api.isUseInternalDNS()) {
dPrint("[URLConf] use internal DNS LookupTxt $host"); dPrint("[URLConf] use internal DNS LookupTxt $host");
return RSHttp.dnsLookupTxt(host); return rust_http.dnsLookupTxt(host: host);
} }
dPrint("[URLConf] use DOH LookupTxt $host"); dPrint("[URLConf] use DOH LookupTxt $host");
return (await DohClient.resolveTXT(host)) ?? []; return (await DohClient.resolveTXT(host)) ?? [];
} }
static Future<String?> getFasterUrl(List<String> 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<String> _genFinalList(List<String> sList) { static List<String> _genFinalList(List<String> sList) {
List<String> list = []; List<String> list = [];
for (var ll in sList) { for (var ll in sList) {

View File

@ -34,4 +34,19 @@ Future<List<String>> dnsLookupTxt({required String host}) =>
Future<List<String>> dnsLookupIps({required String host}) => Future<List<String>> dnsLookupIps({required String host}) =>
RustLib.instance.api.crateApiHttpApiDnsLookupIps(host: 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<String?> getFasterUrl({
required List<String> urls,
String? pathSuffix,
}) => RustLib.instance.api.crateApiHttpApiGetFasterUrl(
urls: urls,
pathSuffix: pathSuffix,
);
enum MyMethod { options, gets, post, put, delete, head, trace, connect, patch } enum MyMethod { options, gets, post, put, delete, head, trace, connect, patch }

View File

@ -6,7 +6,6 @@
import '../frb_generated.dart'; import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_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` // 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<void> sendNotify({ Future<void> sendNotify({

View File

@ -69,7 +69,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
String get codegenVersion => '2.11.1'; String get codegenVersion => '2.11.1';
@override @override
int get rustContentHash => 1227557070; int get rustContentHash => -518970253;
static const kDefaultExternalLibraryLoaderConfig = static const kDefaultExternalLibraryLoaderConfig =
ExternalLibraryLoaderConfig( ExternalLibraryLoaderConfig(
@ -95,6 +95,11 @@ abstract class RustLibApi extends BaseApi {
bool? withCustomDns, bool? withCustomDns,
}); });
Future<String?> crateApiHttpApiGetFasterUrl({
required List<String> urls,
String? pathSuffix,
});
Future<List<ProcessInfo>> crateApiWin32ApiGetProcessListByName({ Future<List<ProcessInfo>> crateApiWin32ApiGetProcessListByName({
required String processName, required String processName,
}); });
@ -288,6 +293,39 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
], ],
); );
@override
Future<String?> crateApiHttpApiGetFasterUrl({
required List<String> 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 @override
Future<List<ProcessInfo>> crateApiWin32ApiGetProcessListByName({ Future<List<ProcessInfo>> crateApiWin32ApiGetProcessListByName({
required String processName, required String processName,

View File

@ -762,6 +762,38 @@ class RustLibWire implements BaseWire {
) )
>(); >();
void wire__crate__api__http_api__get_faster_url(
int port_,
ffi.Pointer<wire_cst_list_String> urls,
ffi.Pointer<wire_cst_list_prim_u_8_strict> 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<wire_cst_list_String>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
)
>
>('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<wire_cst_list_String>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
)
>();
void wire__crate__api__win32_api__get_process_list_by_name( void wire__crate__api__win32_api__get_process_list_by_name(
int port_, int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> process_name, ffi.Pointer<wire_cst_list_prim_u_8_strict> process_name,
@ -1312,6 +1344,13 @@ final class wire_cst_list_record_string_string extends ffi.Struct {
external int len; external int len;
} }
final class wire_cst_list_String extends ffi.Struct {
external ffi.Pointer<ffi.Pointer<wire_cst_list_prim_u_8_strict>> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_rsi_launcher_asar_data extends ffi.Struct { final class wire_cst_rsi_launcher_asar_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> asar_path; external ffi.Pointer<wire_cst_list_prim_u_8_strict> asar_path;
@ -1327,13 +1366,6 @@ final class wire_cst_list_prim_u_8_loose extends ffi.Struct {
external int len; external int len;
} }
final class wire_cst_list_String extends ffi.Struct {
external ffi.Pointer<ffi.Pointer<wire_cst_list_prim_u_8_strict>> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_process_info extends ffi.Struct { final class wire_cst_process_info extends ffi.Struct {
@ffi.Uint32() @ffi.Uint32()
external int pid; external int pid;

View File

@ -30,42 +30,37 @@ class SplashUI extends HookConsumerWidget {
return null; return null;
}, []); }, []);
return makeDefaultPage(context, return makeDefaultPage(
content: Center( context,
child: Column( content: Center(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
Image.asset("assets/app_logo.png", width: 192, height: 192), children: [
const SizedBox(height: 32), Image.asset("assets/app_logo.png", width: 192, height: 192),
const ProgressRing(), const SizedBox(height: 32),
const SizedBox(height: 32), const ProgressRing(),
if (step == 0) Text(S.current.app_splash_checking_availability), const SizedBox(height: 32),
if (step == 1) Text(S.current.app_splash_checking_for_updates), if (step == 0) Text(S.current.app_splash_checking_availability),
if (step == 2) Text(S.current.app_splash_almost_done), 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( automaticallyImplyLeading: false,
alignment: AlignmentDirectional.centerStart, titleRow: Align(
child: Row( alignment: AlignmentDirectional.centerStart,
children: [ child: Row(
Image.asset( children: [
"assets/app_logo_mini.png", Image.asset("assets/app_logo_mini.png", width: 20, height: 20, fit: BoxFit.cover),
width: 20, const SizedBox(width: 12),
height: 20, Text(S.current.app_index_version_info(ConstConf.appVersion, ConstConf.isMSE ? "" : " Dev")),
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, void _initApp(BuildContext context, AppGlobalModel appModel, ValueNotifier<int> stepState, WidgetRef ref) async {
ValueNotifier<int> stepState, WidgetRef ref) async {
await appModel.initApp(); await appModel.initApp();
final appConf = await Hive.openBox("app_conf"); final appConf = await Hive.openBox("app_conf");
final v = appConf.get("splash_alert_info_version", defaultValue: 0); final v = appConf.get("splash_alert_info_version", defaultValue: 0);
@ -92,11 +87,11 @@ class SplashUI extends HookConsumerWidget {
Future<void> _showAlert(BuildContext context, Box<dynamic> appConf) async { Future<void> _showAlert(BuildContext context, Box<dynamic> appConf) async {
final userOk = await showConfirmDialogs( final userOk = await showConfirmDialogs(
context, context,
S.current.app_splash_dialog_u_a_p_p, S.current.app_splash_dialog_u_a_p_p,
MarkdownWidget(data: S.current.app_splash_dialog_u_a_p_p_content), MarkdownWidget(data: S.current.app_splash_dialog_u_a_p_p_content),
constraints: constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .5),
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .5)); );
if (userOk) { if (userOk) {
await appConf.put("splash_alert_info_version", _alertInfoVersion); await appConf.put("splash_alert_info_version", _alertInfoVersion);
} else { } else {

View File

@ -59,3 +59,17 @@ pub async fn dns_lookup_txt(host: String) -> anyhow::Result<Vec<String>> {
pub async fn dns_lookup_ips(host: String) -> anyhow::Result<Vec<String>> { pub async fn dns_lookup_ips(host: String) -> anyhow::Result<Vec<String>> {
http_package::dns_lookup_ips(host).await 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<String>,
path_suffix: Option<String>,
) -> anyhow::Result<Option<String>> {
http_package::get_faster_url(urls, path_suffix).await
}

View File

@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
default_rust_auto_opaque = RustAutoOpaqueNom, default_rust_auto_opaque = RustAutoOpaqueNom,
); );
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; 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 // 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<Vec<String>>,
path_suffix: impl CstDecode<Option<String>>,
) {
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::DcoCodec, _, _, _>(
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( fn wire__crate__api__win32_api__get_process_list_by_name_impl(
port_: flutter_rust_bridge::for_generated::MessagePort, port_: flutter_rust_bridge::for_generated::MessagePort,
process_name: impl CstDecode<String>, process_name: impl CstDecode<String>,
@ -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)] #[unsafe(no_mangle)]
pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_list_by_name( pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__get_process_list_by_name(
port_: i64, port_: i64,

View File

@ -9,6 +9,8 @@ use std::str::FromStr;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
use tokio::sync::mpsc;
use tokio::task::JoinHandle;
#[derive(Debug)] #[derive(Debug)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
@ -183,3 +185,90 @@ fn _mix_header(
req = req.headers(dh.clone()); req = req.headers(dh.clone());
req 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<String>,
path_suffix: Option<String>,
) -> anyhow::Result<Option<String>> {
if urls.is_empty() {
return Ok(None);
}
let (tx, mut rx) = mpsc::channel(urls.len());
let mut handles: Vec<JoinHandle<()>> = 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)
}