Enhance request pipeline with safe operation dispatch, support for paused server states, and bridge status updates.

This commit is contained in:
2026-08-27 11:57:02 +02:00
parent 439105ec60
commit dd801de28d
2 changed files with 43 additions and 5 deletions

View File

@@ -1,18 +1,29 @@
---@diagnostic disable: need-check-nil
-- Registration deferred to register(), called from RDB_Bootstrap's
-- onServerStarted, see RDB_OpWorldGetStats.lua.
RDB_OpBridgeStatus = {}
-- Mirrors zombie.GameTime.isGamePaused()'s server-side branch exactly
-- (decompiled: `GameServer.Players.isEmpty() && ServerOptions.instance.pauseEmpty.getValue()`),
-- true precisely when RDB_RequestPipeline would be relying on Events.OnTickEvenPaused
-- instead of Events.OnTick to even see this request.
local function isPaused()
return ServerOptions.instance:getBoolean("PauseEmpty") and getOnlinePlayers():size() == 0
end
function RDB_OpBridgeStatus.register()
RDB_OpRegistry.register("get_bridge_status", {
handler = function(_)
local bootedAt = RDB_Bootstrap.getBootTimestamp()
local now = getTimestampMs()
local now = getTimestampMs()
return true, {
version = RDB_Constants.PROTOCOL_VERSION,
bootedAt = bootedAt,
uptimeSeconds = (now - bootedAt) / 1000,
paused = isPaused(),
rateLimit = RDB_Security.getRateLimitStatus(),
config = RDB_Config.getAll(),
}

View File

@@ -44,6 +44,19 @@ local function audit(id, op, ok, code)
RDB_AuditLog.record({ id = id, op = op, ok = ok, code = code })
end
-- If an op handler throws (a bug, a bad deploy, a version mismatch between
-- files), surface it as a fast INTERNAL_ERROR response instead of silently
-- swallowing it. The raw Lua error is logged server-side only, never
-- sent to the client.
local function safeDispatch(op, args)
local ok, a, b, c = pcall(RDB_OpRegistry.dispatch, op, args)
if not ok then
RDB_Log.error("op '" .. tostring(op) .. "' handler threw: " .. tostring(a))
return false, RDB_Constants.ERROR_CODES.INTERNAL_ERROR, "Unhandled error in operation handler."
end
return a, b, c
end
-- Handles one raw request string end to end. raw is base64: RCON's
-- changeoption command strips every literal double-quote character from
-- every argument (see RESEARCH_LOG.md), so a client can't send raw JSON --
@@ -109,7 +122,7 @@ local function processRequest(rawBase64)
return
end
local dispatchOk, dataOrCode, message = RDB_OpRegistry.dispatch(op, args)
local dispatchOk, dataOrCode, message = safeDispatch(op, args)
local response
if dispatchOk then
response = buildOkResponse(id, dataOrCode)
@@ -148,14 +161,28 @@ end
local initialized = false
-- Guarded: Events.OnServerStarted fires more than once per boot (see
-- RESEARCH_LOG.md), and Events.OnTick.Add does not deduplicate identical
-- listeners, so calling this twice would otherwise double-register onTick.
-- Guarded: Events.OnServerStarted fires more than once per boot,
-- and Events.OnTick.Add does not deduplicate identical listeners,
-- so calling this twice would otherwise double-register onTick.
--
-- Registered on BOTH OnTick and OnTickEvenPaused: decompiling GameWindow's
-- main loop shows these two are mutually exclusive per real engine tick;
-- normal ticks fire OnTick (via IngameState's update path), while a tick
-- where the server is paused (PauseEmpty=true and no players connected;
-- see zombie.GameTime.isGamePaused) fires OnTickEvenPaused instead, never
-- both. Without the second registration, the entire bridge (every op, not
-- just one) goes completely inert on any server running PauseEmpty=true
-- whenever it's empty: no event ever calls onTick, so a request just sits
-- in RconDataBridge_Request forever with no response and no error, and a
-- client sees a silent timeout with nothing to diagnose.
function RDB_RequestPipeline.init()
if initialized then
return
end
initialized = true
lastSeenRaw = RDB_OptionsRegistry.get(RDB_Constants.OPT.REQUEST)
Events.OnTick.Add(onTick)
Events.OnTickEvenPaused.Add(onTick)
end