Author SHA1 Message Date
Muse d4d259fa58 probe ws: frame cadence follows probe_fast_interval (V2.0.2)
CI / go-test (push) Waiting to run
CI / postgres-durable-first (push) Waiting to run
CI / codegen (push) Waiting to run
CI / govulncheck (push) Waiting to run
CI / race (push) Waiting to run
CI / fuzz-smoke (push) Waiting to run
CI / golangci (push) Waiting to run
CI / frontend (push) Waiting to run
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
2026-10-23 18:15:05 +08:00
Muse 9caa4e0cf9 install-agent: RELEASE_BASE -> V2.0.1
Release 3X-UI / build (386) (push) Waiting to run
Release 3X-UI / build (amd64) (push) Waiting to run
Release 3X-UI / build (arm64) (push) Waiting to run
Release 3X-UI / build (armv5) (push) Waiting to run
Release 3X-UI / build (armv6) (push) Waiting to run
Release 3X-UI / build (armv7) (push) Waiting to run
Release 3X-UI / build (s390x) (push) Waiting to run
Release 3X-UI / Build for Windows (amd64) (push) Waiting to run
Release 3X-UI / Publish rolling dev release (push) Blocked by required conditions
2026-10-23 16:13:25 +08:00
3 changed files with 76 additions and 10 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ esac
# sing-box 核心在 NexusGate 仓的专用发行里(见下方 SINGBOX_BASE)。 # sing-box 核心在 NexusGate 仓的专用发行里(见下方 SINGBOX_BASE)。
# 注意:本脚本是 V2 版——只装 sing-box 运行时;主控未升 V2 前不要用它 # 注意:本脚本是 V2 版——只装 sing-box 运行时;主控未升 V2 前不要用它
# 重装节点(节点升级走面板一键更新,不需要本脚本)。 # 重装节点(节点升级走面板一键更新,不需要本脚本)。
RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V2.0.0" RELEASE_BASE="https://git.881218.xyz/rogue/3x-ui-agent/releases/download/V2.0.1"
XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-linux-${BIN_ARCH}}" XUI_URL="${XUI_URL:-${RELEASE_BASE}/x-ui-linux-${BIN_ARCH}}"
ROOT="${XUI_ROOT:-}" ROOT="${XUI_ROOT:-}"
+12 -9
View File
@@ -211,10 +211,12 @@ func (pc *PublicProbeController) nodeMetrics(c *gin.Context) {
c.JSON(http.StatusOK, resp) c.JSON(http.StatusOK, resp)
} }
// liveWS is the hub's browser stream: a snapshot frame every 2 s, // liveWS is the hub's browser stream: a snapshot frame on the
// text by default, binary gzip frames for the anonymous audience when // operator's fast-report cadence (V2.0.2; the hub's fixed 2 s tick
// opened with ?gzip. Audience eligibility is re-checked every tick — // made a 1 s agent cadence invisible up front), text by default,
// a socket outlives both the session and the public-page switch. // binary gzip frames for the anonymous audience when opened with
// ?gzip. Audience eligibility is re-checked every tick — a socket
// outlives both the session and the public-page switch.
func (pc *PublicProbeController) liveWS(c *gin.Context) { func (pc *PublicProbeController) liveWS(c *gin.Context) {
authed := probeAuthed(c) && !fromOtherOrigin(c) authed := probeAuthed(c) && !fromOtherOrigin(c)
if !authed && !service.ProbePublicPageEnabled() { if !authed && !service.ProbePublicPageEnabled() {
@@ -280,18 +282,19 @@ func (pc *PublicProbeController) liveWS(c *gin.Context) {
return conn.WriteMessage(frameType, frame) == nil return conn.WriteMessage(frameType, frame) == nil
} }
// The first frame goes out immediately (hub's interval ticks at // The first frame goes out immediately; later frames follow the
// once), then one per 2 s tick. // fast-report cadence, re-read after every frame so a settings
// change lands on a live socket without a reconnect.
if !send() { if !send() {
return return
} }
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
for { for {
timer := time.NewTimer(time.Duration(service.ProbeFastIntervalSec()) * time.Second)
select { select {
case <-done: case <-done:
timer.Stop()
return return
case <-ticker.C: case <-timer.C:
if !send() { if !send() {
return return
} }
@@ -0,0 +1,63 @@
package controller
import (
"net/http/httptest"
"strings"
"testing"
"time"
ws "github.com/gorilla/websocket"
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
)
// V2.0.2: /api/ws frame cadence follows the operator's fast-report
// interval (probe_fast_interval) instead of the hub's fixed 2 s tick.
// A mid-stream settings change re-paces a live socket: the timer
// already in flight still fires on the old cadence, the next one is
// armed from the re-read setting.
func TestProbeWebSocketCadenceFollowsFastInterval(t *testing.T) {
rig := newProbeTestRig(t)
_, _ = rig.seedAgent(t, "CadenceNode", true)
rig.setSetting(t, "public_page", "on")
rig.setSetting(t, "probe_fast_interval", "1")
service.InvalidateProbeSnapshot()
srv := httptest.NewServer(rig.engine)
defer srv.Close()
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/api/ws"
conn, _, err := ws.DefaultDialer.Dial(wsURL, nil)
if err != nil {
t.Fatalf("dial: %v", err)
}
defer conn.Close()
readAt := func() time.Time {
t.Helper()
_ = conn.SetReadDeadline(time.Now().Add(10 * time.Second))
if _, _, err := conn.ReadMessage(); err != nil {
t.Fatalf("ws read: %v", err)
}
return time.Now()
}
_ = readAt() // first frame: immediate
prev := readAt()
for i := 0; i < 2; i++ {
now := readAt()
if gap := now.Sub(prev); gap < 500*time.Millisecond || gap > 2500*time.Millisecond {
t.Fatalf("frame gap at 1 s cadence: %v", gap)
}
prev = now
}
// Change to 3 s mid-stream: one in-flight 1 s frame, then the
// stream settles onto the new cadence without a reconnect.
rig.setSetting(t, "probe_fast_interval", "3")
prev = readAt()
now := readAt()
if gap := now.Sub(prev); gap < 2200*time.Millisecond || gap > 6*time.Second {
t.Fatalf("frame gap after change to 3 s: %v", gap)
}
}