mirror of
				https://github.com/MHSanaei/3x-ui.git
				synced 2025-10-27 10:30:08 +00:00 
			
		
		
		
	Compare commits
	
		
			5 commits
		
	
	
		
			135f843b3e
			...
			417c323e0a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 417c323e0a | ||
|   | 1c8689dea9 | ||
|   | a128f75f64 | ||
|   | 299572a4c2 | ||
|   | 22afa50901 | 
					 6 changed files with 654 additions and 611 deletions
				
			
		|  | @ -21,17 +21,14 @@ type ServerController struct { | ||||||
| 	serverService  service.ServerService | 	serverService  service.ServerService | ||||||
| 	settingService service.SettingService | 	settingService service.SettingService | ||||||
| 
 | 
 | ||||||
| 	lastStatus        *service.Status | 	lastStatus *service.Status | ||||||
| 	lastGetStatusTime time.Time |  | ||||||
| 
 | 
 | ||||||
| 	lastVersions        []string | 	lastVersions        []string | ||||||
| 	lastGetVersionsTime time.Time | 	lastGetVersionsTime int64 // unix seconds
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func NewServerController(g *gin.RouterGroup) *ServerController { | func NewServerController(g *gin.RouterGroup) *ServerController { | ||||||
| 	a := &ServerController{ | 	a := &ServerController{} | ||||||
| 		lastGetStatusTime: time.Now(), |  | ||||||
| 	} |  | ||||||
| 	a.initRouter(g) | 	a.initRouter(g) | ||||||
| 	a.startTask() | 	a.startTask() | ||||||
| 	return a | 	return a | ||||||
|  | @ -40,7 +37,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController { | ||||||
| func (a *ServerController) initRouter(g *gin.RouterGroup) { | func (a *ServerController) initRouter(g *gin.RouterGroup) { | ||||||
| 
 | 
 | ||||||
| 	g.GET("/status", a.status) | 	g.GET("/status", a.status) | ||||||
| 	g.GET("/cpuHistory", a.getCpuHistory) | 	g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) | ||||||
| 	g.GET("/getXrayVersion", a.getXrayVersion) | 	g.GET("/getXrayVersion", a.getXrayVersion) | ||||||
| 	g.GET("/getConfigJson", a.getConfigJson) | 	g.GET("/getConfigJson", a.getConfigJson) | ||||||
| 	g.GET("/getDb", a.getDb) | 	g.GET("/getDb", a.getDb) | ||||||
|  | @ -79,35 +76,34 @@ func (a *ServerController) startTask() { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *ServerController) status(c *gin.Context) { | func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) } | ||||||
| 	a.lastGetStatusTime = time.Now() |  | ||||||
| 
 | 
 | ||||||
| 	jsonObj(c, a.lastStatus, nil) | func (a *ServerController) getCpuHistoryBucket(c *gin.Context) { | ||||||
| } | 	bucketStr := c.Param("bucket") | ||||||
| 
 | 	bucket, err := strconv.Atoi(bucketStr) | ||||||
| // getCpuHistory returns recent CPU utilization points.
 | 	if err != nil || bucket <= 0 { | ||||||
| // Query param q=minutes (int). Bounds: 1..360 (6 hours). Defaults to 60.
 | 		jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket")) | ||||||
| func (a *ServerController) getCpuHistory(c *gin.Context) { | 		return | ||||||
| 	minsStr := c.Query("q") |  | ||||||
| 	mins := 60 |  | ||||||
| 	if minsStr != "" { |  | ||||||
| 		if v, err := strconv.Atoi(minsStr); err == nil { |  | ||||||
| 			mins = v |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| 	if mins < 1 { | 	allowed := map[int]bool{ | ||||||
| 		mins = 1 | 		2:   true, // Real-time view
 | ||||||
|  | 		30:  true, // 30s intervals
 | ||||||
|  | 		60:  true, // 1m intervals
 | ||||||
|  | 		120: true, // 2m intervals
 | ||||||
|  | 		180: true, // 3m intervals
 | ||||||
|  | 		300: true, // 5m intervals
 | ||||||
| 	} | 	} | ||||||
| 	if mins > 360 { | 	if !allowed[bucket] { | ||||||
| 		mins = 360 | 		jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket")) | ||||||
|  | 		return | ||||||
| 	} | 	} | ||||||
| 	res := a.serverService.GetCpuHistory(mins) | 	points := a.serverService.AggregateCpuHistory(bucket, 60) | ||||||
| 	jsonObj(c, res, nil) | 	jsonObj(c, points, nil) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *ServerController) getXrayVersion(c *gin.Context) { | func (a *ServerController) getXrayVersion(c *gin.Context) { | ||||||
| 	now := time.Now() | 	now := time.Now().Unix() | ||||||
| 	if now.Sub(a.lastGetVersionsTime) <= time.Minute { | 	if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
 | ||||||
| 		jsonObj(c, a.lastVersions, nil) | 		jsonObj(c, a.lastVersions, nil) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | @ -119,7 +115,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	a.lastVersions = versions | 	a.lastVersions = versions | ||||||
| 	a.lastGetVersionsTime = time.Now() | 	a.lastGetVersionsTime = now | ||||||
| 
 | 
 | ||||||
| 	jsonObj(c, versions, nil) | 	jsonObj(c, versions, nil) | ||||||
| } | } | ||||||
|  | @ -137,7 +133,6 @@ func (a *ServerController) updateGeofile(c *gin.Context) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (a *ServerController) stopXrayService(c *gin.Context) { | func (a *ServerController) stopXrayService(c *gin.Context) { | ||||||
| 	a.lastGetStatusTime = time.Now() |  | ||||||
| 	err := a.serverService.StopXrayService() | 	err := a.serverService.StopXrayService() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err) | 		jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err) | ||||||
|  | @ -253,9 +248,7 @@ func (a *ServerController) importDB(c *gin.Context) { | ||||||
| 	defer file.Close() | 	defer file.Close() | ||||||
| 	// Always restart Xray before return
 | 	// Always restart Xray before return
 | ||||||
| 	defer a.serverService.RestartXrayService() | 	defer a.serverService.RestartXrayService() | ||||||
| 	defer func() { | 	// lastGetStatusTime removed; no longer needed
 | ||||||
| 		a.lastGetStatusTime = time.Now() |  | ||||||
| 	}() |  | ||||||
| 	// Import it
 | 	// Import it
 | ||||||
| 	err = a.serverService.ImportDB(file) | 	err = a.serverService.ImportDB(file) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|  |  | ||||||
|  | @ -23,13 +23,13 @@ func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { | ||||||
| 
 | 
 | ||||||
| func (a *XraySettingController) initRouter(g *gin.RouterGroup) { | func (a *XraySettingController) initRouter(g *gin.RouterGroup) { | ||||||
| 	g = g.Group("/xray") | 	g = g.Group("/xray") | ||||||
|  | 	g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) | ||||||
|  | 	g.GET("/getOutboundsTraffic", a.getOutboundsTraffic) | ||||||
|  | 	g.GET("/getXrayResult", a.getXrayResult) | ||||||
| 
 | 
 | ||||||
| 	g.POST("/", a.getXraySetting) | 	g.POST("/", a.getXraySetting) | ||||||
| 	g.POST("/update", a.updateSetting) |  | ||||||
| 	g.GET("/getXrayResult", a.getXrayResult) |  | ||||||
| 	g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) |  | ||||||
| 	g.POST("/warp/:action", a.warp) | 	g.POST("/warp/:action", a.warp) | ||||||
| 	g.GET("/getOutboundsTraffic", a.getOutboundsTraffic) | 	g.POST("/update", a.updateSetting) | ||||||
| 	g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) | 	g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										1024
									
								
								web/html/index.html
									
									
									
									
									
								
							
							
						
						
									
										1024
									
								
								web/html/index.html
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -1951,8 +1951,8 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	if t != nil && client != nil { | 	if t != nil && client != nil { | ||||||
| 		// Ensure enable mirrors the client's current enable flag in settings
 |  | ||||||
| 		t.Enable = client.Enable | 		t.Enable = client.Enable | ||||||
|  | 		t.SubId = client.SubID | ||||||
| 		return t, nil | 		return t, nil | ||||||
| 	} | 	} | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
|  | @ -1993,6 +1993,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, | ||||||
| 	for i := range traffics { | 	for i := range traffics { | ||||||
| 		if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil { | 		if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil { | ||||||
| 			traffics[i].Enable = client.Enable | 			traffics[i].Enable = client.Enable | ||||||
|  | 			traffics[i].SubId = client.SubID | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	return traffics, err | 	return traffics, err | ||||||
|  |  | ||||||
|  | @ -94,22 +94,81 @@ type Release struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type ServerService struct { | type ServerService struct { | ||||||
| 	xrayService    XrayService | 	xrayService        XrayService | ||||||
| 	inboundService InboundService | 	inboundService     InboundService | ||||||
| 	cachedIPv4     string | 	cachedIPv4         string | ||||||
| 	cachedIPv6     string | 	cachedIPv6         string | ||||||
| 	noIPv6         bool | 	noIPv6             bool | ||||||
| 	// CPU utilization smoothing state
 | 	mu                 sync.Mutex | ||||||
| 	mu               sync.Mutex | 	lastCPUTimes       cpu.TimesStat | ||||||
| 	lastCPUTimes     cpu.TimesStat | 	hasLastCPUSample   bool | ||||||
| 	hasLastCPUSample bool | 	emaCPU             float64 | ||||||
| 	emaCPU           float64 | 	cpuHistory         []CPUSample | ||||||
| 	// CPU history buffer (in-memory, protected by mu)
 | 	cachedCpuSpeedMhz  float64 | ||||||
| 	cpuHistory  []CPUSample | 	lastCpuInfoAttempt time.Time | ||||||
| 	cpuCapacity int |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // CPUSample represents a single CPU utilization sample with timestamp
 | // AggregateCpuHistory returns up to maxPoints averaged buckets of size bucketSeconds over recent data.
 | ||||||
|  | func (s *ServerService) AggregateCpuHistory(bucketSeconds int, maxPoints int) []map[string]any { | ||||||
|  | 	if bucketSeconds <= 0 || maxPoints <= 0 { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	cutoff := time.Now().Add(-time.Duration(bucketSeconds*maxPoints) * time.Second).Unix() | ||||||
|  | 	s.mu.Lock() | ||||||
|  | 	// find start index (history sorted ascending)
 | ||||||
|  | 	hist := s.cpuHistory | ||||||
|  | 	// binary-ish scan (simple linear from end since size capped ~10800 is fine)
 | ||||||
|  | 	startIdx := 0 | ||||||
|  | 	for i := len(hist) - 1; i >= 0; i-- { | ||||||
|  | 		if hist[i].T < cutoff { | ||||||
|  | 			startIdx = i + 1 | ||||||
|  | 			break | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if startIdx >= len(hist) { | ||||||
|  | 		s.mu.Unlock() | ||||||
|  | 		return []map[string]any{} | ||||||
|  | 	} | ||||||
|  | 	slice := hist[startIdx:] | ||||||
|  | 	// copy for unlock
 | ||||||
|  | 	tmp := make([]CPUSample, len(slice)) | ||||||
|  | 	copy(tmp, slice) | ||||||
|  | 	s.mu.Unlock() | ||||||
|  | 	if len(tmp) == 0 { | ||||||
|  | 		return []map[string]any{} | ||||||
|  | 	} | ||||||
|  | 	var out []map[string]any | ||||||
|  | 	var acc []float64 | ||||||
|  | 	bSize := int64(bucketSeconds) | ||||||
|  | 	curBucket := (tmp[0].T / bSize) * bSize | ||||||
|  | 	flush := func(ts int64) { | ||||||
|  | 		if len(acc) == 0 { | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		sum := 0.0 | ||||||
|  | 		for _, v := range acc { | ||||||
|  | 			sum += v | ||||||
|  | 		} | ||||||
|  | 		avg := sum / float64(len(acc)) | ||||||
|  | 		out = append(out, map[string]any{"t": ts, "cpu": avg}) | ||||||
|  | 		acc = acc[:0] | ||||||
|  | 	} | ||||||
|  | 	for _, p := range tmp { | ||||||
|  | 		b := (p.T / bSize) * bSize | ||||||
|  | 		if b != curBucket { | ||||||
|  | 			flush(curBucket) | ||||||
|  | 			curBucket = b | ||||||
|  | 		} | ||||||
|  | 		acc = append(acc, p.Cpu) | ||||||
|  | 	} | ||||||
|  | 	flush(curBucket) | ||||||
|  | 	if len(out) > maxPoints { | ||||||
|  | 		out = out[len(out)-maxPoints:] | ||||||
|  | 	} | ||||||
|  | 	return out | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // CPUSample single CPU utilization sample
 | ||||||
| type CPUSample struct { | type CPUSample struct { | ||||||
| 	T   int64   `json:"t"`   // unix seconds
 | 	T   int64   `json:"t"`   // unix seconds
 | ||||||
| 	Cpu float64 `json:"cpu"` // percent 0..100
 | 	Cpu float64 `json:"cpu"` // percent 0..100
 | ||||||
|  | @ -178,13 +237,30 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { | ||||||
| 
 | 
 | ||||||
| 	status.LogicalPro = runtime.NumCPU() | 	status.LogicalPro = runtime.NumCPU() | ||||||
| 
 | 
 | ||||||
| 	cpuInfos, err := cpu.Info() | 	if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute { | ||||||
| 	if err != nil { | 		s.lastCpuInfoAttempt = time.Now() | ||||||
| 		logger.Warning("get cpu info failed:", err) | 		done := make(chan struct{}) | ||||||
| 	} else if len(cpuInfos) > 0 { | 		go func() { | ||||||
| 		status.CpuSpeedMhz = cpuInfos[0].Mhz | 			defer close(done) | ||||||
| 	} else { | 			cpuInfos, err := cpu.Info() | ||||||
| 		logger.Warning("could not find cpu info") | 			if err != nil { | ||||||
|  | 				logger.Warning("get cpu info failed:", err) | ||||||
|  | 				return | ||||||
|  | 			} | ||||||
|  | 			if len(cpuInfos) > 0 { | ||||||
|  | 				s.cachedCpuSpeedMhz = cpuInfos[0].Mhz | ||||||
|  | 				status.CpuSpeedMhz = s.cachedCpuSpeedMhz | ||||||
|  | 			} else { | ||||||
|  | 				logger.Warning("could not find cpu info") | ||||||
|  | 			} | ||||||
|  | 		}() | ||||||
|  | 		select { | ||||||
|  | 		case <-done: | ||||||
|  | 		case <-time.After(1500 * time.Millisecond): | ||||||
|  | 			logger.Warning("cpu info query timed out; will retry later") | ||||||
|  | 		} | ||||||
|  | 	} else if s.cachedCpuSpeedMhz != 0 { | ||||||
|  | 		status.CpuSpeedMhz = s.cachedCpuSpeedMhz | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// Uptime
 | 	// Uptime
 | ||||||
|  | @ -332,55 +408,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { | ||||||
| 	return status | 	return status | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // AppendCpuSample appends a CPU sample into the in-memory history with capacity trimming.
 |  | ||||||
| func (s *ServerService) AppendCpuSample(t time.Time, v float64) { | func (s *ServerService) AppendCpuSample(t time.Time, v float64) { | ||||||
|  | 	const capacity = 9000 // ~5 hours @ 2s interval
 | ||||||
| 	s.mu.Lock() | 	s.mu.Lock() | ||||||
| 	defer s.mu.Unlock() | 	defer s.mu.Unlock() | ||||||
| 	if s.cpuCapacity == 0 { |  | ||||||
| 		s.cpuCapacity = 10800 // ~6 hours at 2s per sample
 |  | ||||||
| 	} |  | ||||||
| 	p := CPUSample{T: t.Unix(), Cpu: v} | 	p := CPUSample{T: t.Unix(), Cpu: v} | ||||||
| 	s.cpuHistory = append(s.cpuHistory, p) | 	if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T { | ||||||
| 	if len(s.cpuHistory) > s.cpuCapacity { | 		s.cpuHistory[n-1] = p | ||||||
| 		drop := len(s.cpuHistory) - s.cpuCapacity | 	} else { | ||||||
| 		s.cpuHistory = s.cpuHistory[drop:] | 		s.cpuHistory = append(s.cpuHistory, p) | ||||||
|  | 	} | ||||||
|  | 	if len(s.cpuHistory) > capacity { | ||||||
|  | 		s.cpuHistory = s.cpuHistory[len(s.cpuHistory)-capacity:] | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetCpuHistory returns samples from the last 'mins' minutes (bounded 1..360).
 |  | ||||||
| func (s *ServerService) GetCpuHistory(mins int) []CPUSample { |  | ||||||
| 	if mins < 1 { |  | ||||||
| 		mins = 1 |  | ||||||
| 	} |  | ||||||
| 	if mins > 360 { |  | ||||||
| 		mins = 360 |  | ||||||
| 	} |  | ||||||
| 	cutoff := time.Now().Add(-time.Duration(mins) * time.Minute).Unix() |  | ||||||
| 	s.mu.Lock() |  | ||||||
| 	defer s.mu.Unlock() |  | ||||||
| 	if len(s.cpuHistory) == 0 { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	// find first index >= cutoff (linear scan from end is fine for these sizes)
 |  | ||||||
| 	i := len(s.cpuHistory) - 1 |  | ||||||
| 	for ; i >= 0; i-- { |  | ||||||
| 		if s.cpuHistory[i].T < cutoff { |  | ||||||
| 			i++ |  | ||||||
| 			break |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	if i < 0 { |  | ||||||
| 		i = 0 |  | ||||||
| 	} |  | ||||||
| 	// copy to avoid exposing internal slice
 |  | ||||||
| 	out := make([]CPUSample, len(s.cpuHistory)-i) |  | ||||||
| 	copy(out, s.cpuHistory[i:]) |  | ||||||
| 	return out |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // sampleCPUUtilization returns a smoothed total CPU utilization percentage across all logical processors.
 |  | ||||||
| // It computes utilization from CPU time deltas (non-blocking) and applies an exponential moving average
 |  | ||||||
| // to reduce spikes similar to Task Manager's smoothing.
 |  | ||||||
| func (s *ServerService) sampleCPUUtilization() (float64, error) { | func (s *ServerService) sampleCPUUtilization() (float64, error) { | ||||||
| 	// Prefer native Windows API to avoid external deps for CPU percent
 | 	// Prefer native Windows API to avoid external deps for CPU percent
 | ||||||
| 	if runtime.GOOS == "windows" { | 	if runtime.GOOS == "windows" { | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ type ClientTraffic struct { | ||||||
| 	InboundId  int    `json:"inboundId" form:"inboundId"` | 	InboundId  int    `json:"inboundId" form:"inboundId"` | ||||||
| 	Enable     bool   `json:"enable" form:"enable"` | 	Enable     bool   `json:"enable" form:"enable"` | ||||||
| 	Email      string `json:"email" form:"email" gorm:"unique"` | 	Email      string `json:"email" form:"email" gorm:"unique"` | ||||||
|  | 	SubId      string `json:"subId" form:"subId" gorm:"-"` | ||||||
| 	Up         int64  `json:"up" form:"up"` | 	Up         int64  `json:"up" form:"up"` | ||||||
| 	Down       int64  `json:"down" form:"down"` | 	Down       int64  `json:"down" form:"down"` | ||||||
| 	AllTime    int64  `json:"allTime" form:"allTime"` | 	AllTime    int64  `json:"allTime" form:"allTime"` | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue