mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-06 10:27:12 +00:00
1aa81428b8
The Speed column showed "--" for AmneziaWG (and MTProto, which has the identical gap) even while cumulative traffic totals were correct. XrayTrafficJob drives live speed by querying xray-core's own stats API and broadcasting the delta over websocket -- but AmneziaWG/MTProto never run inside xray-core's own runtime inbounds, so they're invisible to that API. Their own jobs already compute the same per-poll delta shape (that's what keeps cumulative totals correct) but never broadcast it. Reusing the existing "traffics"/"clientTraffics" broadcast would have two real bugs: the frontend's existing scope/replace logic would let each side clobber the other's speed on its next unrelated tick, and the websocket hub's per-message-type throttle is keyed only by message type, not caller -- since both sidecar jobs run on identical "@every 10s" grids registered milliseconds apart, one would silently lose almost every broadcast if both protocols were ever configured together. Fixed with a small unthrottled broadcast path (both sidecar jobs are already self-rate-limited by their own cron cadence) and protocol- namespaced wire keys, tracked in their own frontend state and merged into the existing inboundSpeed/clientSpeed only at read time -- so every existing consumer needs zero changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
4.0 KiB
Go
129 lines
4.0 KiB
Go
// Package websocket provides WebSocket hub for real-time updates and notifications.
|
|
package websocket
|
|
|
|
import (
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/global"
|
|
)
|
|
|
|
// GetHub returns the global WebSocket hub instance.
|
|
func GetHub() *Hub {
|
|
webServer := global.GetWebServer()
|
|
if webServer == nil {
|
|
return nil
|
|
}
|
|
hub := webServer.GetWSHub()
|
|
if hub == nil {
|
|
return nil
|
|
}
|
|
wsHub, ok := hub.(*Hub)
|
|
if !ok {
|
|
logger.Warning("WebSocket hub type assertion failed")
|
|
return nil
|
|
}
|
|
return wsHub
|
|
}
|
|
|
|
// HasClients returns true if any WebSocket client is connected.
|
|
// Use this to skip expensive work (DB queries, serialization) when no browser is open.
|
|
func HasClients() bool {
|
|
hub := GetHub()
|
|
return hub != nil && hub.GetClientCount() > 0
|
|
}
|
|
|
|
// BroadcastStatus broadcasts server status update to all connected clients.
|
|
func BroadcastStatus(status any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.Broadcast(MessageTypeStatus, status)
|
|
}
|
|
}
|
|
|
|
// BroadcastTraffic broadcasts traffic statistics update to all connected clients.
|
|
func BroadcastTraffic(traffic any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.Broadcast(MessageTypeTraffic, traffic)
|
|
}
|
|
}
|
|
|
|
// BroadcastSidecarTraffic broadcasts an AmneziaWG/MTProto traffic delta under
|
|
// the same "traffic" message type BroadcastTraffic uses, but bypasses the
|
|
// hub's per-type throttle (see Hub.BroadcastUnthrottled) so the two sidecar
|
|
// jobs' independent ~10s broadcasts can never starve each other. The payload
|
|
// carries protocol-namespaced keys (see internal/web/job/sidecar_traffic.go),
|
|
// so no new frontend message-type wiring is needed -- applyTrafficEvent
|
|
// already receives every "traffic" message.
|
|
func BroadcastSidecarTraffic(traffic any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.BroadcastUnthrottled(MessageTypeTraffic, traffic)
|
|
}
|
|
}
|
|
|
|
// BroadcastClientStats broadcasts absolute per-client traffic counters. Small
|
|
// installs send the complete row set each cycle (payload key snapshot=true);
|
|
// above the traffic job's snapshot threshold only the rows active in the
|
|
// latest collection window are sent (snapshot=false), which keeps the payload
|
|
// under the hub's cap at any client count.
|
|
func BroadcastClientStats(stats any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.Broadcast(MessageTypeClientStats, stats)
|
|
}
|
|
}
|
|
|
|
// BroadcastInbounds broadcasts inbounds list update to all connected clients.
|
|
func BroadcastInbounds(inbounds any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.Broadcast(MessageTypeInbounds, inbounds)
|
|
}
|
|
}
|
|
|
|
// BroadcastNodes broadcasts the fresh node list to all connected clients.
|
|
// Pushed by NodeHeartbeatJob at the end of each 10s tick so the Nodes page
|
|
// reflects status / latency / cpu / mem updates without polling.
|
|
func BroadcastNodes(nodes any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.Broadcast(MessageTypeNodes, nodes)
|
|
}
|
|
}
|
|
|
|
// BroadcastOutbounds broadcasts outbounds list update to all connected clients.
|
|
func BroadcastOutbounds(outbounds any) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.Broadcast(MessageTypeOutbounds, outbounds)
|
|
}
|
|
}
|
|
|
|
// BroadcastNotification broadcasts a system notification to all connected clients.
|
|
func BroadcastNotification(title, message, level string) {
|
|
hub := GetHub()
|
|
if hub == nil {
|
|
return
|
|
}
|
|
hub.Broadcast(MessageTypeNotification, map[string]string{
|
|
"title": title,
|
|
"message": message,
|
|
"level": level,
|
|
})
|
|
}
|
|
|
|
// BroadcastXrayState broadcasts Xray state change to all connected clients.
|
|
func BroadcastXrayState(state string, errorMsg string) {
|
|
hub := GetHub()
|
|
if hub == nil {
|
|
return
|
|
}
|
|
hub.Broadcast(MessageTypeXrayState, map[string]string{
|
|
"state": state,
|
|
"errorMsg": errorMsg,
|
|
})
|
|
}
|
|
|
|
// BroadcastInvalidate sends a lightweight signal telling clients to re-fetch
|
|
// the named data type via REST. Use this when the caller already knows the
|
|
// payload is too large to push directly (e.g., 10k+ clients) to skip the
|
|
// JSON-marshal cost on the hot path.
|
|
func BroadcastInvalidate(dataType MessageType) {
|
|
if hub := GetHub(); hub != nil {
|
|
hub.broadcastInvalidate(dataType)
|
|
}
|
|
}
|