github.com/metacubex/mihomo@v1.18.5/adapter/provider/healthcheck.go (about) 1 package provider 2 3 import ( 4 "context" 5 "strings" 6 "sync" 7 "time" 8 9 "github.com/metacubex/mihomo/common/atomic" 10 "github.com/metacubex/mihomo/common/batch" 11 "github.com/metacubex/mihomo/common/singledo" 12 "github.com/metacubex/mihomo/common/utils" 13 C "github.com/metacubex/mihomo/constant" 14 "github.com/metacubex/mihomo/log" 15 16 "github.com/dlclark/regexp2" 17 ) 18 19 type HealthCheckOption struct { 20 URL string 21 Interval uint 22 } 23 24 type extraOption struct { 25 expectedStatus utils.IntRanges[uint16] 26 filters map[string]struct{} 27 } 28 29 type HealthCheck struct { 30 url string 31 extra map[string]*extraOption 32 mu sync.Mutex 33 started atomic.Bool 34 proxies []C.Proxy 35 interval time.Duration 36 lazy bool 37 expectedStatus utils.IntRanges[uint16] 38 lastTouch atomic.TypedValue[time.Time] 39 done chan struct{} 40 singleDo *singledo.Single[struct{}] 41 timeout time.Duration 42 } 43 44 func (hc *HealthCheck) process() { 45 if hc.started.Load() { 46 log.Warnln("Skip start health check timer due to it's started") 47 return 48 } 49 50 ticker := time.NewTicker(hc.interval) 51 hc.start() 52 for { 53 select { 54 case <-ticker.C: 55 lastTouch := hc.lastTouch.Load() 56 since := time.Since(lastTouch) 57 if !hc.lazy || since < hc.interval { 58 hc.check() 59 } else { 60 log.Debugln("Skip once health check because we are lazy") 61 } 62 case <-hc.done: 63 ticker.Stop() 64 hc.stop() 65 return 66 } 67 } 68 } 69 70 func (hc *HealthCheck) setProxy(proxies []C.Proxy) { 71 hc.proxies = proxies 72 } 73 74 func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) { 75 url = strings.TrimSpace(url) 76 if len(url) == 0 || url == hc.url { 77 log.Debugln("ignore invalid health check url: %s", url) 78 return 79 } 80 81 hc.mu.Lock() 82 defer hc.mu.Unlock() 83 84 // if the provider has not set up health checks, then modify it to be the same as the group's interval 85 if hc.interval == 0 { 86 hc.interval = time.Duration(interval) * time.Second 87 } 88 89 if hc.extra == nil { 90 hc.extra = make(map[string]*extraOption) 91 } 92 93 // prioritize the use of previously registered configurations, especially those from provider 94 if _, ok := hc.extra[url]; ok { 95 // provider default health check does not set filter 96 if url != hc.url && len(filter) != 0 { 97 splitAndAddFiltersToExtra(filter, hc.extra[url]) 98 } 99 100 log.Debugln("health check url: %s exists", url) 101 return 102 } 103 104 option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus} 105 splitAndAddFiltersToExtra(filter, option) 106 hc.extra[url] = option 107 108 if hc.auto() && !hc.started.Load() { 109 go hc.process() 110 } 111 } 112 113 func splitAndAddFiltersToExtra(filter string, option *extraOption) { 114 filter = strings.TrimSpace(filter) 115 if len(filter) != 0 { 116 for _, regex := range strings.Split(filter, "`") { 117 regex = strings.TrimSpace(regex) 118 if len(regex) != 0 { 119 option.filters[regex] = struct{}{} 120 } 121 } 122 } 123 } 124 125 func (hc *HealthCheck) auto() bool { 126 return hc.interval != 0 127 } 128 129 func (hc *HealthCheck) touch() { 130 hc.lastTouch.Store(time.Now()) 131 } 132 133 func (hc *HealthCheck) start() { 134 hc.started.Store(true) 135 } 136 137 func (hc *HealthCheck) stop() { 138 hc.started.Store(false) 139 } 140 141 func (hc *HealthCheck) check() { 142 if len(hc.proxies) == 0 { 143 return 144 } 145 146 _, _, _ = hc.singleDo.Do(func() (struct{}, error) { 147 id := utils.NewUUIDV4().String() 148 log.Debugln("Start New Health Checking {%s}", id) 149 b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10)) 150 151 // execute default health check 152 option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus} 153 hc.execute(b, hc.url, id, option) 154 155 // execute extra health check 156 if len(hc.extra) != 0 { 157 for url, option := range hc.extra { 158 hc.execute(b, url, id, option) 159 } 160 } 161 b.Wait() 162 log.Debugln("Finish A Health Checking {%s}", id) 163 return struct{}{}, nil 164 }) 165 } 166 167 func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) { 168 url = strings.TrimSpace(url) 169 if len(url) == 0 { 170 log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid) 171 return 172 } 173 174 var filterReg *regexp2.Regexp 175 var expectedStatus utils.IntRanges[uint16] 176 if option != nil { 177 expectedStatus = option.expectedStatus 178 if len(option.filters) != 0 { 179 filters := make([]string, 0, len(option.filters)) 180 for filter := range option.filters { 181 filters = append(filters, filter) 182 } 183 184 filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None) 185 } 186 } 187 188 for _, proxy := range hc.proxies { 189 // skip proxies that do not require health check 190 if filterReg != nil { 191 if match, _ := filterReg.MatchString(proxy.Name()); !match { 192 continue 193 } 194 } 195 196 p := proxy 197 b.Go(p.Name(), func() (bool, error) { 198 ctx, cancel := context.WithTimeout(context.Background(), hc.timeout) 199 defer cancel() 200 log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid) 201 _, _ = p.URLTest(ctx, url, expectedStatus) 202 log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid) 203 return false, nil 204 }) 205 } 206 } 207 208 func (hc *HealthCheck) close() { 209 hc.done <- struct{}{} 210 } 211 212 func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck { 213 if url == "" { 214 expectedStatus = nil 215 interval = 0 216 } 217 if timeout == 0 { 218 timeout = 5000 219 } 220 221 return &HealthCheck{ 222 proxies: proxies, 223 url: url, 224 timeout: time.Duration(timeout) * time.Millisecond, 225 extra: map[string]*extraOption{}, 226 interval: time.Duration(interval) * time.Second, 227 lazy: lazy, 228 expectedStatus: expectedStatus, 229 done: make(chan struct{}, 1), 230 singleDo: singledo.NewSingle[struct{}](time.Second), 231 } 232 }