mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 09:33:25 -08:00
[Network] Do not format discarded debug messages, parse without memmove (#7606)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Network] Do not format discarded debug messages, parse without memmove Protocol debug logging gets the full protobuf message rendered through TextFormat and reflection. On the server this happened before every session/room/game/moderator/admin/developer command, even though the default Server_ProtocolHandler::logDebugMessage() is a no-op and even when servatrice has no log file configured. On the clients the same string was built as an argument to qCDebug(), which evaluates its arguments regardless of whether the category is enabled. Server_ProtocolHandler now exposes a virtual logsDebugMessages() hook. The base implementation returns false, so handlers without a log destination spend nothing. AbstractServerSocketInterface overrides it with ServerLogger::isLoggingEnabled(), which checks the log file and the server/writelog setting that logMessage() itself consults. All debug message call sites check the hook before formatting. The client send/receive paths check QLoggingCategory::isDebugEnabled() first, the same pattern AbstractClient already uses for its slow-command warning. ISL message decoding does the same. Byte stream decoding called inputBuffer.remove(0, n) for the length header and then again for every message. Each call shifts all remaining bytes, so a read carrying k messages cost O(k * bytes remaining). The three receive loops (remote client, TCP server interface, ISL interface) now parse at a read offset and compact the consumed prefix once, when they stop because a full message is not buffered yet. No buffering or size-limit behavior changes. Also in this batch: - Server_ProtocolHandler::processGameCommandContainer() copied the whole games map under gameListMutex just to look up one game id. A new getGameInfo() performs the single lookup and returns the room/player ids. - The TCP receive loop's `ok` flag is initialized, so a ParseFromArray exception can no longer leave it indeterminate before `if (ok)`. - The exception log prints only the message being parsed instead of hex-dumping the entire pending buffer. Full build and ctest (628 tests) pass, including the server tests. * [Network] Address review comments - Move getGameInfo() out of the header into server_abstractuserinterface.cpp. - Drop the consumed prefix once after the parse loop instead of on every early return: the two "not enough data" exits now break out of the loop. Applied to the remote client, TCP server and ISL receive loops. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
parent
a4c4c67674
commit
b4e24f958a
11 changed files with 164 additions and 65 deletions
|
|
@ -39,14 +39,18 @@ LocalClient::~LocalClient()
|
|||
|
||||
void LocalClient::sendCommandContainer(const CommandContainer &cont)
|
||||
{
|
||||
qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont);
|
||||
if (LocalClientLog().isDebugEnabled()) {
|
||||
qCDebug(LocalClientLog).noquote() << userName << "OUT" << getSafeDebugString(cont);
|
||||
}
|
||||
|
||||
lsi->itemFromClient(cont);
|
||||
}
|
||||
|
||||
void LocalClient::itemFromServer(const ServerMessage &item)
|
||||
{
|
||||
qCDebug(LocalClientLog).noquote() << userName << "IN" << getSafeDebugString(item);
|
||||
if (LocalClientLog().isDebugEnabled()) {
|
||||
qCDebug(LocalClientLog).noquote() << userName << "IN" << getSafeDebugString(item);
|
||||
}
|
||||
|
||||
processProtocolItem(item);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -496,9 +496,15 @@ void RemoteClient::readData()
|
|||
|
||||
inputBuffer.append(data);
|
||||
|
||||
do {
|
||||
// Parse messages straight out of inputBuffer at readOffset. Removing each
|
||||
// consumed message from the front of the buffer would memmove all of the
|
||||
// remaining bytes once per message; instead the consumed prefix is dropped
|
||||
// once parsing stops.
|
||||
qsizetype readOffset = 0;
|
||||
|
||||
while (true) {
|
||||
if (!messageInProgress) {
|
||||
if (inputBuffer.size() >= 4) {
|
||||
if (inputBuffer.size() - readOffset >= 4) {
|
||||
// dirty hack to be compatible with v14 server that sends 60 bytes of garbage at the beginning
|
||||
if (!handshakeStarted) {
|
||||
handshakeStarted = true;
|
||||
|
|
@ -508,29 +514,30 @@ void RemoteClient::readData()
|
|||
}
|
||||
} else {
|
||||
// end of hack
|
||||
messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) +
|
||||
(((quint32)(unsigned char)inputBuffer[1]) << 16) +
|
||||
(((quint32)(unsigned char)inputBuffer[2]) << 8) +
|
||||
((quint32)(unsigned char)inputBuffer[3]);
|
||||
inputBuffer.remove(0, 4);
|
||||
const auto *header = reinterpret_cast<const unsigned char *>(inputBuffer.constData() + readOffset);
|
||||
messageLength = (static_cast<quint32>(header[0]) << 24) + (static_cast<quint32>(header[1]) << 16) +
|
||||
(static_cast<quint32>(header[2]) << 8) + static_cast<quint32>(header[3]);
|
||||
readOffset += 4;
|
||||
messageInProgress = true;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (inputBuffer.size() < messageLength) {
|
||||
return;
|
||||
if (inputBuffer.size() - readOffset < messageLength) {
|
||||
break;
|
||||
}
|
||||
|
||||
ServerMessage newServerMessage;
|
||||
bool ok = newServerMessage.ParseFromArray(inputBuffer.data(), messageLength);
|
||||
bool ok = newServerMessage.ParseFromArray(inputBuffer.constData() + readOffset, messageLength);
|
||||
|
||||
inputBuffer.remove(0, messageLength);
|
||||
readOffset += messageLength;
|
||||
messageInProgress = false;
|
||||
|
||||
if (ok) {
|
||||
qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage);
|
||||
if (RemoteClientLog().isDebugEnabled()) {
|
||||
qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage);
|
||||
}
|
||||
|
||||
processProtocolItem(newServerMessage);
|
||||
} else {
|
||||
|
|
@ -540,7 +547,12 @@ void RemoteClient::readData()
|
|||
if (getStatus() == StatusDisconnecting) { // use thread-safe getter
|
||||
doDisconnectFromServer();
|
||||
}
|
||||
} while (!inputBuffer.isEmpty());
|
||||
}
|
||||
|
||||
// Drop the consumed prefix once, instead of shifting the buffer per message.
|
||||
if (readOffset > 0) {
|
||||
inputBuffer.remove(0, readOffset);
|
||||
}
|
||||
}
|
||||
|
||||
void RemoteClient::websocketMessageReceived(const QByteArray &message)
|
||||
|
|
@ -548,7 +560,9 @@ void RemoteClient::websocketMessageReceived(const QByteArray &message)
|
|||
lastDataReceived = timeRunning;
|
||||
ServerMessage newServerMessage;
|
||||
if (newServerMessage.ParseFromArray(message.data(), message.length())) {
|
||||
qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage);
|
||||
if (RemoteClientLog().isDebugEnabled()) {
|
||||
qCDebug(RemoteClientLog).noquote() << "IN" << getSafeDebugString(newServerMessage);
|
||||
}
|
||||
|
||||
processProtocolItem(newServerMessage);
|
||||
} else {
|
||||
|
|
@ -564,7 +578,9 @@ void RemoteClient::sendCommandContainer(const CommandContainer &cont)
|
|||
auto size = static_cast<unsigned int>(cont.ByteSize());
|
||||
#endif
|
||||
|
||||
qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont);
|
||||
if (RemoteClientLog().isDebugEnabled()) {
|
||||
qCDebug(RemoteClientLog).noquote() << "OUT" << getSafeDebugString(cont);
|
||||
}
|
||||
|
||||
QByteArray buf;
|
||||
bool ok;
|
||||
|
|
|
|||
|
|
@ -84,6 +84,18 @@ void Server_AbstractUserInterface::onLogin(ResponseContainer &)
|
|||
{
|
||||
}
|
||||
|
||||
bool Server_AbstractUserInterface::getGameInfo(int gameId, int &roomId, int &playerId) const
|
||||
{
|
||||
QMutexLocker locker(&gameListMutex);
|
||||
const auto it = games.constFind(gameId);
|
||||
if (it == games.constEnd()) {
|
||||
return false;
|
||||
}
|
||||
roomId = it.value().first;
|
||||
playerId = it.value().second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Server_AbstractUserInterface::playerRemovedFromGame(Server_Game *game)
|
||||
{
|
||||
qDebug() << "Server_AbstractUserInterface::playerRemovedFromGame(): gameId =" << game->getGameId();
|
||||
|
|
|
|||
|
|
@ -58,6 +58,13 @@ public:
|
|||
return games;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Looks up the room and player id of one game without copying the game list.
|
||||
*
|
||||
* @return True if the game is known; otherwise false and the outputs are untouched.
|
||||
*/
|
||||
bool getGameInfo(int gameId, int &roomId, int &playerId) const;
|
||||
|
||||
virtual void sendProtocolItem(const Response &item) = 0;
|
||||
virtual void sendProtocolItem(const SessionEvent &item) = 0;
|
||||
virtual void sendProtocolItem(const GameEventContainer &item) = 0;
|
||||
|
|
|
|||
|
|
@ -208,7 +208,7 @@ Response::ResponseCode Server_ProtocolHandler::processSessionCommandContainer(co
|
|||
Response::ResponseCode resp = Response::RespInvalidCommand;
|
||||
const SessionCommand &sc = cont.session_command(i);
|
||||
const int num = getPbExtension(sc);
|
||||
if (num != SessionCommand::PING) { // don't log ping commands
|
||||
if (num != SessionCommand::PING && logsDebugMessages()) { // don't log ping commands
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
const auto commandType = static_cast<SessionCommand::SessionCommandType>(num);
|
||||
|
|
@ -271,7 +271,9 @@ Response::ResponseCode Server_ProtocolHandler::processRoomCommandContainer(const
|
|||
Response::ResponseCode resp = Response::RespInvalidCommand;
|
||||
const RoomCommand &sc = cont.room_command(i);
|
||||
const int num = getPbExtension(sc);
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
if (logsDebugMessages()) {
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
switch ((RoomCommand::RoomCommandType)num) {
|
||||
case RoomCommand::LEAVE_ROOM:
|
||||
resp = cmdLeaveRoom(sc.GetExtension(Command_LeaveRoom::ext), room, rc);
|
||||
|
|
@ -320,14 +322,14 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const
|
|||
return Response::RespLoginNeeded;
|
||||
}
|
||||
|
||||
QMap<int, QPair<int, int>> gameMap = getGames();
|
||||
if (!gameMap.contains(cont.game_id())) {
|
||||
int roomId;
|
||||
int playerId;
|
||||
if (!getGameInfo(cont.game_id(), roomId, playerId)) {
|
||||
return Response::RespNotInRoom;
|
||||
}
|
||||
const QPair<int, int> roomIdAndPlayerId = gameMap.value(cont.game_id());
|
||||
|
||||
QReadLocker roomsLocker(&server->roomsLock);
|
||||
Server_Room *room = server->getRooms().value(roomIdAndPlayerId.first);
|
||||
Server_Room *room = server->getRooms().value(roomId);
|
||||
if (!room) {
|
||||
return Response::RespNotInRoom;
|
||||
}
|
||||
|
|
@ -337,14 +339,14 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const
|
|||
if (!game) {
|
||||
if (room->getExternalGames().contains(cont.game_id())) {
|
||||
server->sendIsl_GameCommand(cont, room->getExternalGames().value(cont.game_id()).server_id(),
|
||||
userInfo->session_id(), roomIdAndPlayerId.first, roomIdAndPlayerId.second);
|
||||
userInfo->session_id(), roomId, playerId);
|
||||
return Response::RespNothing;
|
||||
}
|
||||
return Response::RespNotInRoom;
|
||||
}
|
||||
|
||||
QMutexLocker gameLocker(&game->gameMutex);
|
||||
auto *participant = game->getParticipants().value(roomIdAndPlayerId.second);
|
||||
auto *participant = game->getParticipants().value(playerId);
|
||||
if (!participant) {
|
||||
return Response::RespNotInRoom;
|
||||
}
|
||||
|
|
@ -357,8 +359,9 @@ Response::ResponseCode Server_ProtocolHandler::processGameCommandContainer(const
|
|||
Response::ResponseCode finalResponseCode = Response::RespOk;
|
||||
for (int i = cont.game_command_size() - 1; i >= 0; --i) {
|
||||
const GameCommand &sc = cont.game_command(i);
|
||||
logDebugMessage(QString("game %1 player %2: ").arg(cont.game_id()).arg(roomIdAndPlayerId.second) +
|
||||
getSafeDebugString(sc));
|
||||
if (logsDebugMessages()) {
|
||||
logDebugMessage(QString("game %1 player %2: ").arg(cont.game_id()).arg(playerId) + getSafeDebugString(sc));
|
||||
}
|
||||
|
||||
if (commandCountingInterval > 0) {
|
||||
int totalCount = 0;
|
||||
|
|
@ -407,7 +410,9 @@ Response::ResponseCode Server_ProtocolHandler::processModeratorCommandContainer(
|
|||
Response::ResponseCode resp = Response::RespInvalidCommand;
|
||||
const ModeratorCommand &sc = cont.moderator_command(i);
|
||||
const int num = getPbExtension(sc);
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
if (logsDebugMessages()) {
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
|
||||
resp = processExtendedModeratorCommand(num, sc, rc);
|
||||
if (resp != Response::RespOk) {
|
||||
|
|
@ -434,7 +439,9 @@ Response::ResponseCode Server_ProtocolHandler::processAdminCommandContainer(cons
|
|||
Response::ResponseCode resp = Response::RespInvalidCommand;
|
||||
const AdminCommand &sc = cont.admin_command(i);
|
||||
const int num = getPbExtension(sc);
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
if (logsDebugMessages()) {
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
|
||||
resp = processExtendedAdminCommand(num, sc, rc);
|
||||
if (resp != Response::RespOk) {
|
||||
|
|
@ -461,7 +468,9 @@ Response::ResponseCode Server_ProtocolHandler::processDeveloperCommandContainer(
|
|||
Response::ResponseCode resp = Response::RespInvalidCommand;
|
||||
const DeveloperCommand &sc = cont.developer_command(i);
|
||||
const int num = getPbExtension(sc);
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
if (logsDebugMessages()) {
|
||||
logDebugMessage(getSafeDebugString(sc));
|
||||
}
|
||||
|
||||
resp = processExtendedDeveloperCommand(num, sc, rc);
|
||||
if (resp != Response::RespOk) {
|
||||
|
|
|
|||
|
|
@ -55,6 +55,17 @@ protected:
|
|||
virtual void logDebugMessage(const QString & /* message */)
|
||||
{
|
||||
}
|
||||
/**
|
||||
* @brief Whether debug messages produced by this handler are actually written anywhere.
|
||||
*
|
||||
* Formatting a protocol message for the log is expensive, so callers only
|
||||
* build the debug string when this returns true. The base handler has no
|
||||
* log destination and therefore never needs one.
|
||||
*/
|
||||
[[nodiscard]] virtual bool logsDebugMessages() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
QList<int> messageSizeOverTime, messageCountOverTime, commandCountOverTime;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue