14.04.2026 Update
This commit is contained in:
832
monitor/monitor.go
Normal file
832
monitor/monitor.go
Normal file
@@ -0,0 +1,832 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"nano-router/config"
|
||||
"nano-router/mihomo"
|
||||
)
|
||||
|
||||
const (
|
||||
checkInterval = 30 * time.Second
|
||||
speedInterval = 5 * time.Second
|
||||
ipCheckInterval = 10 * time.Minute
|
||||
minuteSlots = 60
|
||||
)
|
||||
|
||||
type MinuteSlot struct {
|
||||
Minute int64 `json:"minute"`
|
||||
Status string `json:"status"`
|
||||
Pings map[string]int `json:"pings,omitempty"`
|
||||
PingCF int `json:"ping_cf,omitempty"`
|
||||
PingGG int `json:"ping_gg,omitempty"`
|
||||
}
|
||||
|
||||
type ConnectivityState struct {
|
||||
DirectUp bool `json:"direct_up"`
|
||||
VPNUp bool `json:"vpn_up"`
|
||||
DirectUpSince string `json:"direct_up_since,omitempty"`
|
||||
VPNUpSince string `json:"vpn_up_since,omitempty"`
|
||||
MinutesD []MinuteSlot `json:"minutes_direct"`
|
||||
MinutesV []MinuteSlot `json:"minutes_vpn"`
|
||||
EndpointNames []string `json:"endpoint_names"`
|
||||
}
|
||||
|
||||
type Sample struct {
|
||||
Time time.Time `json:"time"`
|
||||
RxPS float64 `json:"rx_bps"`
|
||||
TxPS float64 `json:"tx_bps"`
|
||||
}
|
||||
|
||||
type IPInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Country string `json:"country"`
|
||||
CC string `json:"cc"`
|
||||
}
|
||||
|
||||
type SystemStats struct {
|
||||
CPUPct float64 `json:"cpu_pct"`
|
||||
MemPct float64 `json:"mem_pct"`
|
||||
MemTotal uint64 `json:"mem_total"`
|
||||
MemUsed uint64 `json:"mem_used"`
|
||||
MemFree uint64 `json:"mem_free"`
|
||||
Uptime int64 `json:"uptime"`
|
||||
}
|
||||
|
||||
type DashboardData struct {
|
||||
System SystemStats `json:"system"`
|
||||
Mihomo MihomoStatus `json:"mihomo"`
|
||||
Connectivity ConnectivityState `json:"connectivity"`
|
||||
GatewayIface string `json:"gateway_iface"`
|
||||
TrafficAvg []Sample `json:"traffic_avg"`
|
||||
TrafficReal []Sample `json:"traffic_real"`
|
||||
IPDirect *IPInfo `json:"ip_direct"`
|
||||
IPVPN *IPInfo `json:"ip_vpn"`
|
||||
}
|
||||
|
||||
type MihomoStatus struct {
|
||||
Running bool `json:"running"`
|
||||
PID int `json:"pid,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
mu sync.RWMutex
|
||||
connState ConnectivityState
|
||||
minuteSlotsD [minuteSlots]MinuteSlot
|
||||
minuteSlotsV [minuteSlots]MinuteSlot
|
||||
gatewayIface string
|
||||
prevRx uint64
|
||||
prevTx uint64
|
||||
prevSpeedTime time.Time
|
||||
curRxPS float64
|
||||
curTxPS float64
|
||||
avgSamples []Sample
|
||||
realSamples []Sample
|
||||
ipDirect *IPInfo
|
||||
ipVPN *IPInfo
|
||||
ipDirectT time.Time
|
||||
ipVPNT time.Time
|
||||
|
||||
directUpSince *time.Time
|
||||
vpnUpSince *time.Time
|
||||
wasDirectUp bool
|
||||
wasVPNUp bool
|
||||
|
||||
prevCPUTotal uint64
|
||||
prevCPUIdle uint64
|
||||
cpuInitialized bool
|
||||
|
||||
stopCh chan struct{}
|
||||
)
|
||||
|
||||
func Start() {
|
||||
stopCh = make(chan struct{})
|
||||
gatewayIface = detectGatewayIface()
|
||||
for i := 0; i < minuteSlots; i++ {
|
||||
minuteSlotsD[i] = MinuteSlot{Status: "no_data"}
|
||||
minuteSlotsV[i] = MinuteSlot{Status: "no_data"}
|
||||
}
|
||||
go connectivityLoop()
|
||||
go speedLoop()
|
||||
go ipCheckLoop()
|
||||
}
|
||||
|
||||
func Stop() {
|
||||
close(stopCh)
|
||||
}
|
||||
|
||||
func GetData() DashboardData {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
|
||||
cfg, _ := config.Load()
|
||||
var epNames []string
|
||||
if cfg != nil && len(cfg.Connectivity.Direct) > 0 {
|
||||
for _, ep := range cfg.Connectivity.Direct {
|
||||
epNames = append(epNames, ep.Name)
|
||||
}
|
||||
} else {
|
||||
epNames = []string{"Cloudflare", "Google"}
|
||||
}
|
||||
connState.EndpointNames = epNames
|
||||
|
||||
d := DashboardData{
|
||||
System: readSystemStats(),
|
||||
Mihomo: getMihomoRunning(),
|
||||
Connectivity: connState,
|
||||
GatewayIface: gatewayIface,
|
||||
IPDirect: ipDirect,
|
||||
IPVPN: ipVPN,
|
||||
}
|
||||
|
||||
d.Connectivity.MinutesD = buildMinuteSlots(minuteSlotsD[:])
|
||||
d.Connectivity.MinutesV = buildMinuteSlots(minuteSlotsV[:])
|
||||
|
||||
now := time.Now()
|
||||
cutoff10m := now.Add(-10 * time.Minute)
|
||||
cutoff1m := now.Add(-1 * time.Minute)
|
||||
|
||||
filteredAvg := make([]Sample, 0, len(avgSamples))
|
||||
for _, s := range avgSamples {
|
||||
if !s.Time.Before(cutoff10m) {
|
||||
filteredAvg = append(filteredAvg, s)
|
||||
}
|
||||
}
|
||||
d.TrafficAvg = filteredAvg
|
||||
|
||||
filteredReal := make([]Sample, 0, len(realSamples))
|
||||
for _, s := range realSamples {
|
||||
if !s.Time.Before(cutoff1m) {
|
||||
filteredReal = append(filteredReal, s)
|
||||
}
|
||||
}
|
||||
d.TrafficReal = filteredReal
|
||||
|
||||
if d.IPDirect != nil && time.Since(ipDirectT) > ipCheckInterval+time.Minute {
|
||||
d.IPDirect = nil
|
||||
}
|
||||
if d.IPVPN != nil && time.Since(ipVPNT) > ipCheckInterval+time.Minute {
|
||||
d.IPVPN = nil
|
||||
}
|
||||
|
||||
return d
|
||||
}
|
||||
|
||||
func getMihomoRunning() MihomoStatus {
|
||||
st := mihomo.Status()
|
||||
r, _ := st["running"].(bool)
|
||||
s := MihomoStatus{Running: r}
|
||||
if r {
|
||||
if pid, ok := st["pid"].(int); ok {
|
||||
s.PID = pid
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func readSystemStats() SystemStats {
|
||||
s := SystemStats{}
|
||||
readCPUStats(&s)
|
||||
readMemStats(&s)
|
||||
readUptime(&s)
|
||||
return s
|
||||
}
|
||||
|
||||
func readCPUStats(s *SystemStats) {
|
||||
f, err := os.Open("/proc/stat")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
if !scanner.Scan() {
|
||||
return
|
||||
}
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "cpu ") {
|
||||
return
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 5 {
|
||||
return
|
||||
}
|
||||
|
||||
var total, idle uint64
|
||||
for i := 1; i < len(fields) && i <= 8; i++ {
|
||||
v, _ := strconv.ParseUint(fields[i], 10, 64)
|
||||
total += v
|
||||
if i == 4 {
|
||||
idle = v
|
||||
}
|
||||
}
|
||||
|
||||
if cpuInitialized && prevCPUTotal > 0 {
|
||||
dTotal := total - prevCPUTotal
|
||||
dIdle := idle - prevCPUIdle
|
||||
if dTotal > 0 {
|
||||
s.CPUPct = float64(dTotal-dIdle) / float64(dTotal) * 100.0
|
||||
}
|
||||
}
|
||||
prevCPUTotal = total
|
||||
prevCPUIdle = idle
|
||||
cpuInitialized = true
|
||||
}
|
||||
|
||||
func readMemStats(s *SystemStats) {
|
||||
f, err := os.Open("/proc/meminfo")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var memTotal, memFree, memAvailable, buffers, cached uint64
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 2 {
|
||||
continue
|
||||
}
|
||||
val, _ := strconv.ParseUint(parts[1], 10, 64)
|
||||
switch {
|
||||
case strings.HasPrefix(parts[0], "MemTotal:"):
|
||||
memTotal = val
|
||||
case strings.HasPrefix(parts[0], "MemFree:"):
|
||||
memFree = val
|
||||
case strings.HasPrefix(parts[0], "MemAvailable:"):
|
||||
memAvailable = val
|
||||
case strings.HasPrefix(parts[0], "Buffers:"):
|
||||
buffers = val
|
||||
case strings.HasPrefix(parts[0], "Cached:"):
|
||||
cached = val
|
||||
}
|
||||
}
|
||||
|
||||
s.MemTotal = memTotal * 1024
|
||||
s.MemFree = memFree * 1024
|
||||
used := memTotal - memAvailable
|
||||
if memAvailable == 0 {
|
||||
used = memTotal - memFree - buffers - cached
|
||||
}
|
||||
s.MemUsed = used * 1024
|
||||
if memTotal > 0 {
|
||||
s.MemPct = float64(used) / float64(memTotal) * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
func readUptime(s *SystemStats) {
|
||||
f, err := os.Open("/proc/uptime")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
var upSec float64
|
||||
fmt.Fscanf(f, "%f", &upSec)
|
||||
s.Uptime = int64(upSec)
|
||||
}
|
||||
|
||||
func detectGatewayIface() string {
|
||||
out, err := exec.Command("ip", "route", "show", "default").Output()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
fields := strings.Fields(line)
|
||||
for i, f := range fields {
|
||||
if f == "dev" && i+1 < len(fields) {
|
||||
return fields[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func readIfaceBytes(iface string) (rx, tx uint64) {
|
||||
f, err := os.Open("/proc/net/dev")
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
colon := strings.Index(line, ":")
|
||||
if colon < 0 {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(line[:colon])
|
||||
if name != iface {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line[colon+1:])
|
||||
if len(fields) >= 10 {
|
||||
rx, _ = strconv.ParseUint(fields[0], 10, 64)
|
||||
tx, _ = strconv.ParseUint(fields[8], 10, 64)
|
||||
}
|
||||
return
|
||||
}
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func connectivityLoop() {
|
||||
ticker := time.NewTicker(checkInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
checkConnectivity()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
checkConnectivity()
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkConnectivity() {
|
||||
directResult := checkDirectConnectivity()
|
||||
vpnResult := checkVPNConnectivity()
|
||||
|
||||
now := time.Now()
|
||||
minuteIdx := int(now.Unix()/60) % minuteSlots
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
connState.DirectUp = directResult.Up
|
||||
connState.VPNUp = vpnResult.Up
|
||||
|
||||
if directResult.Up && !wasDirectUp {
|
||||
directUpSince = &time.Time{}
|
||||
*directUpSince = now
|
||||
} else if !directResult.Up {
|
||||
directUpSince = nil
|
||||
}
|
||||
wasDirectUp = directResult.Up
|
||||
|
||||
mihomoRunning := mihomo.IsRunning()
|
||||
if vpnResult.Up && !wasVPNUp {
|
||||
vpnUpSince = &time.Time{}
|
||||
*vpnUpSince = now
|
||||
} else if !vpnResult.Up {
|
||||
vpnUpSince = nil
|
||||
}
|
||||
wasVPNUp = vpnResult.Up
|
||||
|
||||
if directUpSince != nil {
|
||||
dur := now.Sub(*directUpSince)
|
||||
connState.DirectUpSince = fmtDuration(dur)
|
||||
} else {
|
||||
connState.DirectUpSince = ""
|
||||
}
|
||||
if vpnUpSince != nil {
|
||||
dur := now.Sub(*vpnUpSince)
|
||||
connState.VPNUpSince = fmtDuration(dur)
|
||||
} else {
|
||||
connState.VPNUpSince = ""
|
||||
}
|
||||
|
||||
statusD := "up"
|
||||
if !directResult.Up {
|
||||
statusD = "down"
|
||||
}
|
||||
statusV := "up"
|
||||
if !vpnResult.Up {
|
||||
statusV = "down"
|
||||
}
|
||||
if !mihomoRunning {
|
||||
statusV = "no_data"
|
||||
}
|
||||
|
||||
dSlot := MinuteSlot{Status: statusD, Pings: directResult.Pings}
|
||||
if v, ok := directResult.Pings["Cloudflare"]; ok {
|
||||
dSlot.PingCF = v
|
||||
}
|
||||
if v, ok := directResult.Pings["Google"]; ok {
|
||||
dSlot.PingGG = v
|
||||
}
|
||||
minuteSlotsD[minuteIdx] = mergeSlot(minuteSlotsD[minuteIdx], dSlot)
|
||||
|
||||
vSlot := MinuteSlot{Status: statusV, Pings: make(map[string]int)}
|
||||
if vpnResult.Up {
|
||||
vSlot.Pings = vpnResult.Pings
|
||||
if v, ok := vpnResult.Pings["Cloudflare"]; ok {
|
||||
vSlot.PingCF = v
|
||||
}
|
||||
if v, ok := vpnResult.Pings["Google"]; ok {
|
||||
vSlot.PingGG = v
|
||||
}
|
||||
}
|
||||
minuteSlotsV[minuteIdx] = mergeSlot(minuteSlotsV[minuteIdx], vSlot)
|
||||
}
|
||||
|
||||
func fmtDuration(d time.Duration) string {
|
||||
s := int(d.Seconds())
|
||||
if s < 0 {
|
||||
s = 0
|
||||
}
|
||||
days := s / 86400
|
||||
s %= 86400
|
||||
hours := s / 3600
|
||||
s %= 3600
|
||||
mins := s / 60
|
||||
secs := s % 60
|
||||
|
||||
parts := []string{}
|
||||
if days > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%dd", days))
|
||||
}
|
||||
if hours > 0 || days > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%dh", hours))
|
||||
}
|
||||
if mins > 0 || hours > 0 || days > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%dm", mins))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%ds", secs))
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func mergeSlot(existing, new MinuteSlot) MinuteSlot {
|
||||
merged := MinuteSlot{
|
||||
Status: new.Status,
|
||||
Pings: make(map[string]int),
|
||||
}
|
||||
if new.Status == "down" && existing.Status == "up" {
|
||||
merged.Status = "down"
|
||||
} else if new.Status == "up" && existing.Status == "down" {
|
||||
merged.Status = "down"
|
||||
}
|
||||
for k, v := range existing.Pings {
|
||||
merged.Pings[k] = v
|
||||
}
|
||||
for k, v := range new.Pings {
|
||||
if v > 0 {
|
||||
merged.Pings[k] = v
|
||||
}
|
||||
}
|
||||
if v, ok := merged.Pings["Cloudflare"]; ok {
|
||||
merged.PingCF = v
|
||||
}
|
||||
if v, ok := merged.Pings["Google"]; ok {
|
||||
merged.PingGG = v
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
type connResult struct {
|
||||
Up bool
|
||||
Pings map[string]int
|
||||
PingCF int
|
||||
PingGG int
|
||||
}
|
||||
|
||||
func checkDirectConnectivity() connResult {
|
||||
result := connResult{Pings: make(map[string]int)}
|
||||
client := &http.Client{Timeout: 8 * time.Second}
|
||||
|
||||
cfg, _ := config.Load()
|
||||
if cfg == nil || len(cfg.Connectivity.Direct) == 0 {
|
||||
def := config.DefaultConnectivity()
|
||||
cfg = &config.AppConfig{Connectivity: def}
|
||||
}
|
||||
endpoints := cfg.Connectivity.Direct
|
||||
|
||||
anyUp := false
|
||||
for _, ep := range endpoints {
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ep.URL, nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
elapsed := time.Since(start)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
ms := int(elapsed.Milliseconds())
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
anyUp = true
|
||||
result.Pings[ep.Name] = ms
|
||||
}
|
||||
}
|
||||
result.Up = anyUp
|
||||
return result
|
||||
}
|
||||
|
||||
func checkVPNConnectivity() connResult {
|
||||
result := connResult{Pings: make(map[string]int)}
|
||||
if !mihomo.IsRunning() {
|
||||
return result
|
||||
}
|
||||
|
||||
mixedPort := getMihomoMixedPort()
|
||||
if mixedPort == 0 {
|
||||
mixedPort = 7890
|
||||
}
|
||||
|
||||
proxyAddr := fmt.Sprintf("127.0.0.1:%d", mixedPort)
|
||||
proxyURL, _ := url.Parse("http://" + proxyAddr)
|
||||
|
||||
proxyClient := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
},
|
||||
}
|
||||
|
||||
cfg, _ := config.Load()
|
||||
if cfg == nil || len(cfg.Connectivity.ViaProxy) == 0 {
|
||||
def := config.DefaultConnectivity()
|
||||
cfg = &config.AppConfig{Connectivity: def}
|
||||
}
|
||||
endpoints := cfg.Connectivity.ViaProxy
|
||||
|
||||
anyUp := false
|
||||
for _, ep := range endpoints {
|
||||
start := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ep.URL, nil)
|
||||
if err != nil {
|
||||
cancel()
|
||||
continue
|
||||
}
|
||||
resp, err := proxyClient.Do(req)
|
||||
elapsed := time.Since(start)
|
||||
cancel()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
ms := int(elapsed.Milliseconds())
|
||||
if resp.StatusCode >= 200 && resp.StatusCode < 400 {
|
||||
anyUp = true
|
||||
result.Pings[ep.Name] = ms
|
||||
}
|
||||
}
|
||||
result.Up = anyUp
|
||||
return result
|
||||
}
|
||||
|
||||
func getMihomoMixedPort() int {
|
||||
cfg, err := mihomo.LoadConfig()
|
||||
if err != nil {
|
||||
return 7890
|
||||
}
|
||||
if mp, ok := cfg["mixed-port"]; ok {
|
||||
switch v := mp.(type) {
|
||||
case int:
|
||||
return v
|
||||
case float64:
|
||||
return int(v)
|
||||
}
|
||||
}
|
||||
return 7890
|
||||
}
|
||||
|
||||
func speedLoop() {
|
||||
iface := gatewayIface
|
||||
if iface == "" {
|
||||
iface = detectGatewayIface()
|
||||
gatewayIface = iface
|
||||
}
|
||||
|
||||
if iface == "" {
|
||||
return
|
||||
}
|
||||
|
||||
rx, tx := readIfaceBytes(iface)
|
||||
prevRx = rx
|
||||
prevTx = tx
|
||||
prevSpeedTime = time.Now()
|
||||
|
||||
ticker := time.NewTicker(speedInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
captureSpeedSample()
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func captureSpeedSample() {
|
||||
iface := gatewayIface
|
||||
if iface == "" {
|
||||
iface = detectGatewayIface()
|
||||
if iface == "" {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
gatewayIface = iface
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
rx, tx := readIfaceBytes(iface)
|
||||
now := time.Now()
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
dt := now.Sub(prevSpeedTime).Seconds()
|
||||
if dt > 0 && prevSpeedTime.Before(now) {
|
||||
curRxPS = float64(rx-prevRx) * 8.0 / dt
|
||||
curTxPS = float64(tx-prevTx) * 8.0 / dt
|
||||
}
|
||||
|
||||
prevRx = rx
|
||||
prevTx = tx
|
||||
prevSpeedTime = now
|
||||
|
||||
sample := Sample{Time: now, RxPS: curRxPS, TxPS: curTxPS}
|
||||
realSamples = append(realSamples, sample)
|
||||
|
||||
cutoff1m := now.Add(-1 * time.Minute)
|
||||
filtered := make([]Sample, 0, len(realSamples))
|
||||
for _, s := range realSamples {
|
||||
if !s.Time.Before(cutoff1m) {
|
||||
filtered = append(filtered, s)
|
||||
}
|
||||
}
|
||||
realSamples = filtered
|
||||
|
||||
avgSamples = append(avgSamples, sample)
|
||||
cutoff10m := now.Add(-10 * time.Minute)
|
||||
filteredAvg := make([]Sample, 0, len(avgSamples))
|
||||
for _, s := range avgSamples {
|
||||
if !s.Time.Before(cutoff10m) {
|
||||
filteredAvg = append(filteredAvg, s)
|
||||
}
|
||||
}
|
||||
avgSamples = filteredAvg
|
||||
}
|
||||
|
||||
func ipCheckLoop() {
|
||||
checkIPs()
|
||||
ticker := time.NewTicker(ipCheckInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
checkIPs()
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkIPs() {
|
||||
directInfo := fetchIPDirect()
|
||||
if directInfo != nil {
|
||||
mu.Lock()
|
||||
ipDirect = directInfo
|
||||
ipDirectT = time.Now()
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
if mihomo.IsRunning() {
|
||||
vpnInfo := fetchIPVPN()
|
||||
if vpnInfo != nil {
|
||||
mu.Lock()
|
||||
ipVPN = vpnInfo
|
||||
ipVPNT = time.Now()
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchIPDirect() *IPInfo {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
return fetchIPFromServices(client)
|
||||
}
|
||||
|
||||
func fetchIPVPN() *IPInfo {
|
||||
mixedPort := getMihomoMixedPort()
|
||||
proxyAddr := fmt.Sprintf("127.0.0.1:%d", mixedPort)
|
||||
proxyURL, _ := url.Parse("http://" + proxyAddr)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyURL(proxyURL),
|
||||
},
|
||||
}
|
||||
return fetchIPFromServices(client)
|
||||
}
|
||||
|
||||
func fetchIPFromServices(client *http.Client) *IPInfo {
|
||||
services := []struct {
|
||||
url string
|
||||
parse func(body []byte) *IPInfo
|
||||
}{
|
||||
{
|
||||
"https://ipinfo.io/json",
|
||||
func(body []byte) *IPInfo {
|
||||
var data struct {
|
||||
IP string `json:"ip"`
|
||||
Country string `json:"country"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil || data.IP == "" {
|
||||
return nil
|
||||
}
|
||||
return &IPInfo{IP: data.IP, Country: data.Country, CC: strings.ToLower(data.Country)}
|
||||
},
|
||||
},
|
||||
{
|
||||
"https://ipapi.co/json/",
|
||||
func(body []byte) *IPInfo {
|
||||
var data struct {
|
||||
IP string `json:"ip"`
|
||||
CountryCode string `json:"country_code"`
|
||||
CountryName string `json:"country_name"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &data); err != nil || data.IP == "" {
|
||||
return nil
|
||||
}
|
||||
cc := strings.ToLower(data.CountryCode)
|
||||
country := data.CountryName
|
||||
if country == "" {
|
||||
country = data.CountryCode
|
||||
}
|
||||
return &IPInfo{IP: data.IP, Country: country, CC: cc}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, svc := range services {
|
||||
resp, err := client.Get(svc.url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
info := svc.parse(body)
|
||||
if info != nil {
|
||||
return info
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMinuteSlots(slots []MinuteSlot) []MinuteSlot {
|
||||
result := make([]MinuteSlot, minuteSlots)
|
||||
now := time.Now()
|
||||
currentMinute := now.Unix() / 60
|
||||
|
||||
for i := 0; i < minuteSlots; i++ {
|
||||
minuteOffset := minuteSlots - 1 - i
|
||||
minuteIdx := int((currentMinute - int64(minuteOffset)) % minuteSlots)
|
||||
if minuteIdx < 0 {
|
||||
minuteIdx += minuteSlots
|
||||
}
|
||||
result[i] = slots[minuteIdx]
|
||||
result[i].Minute = currentMinute - int64(minuteOffset)
|
||||
if result[i].Status == "" {
|
||||
result[i].Status = "no_data"
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var countryFlagRe = regexp.MustCompile(`^[a-zA-Z]{2}$`)
|
||||
|
||||
func FlagEmoji(cc string) string {
|
||||
if !countryFlagRe.MatchString(cc) {
|
||||
return ""
|
||||
}
|
||||
cc = strings.ToUpper(cc)
|
||||
runes := []rune(cc)
|
||||
if len(runes) != 2 {
|
||||
return ""
|
||||
}
|
||||
base := rune(0x1F1E6)
|
||||
return string(base+(runes[0]-'A')) + string(base+(runes[1]-'A'))
|
||||
}
|
||||
Reference in New Issue
Block a user