Files
chen08209 2fa2f7f816 feat(core): bind the Android Core with gomobile
Changelog: Share link import and mrs rule sets
2026-10-11 12:28:52 +08:00

185 lines
4.3 KiB
Go

package core
import (
"context"
"maps"
"sync"
"time"
"github.com/metacubex/mihomo/tunnel"
)
// mihomo has no hook for a health check moving a URLTest or Fallback pick, so
// the picks are re-read on this timer while the host watches.
const routePollInterval = time.Second
type routeTracker struct {
mu sync.Mutex
epoch uint64
picksVersion uint64
picks map[string]string
providerVersions map[string]uint32
ruleSets map[string]any
watched bool
watchSeq uint64
stopPoll context.CancelFunc
}
var (
currentRoute routeTracker
publishRoute = func(state RouteState) {
sendMessage(RouteChangedMessage, state)
}
)
func routeStateLocked() RouteState {
picks := currentRoute.picks
if picks == nil {
picks = map[string]string{}
}
return RouteState{
CoreEpoch: currentRoute.epoch,
PicksVersion: currentRoute.picksVersion,
Picks: maps.Clone(picks),
}
}
func routeState() RouteState {
currentRoute.mu.Lock()
defer currentRoute.mu.Unlock()
return routeStateLocked()
}
func routeStamp() (epoch, picksVersion uint64) {
currentRoute.mu.Lock()
defer currentRoute.mu.Unlock()
return currentRoute.epoch, currentRoute.picksVersion
}
func readPicks() (map[string]string, map[string]uint32) {
picks := map[string]string{}
for name, proxy := range tunnel.AllProxies() {
if group, ok := adapterAs[pickableGroup](proxy); ok {
picks[name] = group.Now()
}
}
providers := tunnel.ProvidersSnapshot()
versions := make(map[string]uint32, len(providers))
for name, p := range providers {
versions[name] = p.Version()
}
return picks, versions
}
// mihomo swaps in a new strategy each time a rule set, manually or on its
// interval, loads new content.
func readRuleSets() map[string]any {
providers := tunnel.RuleProvidersSnapshot()
ruleSets := make(map[string]any, len(providers))
for name, p := range providers {
ruleSets[name] = p.Strategy()
}
return ruleSets
}
// A provider that swapped its list is a structural change: the names may
// survive while the servers behind them do not. The caller holds selectMu.
func refreshRouteLocked(structural bool) {
picks, versions := readPicks()
ruleSets := readRuleSets()
currentRoute.mu.Lock()
if currentRoute.providerVersions != nil && !maps.Equal(versions, currentRoute.providerVersions) {
structural = true
}
if currentRoute.ruleSets != nil && !maps.Equal(ruleSets, currentRoute.ruleSets) {
structural = true
}
currentRoute.providerVersions = versions
currentRoute.ruleSets = ruleSets
if structural {
currentRoute.epoch++
}
changed := structural
if !maps.Equal(picks, currentRoute.picks) {
currentRoute.picks = picks
currentRoute.picksVersion++
changed = true
}
if !changed || !currentRoute.watched {
currentRoute.mu.Unlock()
return
}
state := routeStateLocked()
currentRoute.mu.Unlock()
publishRoute(state)
}
func refreshRoute() {
selectMu.Lock()
defer selectMu.Unlock()
refreshRouteLocked(false)
}
func bumpRouteEpoch() {
selectMu.Lock()
defer selectMu.Unlock()
refreshRouteLocked(true)
}
func handleWatchRoute(watch bool, seq uint64) RouteState {
refreshRoute()
currentRoute.mu.Lock()
defer currentRoute.mu.Unlock()
if seq < currentRoute.watchSeq {
return routeStateLocked()
}
currentRoute.watchSeq = seq
currentRoute.watched = watch
if watch && currentRoute.stopPoll == nil {
ctx, cancel := context.WithCancel(context.Background())
currentRoute.stopPoll = cancel
go pollRoute(ctx)
}
if !watch {
stopRoutePollLocked()
}
return routeStateLocked()
}
// The stop takes its own place in the call order: a watch dispatched before
// it, the newest one included, may still be waiting to run.
func stopRouteWatch() {
currentRoute.mu.Lock()
defer currentRoute.mu.Unlock()
currentRoute.watchSeq = max(currentRoute.watchSeq, methodCallSeq.Add(1))
currentRoute.watched = false
stopRoutePollLocked()
}
func stopRoutePollLocked() {
if currentRoute.stopPoll != nil {
currentRoute.stopPoll()
currentRoute.stopPoll = nil
}
}
// With the listeners down the host pins every probe to DIRECT, so the picks
// do not matter until they come back.
func pollRoute(ctx context.Context) {
ticker := time.NewTicker(routePollInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if isRunning.Load() {
refreshRoute()
}
}
}
}