From dc310a6e9e54d5745ffb6ad9a4f03c5f5da9e62e Mon Sep 17 00:00:00 2001 From: blinkysc <37940565+blinkysc@users.noreply.github.com> Date: Wed, 24 Jun 2026 08:22:37 -0500 Subject: [PATCH] fix(Apps/SOAP): prevent shutdown deadlock on in-flight commands (#26335) Co-authored-by: blinkysc --- src/server/apps/worldserver/ACSoap/ACSoap.cpp | 29 +++++++++++++------ src/server/apps/worldserver/ACSoap/ACSoap.h | 3 ++ 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/server/apps/worldserver/ACSoap/ACSoap.cpp b/src/server/apps/worldserver/ACSoap/ACSoap.cpp index dfd3d07a2..4b76d2b58 100644 --- a/src/server/apps/worldserver/ACSoap/ACSoap.cpp +++ b/src/server/apps/worldserver/ACSoap/ACSoap.cpp @@ -20,6 +20,8 @@ #include "Log.h" #include "World.h" #include "soapStub.h" +#include +#include void ACSoapThread(const std::string& host, uint16 port) { @@ -108,21 +110,28 @@ int ns1__executeCommand(soap* soap, char* command, char** result) return soap_sender_fault(soap, "Command can not be empty", "The supplied command was an empty string"); LOG_DEBUG("network.soap", "ACSoap: got command '{}'", command); - SOAPCommand connection; + + // Shared so the object survives if we stop waiting below: the queued command keeps a raw + // pointer to it and the world thread may still run it after that. The extra reference is + // released by commandFinished() once the world side is done with it. + auto connection = std::make_shared(); + connection->m_self = connection; // commands are executed in the world thread. We have to wait for them to be completed + sWorld->QueueCliCommand(new CliCommandHolder(connection.get(), command, &SOAPCommand::print, &SOAPCommand::commandFinished)); + + // Wait for the command to finish, but bail on shutdown: ProcessCliCommands() (which fulfils + // the promise) stops once the world loop exits, so an unbounded wait here would deadlock. + std::future finished = connection->finishedPromise.get_future(); + while (finished.wait_for(std::chrono::seconds(1)) != std::future_status::ready) { - // CliCommandHolder will be deleted from world, accessing after queueing is NOT save - CliCommandHolder* cmd = new CliCommandHolder(&connection, command, &SOAPCommand::print, &SOAPCommand::commandFinished); - sWorld->QueueCliCommand(cmd); + if (World::IsStopped()) + return soap_receiver_fault(soap, "Server is shutting down", "Command aborted: the server is shutting down"); } - // Wait until the command has finished executing - connection.finishedPromise.get_future().wait(); - // The command has finished executing already - char* printBuffer = soap_strdup(soap, connection.m_printBuffer.c_str()); - if (connection.hasCommandSucceeded()) + char* printBuffer = soap_strdup(soap, connection->m_printBuffer.c_str()); + if (connection->hasCommandSucceeded()) { *result = printBuffer; return SOAP_OK; @@ -135,6 +144,8 @@ void SOAPCommand::commandFinished(void* soapconnection, bool success) { SOAPCommand* con = (SOAPCommand*)soapconnection; con->setCommandSuccess(success); + // world side is done with us; drop the keep-alive (may free the object) + con->m_self.reset(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/server/apps/worldserver/ACSoap/ACSoap.h b/src/server/apps/worldserver/ACSoap/ACSoap.h index bcdccc07b..36162a33c 100644 --- a/src/server/apps/worldserver/ACSoap/ACSoap.h +++ b/src/server/apps/worldserver/ACSoap/ACSoap.h @@ -20,6 +20,7 @@ #include "Define.h" #include +#include void process_message(struct soap* soap_message); void ACSoapThread(const std::string& host, uint16 port); @@ -58,6 +59,8 @@ public: bool m_success; std::string m_printBuffer; std::promise finishedPromise; + // keep-alive while a queued command still references this object; released in commandFinished() + std::shared_ptr m_self; }; #endif