feat(master): UPDATE_STATUS from the dashboard, logged when it changes

Appended message ID 50 (dashboard -> master): the update check's state and a
one-line summary. Master logs it whenever the summary changes, so the first
one after master starts says when an update is out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-30 10:47:50 -05:00
parent a2cc0d5a82
commit 4fe3b41be2
5 changed files with 75 additions and 0 deletions

View File

@@ -102,5 +102,8 @@ namespace MessageType {
WORLD_RELOAD, WORLD_RELOAD,
// Master -> dashboard: each running zone's files, whether they changed on disk, and which instances are stale // Master -> dashboard: each running zone's files, whether they changed on disk, and which instances are stale
WORLD_FILES_STATUS, WORLD_FILES_STATUS,
// Dashboard -> master: what the update check found about this build (see UpdateStatus.h), for master's log
UPDATE_STATUS,
}; };
} }

View File

@@ -63,6 +63,7 @@
#include "CDClientReloader.h" #include "CDClientReloader.h"
#include "WorldReloader.h" #include "WorldReloader.h"
#include "master/WorldFiles.h" #include "master/WorldFiles.h"
#include "master/UpdateStatus.h"
#include "BuildInfo.h" #include "BuildInfo.h"
#ifdef DARKFLAME_PLATFORM_UNIX #ifdef DARKFLAME_PLATFORM_UNIX
@@ -1211,6 +1212,14 @@ namespace {
Game::lastSignal = -1; Game::lastSignal = -1;
} }
// The dashboard's update check: logged when what it says changes (so the first one after master starts, and news)
void OnUpdateStatus(const UpdateStatus& status, const SystemAddress& sysAddr) {
static std::string lastSummary;
if (sysAddr != dashboardServerMasterPeerSysAddr || status.summary.empty() || status.summary == lastSummary) return;
lastSummary = status.summary;
LOG("%s", status.summary.c_str());
}
void OnMessageCaptureControl(const MessageCaptureControl& control, const SystemAddress& sysAddr) { void OnMessageCaptureControl(const MessageCaptureControl& control, const SystemAddress& sysAddr) {
// Only the dashboard starts message captures; every world gets it, and the one with the player acts on it // Only the dashboard starts message captures; every world gets it, and the one with the player acts on it
if (sysAddr != dashboardServerMasterPeerSysAddr) { if (sysAddr != dashboardServerMasterPeerSysAddr) {
@@ -1270,6 +1279,7 @@ namespace {
handlers.On<ConfigReload>(Master::CONFIG_RELOAD, OnConfigReload); handlers.On<ConfigReload>(Master::CONFIG_RELOAD, OnConfigReload);
handlers.On<InstanceShutdown>(Master::INSTANCE_SHUTDOWN, OnInstanceShutdown); handlers.On<InstanceShutdown>(Master::INSTANCE_SHUTDOWN, OnInstanceShutdown);
handlers.On<DashboardShutdown>(Master::DASHBOARD_SHUTDOWN, OnDashboardShutdown); handlers.On<DashboardShutdown>(Master::DASHBOARD_SHUTDOWN, OnDashboardShutdown);
handlers.On<UpdateStatus>(Master::UPDATE_STATUS, OnUpdateStatus);
// Only world servers report game writes; pass them on unchanged // Only world servers report game writes; pass them on unchanged
handlers.On<DataChanged>(Master::DATA_CHANGED, ForwardWorldToDashboard<DataChanged>); handlers.On<DataChanged>(Master::DATA_CHANGED, ForwardWorldToDashboard<DataChanged>);
handlers.On<MessageCaptureControl>(Master::MESSAGE_CAPTURE_CONTROL, OnMessageCaptureControl); handlers.On<MessageCaptureControl>(Master::MESSAGE_CAPTURE_CONTROL, OnMessageCaptureControl);

View File

@@ -0,0 +1,35 @@
#ifndef __UPDATESTATUS__H__
#define __UPDATESTATUS__H__
#include <cstdint>
#include <string>
#include "BitStream.h"
#include "BitStreamUtils.h"
#include "DashboardMessages.h"
#include "MessageType/Master.h"
/**
* UPDATE_STATUS (dashboard -> master): what the dashboard's update check found, sent after each check and when the
* link to master comes up, so master's log says when a newer release or newer commits are out. Nothing is updated.
*/
struct UpdateStatus : public LUBitStream {
UpdateStatus() : LUBitStream(ServiceType::MASTER, MessageType::Master::UPDATE_STATUS) {}
static constexpr uint16_t MAX_SUMMARY = 1024;
// UpdateCheck::eState: 0 not checked, 1 up to date, 2 update available, 3 the check failed
uint8_t state{};
std::string summary; // one line for the log
void Serialize(RakNet::BitStream& stream) const override {
stream.Write(state);
DashboardMessages::WriteText(stream, summary, MAX_SUMMARY);
}
bool Deserialize(RakNet::BitStream& stream) override {
return stream.Read(state) && DashboardMessages::ReadText(stream, summary, MAX_SUMMARY);
}
};
#endif //!__UPDATESTATUS__H__

View File

@@ -1816,6 +1816,7 @@ static_assert(static_cast<int64_t>(MessageType::Master::CDCLIENT_RELOAD) == 46);
static_assert(static_cast<int64_t>(MessageType::Master::WORLD_FILES) == 47); static_assert(static_cast<int64_t>(MessageType::Master::WORLD_FILES) == 47);
static_assert(static_cast<int64_t>(MessageType::Master::WORLD_RELOAD) == 48); static_assert(static_cast<int64_t>(MessageType::Master::WORLD_RELOAD) == 48);
static_assert(static_cast<int64_t>(MessageType::Master::WORLD_FILES_STATUS) == 49); static_assert(static_cast<int64_t>(MessageType::Master::WORLD_FILES_STATUS) == 49);
static_assert(static_cast<int64_t>(MessageType::Master::UPDATE_STATUS) == 50);
// MessageType::Server: 3 enumerators // MessageType::Server: 3 enumerators
static_assert(static_cast<int64_t>(MessageType::Server::VERSION_CONFIRM) == 0); static_assert(static_cast<int64_t>(MessageType::Server::VERSION_CONFIRM) == 0);

View File

@@ -6,6 +6,7 @@
#include "master/WorldFiles.h" #include "master/WorldFiles.h"
#include "master/MessageCapture.h" #include "master/MessageCapture.h"
#include "master/PlayerAction.h" #include "master/PlayerAction.h"
#include "master/UpdateStatus.h"
#include "PacketDispatcher.h" #include "PacketDispatcher.h"
#include "PacketTestUtils.h" #include "PacketTestUtils.h"
#include "Game.h" #include "Game.h"
@@ -672,6 +673,31 @@ TEST(MasterPacketsTests, WorldFilesStatus) {
EXPECT_EQ(read.zones, status.zones); EXPECT_EQ(read.zones, status.zones);
} }
TEST(MasterPacketsTests, UpdateStatus) {
UpdateStatus status;
status.state = 2;
status.summary = "Update available: release v3.1.0 (this server is running release v3.0.0)";
ExpectHeaderThenSerialize(status, MessageType::Master::UPDATE_STATUS);
RakNet::BitStream stream;
status.Serialize(stream);
UpdateStatus read;
ASSERT_TRUE(read.Deserialize(stream));
EXPECT_EQ(read.state, 2);
EXPECT_EQ(read.summary, status.summary);
// Longer than the cap: cut on the way out, refused on the way in
status.summary.assign(UpdateStatus::MAX_SUMMARY + 10, 'x');
RakNet::BitStream cut;
status.Serialize(cut);
ASSERT_TRUE(read.Deserialize(cut));
EXPECT_EQ(read.summary.size(), UpdateStatus::MAX_SUMMARY);
RakNet::BitStream tooLong;
tooLong.Write<uint8_t>(1);
tooLong.Write<uint16_t>(UpdateStatus::MAX_SUMMARY + 1);
EXPECT_FALSE(read.Deserialize(tooLong));
}
// A master without the outdated flags (written after the endpoints) still reads: nothing is outdated // A master without the outdated flags (written after the endpoints) still reads: nothing is outdated
TEST(MasterPacketsTests, ServerListWithoutOutdatedFlags) { TEST(MasterPacketsTests, ServerListWithoutOutdatedFlags) {
MasterPackets::ServerListResponse response; MasterPackets::ServerListResponse response;