Skip to content
Open
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
18 changes: 14 additions & 4 deletions app/src/main/java/com/github/kr328/clash/ProxyActivity.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package com.github.kr328.clash

import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.design.ProxyDesign
import com.github.kr328.clash.design.model.ProxyState
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
Expand Down Expand Up @@ -92,11 +94,19 @@ class ProxyActivity : BaseActivity<ProxyDesign>() {
}
is ProxyDesign.Request.UrlTest -> {
launch {
withClash {
healthCheck(names[it.index])
}
try {
withClash {
healthCheck(names[it.index])
}

design.requests.send(ProxyDesign.Request.Reload(it.index))
design.requests.send(ProxyDesign.Request.Reload(it.index))
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.w("Request url test for `${names[it.index]}`", e)

design.finishUrlTesting(it.index)
}
}
}
is ProxyDesign.Request.PatchMode -> {
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/golang/native/tunnel.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ func healthCheck(completable unsafe.Pointer, name C.c_string) {
tunnel.HealthCheck(name)

C.complete(completable, nil)

C.release_object(completable)
}(C.GoString(name))
}

Expand Down
68 changes: 62 additions & 6 deletions core/src/main/golang/native/tunnel/connectivity.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
package tunnel

import (
"context"
"sync"
"time"

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

const (
healthCheckTimeout = 5 * time.Second
healthCheckConcurrency = 10
)

// healthCheckURLer is implemented by mihomo proxy providers and exposes the url they test with.
type healthCheckURLer interface {
HealthCheckURL() string
}

// groupTestURL returns the url the members of the group should be tested with.
//
// It follows the same rules as mihomo's own parser: the provider owned by the group first (a group
// which lists `proxies:` gets one and its url is the group's `url:`, falling back to the core
// default test url), then the first provider with a configured health check url (subscriptions
// referenced by `use:`), and finally the core default test url.
func groupTestURL(g outboundgroup.ProxyGroup) string {
for _, pr := range g.Providers() {
if u, ok := pr.(healthCheckURLer); ok {
if url := u.HealthCheckURL(); url != "" {
return url
}
}
}

return C.DefaultTestURL
}

// HealthCheck tests every proxy of the group with groupTestURL(g), so the result can be read back
// afterwards with proxy.LastDelayForTestUrl(groupTestURL(g)).
//
// provider.HealthCheck() is intentionally not used here: it only runs the urls a provider already
// knows about, and it silently does nothing when that url is empty. mihomo builds the reserved
// `default` provider (which backs the auto created GLOBAL group) with an empty health check url
// and never registers a test url on it, so the delay test looked like a no-op for such groups.
func HealthCheck(name string) {
p := tunnel.Proxies()[name]

Expand All @@ -25,16 +62,35 @@ func HealthCheck(name string) {
return
}

wg := &sync.WaitGroup{}
url := groupTestURL(g)

var proxies []C.Proxy
for _, pr := range g.Providers() {
proxies = append(proxies, pr.Proxies()...)
}

log.Debugln("Health checking group `%s` with url `%s` (%d proxies)", name, url, len(proxies))

limit := make(chan struct{}, healthCheckConcurrency)
wg := &sync.WaitGroup{}

for _, px := range proxies {
px := px

limit <- struct{}{}
wg.Add(1)

go func(provider provider.ProxyProvider) {
provider.HealthCheck()
go func() {
defer wg.Done()
defer func() { <-limit }()

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

wg.Done()
}(pr)
if _, err := px.URLTest(ctx, url, nil); err != nil {
log.Debugln("Health check `%s` with url `%s`: %s", px.Name(), url, err.Error())
}
}()
}

wg.Wait()
Expand Down
69 changes: 48 additions & 21 deletions core/src/main/golang/native/tunnel/proxies.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (
Delay
)

// noDelay is mihomo's sentinel for "no usable delay for this test url". The ui renders it as blank.
const noDelay uint16 = 0xffff

type Proxy struct {
Name string `json:"name"`
Title string `json:"title"`
Expand Down Expand Up @@ -98,8 +101,10 @@ func QueryProxyGroup(name string, sortMode SortMode, uiSubtitlePattern *regexp2.
return nil
}

proxies := convertProxies(g.Proxies(), uiSubtitlePattern)
// proxies := collectProviders(g.Providers(), uiSubtitlePattern)
testURL := groupTestURL(g)

proxies := convertProxies(g.Proxies(), uiSubtitlePattern, testURL)
// proxies := collectProviders(g.Providers(), uiSubtitlePattern, testURL)

switch sortMode {
case Title:
Expand Down Expand Up @@ -165,7 +170,44 @@ func PatchSelector(selector, name string) bool {
return true
}

func convertProxies(proxies []C.Proxy, uiSubtitlePattern *regexp2.Regexp) []*Proxy {
// lastDelay returns the delay of the proxy for the test url the group is checked with.
//
// mihomo keeps one delay history per test url and LastDelayForTestUrl returns 0xffff unless the
// last test of that exact url was alive, so the url must never be picked at random (map iteration
// order is randomized): ask for the group's own url first, then fall back to any alive url.
func lastDelay(p C.Proxy, testURL string) uint16 {
if testURL != "" {
if delay := p.LastDelayForTestUrl(testURL); delay != noDelay {
return delay
}
}

var best uint16

for _, state := range p.ExtraDelayHistories() {
if !state.Alive || len(state.History) == 0 {
continue
}

delay := state.History[len(state.History)-1].Delay

if delay == 0 || delay == noDelay {
continue
}

if best == 0 || delay < best {
best = delay
}
}

if best == 0 {
return noDelay
}

return best
}

func convertProxies(proxies []C.Proxy, uiSubtitlePattern *regexp2.Regexp, testURL string) []*Proxy {
result := make([]*Proxy, 0, 128)

for _, p := range proxies {
Expand All @@ -183,28 +225,21 @@ func convertProxies(proxies []C.Proxy, uiSubtitlePattern *regexp2.Regexp) []*Pro
}
}
}
testURL := "https://www.gstatic.com/generate_204"
for k := range p.ExtraDelayHistories() {
if len(k) > 0 {
testURL = k
break
}
}
_, isGroup := p.Adapter().(outboundgroup.ProxyGroup)

result = append(result, &Proxy{
Name: name,
Title: strings.TrimSpace(title),
Subtitle: strings.TrimSpace(subtitle),
Type: p.Type().String(),
Delay: int(p.LastDelayForTestUrl(testURL)),
Delay: int(lastDelay(p, testURL)),
IsGroup: isGroup,
})
}
return result
}

func collectProviders(providers []provider.ProxyProvider, uiSubtitlePattern *regexp2.Regexp) []*Proxy {
func collectProviders(providers []provider.ProxyProvider, uiSubtitlePattern *regexp2.Regexp, testURL string) []*Proxy {
result := make([]*Proxy, 0, 128)

for _, p := range providers {
Expand All @@ -223,22 +258,14 @@ func collectProviders(providers []provider.ProxyProvider, uiSubtitlePattern *reg
}
}
}

testURL := "https://www.gstatic.com/generate_204"
for k := range px.ExtraDelayHistories() {
if len(k) > 0 {
testURL = k
break
}
}
_, isGroup := px.Adapter().(outboundgroup.ProxyGroup)

result = append(result, &Proxy{
Name: name,
Title: strings.TrimSpace(title),
Subtitle: strings.TrimSpace(subtitle),
Type: px.Type().String(),
Delay: int(px.LastDelayForTestUrl(testURL)),
Delay: int(lastDelay(px, testURL)),
IsGroup: isGroup,
})
}
Expand Down
16 changes: 16 additions & 0 deletions design/src/main/java/com/github/kr328/clash/design/ProxyDesign.kt
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,22 @@ class ProxyDesign(
updateUrlTestButtonStatus()
}

// Called when the url test of a page is over, even if it failed, otherwise the progress
// indicator of the toolbar would spin forever.
suspend fun finishUrlTesting(position: Int) {
withContext(Dispatchers.Main) {
if (binding.pagesView.adapter !is ProxyPageAdapter)
return@withContext

if (position !in adapter.states.indices)
return@withContext

adapter.states[position].urlTesting = false

updateUrlTestButtonStatus()
}
}

private fun updateUrlTestButtonStatus() {
if (verticalBottomScrolled || horizontalScrolling || urlTesting) {
binding.urlTestFloatView.hide()
Expand Down