Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions core/src/main/cpp/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,8 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeQueryGroup(JNIEnv *env, job

JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeHealthCheck(JNIEnv *env, jobject thiz,
jobject completable,
jstring name) {
jobject completable,
jstring name) {
TRACE_METHOD();

jobject _completable = new_global(completable);
Expand All @@ -195,6 +195,20 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeHealthCheck(JNIEnv *env, jo
healthCheck(_completable, _name);
}

JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeHealthCheckProxy(JNIEnv *env, jobject thiz,
jobject completable,
jstring groupName,
jstring proxyName) {
TRACE_METHOD();

jobject _completable = new_global(completable);
scoped_string _groupName = get_string(groupName);
scoped_string _proxyName = get_string(proxyName);

healthCheckProxy(_completable, _groupName, _proxyName);
}

JNIEXPORT void JNICALL
Java_com_github_kr328_clash_core_bridge_Bridge_nativeHealthCheckAll(JNIEnv *env, jobject thiz) {
TRACE_METHOD();
Expand Down Expand Up @@ -526,4 +540,4 @@ Java_com_github_kr328_clash_core_bridge_Bridge_nativeCoreVersion(JNIEnv *env, jo
char* Version = make_String(GIT_VERSION);

return new_string(Version);
}
}
9 changes: 9 additions & 0 deletions core/src/main/golang/native/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ func healthCheck(completable unsafe.Pointer, name C.c_string) {
}(C.GoString(name))
}

//export healthCheckProxy
func healthCheckProxy(completable unsafe.Pointer, groupName C.c_string, proxyName C.c_string) {
go func(groupName string, proxyName string) {
tunnel.HealthCheckProxy(groupName, proxyName)

C.complete(completable, nil)
}(C.GoString(groupName), C.GoString(proxyName))
}

//export healthCheckAll
func healthCheckAll() {
tunnel.HealthCheckAll()
Expand Down
66 changes: 65 additions & 1 deletion core/src/main/golang/native/tunnel/connectivity.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,42 @@
package tunnel

import (
"context"
"sync"
"time"

"github.com/metacubex/mihomo/adapter/outboundgroup"
C "github.com/metacubex/mihomo/constant"
"github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/log"
"github.com/metacubex/mihomo/tunnel"
)

const healthCheckTimeout = 5 * time.Second
const defaultHealthCheckURL = "https://www.gstatic.com/generate_204"

func probeURL(proxy C.Proxy, proxyName string) {
testURL := defaultHealthCheckURL
for k := range proxy.ExtraDelayHistories() {
if len(k) > 0 {
testURL = k
break
}
}

ctx, cancel := context.WithTimeout(context.Background(), healthCheckTimeout)
defer cancel()

if _, _, err := proxy.URLTest(ctx, testURL, nil); err != nil && ctx.Err() == nil {
log.Warnln(
"Request health check for `%s` with url `%s` failed: %s",
proxyName,
testURL,
err.Error(),
)
}
}

func HealthCheck(name string) {
p := tunnel.Proxies()[name]

Expand All @@ -20,7 +48,7 @@ func HealthCheck(name string) {

g, ok := p.Adapter().(outboundgroup.ProxyGroup)
if !ok {
log.Warnln("Request health check for `%s`: invalid type %s", name, p.Type().String())
probeURL(p, name)

return
}
Expand All @@ -47,3 +75,39 @@ func HealthCheckAll() {
}(g)
}
}

func HealthCheckProxy(groupName string, proxyName string) {
p := tunnel.Proxies()[groupName]

if p == nil {
log.Warnln(
"Request health check for proxy `%s` in group `%s`: group not found",
proxyName,
groupName,
)
return
}

g, ok := p.Adapter().(outboundgroup.ProxyGroup)
if !ok {
log.Warnln(
"Request health check for proxy `%s` in group `%s`: not a proxy group",
proxyName,
groupName,
)
return
}

for _, proxy := range g.Proxies() {
if proxy.Name() == proxyName {
probeURL(proxy, proxyName)
return
}
}

log.Warnln(
"Request health check for proxy `%s` in group `%s`: proxy not found",
proxyName,
groupName,
)
}
6 changes: 6 additions & 0 deletions core/src/main/kotlin/com/github/kr328/clash/core/Clash.kt
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ object Clash {
return CompletableDeferred<Unit>().apply { Bridge.nativeHealthCheck(this, name) }
}

fun healthCheckProxy(groupName: String, proxyName: String): CompletableDeferred<Unit> {
return CompletableDeferred<Unit>().apply {
Bridge.nativeHealthCheckProxy(this, groupName, proxyName)
}
}

fun healthCheckAll() {
Bridge.nativeHealthCheckAll()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ object Bridge {
external fun nativeQueryGroup(name: String, sort: String): String?

external fun nativeHealthCheck(completable: CompletableDeferred<Unit>, name: String)
external fun nativeHealthCheckProxy(
completable: CompletableDeferred<Unit>,
groupName: String,
proxyName: String,
)

external fun nativeHealthCheckAll()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class ClashManager(private val context: Context) :
return Clash.healthCheck(group).await()
}

override suspend fun healthCheckProxy(group: String, name: String) {
return Clash.healthCheckProxy(group, name).await()
}

override suspend fun updateProvider(type: Provider.Type, name: String) {
return Clash.updateProvider(type, name).await()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface IClashManager {
fun patchSelector(group: String, name: String): Boolean

suspend fun healthCheck(group: String)
suspend fun healthCheckProxy(group: String, name: String)

suspend fun updateProvider(type: Provider.Type, name: String)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
Expand Down Expand Up @@ -106,6 +107,7 @@ internal fun ProxyScreen(
onProxySortChanged = viewModel::onProxySortChanged,
onOverrideModeSelected = viewModel::onOverrideModeSelected,
onProxySelected = viewModel::onProxySelected,
onProxyDelayTest = viewModel::onProxyDelayTest,
)
}

Expand All @@ -122,6 +124,7 @@ private fun ProxyContent(
onProxySortChanged: (ProxySort) -> Unit,
onOverrideModeSelected: (TunnelState.Mode?) -> Unit,
onProxySelected: (Int, String) -> Unit,
onProxyDelayTest: (Int, String) -> Unit,
) {
var menuVisible by remember { mutableStateOf(false) }
val currentGroup = uiState.groups.getOrNull(uiState.currentPage)
Expand Down Expand Up @@ -200,6 +203,7 @@ private fun ProxyContent(
selectedProxies = selectedProxies,
onPageChanged = onPageChanged,
onProxySelected = onProxySelected,
onProxyDelayTest = onProxyDelayTest,
)
}
}
Expand All @@ -212,6 +216,7 @@ private fun ProxyPagerContent(
selectedProxies: List<SelectedProxy>,
onPageChanged: (Int) -> Unit,
onProxySelected: (Int, String) -> Unit,
onProxyDelayTest: (Int, String) -> Unit,
) {
val groupNames = uiState.groupNames
if (groupNames.isEmpty()) return
Expand Down Expand Up @@ -247,6 +252,7 @@ private fun ProxyPagerContent(
group = uiState.groups.getOrNull(page) ?: ProxyViewModel.UiState.ProxyGroupUiState(),
selectedProxies = selectedProxies,
onProxySelected = onProxySelected,
onProxyDelayTest = onProxyDelayTest,
)
}
}
Expand All @@ -259,6 +265,7 @@ private fun ProxyGroupPage(
group: ProxyViewModel.UiState.ProxyGroupUiState,
selectedProxies: List<SelectedProxy>,
onProxySelected: (Int, String) -> Unit,
onProxyDelayTest: (Int, String) -> Unit,
) {
val sources = group.sources
val refreshVersion = group.refreshVersion
Expand All @@ -285,17 +292,19 @@ private fun ProxyGroupPage(
linkNow = linkNow,
proxyLine = proxyLine,
selectedControl = selectedControl,
selectedBackground = selectedBackground,
unselectedControl = unselectedControl,
unselectedBackground = unselectedBackground,
)
selectedBackground = selectedBackground,
unselectedControl = unselectedControl,
unselectedBackground = unselectedBackground,
delayTesting = source.proxy.name in group.delayTestingKeys,
)
}

ProxyItemCard(
item = item,
proxyLine = proxyLine,
selectable = group.selectable,
onClick = { onProxySelected(index, item.key) },
onDelayClick = { onProxyDelayTest(index, item.key) },
)
}
}
Expand All @@ -307,6 +316,7 @@ private fun ProxyItemCard(
proxyLine: Int,
selectable: Boolean,
onClick: () -> Unit,
onDelayClick: () -> Unit,
) {
val shape = RoundedCornerShape(if (proxyLine == 1) 0.dp else 5.dp)
val modifier =
Expand Down Expand Up @@ -342,14 +352,18 @@ private fun ProxyItemCard(
)
}

if (item.delayText.isNotEmpty()) {
Text(
text = item.delayText,
color = item.controls,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
)
}
val badgeText = if (item.delayTesting) delayTestingPlaceholder else item.delayText
Text(
modifier =
Modifier.clip(CircleShape)
.clickable(onClick = onDelayClick)
.background(item.controls.copy(alpha = if (item.delayTesting) 0.33f else 0.14f))
.padding(horizontal = 8.dp, vertical = 2.dp),
text = badgeText,
color = item.controls,
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
)
}
}

Expand Down Expand Up @@ -479,6 +493,7 @@ private fun ProxyMenuRadioRow(title: String, selected: Boolean, onClick: () -> U
}

private val gridContentPadding = 12.dp
private const val delayTestingPlaceholder = "···"

private fun columnsForProxyLine(proxyLine: Int): Int =
when (proxyLine) {
Expand Down Expand Up @@ -563,5 +578,6 @@ private fun ProxyContentPreview() {
onProxySortChanged = {},
onOverrideModeSelected = {},
onProxySelected = { _, _ -> },
onProxyDelayTest = { _, _ -> },
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,29 @@ internal class ProxyViewModel(app: Application) : AndroidViewModel(app), Default
}
}

fun onProxyDelayTest(index: Int, name: String) {
val names = uiState.value.groupNames
if (index !in names.indices) return

updateGroupState(index) {
it.copy(delayTestingKeys = it.delayTestingKeys + name, refreshVersion = it.refreshVersion + 1)
}

viewModelScope.launch {
try {
withClash { healthCheckProxy(names[index], name) }
reload(index)
} finally {
updateGroupState(index) {
it.copy(
delayTestingKeys = it.delayTestingKeys - name,
refreshVersion = it.refreshVersion + 1,
)
}
}
}
}

fun reloadAll() {
val names = uiState.value.groupNames
names.indices.forEach { idx -> reload(idx) }
Expand Down Expand Up @@ -207,6 +230,7 @@ internal class ProxyViewModel(app: Application) : AndroidViewModel(app), Default
selectable = group.type == Proxy.Type.Selector,
urlTesting = false,
sources = sources,
delayTestingKeys = it.delayTestingKeys.intersect(sources.mapTo(mutableSetOf()) { s -> s.proxy.name }),
refreshVersion = it.refreshVersion + 1,
)
}
Expand Down Expand Up @@ -239,6 +263,7 @@ internal class ProxyViewModel(app: Application) : AndroidViewModel(app), Default
val selectable: Boolean = false,
val urlTesting: Boolean = false,
val sources: List<ProxyItemSource> = emptyList(),
val delayTestingKeys: Set<String> = emptySet(),
val refreshVersion: Int = 0,
)

Expand All @@ -251,6 +276,7 @@ internal class ProxyViewModel(app: Application) : AndroidViewModel(app), Default
selectedBackground: Color,
unselectedControl: Color,
unselectedBackground: Color,
delayTesting: Boolean,
): ProxyItemUiState {
val selected = proxy.name == parentNow?.name
val background =
Expand Down Expand Up @@ -278,7 +304,8 @@ internal class ProxyViewModel(app: Application) : AndroidViewModel(app), Default
key = proxy.name,
title = title,
subtitle = subtitle,
delayText = if (proxy.delay in 0..Short.MAX_VALUE) proxy.delay.toString() else "",
delayText = if (proxy.delay in 0..Short.MAX_VALUE) proxy.delay.toString() else "--",
delayTesting = delayTesting,
selected = selected,
background = background,
controls = controls,
Expand All @@ -291,6 +318,7 @@ internal class ProxyViewModel(app: Application) : AndroidViewModel(app), Default
val title: String,
val subtitle: String,
val delayText: String,
val delayTesting: Boolean,
val selected: Boolean,
val background: Color,
val controls: Color,
Expand Down
Loading