поправлен агент

This commit is contained in:
2026-09-24 16:53:36 +03:00
parent b59434abaf
commit 258f0991ff
5 changed files with 209883 additions and 225269 deletions

View File

@@ -1,11 +1,18 @@
Hockey vMix Agent 1.5.1
Hockey vMix Agent 1.5.2
Changes:
- vMix 29 tally now reads Overlay channels 1 through 8.
- Existing agent_config.json is NOT included. Keep your current config next to agent.exe.
- More reliable Agent ↔ Hockey Server reconnect after WebSocket drops.
- After a working session the Agent retries the last healthy server first, then falls back to the other configured server.
- WebSocket TCP keep-alive is enabled and the connection handshake timeout is increased to 12 seconds.
- The Agent is marked ONLINE only after the server accepts the hello/credentials, not immediately after TCP/WebSocket upgrade.
- A failed command/ACK write now immediately tears down the broken WebSocket and starts reconnect instead of continuing to send into a dead connection.
- Reverse-proxy HTTP errors such as 502/503 now include a short diagnostic response in agent.log.
- Server heartbeat ACK is sent before the PostgreSQL status update, so a slow database cannot make a healthy Agent connection look dead.
Update:
1. Close Agent.
1. Completely close the current Agent through tray → Exit.
2. Replace only agent.exe.
3. Keep the existing agent_config.json.
3. Keep the existing agent_config.json next to agent.exe.
4. Start Agent again.
Device ID, Device Secret and account pairing remain in agent_config.json and are not changed by this update.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -27,7 +27,7 @@ import (
)
const (
agentVersion = "1.5.0"
agentVersion = "1.5.2"
protocolVersion = 1
websocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
heartbeatInterval = 8 * time.Second
@@ -445,6 +445,7 @@ func suffixVersion(v string) string {
func (a *Agent) connectionLoop() {
backoff := time.Second
preferredServer := ""
outer:
for {
select {
@@ -459,6 +460,19 @@ outer:
if len(candidates) == 0 {
candidates = []string{"http://127.0.0.1:8000"}
}
// Once a server has carried a real session, try that server first after
// a disconnect. This avoids wasting the 12-second local/remote dial
// timeout on a known-unavailable address every time the live socket drops.
if preferredServer != "" {
for index, candidate := range candidates {
if candidate == preferredServer {
if index > 0 {
candidates[0], candidates[index] = candidates[index], candidates[0]
}
break
}
}
}
var lastErr error
for _, serverBase := range candidates {
a.mu.Lock()
@@ -467,7 +481,12 @@ outer:
a.mu.Unlock()
connectionStarted := time.Now()
err := a.runConnection(serverBase)
if time.Since(connectionStarted) >= 5*time.Second {
connectionDuration := time.Since(connectionStarted)
if connectionDuration >= 5*time.Second {
// A socket that survived at least a few seconds is a useful
// signal that this is currently the healthy route. Prefer it
// on the next reconnect, but still fall back if it is unavailable.
preferredServer = serverBase
backoff = time.Second
}
a.mu.Lock()
@@ -526,20 +545,23 @@ func (a *Agent) runConnection(serverBase string) error {
return err
}
a.logf("Connecting to %s", target)
ws, err := dialWebSocket(target, 7*time.Second)
ws, err := dialWebSocket(target, 12*time.Second)
if err != nil {
return err
}
defer ws.Close()
a.mu.Lock()
a.currentWS = ws
a.state.ServerConnected = true
a.state.ServerConnecting = false
// TCP/TLS/WebSocket upgrade is not enough to call the Agent online: the
// server still has to accept hello + device credentials. The UI therefore
// becomes ONLINE only after hello.ok arrives in handleMessage.
a.state.ServerConnected = false
a.state.ServerConnecting = true
a.state.ServerError = ""
a.state.LastServerAt = time.Now()
a.state.ConnectedServer = serverBase
a.mu.Unlock()
a.logf("Server connected: %s", serverBase)
a.logf("WebSocket established: %s; waiting for server hello", serverBase)
vmix, inventory := scanVmix(cfg.VmixURL)
a.mu.Lock()
@@ -642,6 +664,10 @@ func (a *Agent) runConnection(serverBase string) error {
a.mu.Unlock()
if err := a.handleMessage(ws, msg); err != nil {
a.logf("Message error: %v", err)
// A message handler error is normally a failed ACK/response write.
// Continuing to consume a socket that has already failed only queues
// more commands into a dead connection. Reconnect immediately instead.
return err
}
}
}
@@ -650,6 +676,12 @@ func (a *Agent) handleMessage(ws *wsConn, msg map[string]any) error {
typ := stringValue(msg["type"])
switch typ {
case "hello.ok":
a.mu.Lock()
a.state.ServerConnected = true
a.state.ServerConnecting = false
a.state.ServerError = ""
a.state.LastServerAt = time.Now()
a.mu.Unlock()
a.logf("Device registered on server")
if dev, ok := msg["device"].(map[string]any); ok {
a.mu.Lock()
@@ -1126,7 +1158,7 @@ func dialWebSocket(target string, timeout time.Duration) (*wsConn, error) {
}
}
address := net.JoinHostPort(host, port)
d := net.Dialer{Timeout: timeout}
d := net.Dialer{Timeout: timeout, KeepAlive: 15 * time.Second}
var conn net.Conn
if u.Scheme == "wss" {
conn, err = tls.DialWithDialer(&d, "tcp", address, &tls.Config{ServerName: host, MinVersion: tls.VersionTLS12})
@@ -1159,7 +1191,25 @@ func dialWebSocket(target string, timeout time.Duration) (*wsConn, error) {
return nil, err
}
if !strings.Contains(statusLine, " 101 ") {
// Keep a small diagnostic preview from reverse proxies (502/503/504).
// This makes intermittent public-server failures distinguishable from
// a local TCP/DNS problem without dumping an HTML error page to the log.
preview := ""
if statusLine != "" {
_ = conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
buf := make([]byte, 2048)
if n, readErr := reader.Read(buf); readErr == nil || n > 0 {
preview = strings.TrimSpace(strings.ReplaceAll(string(buf[:n]), "\r", " "))
preview = strings.Join(strings.Fields(preview), " ")
}
}
conn.Close()
if len(preview) > 300 {
preview = preview[:300] + "…"
}
if preview != "" {
return nil, fmt.Errorf("websocket upgrade failed: %s · %s", strings.TrimSpace(statusLine), preview)
}
return nil, fmt.Errorf("websocket upgrade failed: %s", strings.TrimSpace(statusLine))
}
headers := map[string]string{}