Add server control (start/stop/restart, live logs, broadcast, kick) to webadmin

Docker SDK drives login/char/worldserver containers via docker.sock (mounted
read-only into webadmin); live logs stream over SSE with nginx buffering
disabled so lines arrive in real time. Broadcast/kick go through a new
admin_queue DB table that the world server polls every ~2s in its existing
WorldProcess tick loop, avoiding a second IPC channel to the game server.
This commit is contained in:
Thomas committed 2026-07-10 14:19:14 +02:00
1 parent 3554c85f2f
commit 15e562cb04
11 files changed
+331 -2

No files matched your search

+8
View File
@@ -403,8 +403,16 @@ PVOID VisibilityProcess(PVOID TS)
// World Process
PVOID WorldProcess( PVOID TS )
{
int adminQueueTicks = 0;
while( GServer->ServerOnline )
{
// Poll roughly every 2 seconds regardless of WorldDelay, instead of
// hitting the DB on every tick.
if( ++adminQueueTicks * GServer->Config.WorldDelay >= 2000 )
{
adminQueueTicks = 0;
GServer->PollAdminQueue( );
}
pthread_mutex_lock( &GServer->MapMutex );
for(UINT i=0;i<GServer->MapList.Map.size();i++)
{
+42
View File
@@ -66,6 +66,48 @@ bool CWorldServer::NPCAnnounce( const char msg[200], const char npc[50])
return true;
}
// Poll the admin_queue table (written by the webadmin panel) for pending
// broadcast/kick actions and execute them. There's no direct socket between
// webadmin and the world server, so this DB-polling approach reuses the
// existing MYSQL connection instead of adding a second IPC channel.
void CWorldServer::PollAdminQueue( )
{
// QStore holds SQLMutex locked until QFree() -- copy the rows out and
// free the result set first, since NPCAnnounce/pakGMKick/QExecute below
// issue their own queries and would deadlock on the same (non-recursive)
// mutex if called while this result set is still open.
struct QueueItem { int id; char type[20]; char payload[255]; };
QueueItem items[20];
int count = 0;
MYSQL_RES* result = DB->QStore("SELECT id, type, payload FROM admin_queue WHERE executed=0 ORDER BY id LIMIT 20");
if(result != NULL)
{
MYSQL_ROW row;
while(count < 20 && (row = mysql_fetch_row(result)) != NULL)
{
items[count].id = atoi(row[0]);
strncpy_s(items[count].type, row[1], sizeof(items[count].type)-1);
strncpy_s(items[count].payload, row[2], sizeof(items[count].payload)-1);
count++;
}
}
DB->QFree( );
for(int i = 0; i < count; i++)
{
if(strcmp(items[i].type, "broadcast") == 0)
{
NPCAnnounce(items[i].payload, "[Admin]");
}
else if(strcmp(items[i].type, "kick") == 0)
{
pakGMKick(NULL, items[i].payload);
}
DB->QExecute("UPDATE admin_queue SET executed=1 WHERE id=%i", items[i].id);
}
}
// NPC Whisper to a character
bool CWorldServer::NPCWhisper( CPlayer* thisclient, CMonster* thismon, const char msg[200], const char npc[50] )
{
+1
View File
@@ -342,6 +342,7 @@ class CWorldServer : public CServerSocket
bool NPCMessage( CMonster* thismon, const char msg[200], const char npc[50] );
bool NPCWhisper( CPlayer* thisclient, CMonster* thismon, const char msg[200], const char npc[50] );
bool NPCAnnounce( const char msg[200], const char npc[50] );
void PollAdminQueue( );
UINT GetMaxPartyExp( UINT partylevel );
bool DoSkillScript( CCharacter* character, CSkills* thisskill );