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 | ||||
| 	settingService service.SettingService | ||||
| 
 | ||||
| 	lastStatus        *service.Status | ||||
| 	lastGetStatusTime time.Time | ||||
| 	lastStatus *service.Status | ||||
| 
 | ||||
| 	lastVersions        []string | ||||
| 	lastGetVersionsTime time.Time | ||||
| 	lastGetVersionsTime int64 // unix seconds
 | ||||
| } | ||||
| 
 | ||||
| func NewServerController(g *gin.RouterGroup) *ServerController { | ||||
| 	a := &ServerController{ | ||||
| 		lastGetStatusTime: time.Now(), | ||||
| 	} | ||||
| 	a := &ServerController{} | ||||
| 	a.initRouter(g) | ||||
| 	a.startTask() | ||||
| 	return a | ||||
|  | @ -40,7 +37,7 @@ func NewServerController(g *gin.RouterGroup) *ServerController { | |||
| func (a *ServerController) initRouter(g *gin.RouterGroup) { | ||||
| 
 | ||||
| 	g.GET("/status", a.status) | ||||
| 	g.GET("/cpuHistory", a.getCpuHistory) | ||||
| 	g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) | ||||
| 	g.GET("/getXrayVersion", a.getXrayVersion) | ||||
| 	g.GET("/getConfigJson", a.getConfigJson) | ||||
| 	g.GET("/getDb", a.getDb) | ||||
|  | @ -79,35 +76,34 @@ func (a *ServerController) startTask() { | |||
| 	}) | ||||
| } | ||||
| 
 | ||||
| func (a *ServerController) status(c *gin.Context) { | ||||
| 	a.lastGetStatusTime = time.Now() | ||||
| func (a *ServerController) status(c *gin.Context) { jsonObj(c, a.lastStatus, nil) } | ||||
| 
 | ||||
| 	jsonObj(c, a.lastStatus, nil) | ||||
| } | ||||
| 
 | ||||
| // getCpuHistory returns recent CPU utilization points.
 | ||||
| // Query param q=minutes (int). Bounds: 1..360 (6 hours). Defaults to 60.
 | ||||
| func (a *ServerController) getCpuHistory(c *gin.Context) { | ||||
| 	minsStr := c.Query("q") | ||||
| 	mins := 60 | ||||
| 	if minsStr != "" { | ||||
| 		if v, err := strconv.Atoi(minsStr); err == nil { | ||||
| 			mins = v | ||||
| 		} | ||||
| func (a *ServerController) getCpuHistoryBucket(c *gin.Context) { | ||||
| 	bucketStr := c.Param("bucket") | ||||
| 	bucket, err := strconv.Atoi(bucketStr) | ||||
| 	if err != nil || bucket <= 0 { | ||||
| 		jsonMsg(c, "invalid bucket", fmt.Errorf("bad bucket")) | ||||
| 		return | ||||
| 	} | ||||
| 	if mins < 1 { | ||||
| 		mins = 1 | ||||
| 	allowed := map[int]bool{ | ||||
| 		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 { | ||||
| 		mins = 360 | ||||
| 	if !allowed[bucket] { | ||||
| 		jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket")) | ||||
| 		return | ||||
| 	} | ||||
| 	res := a.serverService.GetCpuHistory(mins) | ||||
| 	jsonObj(c, res, nil) | ||||
| 	points := a.serverService.AggregateCpuHistory(bucket, 60) | ||||
| 	jsonObj(c, points, nil) | ||||
| } | ||||
| 
 | ||||
| func (a *ServerController) getXrayVersion(c *gin.Context) { | ||||
| 	now := time.Now() | ||||
| 	if now.Sub(a.lastGetVersionsTime) <= time.Minute { | ||||
| 	now := time.Now().Unix() | ||||
| 	if now-a.lastGetVersionsTime <= 60 { // 1 minute cache
 | ||||
| 		jsonObj(c, a.lastVersions, nil) | ||||
| 		return | ||||
| 	} | ||||
|  | @ -119,7 +115,7 @@ func (a *ServerController) getXrayVersion(c *gin.Context) { | |||
| 	} | ||||
| 
 | ||||
| 	a.lastVersions = versions | ||||
| 	a.lastGetVersionsTime = time.Now() | ||||
| 	a.lastGetVersionsTime = now | ||||
| 
 | ||||
| 	jsonObj(c, versions, nil) | ||||
| } | ||||
|  | @ -137,7 +133,6 @@ func (a *ServerController) updateGeofile(c *gin.Context) { | |||
| } | ||||
| 
 | ||||
| func (a *ServerController) stopXrayService(c *gin.Context) { | ||||
| 	a.lastGetStatusTime = time.Now() | ||||
| 	err := a.serverService.StopXrayService() | ||||
| 	if err != nil { | ||||
| 		jsonMsg(c, I18nWeb(c, "pages.xray.stopError"), err) | ||||
|  | @ -253,9 +248,7 @@ func (a *ServerController) importDB(c *gin.Context) { | |||
| 	defer file.Close() | ||||
| 	// Always restart Xray before return
 | ||||
| 	defer a.serverService.RestartXrayService() | ||||
| 	defer func() { | ||||
| 		a.lastGetStatusTime = time.Now() | ||||
| 	}() | ||||
| 	// lastGetStatusTime removed; no longer needed
 | ||||
| 	// Import it
 | ||||
| 	err = a.serverService.ImportDB(file) | ||||
| 	if err != nil { | ||||
|  |  | |||
|  | @ -23,13 +23,13 @@ func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { | |||
| 
 | ||||
| func (a *XraySettingController) initRouter(g *gin.RouterGroup) { | ||||
| 	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("/update", a.updateSetting) | ||||
| 	g.GET("/getXrayResult", a.getXrayResult) | ||||
| 	g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) | ||||
| 	g.POST("/warp/:action", a.warp) | ||||
| 	g.GET("/getOutboundsTraffic", a.getOutboundsTraffic) | ||||
| 	g.POST("/update", a.updateSetting) | ||||
| 	g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										1020
									
								
								web/html/index.html
									
									
									
									
									
								
							
							
						
						
									
										1020
									
								
								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 | ||||
| 	} | ||||
| 	if t != nil && client != nil { | ||||
| 		// Ensure enable mirrors the client's current enable flag in settings
 | ||||
| 		t.Enable = client.Enable | ||||
| 		t.SubId = client.SubID | ||||
| 		return t, nil | ||||
| 	} | ||||
| 	return nil, nil | ||||
|  | @ -1993,6 +1993,7 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic, | |||
| 	for i := range traffics { | ||||
| 		if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil { | ||||
| 			traffics[i].Enable = client.Enable | ||||
| 			traffics[i].SubId = client.SubID | ||||
| 		} | ||||
| 	} | ||||
| 	return traffics, err | ||||
|  |  | |||
|  | @ -94,22 +94,81 @@ type Release struct { | |||
| } | ||||
| 
 | ||||
| type ServerService struct { | ||||
| 	xrayService    XrayService | ||||
| 	inboundService InboundService | ||||
| 	cachedIPv4     string | ||||
| 	cachedIPv6     string | ||||
| 	noIPv6         bool | ||||
| 	// CPU utilization smoothing state
 | ||||
| 	mu               sync.Mutex | ||||
| 	lastCPUTimes     cpu.TimesStat | ||||
| 	hasLastCPUSample bool | ||||
| 	emaCPU           float64 | ||||
| 	// CPU history buffer (in-memory, protected by mu)
 | ||||
| 	cpuHistory  []CPUSample | ||||
| 	cpuCapacity int | ||||
| 	xrayService        XrayService | ||||
| 	inboundService     InboundService | ||||
| 	cachedIPv4         string | ||||
| 	cachedIPv6         string | ||||
| 	noIPv6             bool | ||||
| 	mu                 sync.Mutex | ||||
| 	lastCPUTimes       cpu.TimesStat | ||||
| 	hasLastCPUSample   bool | ||||
| 	emaCPU             float64 | ||||
| 	cpuHistory         []CPUSample | ||||
| 	cachedCpuSpeedMhz  float64 | ||||
| 	lastCpuInfoAttempt time.Time | ||||
| } | ||||
| 
 | ||||
| // 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 { | ||||
| 	T   int64   `json:"t"`   // unix seconds
 | ||||
| 	Cpu float64 `json:"cpu"` // percent 0..100
 | ||||
|  | @ -178,13 +237,30 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status { | |||
| 
 | ||||
| 	status.LogicalPro = runtime.NumCPU() | ||||
| 
 | ||||
| 	cpuInfos, err := cpu.Info() | ||||
| 	if err != nil { | ||||
| 		logger.Warning("get cpu info failed:", err) | ||||
| 	} else if len(cpuInfos) > 0 { | ||||
| 		status.CpuSpeedMhz = cpuInfos[0].Mhz | ||||
| 	} else { | ||||
| 		logger.Warning("could not find cpu info") | ||||
| 	if status.CpuSpeedMhz = s.cachedCpuSpeedMhz; s.cachedCpuSpeedMhz == 0 && time.Since(s.lastCpuInfoAttempt) > 5*time.Minute { | ||||
| 		s.lastCpuInfoAttempt = time.Now() | ||||
| 		done := make(chan struct{}) | ||||
| 		go func() { | ||||
| 			defer close(done) | ||||
| 			cpuInfos, err := 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
 | ||||
|  | @ -332,55 +408,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *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) { | ||||
| 	const capacity = 9000 // ~5 hours @ 2s interval
 | ||||
| 	s.mu.Lock() | ||||
| 	defer s.mu.Unlock() | ||||
| 	if s.cpuCapacity == 0 { | ||||
| 		s.cpuCapacity = 10800 // ~6 hours at 2s per sample
 | ||||
| 	} | ||||
| 	p := CPUSample{T: t.Unix(), Cpu: v} | ||||
| 	s.cpuHistory = append(s.cpuHistory, p) | ||||
| 	if len(s.cpuHistory) > s.cpuCapacity { | ||||
| 		drop := len(s.cpuHistory) - s.cpuCapacity | ||||
| 		s.cpuHistory = s.cpuHistory[drop:] | ||||
| 	if n := len(s.cpuHistory); n > 0 && s.cpuHistory[n-1].T == p.T { | ||||
| 		s.cpuHistory[n-1] = p | ||||
| 	} else { | ||||
| 		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) { | ||||
| 	// Prefer native Windows API to avoid external deps for CPU percent
 | ||||
| 	if runtime.GOOS == "windows" { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ type ClientTraffic struct { | |||
| 	InboundId  int    `json:"inboundId" form:"inboundId"` | ||||
| 	Enable     bool   `json:"enable" form:"enable"` | ||||
| 	Email      string `json:"email" form:"email" gorm:"unique"` | ||||
| 	SubId      string `json:"subId" form:"subId" gorm:"-"` | ||||
| 	Up         int64  `json:"up" form:"up"` | ||||
| 	Down       int64  `json:"down" form:"down"` | ||||
| 	AllTime    int64  `json:"allTime" form:"allTime"` | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue