Skip to content

Commit 06d79c4

Browse files
committed
feat(proxy): add single node delay testing with UI integration
ref: MetaCubeX/ClashMetaForAndroid#683 MetaCubeX/ClashMetaForAndroid#683
1 parent cfa6142 commit 06d79c4

25 files changed

Lines changed: 371 additions & 45 deletions

File tree

app/src/main/kotlin/com/github/yumelira/yumebox/screen/settings/AppSettingsScreen.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,12 @@ fun AppSettingsScreen(
251251
checked = showTrafficNotification,
252252
onCheckedChange = { viewModel.onShowTrafficNotificationChange(it) },
253253
)
254+
SuperSwitch(
255+
title = MLang.AppSettings.ServiceSection.SingleNodeTestTitle,
256+
summary = MLang.AppSettings.ServiceSection.SingleNodeTestSummary,
257+
checked = viewModel.singleNodeTest.state.collectAsState().value,
258+
onCheckedChange = { viewModel.onSingleNodeTestChange(it) },
259+
)
254260
}
255261
SmallTitle(MLang.AppSettings.Section.Network)
256262
Card {

app/src/main/kotlin/com/github/yumelira/yumebox/screen/settings/AppSettingsViewModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ class AppSettingsViewModel(
4545
val bottomBarAutoHide: Preference<Boolean> = repository.bottomBarAutoHide
4646
val topBarBlurEnabled: Preference<Boolean> = repository.topBarBlurEnabled
4747
val pageScale: Preference<Float> = repository.pageScale
48+
val singleNodeTest: Preference<Boolean> = repository.singleNodeTest
4849

4950
val customUserAgent: Preference<String> = repository.customUserAgent
5051

@@ -61,6 +62,7 @@ class AppSettingsViewModel(
6162
fun onHideAppIconChange(hide: Boolean) = hideAppIcon.set(hide)
6263
fun onExcludeFromRecentsChange(exclude: Boolean) = excludeFromRecents.set(exclude)
6364
fun onShowTrafficNotificationChange(show: Boolean) = showTrafficNotification.set(show)
65+
fun onSingleNodeTestChange(enabled: Boolean) = singleNodeTest.set(enabled)
6466

6567
fun applyCustomUserAgent(userAgent: String) = repository.applyCustomUserAgent(userAgent)
6668

core/src/cpp/main.c

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ Java_com_github_yumelira_yumebox_core_bridge_Bridge_nativeHealthCheckAll(JNIEnv
233233
healthCheckAll();
234234
}
235235

236+
JNIEXPORT void JNICALL
237+
Java_com_github_yumelira_yumebox_core_bridge_Bridge_nativeHealthCheckProxy(JNIEnv *env, jobject thiz,
238+
jobject completable,
239+
jstring proxy_name) {
240+
TRACE_METHOD();
241+
242+
jobject _completable = new_global(completable);
243+
scoped_string _proxy_name = get_string(proxy_name);
244+
245+
healthCheckProxy(_completable, _proxy_name);
246+
}
247+
236248
JNIEXPORT jboolean JNICALL
237249
Java_com_github_yumelira_yumebox_core_bridge_Bridge_nativePatchSelector(JNIEnv *env, jobject thiz,
238250
jstring selector, jstring name) {
@@ -345,6 +357,7 @@ static jmethodID m_tun_interface_mark_socket;
345357
static jmethodID m_tun_interface_query_socket_uid;
346358
static jmethodID m_completable_complete;
347359
static jmethodID m_completable_complete_exceptionally;
360+
static jmethodID m_completable_complete_with_string;
348361
static jmethodID m_logcat_interface_received;
349362
static jmethodID m_clash_exception;
350363
static jmethodID m_fetch_callback_report;
@@ -403,6 +416,17 @@ static void call_completable_complete_impl(void *completable, const char *except
403416
}
404417
}
405418

419+
static void call_completable_complete_with_string_impl(void *completable, const char *result) {
420+
TRACE_METHOD();
421+
422+
ATTACH_JNI();
423+
424+
(*env)->CallBooleanMethod(env,
425+
(jobject) completable,
426+
(jmethodID) m_completable_complete_with_string,
427+
(jstring) new_string(result));
428+
}
429+
406430
static void call_fetch_callback_report_impl(void *fetch_callback, const char *status_json) {
407431
TRACE_METHOD();
408432

@@ -521,6 +545,8 @@ JNI_OnLoad(JavaVM *vm, void *reserved) {
521545
"(Ljava/lang/String;)V");
522546
m_completable_complete_exceptionally = find_method(c_completable, "completeExceptionally",
523547
"(Ljava/lang/Throwable;)Z");
548+
// Reuse complete(Object) for complete_with_string - String is an Object
549+
m_completable_complete_with_string = m_completable_complete;
524550
m_logcat_interface_received = find_method(c_logcat_interface, "received",
525551
"(Ljava/lang/String;)V");
526552
m_clash_exception = find_method(_c_clash_exception, "<init>",
@@ -541,6 +567,7 @@ JNI_OnLoad(JavaVM *vm, void *reserved) {
541567
mark_socket_func = &call_tun_interface_mark_socket_impl;
542568
query_socket_uid_func = &call_tun_interface_query_socket_uid_impl;
543569
complete_func = &call_completable_complete_impl;
570+
complete_with_string_func = &call_completable_complete_with_string_impl;
544571
fetch_report_func = &call_fetch_callback_report_impl;
545572
fetch_complete_func = &call_fetch_callback_complete_impl;
546573
logcat_received_func = &call_logcat_interface_received_impl;

core/src/golang/native/bridge.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ int (*query_socket_uid_func)(void *tun_interface, int protocol, const char *sour
1313

1414
void (*complete_func)(void *completable, const char *exception);
1515

16+
void (*complete_with_string_func)(void *completable, const char *result);
17+
1618
void (*fetch_report_func)(void *fetch_callback, const char *status_json);
1719

1820
void (*fetch_complete_func)(void *fetch_callback, const char *error);
@@ -48,6 +50,14 @@ void complete(void *obj, char *error) {
4850
free(error);
4951
}
5052

53+
void complete_with_string(void *obj, char *result) {
54+
TRACE_METHOD();
55+
56+
complete_with_string_func(obj, result);
57+
58+
free(result);
59+
}
60+
5161
void fetch_complete(void *fetch_callback, char *exception) {
5262
TRACE_METHOD();
5363

core/src/golang/native/bridge.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ extern int (*query_socket_uid_func)(void *tun_interface, int protocol, const cha
2121

2222
extern void (*complete_func)(void *completable, const char *exception);
2323

24+
extern void (*complete_with_string_func)(void *completable, const char *result);
25+
2426
extern void (*fetch_report_func)(void *fetch_callback, const char *status_json);
2527

2628
extern void (*fetch_complete_func)(void *fetch_callback, const char *error);
@@ -38,6 +40,8 @@ extern int query_socket_uid(void *interface, int protocol, char *source, char *t
3840

3941
extern void complete(void *obj, char *error);
4042

43+
extern void complete_with_string(void *obj, char *result);
44+
4145
extern void fetch_complete(void *completable, char *exception);
4246

4347
extern void fetch_report(void *fetch_callback, char *status_json);

core/src/golang/native/tunnel.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ func healthCheckAll() {
7979
tunnel.HealthCheckAll()
8080
}
8181

82+
//export healthCheckProxy
83+
func healthCheckProxy(completable unsafe.Pointer, proxyName C.c_string) {
84+
go func(name string) {
85+
delay := tunnel.HealthCheckProxy(name)
86+
87+
// Return delay as JSON response
88+
response := &struct {
89+
Delay int `json:"delay"`
90+
}{delay}
91+
92+
C.complete_with_string(completable, marshalJson(response))
93+
}(C.GoString(proxyName))
94+
}
95+
8296
//export patchSelector
8397
func patchSelector(selector, name C.c_string) C.int {
8498
s := C.GoString(selector)

core/src/golang/native/tunnel/connectivity.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package tunnel
22

33
import (
4+
"context"
45
"sync"
6+
"time"
57

68
"github.com/metacubex/mihomo/adapter/outboundgroup"
79
"github.com/metacubex/mihomo/constant/provider"
@@ -47,3 +49,28 @@ func HealthCheckAll() {
4749
}(g)
4850
}
4951
}
52+
53+
// HealthCheckProxy performs health check on a single proxy by name
54+
func HealthCheckProxy(proxyName string) int {
55+
p := tunnel.Proxies()[proxyName]
56+
if p == nil {
57+
log.Warnln("HealthCheckProxy: proxy `%s` not found", proxyName)
58+
return -1
59+
}
60+
61+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
62+
defer cancel()
63+
64+
testURL := "https://www.gstatic.com/generate_204"
65+
_, _ = p.URLTest(ctx, testURL, nil)
66+
delay := p.LastDelayForTestUrl(testURL)
67+
68+
// Convert 65535 (failure) to -1
69+
if delay == 0xffff {
70+
log.Debugln("HealthCheckProxy: proxy=%s, timeout/failed", proxyName)
71+
return -1
72+
}
73+
74+
log.Debugln("HealthCheckProxy: proxy=%s, delay=%dms", proxyName, delay)
75+
return int(delay)
76+
}

core/src/kotlin/com/github/yumelira/yumebox/core/Clash.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ object Clash {
174174
}
175175
}
176176

177+
fun healthCheckProxy(proxyName: String): CompletableDeferred<String> {
178+
return CompletableDeferred<String>().apply {
179+
Bridge.nativeHealthCheckProxy(this, proxyName)
180+
}
181+
}
182+
177183
fun healthCheckAll() {
178184
Bridge.nativeHealthCheckAll()
179185
}

core/src/kotlin/com/github/yumelira/yumebox/core/bridge/Bridge.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ object Bridge {
4747
external fun nativeQueryProfileGroups(path: String, excludeNotSelectable: Boolean): String?
4848
external fun nativeQueryGroup(name: String, sort: String): String?
4949
external fun nativeHealthCheck(completable: CompletableDeferred<Unit>, name: String)
50+
external fun nativeHealthCheckProxy(completable: CompletableDeferred<String>, proxyName: String)
5051
external fun nativeHealthCheckAll()
5152
external fun nativePatchSelector(selector: String, name: String): Boolean
5253
external fun nativeFetchAndValid(

data/settings/src/main/kotlin/com/github/yumelira/yumebox/data/repository/AppSettingsRepository.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class AppSettingsRepository(
4343
val bottomBarAutoHide: Preference<Boolean> = storage.bottomBarAutoHide
4444
val topBarBlurEnabled: Preference<Boolean> = storage.topBarBlurEnabled
4545
val pageScale: Preference<Float> = storage.pageScale
46+
val singleNodeTest: Preference<Boolean> = storage.singleNodeTest
4647

4748
val customUserAgent: Preference<String> = storage.customUserAgent
4849

0 commit comments

Comments
 (0)