change around pings, fix main.js so the actual real fucking number shows instead of it freaking out.

This commit is contained in:
KingKDot
2026-07-09 13:55:36 -05:00
parent f96b3d0721
commit 43ee5a7d4b
12 changed files with 293 additions and 32 deletions
@@ -76,6 +76,78 @@ func TestHandlePing(t *testing.T) {
}
}
func TestHandlePingPreservesZeroTimestamp(t *testing.T) {
writer := &testWriter{}
env := &rt.Env{
Conn: writer,
Cfg: config.Config{},
}
ctx := context.Background()
envelope := map[string]interface{}{
"ts": int64(0),
}
if err := HandlePing(ctx, env, envelope); err != nil {
t.Fatalf("HandlePing failed: %v", err)
}
deadline := time.After(2 * time.Second)
for len(writer.msgs) < 1 {
select {
case <-deadline:
t.Fatal("Timed out waiting for pong message")
default:
time.Sleep(5 * time.Millisecond)
}
}
var pong wire.Pong
if err := msgpack.Unmarshal(writer.msgs[0], &pong); err != nil {
t.Fatalf("Failed to unmarshal pong: %v", err)
}
if pong.TS != 0 {
t.Fatalf("expected timestamp 0, got %d", pong.TS)
}
}
func TestHandlePingFallsBackForInvalidTimestamp(t *testing.T) {
writer := &testWriter{}
env := &rt.Env{
Conn: writer,
Cfg: config.Config{},
}
ctx := context.Background()
before := time.Now().UnixMilli()
envelope := map[string]interface{}{
"ts": "not-a-timestamp",
}
if err := HandlePing(ctx, env, envelope); err != nil {
t.Fatalf("HandlePing failed: %v", err)
}
deadline := time.After(2 * time.Second)
for len(writer.msgs) < 1 {
select {
case <-deadline:
t.Fatal("Timed out waiting for pong message")
default:
time.Sleep(5 * time.Millisecond)
}
}
var pong wire.Pong
if err := msgpack.Unmarshal(writer.msgs[0], &pong); err != nil {
t.Fatalf("Failed to unmarshal pong: %v", err)
}
after := time.Now().UnixMilli()
if pong.TS < before || pong.TS > after {
t.Fatalf("expected fallback timestamp between %d and %d, got %d", before, after, pong.TS)
}
}
func TestHandleCommand_Ping(t *testing.T) {
writer := &testWriter{}
env := &rt.Env{
+22 -14
View File
@@ -3,36 +3,44 @@ package handlers
import "encoding/json"
func extractTimestamp(v interface{}) int64 {
ts, ok := extractTimestampIfPresent(v)
if !ok {
return 0
}
return ts
}
func extractTimestampIfPresent(v interface{}) (int64, bool) {
switch t := v.(type) {
case uint64:
if t <= uint64(^uint64(0)>>1) {
return int64(t)
return int64(t), true
}
return 0
return 0, false
case uint32:
return int64(t)
return int64(t), true
case uint16:
return int64(t)
return int64(t), true
case uint8:
return int64(t)
return int64(t), true
case int64:
return t
return t, true
case int32:
return int64(t)
return int64(t), true
case int16:
return int64(t)
return int64(t), true
case int8:
return int64(t)
return int64(t), true
case int:
return int64(t)
return int64(t), true
case float64:
return int64(t)
return int64(t), true
case float32:
return int64(t)
return int64(t), true
case json.Number:
if n, err := t.Int64(); err == nil {
return n
return n, true
}
}
return 0
return 0, false
}
@@ -129,6 +129,22 @@ func TestExtractTimestamp_Zero(t *testing.T) {
}
}
func TestExtractTimestampIfPresent_ZeroIsValid(t *testing.T) {
got, ok := extractTimestampIfPresent(int64(0))
if !ok {
t.Fatal("expected zero timestamp to be valid")
}
if got != 0 {
t.Fatalf("expected 0, got %d", got)
}
}
func TestExtractTimestampIfPresent_InvalidIsNotValid(t *testing.T) {
if got, ok := extractTimestampIfPresent("hello"); ok || got != 0 {
t.Fatalf("expected invalid timestamp to return ok=false and 0, got ok=%v value=%d", ok, got)
}
}
func TestExtractTimestamp_Negative(t *testing.T) {
var ts int64 = -100
if got := extractTimestamp(ts); got != ts {
+2 -2
View File
@@ -10,8 +10,8 @@ import (
func HandlePing(ctx context.Context, env *runtime.Env, envelope map[string]interface{}) error {
ts := extractTimestamp(envelope["ts"])
if ts == 0 {
ts, ok := extractTimestampIfPresent(envelope["ts"])
if !ok {
ts = time.Now().UnixMilli()
}
env.SetLastPong(time.Now().UnixMilli())
+12 -1
View File
@@ -1119,7 +1119,9 @@ function initializeRenderer() {
rendererSetLayout = rSetLayout;
registerRenderer((data, options) => {
renderMerge(data, options);
updateDashboardStatsFromClients(data);
if (isUnfilteredClientView()) {
updateDashboardStatsFromClients(data);
}
if (!options?.fromPluginDashboard) {
refreshDashboardPluginContributions(data?.items || []);
}
@@ -1151,6 +1153,15 @@ function initializeRenderer() {
}
}
function isUnfilteredClientView() {
return !state.searchTerm &&
(state.filterStatus || "all") === "all" &&
(state.filterOs || "all") === "all" &&
(state.filterCountry || "all") === "all" &&
(state.filterGroup || "all") === "all" &&
(state.filterWebcam || "all") === "all";
}
if (logoutBtn && !logoutBtn.dataset.boundLogout) {
logoutBtn.dataset.boundLogout = "true";
logoutBtn.addEventListener("click", async () => {
+2 -4
View File
@@ -5,6 +5,7 @@ import { encodeMessage } from "./protocol";
import { v4 as uuidv4 } from "uuid";
import { metrics } from "./metrics";
import { logAudit, AuditAction } from "./auditLog";
import { sendPingRequest } from "./wsHandlers";
const DEFAULT_PAGE_SIZE = 12;
const CORS_HEADERS = {
@@ -61,10 +62,7 @@ export function handleCommand(target: ClientInfo, action: string, req: Request)
metrics.recordCommand(action);
if (action === "ping") {
const nonce = Date.now() + Math.floor(Math.random() * 1000);
target.lastPingSent = Date.now();
target.lastPingNonce = nonce;
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
sendPingRequest(target, target.ws, "manual", 0);
return OK();
}
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { getHeartbeatBatchSize } from "./maintenance-loops";
import { getHeartbeatBatchSize, shouldSendHeartbeatPing } from "./maintenance-loops";
describe("maintenance heartbeat batching", () => {
test("spreads large fleets across the heartbeat interval", () => {
@@ -13,4 +13,11 @@ describe("maintenance heartbeat batching", () => {
test("handles empty fleets", () => {
expect(getHeartbeatBatchSize(0, 30_000)).toBe(0);
});
test("only sends heartbeat pings after the per-client interval", () => {
const now = Date.now();
expect(shouldSendHeartbeatPing({}, now, 30_000)).toBe(true);
expect(shouldSendHeartbeatPing({ lastPingSent: now - 29_999 }, now, 30_000)).toBe(false);
expect(shouldSendHeartbeatPing({ lastPingSent: now - 30_000 }, now, 30_000)).toBe(true);
});
});
@@ -35,6 +35,16 @@ export function getHeartbeatBatchSize(totalClients: number, heartbeatIntervalMs:
return Math.max(1, Math.ceil(totalClients / ticksPerSweep));
}
export function shouldSendHeartbeatPing(
info: Pick<ClientInfo, "lastPingSent">,
now: number,
heartbeatIntervalMs: number,
): boolean {
if (heartbeatIntervalMs <= 0) return false;
if (!info.lastPingSent) return true;
return now - info.lastPingSent >= heartbeatIntervalMs;
}
export function startMaintenanceLoops(params: StartMaintenanceParams): void {
setInterval(() => {
const startedAt = Date.now();
@@ -82,8 +92,11 @@ export function startMaintenanceLoops(params: StartMaintenanceParams): void {
}
continue;
}
if (!shouldSendHeartbeatPing(info, now, params.heartbeatIntervalMs)) {
continue;
}
try {
sendPingRequest(info, info.ws, "heartbeat");
sendPingRequest(info, info.ws, "heartbeat", params.heartbeatIntervalMs);
} catch (err) {
logger.debug(`[ping] heartbeat failed for ${id}`, err);
}
@@ -0,0 +1,47 @@
import { describe, expect, test } from "bun:test";
import { decodeMessage } from "../../protocol";
import type { ClientInfo } from "../../types";
import { dispatchPingBulk } from "./client-command-routes";
function makeClient(): ClientInfo {
const sent: Uint8Array[] = [];
return {
id: "bulk-ping-client",
role: "client",
lastSeen: Date.now(),
ws: {
send(data: Uint8Array) {
sent.push(data);
},
_sent: sent,
},
} as any;
}
describe("client command ping diagnostics", () => {
test("dispatchPingBulk sends the requested number of pings", () => {
const client = makeClient();
const sent = (client.ws as any)._sent as Uint8Array[];
const count = dispatchPingBulk(client, 3);
expect(count).toBe(3);
expect(sent.length).toBe(3);
for (const raw of sent) {
const payload = decodeMessage(raw) as any;
expect(payload.type).toBe("ping");
expect(typeof payload.ts).toBe("number");
}
expect(typeof client.lastPingNonce).toBe("number");
});
test("dispatchPingBulk clamps invalid and large counts", () => {
const invalid = makeClient();
expect(dispatchPingBulk(invalid, "bad")).toBe(1);
expect((invalid.ws as any)._sent.length).toBe(1);
const large = makeClient();
expect(dispatchPingBulk(large, 5000)).toBe(1000);
expect((large.ws as any)._sent.length).toBe(1000);
});
});
@@ -7,6 +7,8 @@ import { metrics } from "../../metrics";
import { encodeMessage } from "../../protocol";
import { requireClientAccess, requireFeatureAccess, requirePermission } from "../../rbac";
import { clearThumbnail } from "../../thumbnails";
import type { ClientInfo } from "../../types";
import { sendPingRequest } from "../../wsHandlers";
type RequestIpProvider = {
requestIP: (req: Request) => { address?: string } | null | undefined;
@@ -32,6 +34,20 @@ type ClientCommandDeps = {
pendingCommandReplies: Map<string, PendingCommandReply>;
};
function clampPositiveInt(value: unknown, fallback: number, max: number): number {
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
return Math.min(max, Math.max(1, Math.floor(parsed)));
}
export function dispatchPingBulk(target: ClientInfo, countValue: unknown): number {
const count = clampPositiveInt(countValue, 1, 1000);
for (let i = 0; i < count; i++) {
sendPingRequest(target, target.ws, "manual-bulk", 0);
}
return count;
}
export async function handleClientCommandRoute(
req: Request,
url: URL,
@@ -71,12 +87,13 @@ export async function handleClientCommandRoute(
let success = true;
if (action === "ping") {
const nonce = Date.now() + Math.floor(Math.random() * 1000);
target.lastPingSent = Date.now();
target.lastPingNonce = nonce;
target.ws.send(encodeMessage({ type: "ping", ts: nonce }));
sendPingRequest(target, target.ws, "manual", 0);
metrics.recordCommand("ping");
} else if (action === "ping_bulk") {
Math.max(1, Math.min(1000, Number(body?.count || 1)));
const count = dispatchPingBulk(target, body?.count);
metrics.recordCommand("ping_bulk");
logAudit({ timestamp: Date.now(), username: user.username, ip, action: AuditAction.COMMAND, targetClientId: targetId, details: `ping_bulk:${count}`, success: true });
return Response.json({ ok: true, sent: count }, { headers: deps.CORS_HEADERS });
} else if (action === "disconnect") {
try {
requirePermission(user, "clients:disconnect");
+63
View File
@@ -30,6 +30,25 @@ describe("wsHandlers ping/pong", () => {
expect(ws.sent.length).toBe(1);
});
test("sendPingRequest can bypass the interval for manual pings", () => {
const ws: MockWs = {
sent: [],
send(msg) {
this.sent.push(msg);
},
};
const info = {
id: "client-manual-ping",
role: "client",
ws,
lastSeen: Date.now(),
} as any;
expect(sendPingRequest(info, ws, "test")).toBe(true);
expect(sendPingRequest(info, ws, "manual", 0)).toBe(true);
expect(ws.sent.length).toBe(2);
});
test("handlePing responds with pong without starting another server ping", () => {
const ws: MockWs = {
sent: [],
@@ -53,6 +72,50 @@ describe("wsHandlers ping/pong", () => {
expect(info.lastPingNonce).toBeUndefined();
});
test("handlePing preserves zero timestamps", () => {
const ws: MockWs = {
sent: [],
send(msg) {
this.sent.push(msg);
},
};
const info = {
id: "client-zero-ping",
role: "client",
ws,
lastSeen: Date.now(),
} as any;
handlePing(info, { type: "ping", ts: 0 } as any, ws);
const payload = decodeMessage(ws.sent[0]) as any;
expect(payload.type).toBe("pong");
expect(payload.ts).toBe(0);
});
test("handlePing falls back for invalid timestamps", () => {
const ws: MockWs = {
sent: [],
send(msg) {
this.sent.push(msg);
},
};
const info = {
id: "client-invalid-ping",
role: "client",
ws,
lastSeen: Date.now(),
} as any;
const before = Date.now();
handlePing(info, { type: "ping", ts: "bad" } as any, ws);
const payload = decodeMessage(ws.sent[0]) as any;
expect(payload.type).toBe("pong");
expect(payload.ts).toBeGreaterThanOrEqual(before);
expect(payload.ts).toBeLessThanOrEqual(Date.now());
});
test("handlePong clears nonce and records ping", () => {
metrics.reset();
const ws: MockWs = {
+13 -4
View File
@@ -171,19 +171,28 @@ export function handlePing(info: ClientInfo, payload: Ping, ws: any) {
isAdmin: info.isAdmin,
});
}
ws.send(encodeMessage({ type: "pong", ts: payload.ts || Date.now() }));
const ts = typeof payload.ts === "number" && Number.isFinite(payload.ts)
? payload.ts
: Date.now();
ws.send(encodeMessage({ type: "pong", ts }));
}
export function sendPingRequest(info: ClientInfo, ws: any, reason: string) {
export function sendPingRequest(
info: ClientInfo,
ws: any,
reason: string,
minIntervalMs = 1_000,
): boolean {
const now = Date.now();
if (info.lastPingSent && now - info.lastPingSent < 1_000) {
return;
if (minIntervalMs > 0 && info.lastPingSent && now - info.lastPingSent < minIntervalMs) {
return false;
}
const nonce = now + Math.floor(Math.random() * 1000);
info.lastPingSent = now;
info.lastPingNonce = nonce;
//console.log(`[ping] send ping to client=${info.id} reason=${reason} nonce=${nonce}`);
ws.send(encodeMessage({ type: "ping", ts: nonce }));
return true;
}
export function handlePong(info: ClientInfo, payload: WireMessage) {