diff --git a/caching/caching.go b/caching/caching.go new file mode 100644 index 00000000..18a2930a --- /dev/null +++ b/caching/caching.go @@ -0,0 +1,52 @@ +package caching + +import ( + "context" + "time" + + "github.com/patrickmn/go-cache" +) + +type Cache struct { + memoryCache *cache.Cache + + ctx context.Context + cancel context.CancelFunc +} + +func NewCache() *Cache { + ctx, cancel := context.WithCancel(context.Background()) + return &Cache{ + ctx: ctx, + cancel: cancel, + } +} + +func (s *Cache) Init() (err error) { + defer func() { + if err != nil { + s.Flush() + } + }() + + s.memoryCache = cache.New(10*time.Minute, 10*time.Minute) + + return nil +} + +func (s *Cache) Flush() error { + if s.memoryCache != nil { + s.memoryCache.Flush() + } + s.cancel() + + return nil +} + +func (s *Cache) GetCtx() context.Context { + return s.ctx +} + +func (s *Cache) Memory() *cache.Cache { + return s.memoryCache +} \ No newline at end of file diff --git a/go.mod b/go.mod index 63f4f64e..0bc60bec 100644 --- a/go.mod +++ b/go.mod @@ -59,6 +59,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/onsi/ginkgo/v2 v2.22.0 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pires/go-proxyproto v0.8.0 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/quic-go/qpack v0.5.1 // indirect diff --git a/go.sum b/go.sum index 55f92f4b..b54f80ca 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,8 @@ github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8= github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= diff --git a/main.go b/main.go index 84ffca6e..1fd7cee8 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "x-ui/config" "x-ui/database" + "x-ui/caching" "x-ui/logger" "x-ui/sub" "x-ui/web" @@ -61,6 +62,17 @@ func runWebServer() { return } + var cacheInstance *caching.Cache + cacheInstance = caching.NewCache() + global.SetCache(cacheInstance) + err = cacheInstance.Init() + if err != nil { + log.Fatalf("Cache initialization error: %v", err) + return + } else { + log.Println("Cache initialized") + } + sigCh := make(chan os.Signal, 1) // Trap shutdown signals signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM) @@ -79,6 +91,10 @@ func runWebServer() { if err != nil { logger.Debug("Error stopping sub server:", err) } + err = cacheInstance.Flush() + if err != nil { + logger.Debug("Error clearing cache:", err) + } server = web.NewServer() global.SetWebServer(server) @@ -98,10 +114,20 @@ func runWebServer() { } log.Println("Sub server restarted successfully.") + cacheInstance = caching.NewCache() + global.SetCache(cacheInstance) + err = cacheInstance.Init() + if err != nil { + log.Fatalf("Cache re-initialization error: %v", err) + return + } + log.Println("Cache cleared.") + default: server.Stop() subServer.Stop() - log.Println("Shutting down servers.") + cacheInstance.Flush() + log.Println("Shutting down servers and cache clearing.") return } } diff --git a/web/global/global.go b/web/global/global.go index e92c375b..f9d04dab 100644 --- a/web/global/global.go +++ b/web/global/global.go @@ -5,11 +5,13 @@ import ( _ "unsafe" "github.com/robfig/cron/v3" + "github.com/patrickmn/go-cache" ) var ( webServer WebServer subServer SubServer + caching Cache ) type WebServer interface { @@ -21,6 +23,11 @@ type SubServer interface { GetCtx() context.Context } +type Cache interface { + Memory() *cache.Cache + GetCtx() context.Context +} + func SetWebServer(s WebServer) { webServer = s } @@ -36,3 +43,11 @@ func SetSubServer(s SubServer) { func GetSubServer() SubServer { return subServer } + +func SetCache(c Cache) { + caching = c +} + +func GetCache() Cache { + return caching +} diff --git a/web/service/server.go b/web/service/server.go index 73aadf57..e82e0cd1 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -15,6 +15,7 @@ import ( "strconv" "strings" "time" + "regexp" "x-ui/config" "x-ui/database" @@ -22,6 +23,7 @@ import ( "x-ui/util/common" "x-ui/util/sys" "x-ui/xray" + "x-ui/web/global" "github.com/shirou/gopsutil/v4/cpu" "github.com/shirou/gopsutil/v4/disk" @@ -61,8 +63,10 @@ type Status struct { State ProcessState `json:"state"` ErrorMsg string `json:"errorMsg"` Version string `json:"version"` - ApiPort string `json:"apiPort"` } `json:"xray"` + XUI struct { + LatestVersion string `json:"latestVersion"` + } `json:"xui"` Uptime uint64 `json:"uptime"` Loads []float64 `json:"loads"` TcpCount int `json:"tcpCount"` @@ -95,6 +99,14 @@ type ServerService struct { inboundService InboundService } +func extractValue(body string, key string) string { + keystr := "\"" + key + "\":[^,;\\]}]*" + r, _ := regexp.Compile(keystr) + match := r.FindString(body) + keyValMatch := strings.Split(match, ":") + return strings.TrimSpace(strings.ReplaceAll(keyValMatch[1], "\"", "")) +} + func getPublicIP(url string) string { var host string host = os.Getenv("XUI_SERVER_IP") @@ -121,7 +133,37 @@ func getPublicIP(url string) string { return ipString } +func getXuiLatestVersion() string { + cache := global.GetCache().Memory() + if data, found := cache.Get("xui_latest_tag_name"); found { + if tag, ok := data.(string); ok { + return string(tag) + } else { + return "" + } + } else { + url := "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" + + resp, err := http.Get(url) + if err != nil { + return "" + } + defer resp.Body.Close() + + json, err := io.ReadAll(resp.Body) + if err != nil { + return "" + } + + tag := extractValue(string(json), "tag_name") + cache.Set("xui_latest_tag_name", tag, 60*time.Minute) + return tag + } +} + func (s *ServerService) GetStatus(lastStatus *Status) *Status { + cache := global.GetCache().Memory() + now := time.Now() status := &Status{ T: now, @@ -224,8 +266,27 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { logger.Warning("get udp connections failed:", err) } - status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org") - status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org") + if data, found := cache.Get("xui_public_ipv4"); found { + if ipv4, ok := data.(string); ok { + status.PublicIP.IPv4 = string(ipv4) + } else { + status.PublicIP.IPv4 = "N/A" + } + } else { + status.PublicIP.IPv4 = getPublicIP("https://api.ipify.org") + cache.Set("xui_public_ipv4", status.PublicIP.IPv4, 720*time.Hour) + } + + if data, found := cache.Get("xui_public_ipv6"); found { + if ipv6, ok := data.(string); ok { + status.PublicIP.IPv6 = string(ipv6) + } else { + status.PublicIP.IPv6 = "N/A" + } + } else { + status.PublicIP.IPv6 = getPublicIP("https://api6.ipify.org") + cache.Set("xui_public_ipv6", status.PublicIP.IPv6, 720*time.Hour) + } if s.xrayService.IsXrayRunning() { status.Xray.State = Running @@ -240,7 +301,9 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { status.Xray.ErrorMsg = s.xrayService.GetXrayResult() } status.Xray.Version = s.xrayService.GetXrayVersion() - status.Xray.ApiPort = s.xrayService.GetXrayApiPort() + + status.XUI.LatestVersion = getXuiLatestVersion() + var rtm runtime.MemStats runtime.ReadMemStats(&rtm) diff --git a/web/service/xray.go b/web/service/xray.go index 5998b5f0..a910f9ed 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -213,6 +213,10 @@ func (s *XrayService) RestartXray(isForce bool) error { if err != nil { return err } + if isForce { + logger.Debug("Xray Api Port: ", strconv.Itoa(p.GetAPIPort())) + } + return nil }