From d7019a244e1c16189ae6d1e3478085bfde48b486 Mon Sep 17 00:00:00 2001 From: heinrich5991 Date: Tue, 26 Jan 2021 21:22:32 +0100 Subject: [PATCH] Get rid of the MySQL C++ connector as a dependency We now use the C API directly. This has the advantage of using one obscure dependency less, as the C++ connector also used the C API. --- .github/workflows/build.yaml | 2 +- CMakeLists.txt | 2 - cmake/FindMySQL.cmake | 27 +- src/engine/server/databases/connection.h | 25 +- .../server/databases/connection_pool.cpp | 9 - src/engine/server/databases/mysql.cpp | 701 +++++++++++++----- src/engine/server/databases/mysql.h | 83 --- src/engine/server/databases/sqlite.cpp | 77 +- src/engine/server/databases/sqlite.h | 66 -- src/engine/server/server.cpp | 30 +- 10 files changed, 647 insertions(+), 375 deletions(-) delete mode 100644 src/engine/server/databases/mysql.h delete mode 100644 src/engine/server/databases/sqlite.h diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 50a3becd3..5a73a6873 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -63,7 +63,7 @@ jobs: - name: Prepare Linux (fancy) if: contains(matrix.os, 'ubuntu') && matrix.fancy run: | - sudo apt-get install libboost-dev libmariadbclient-dev libmysqlcppconn-dev libwebsockets-dev -y + sudo apt-get install libmariadbclient-dev libwebsockets-dev -y - name: Prepare MacOS if: contains(matrix.os, 'macOS') diff --git a/CMakeLists.txt b/CMakeLists.txt index 8148bdb6e..db843c1fb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1964,9 +1964,7 @@ set_src(ENGINE_SERVER GLOB_RECURSE src/engine/server databases/connection_pool.cpp databases/connection_pool.h databases/mysql.cpp - databases/mysql.h databases/sqlite.cpp - databases/sqlite.h name_ban.cpp name_ban.h register.cpp diff --git a/cmake/FindMySQL.cmake b/cmake/FindMySQL.cmake index ea7c0aecd..6919d3a0b 100644 --- a/cmake/FindMySQL.cmake +++ b/cmake/FindMySQL.cmake @@ -39,30 +39,13 @@ endif() set_extra_dirs_lib(MYSQL mysql) find_library(MYSQL_LIBRARY NAMES "mysqlclient" "mysqlclient_r" "mariadbclient" - HINTS ${HINTS_MYSQL_LIBDIR} ${MYSQL_CONFIG_LIBRARY_PATH} - PATHS ${PATHS_MYSQL_LIBDIR} + HINTS ${MYSQL_CONFIG_LIBRARY_PATH} ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} ) set_extra_dirs_include(MYSQL mysql "${MYSQL_LIBRARY}") find_path(MYSQL_INCLUDEDIR NAMES "mysql.h" - HINTS ${HINTS_MYSQL_INCLUDEDIR} ${MYSQL_CONFIG_INCLUDE_DIR} - PATHS ${PATHS_MYSQL_INCLUDEDIR} - ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} -) - -set_extra_dirs_lib(MYSQL_CPPCONN mysql) -find_library(MYSQL_CPPCONN_LIBRARY - NAMES "mysqlcppconn" "mysqlcppconn-static" - HINTS ${HINTS_MYSQL_CPPCONN_LIBDIR} ${MYSQL_CONFIG_LIBRARY_PATH} - PATHS ${PATHS_MYSQL_CPPCONN_LIBDIR} - ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} -) -set_extra_dirs_include(MYSQL_CPPCONN mysql "${MYSQL_CPPCONN_LIBRARY}") -find_path(MYSQL_CPPCONN_INCLUDEDIR - NAMES "mysql_connection.h" - HINTS ${HINTS_MYSQL_CPPCONN_INCLUDEDIR} ${MYSQL_CONFIG_INCLUDE_DIR} - PATHS ${PATHS_MYSQL_CPPCONN_INCLUDEDIR} + HINTS ${MYSQL_CONFIG_INCLUDE_DIR} ${CROSSCOMPILING_NO_CMAKE_SYSTEM_PATH} ) @@ -70,10 +53,8 @@ include(FindPackageHandleStandardArgs) find_package_handle_standard_args(MySQL DEFAULT_MSG MYSQL_LIBRARY MYSQL_INCLUDEDIR) if(MYSQL_FOUND) - is_bundled(MYSQL_BUNDLED "${MYSQL_LIBRARY}") - - set(MYSQL_LIBRARIES ${MYSQL_LIBRARY} ${MYSQL_CPPCONN_LIBRARY}) - set(MYSQL_INCLUDE_DIRS ${MYSQL_INCLUDEDIR} ${MYSQL_CPPCONN_INCLUDEDIR}) + set(MYSQL_LIBRARIES ${MYSQL_LIBRARY}) + set(MYSQL_INCLUDE_DIRS ${MYSQL_INCLUDEDIR}) mark_as_advanced(MYSQL_INCLUDEDIR MYSQL_LIBRARY) endif() diff --git a/src/engine/server/databases/connection.h b/src/engine/server/databases/connection.h index 42483b698..5199fefab 100644 --- a/src/engine/server/databases/connection.h +++ b/src/engine/server/databases/connection.h @@ -64,16 +64,15 @@ public: // when called multiple times the next row is selected virtual bool Step() = 0; // executes the query and returns the number of rows affected by the update/insert/delete - // FIXME(2020-01-20): change function to AffectedRows() when moved to c-api of MySQL virtual int ExecuteUpdate() = 0; - virtual bool IsNull(int Col) const = 0; - virtual float GetFloat(int Col) const = 0; - virtual int GetInt(int Col) const = 0; + virtual bool IsNull(int Col) = 0; + virtual float GetFloat(int Col) = 0; + virtual int GetInt(int Col) = 0; // ensures that the string is null terminated - virtual void GetString(int Col, char *pBuffer, int BufferSize) const = 0; + virtual void GetString(int Col, char *pBuffer, int BufferSize) = 0; // returns number of bytes read into the buffer - virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const = 0; + virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) = 0; // SQL statements, that can't be abstracted, has side effects to the result virtual void AddPoints(const char *pPlayer, int Points) = 0; @@ -89,4 +88,18 @@ protected: void FormatCreatePoints(char *aBuf, unsigned int BufferSize); }; +int MysqlInit(); +void MysqlUninit(); + +IDbConnection *CreateSqliteConnection(const char *pFilename, bool Setup); +// Returns nullptr if MySQL support is not compiled in. +IDbConnection *CreateMysqlConnection( + const char *pDatabase, + const char *pPrefix, + const char *pUser, + const char *pPass, + const char *pIp, + int Port, + bool Setup); + #endif // ENGINE_SERVER_DATABASES_CONNECTION_H diff --git a/src/engine/server/databases/connection_pool.cpp b/src/engine/server/databases/connection_pool.cpp index a1861a45f..008ef2f18 100644 --- a/src/engine/server/databases/connection_pool.cpp +++ b/src/engine/server/databases/connection_pool.cpp @@ -2,9 +2,6 @@ #include "connection.h" #include -#if defined(CONF_SQL) -#include -#endif #include // helper struct to hold thread data @@ -223,12 +220,6 @@ bool CDbConnectionPool::ExecSqlFunc(IDbConnection *pConnection, CSqlExecData *pD break; } } -#if defined(CONF_SQL) - catch(sql::SQLException &e) - { - dbg_msg("sql", "%s MySQL Error: %s", pData->m_pName, e.what()); - } -#endif catch(std::runtime_error &e) { dbg_msg("sql", "%s SQLite Error: %s", pData->m_pName, e.what()); diff --git a/src/engine/server/databases/mysql.cpp b/src/engine/server/databases/mysql.cpp index 838c293da..fafc3adcf 100644 --- a/src/engine/server/databases/mysql.cpp +++ b/src/engine/server/databases/mysql.cpp @@ -1,17 +1,145 @@ -#include "mysql.h" +#include "connection.h" + +#if defined(CONF_SQL) +#include #include #include -#if defined(CONF_SQL) -#include -#include -#include -#include -#endif -#include +#include +#include +#include +#include -CLock CMysqlConnection::m_SqlDriverLock; +enum +{ + MYSQLSTATE_UNINITIALIZED, + MYSQLSTATE_INITIALIZED, + MYSQLSTATE_SHUTTINGDOWN, +}; + +std::atomic_int g_MysqlState = {MYSQLSTATE_UNINITIALIZED}; +std::atomic_int g_MysqlNumConnections; + +int MysqlInit() +{ + dbg_assert(mysql_thread_safe(), "MySQL library without thread safety"); + dbg_assert(g_MysqlState == MYSQLSTATE_UNINITIALIZED, "double MySQL initialization"); + if(mysql_library_init(0, nullptr, nullptr)) + { + return 1; + } + int Uninitialized = MYSQLSTATE_UNINITIALIZED; + bool Swapped = g_MysqlState.compare_exchange_strong(Uninitialized, MYSQLSTATE_INITIALIZED); + (void)Swapped; + dbg_assert(Swapped, "MySQL double initialization"); + return 0; +} + +void MysqlUninit() +{ + int Initialized = MYSQLSTATE_INITIALIZED; + bool Swapped = g_MysqlState.compare_exchange_strong(Initialized, MYSQLSTATE_SHUTTINGDOWN); + (void)Swapped; + dbg_assert(Swapped, "double MySQL free or free without initialization"); + int Counter = g_MysqlNumConnections; + if(Counter != 0) + { + dbg_msg("mysql", "can't deinitialize, connections remaining: %d", Counter); + return; + } + mysql_library_end(); +} + +class CMysqlConnection : public IDbConnection +{ +public: + CMysqlConnection( + const char *pDatabase, + const char *pPrefix, + const char *pUser, + const char *pPass, + const char *pIp, + int Port, + bool Setup); + virtual ~CMysqlConnection(); + virtual void Print(IConsole *pConsole, const char *Mode); + + virtual CMysqlConnection *Copy(); + + virtual const char *BinaryCollate() const { return "utf8mb4_bin"; } + virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize); + virtual const char *InsertTimestampAsUtc() const { return "?"; } + virtual const char *CollateNocase() const { return "CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci"; } + virtual const char *InsertIgnore() const { return "INSERT IGNORE"; }; + virtual const char *Random() const { return "RAND()"; }; + virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const; + + virtual Status Connect(); + virtual void Disconnect(); + + virtual void PrepareStatement(const char *pStmt); + + virtual void BindString(int Idx, const char *pString); + virtual void BindBlob(int Idx, unsigned char *pBlob, int Size); + virtual void BindInt(int Idx, int Value); + virtual void BindFloat(int Idx, float Value); + + virtual void Print() {} + virtual bool Step(); + virtual int ExecuteUpdate(); + + virtual bool IsNull(int Col); + virtual float GetFloat(int Col); + virtual int GetInt(int Col); + virtual void GetString(int Col, char *pBuffer, int BufferSize); + virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize); + + virtual void AddPoints(const char *pPlayer, int Points); + +private: + class CStmtDeleter + { + public: + void operator()(MYSQL_STMT *pStmt) const; + }; + + char m_aErrorDetail[128]; + void StoreErrorMysql(const char *pContext); + void StoreErrorStmt(const char *pContext); + bool ConnectImpl(); + bool PrepareAndExecuteStatement(const char *pStmt); + //static void DeleteResult(MYSQL_RES *pResult); + + union UParameterExtra + { + int i; + unsigned long ul; + float f; + }; + + bool m_NewQuery = false; + bool m_HaveConnection = false; + MYSQL m_Mysql; + std::unique_ptr m_pStmt = nullptr; + std::vector m_aStmtParameters; + std::vector m_aStmtParameterExtras; + + // copy of config vars + char m_aDatabase[64]; + char m_aUser[64]; + char m_aPass[64]; + char m_aIp[64]; + int m_Port; + bool m_Setup; + + std::atomic_bool m_InUse; +}; + +void CMysqlConnection::CStmtDeleter::operator()(MYSQL_STMT *pStmt) const +{ + mysql_stmt_close(pStmt); +} CMysqlConnection::CMysqlConnection( const char *pDatabase, @@ -22,24 +150,52 @@ CMysqlConnection::CMysqlConnection( int Port, bool Setup) : IDbConnection(pPrefix), -#if defined(CONF_SQL) - m_NewQuery(false), -#endif m_Port(Port), m_Setup(Setup), m_InUse(false) { + g_MysqlNumConnections += 1; + dbg_assert(g_MysqlState == MYSQLSTATE_INITIALIZED, "MySQL library not in initialized state"); + + mem_zero(m_aErrorDetail, sizeof(m_aErrorDetail)); + mem_zero(&m_Mysql, sizeof(m_Mysql)); + mysql_init(&m_Mysql); + str_copy(m_aDatabase, pDatabase, sizeof(m_aDatabase)); str_copy(m_aUser, pUser, sizeof(m_aUser)); str_copy(m_aPass, pPass, sizeof(m_aPass)); str_copy(m_aIp, pIp, sizeof(m_aIp)); -#ifndef CONF_SQL - dbg_msg("sql", "Adding MySQL server failed due to MySQL support not enabled during compile time"); -#endif } CMysqlConnection::~CMysqlConnection() { + mysql_close(&m_Mysql); + g_MysqlNumConnections -= 1; +} + +void CMysqlConnection::StoreErrorMysql(const char *pContext) +{ + str_format(m_aErrorDetail, sizeof(m_aErrorDetail), "(%s:mysql:%d): %s", pContext, mysql_errno(&m_Mysql), mysql_error(&m_Mysql)); +} + +void CMysqlConnection::StoreErrorStmt(const char *pContext) +{ + str_format(m_aErrorDetail, sizeof(m_aErrorDetail), "(%s:stmt:%d): %s", pContext, mysql_stmt_errno(m_pStmt.get()), mysql_stmt_error(m_pStmt.get())); +} + +bool CMysqlConnection::PrepareAndExecuteStatement(const char *pStmt) +{ + if(mysql_stmt_prepare(m_pStmt.get(), pStmt, str_length(pStmt))) + { + StoreErrorStmt("prepare"); + return true; + } + if(mysql_stmt_execute(m_pStmt.get())) + { + StoreErrorStmt("execute"); + return true; + } + return false; } void CMysqlConnection::Print(IConsole *pConsole, const char *Mode) @@ -63,119 +219,109 @@ void CMysqlConnection::ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsig IDbConnection::Status CMysqlConnection::Connect() { -#if defined(CONF_SQL) if(m_InUse.exchange(true)) return Status::IN_USE; m_NewQuery = true; - if(m_pConnection != nullptr) + if(ConnectImpl()) { - try - { - // Connect to specific database - m_pConnection->setSchema(m_aDatabase); - return Status::SUCCESS; - } - catch(sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - } - catch(const std::exception &ex) - { - dbg_msg("sql", "MySQL Error: %s", ex.what()); - } - catch(const std::string &ex) - { - dbg_msg("sql", "MySQL Error: %s", ex.c_str()); - } - catch(...) - { - dbg_msg("sql", "Unknown Error cause by the MySQL/C++ Connector"); - } + dbg_msg("mysql", "connect error %s", m_aErrorDetail); + m_InUse.store(false); + return Status::FAILURE; + } + return Status::SUCCESS; +} - dbg_msg("sql", "FAILURE: SQL connection failed, trying to reconnect"); +bool CMysqlConnection::ConnectImpl() +{ + if(m_HaveConnection) + { + if(m_pStmt && mysql_stmt_free_result(m_pStmt.get())) + { + StoreErrorStmt("free_result"); + dbg_msg("mysql", "can't free last result %s", m_aErrorDetail); + } + if(!mysql_select_db(&m_Mysql, m_aDatabase)) + { + // Success. + return false; + } + StoreErrorMysql("select_db"); + dbg_msg("mysql", "ping error, trying to reconnect %s", m_aErrorDetail); + mysql_close(&m_Mysql); + mem_zero(&m_Mysql, sizeof(m_Mysql)); + mysql_init(&m_Mysql); } - try + m_pStmt = nullptr; + unsigned int OptConnectTimeout = 60; + unsigned int OptReadTimeout = 60; + unsigned int OptWriteTimeout = 120; + my_bool OptReconnect = true; + mysql_options(&m_Mysql, MYSQL_OPT_CONNECT_TIMEOUT, &OptConnectTimeout); + mysql_options(&m_Mysql, MYSQL_OPT_READ_TIMEOUT, &OptReadTimeout); + mysql_options(&m_Mysql, MYSQL_OPT_WRITE_TIMEOUT, &OptWriteTimeout); + mysql_options(&m_Mysql, MYSQL_OPT_RECONNECT, &OptReconnect); + mysql_options(&m_Mysql, MYSQL_SET_CHARSET_NAME, "utf8mb4"); + + if(!mysql_real_connect(&m_Mysql, m_aIp, m_aUser, m_aPass, nullptr, m_Port, nullptr, CLIENT_IGNORE_SIGPIPE)) { - m_pConnection.reset(); - m_pPreparedStmt.reset(); - m_pResults.reset(); + StoreErrorMysql("real_connect"); + return true; + } + m_HaveConnection = true; - sql::ConnectOptionsMap connection_properties; - connection_properties["hostName"] = sql::SQLString(m_aIp); - connection_properties["port"] = m_Port; - connection_properties["userName"] = sql::SQLString(m_aUser); - connection_properties["password"] = sql::SQLString(m_aPass); - connection_properties["OPT_CONNECT_TIMEOUT"] = 60; - connection_properties["OPT_READ_TIMEOUT"] = 60; - connection_properties["OPT_WRITE_TIMEOUT"] = 120; - connection_properties["OPT_RECONNECT"] = true; - connection_properties["OPT_CHARSET_NAME"] = sql::SQLString("utf8mb4"); - connection_properties["OPT_SET_CHARSET_NAME"] = sql::SQLString("utf8mb4"); + m_pStmt = std::unique_ptr(mysql_stmt_init(&m_Mysql)); - // Create connection + // Apparently MYSQL_SET_CHARSET_NAME is not enough + if(PrepareAndExecuteStatement("SET CHARACTER SET utf8mb4")) + { + return true; + } + + if(m_Setup) + { + char aCreateDatabase[1024]; + // create database + str_format(aCreateDatabase, sizeof(aCreateDatabase), "CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4", m_aDatabase); + if(PrepareAndExecuteStatement(aCreateDatabase)) { - CScopeLock GlobalLockScope(&m_SqlDriverLock); - sql::Driver *pDriver = get_driver_instance(); - m_pConnection.reset(pDriver->connect(connection_properties)); + return true; } + } - // Create Statement - m_pStmt = std::unique_ptr(m_pConnection->createStatement()); + // Connect to specific database + if(mysql_select_db(&m_Mysql, m_aDatabase)) + { + StoreErrorMysql("select_db"); + return true; + } - // Apparently OPT_CHARSET_NAME and OPT_SET_CHARSET_NAME are not enough - m_pStmt->execute("SET CHARACTER SET utf8mb4;"); + if(m_Setup) + { + char aCreateRace[1024]; + char aCreateTeamrace[1024]; + char aCreateMaps[1024]; + char aCreateSaves[1024]; + char aCreatePoints[1024]; + FormatCreateRace(aCreateRace, sizeof(aCreateRace)); + FormatCreateTeamrace(aCreateTeamrace, sizeof(aCreateTeamrace), "VARBINARY(16)"); + FormatCreateMaps(aCreateMaps, sizeof(aCreateMaps)); + FormatCreateSaves(aCreateSaves, sizeof(aCreateSaves)); + FormatCreatePoints(aCreatePoints, sizeof(aCreatePoints)); - if(m_Setup) + if(PrepareAndExecuteStatement(aCreateRace) || + PrepareAndExecuteStatement(aCreateTeamrace) || + PrepareAndExecuteStatement(aCreateMaps) || + PrepareAndExecuteStatement(aCreateSaves) || + PrepareAndExecuteStatement(aCreatePoints)) { - char aBuf[1024]; - // create database - str_format(aBuf, sizeof(aBuf), "CREATE DATABASE IF NOT EXISTS %s CHARACTER SET utf8mb4", m_aDatabase); - m_pStmt->execute(aBuf); - // Connect to specific database - m_pConnection->setSchema(m_aDatabase); - FormatCreateRace(aBuf, sizeof(aBuf)); - m_pStmt->execute(aBuf); - FormatCreateTeamrace(aBuf, sizeof(aBuf), "VARBINARY(16)"); - m_pStmt->execute(aBuf); - FormatCreateMaps(aBuf, sizeof(aBuf)); - m_pStmt->execute(aBuf); - FormatCreateSaves(aBuf, sizeof(aBuf)); - m_pStmt->execute(aBuf); - FormatCreatePoints(aBuf, sizeof(aBuf)); - m_pStmt->execute(aBuf); - m_Setup = false; + return true; } - else - { - // Connect to specific database - m_pConnection->setSchema(m_aDatabase); - } - dbg_msg("sql", "sql connection established"); - return Status::SUCCESS; + m_Setup = false; } - catch(sql::SQLException &e) - { - dbg_msg("sql", "MySQL Error: %s", e.what()); - } - catch(const std::exception &ex) - { - dbg_msg("sql", "MySQL Error: %s", ex.what()); - } - catch(const std::string &ex) - { - dbg_msg("sql", "MySQL Error: %s", ex.c_str()); - } - catch(...) - { - dbg_msg("sql", "Unknown Error cause by the MySQL/C++ Connector"); - } - m_InUse.store(false); - -#endif - dbg_msg("sql", "FAILURE: sql connection failed"); - return Status::FAILURE; + dbg_msg("mysql", "connection established"); + return false; } void CMysqlConnection::Disconnect() @@ -185,118 +331,303 @@ void CMysqlConnection::Disconnect() void CMysqlConnection::PrepareStatement(const char *pStmt) { -#if defined(CONF_SQL) - m_pPreparedStmt.reset(m_pConnection->prepareStatement(pStmt)); + if(mysql_stmt_prepare(m_pStmt.get(), pStmt, str_length(pStmt))) + { + StoreErrorStmt("prepare"); + dbg_msg("mysql", "error preparing statement %s", m_aErrorDetail); + throw std::runtime_error(m_aErrorDetail); + // TODO: return true; + } m_NewQuery = true; -#endif + unsigned NumParameters = mysql_stmt_param_count(m_pStmt.get()); + m_aStmtParameters.resize(NumParameters); + m_aStmtParameterExtras.resize(NumParameters); + mem_zero(&m_aStmtParameters[0], sizeof(m_aStmtParameters[0]) * m_aStmtParameters.size()); + mem_zero(&m_aStmtParameterExtras[0], sizeof(m_aStmtParameterExtras[0]) * m_aStmtParameterExtras.size()); + // TODO: return false; } void CMysqlConnection::BindString(int Idx, const char *pString) { -#if defined(CONF_SQL) - m_pPreparedStmt->setString(Idx, pString); m_NewQuery = true; -#endif + Idx -= 1; + dbg_assert(0 <= Idx && Idx < (int)m_aStmtParameters.size(), "index out of bounds"); + + int Length = str_length(pString); + m_aStmtParameterExtras[Idx].ul = Length; + MYSQL_BIND *pParam = &m_aStmtParameters[Idx]; + pParam->buffer_type = MYSQL_TYPE_STRING; + pParam->buffer = (void *)pString; + pParam->buffer_length = Length + 1; + pParam->length = &m_aStmtParameterExtras[Idx].ul; + pParam->is_null = nullptr; + pParam->is_unsigned = false; + pParam->error = nullptr; } void CMysqlConnection::BindBlob(int Idx, unsigned char *pBlob, int Size) { -#if defined(CONF_SQL) - // copy blob into string - auto Blob = std::string(pBlob, pBlob + Size); - m_pPreparedStmt->setString(Idx, Blob); m_NewQuery = true; -#endif + Idx -= 1; + dbg_assert(0 <= Idx && Idx < (int)m_aStmtParameters.size(), "index out of bounds"); + + m_aStmtParameterExtras[Idx].ul = Size; + MYSQL_BIND *pParam = &m_aStmtParameters[Idx]; + pParam->buffer_type = MYSQL_TYPE_BLOB; + pParam->buffer = pBlob; + pParam->buffer_length = Size; + pParam->length = &m_aStmtParameterExtras[Idx].ul; + pParam->is_null = nullptr; + pParam->is_unsigned = false; + pParam->error = nullptr; } void CMysqlConnection::BindInt(int Idx, int Value) { -#if defined(CONF_SQL) - m_pPreparedStmt->setInt(Idx, Value); m_NewQuery = true; -#endif + Idx -= 1; + dbg_assert(0 <= Idx && Idx < (int)m_aStmtParameters.size(), "index out of bounds"); + + m_aStmtParameterExtras[Idx].i = Value; + MYSQL_BIND *pParam = &m_aStmtParameters[Idx]; + pParam->buffer_type = MYSQL_TYPE_LONG; + pParam->buffer = &m_aStmtParameterExtras[Idx].i; + pParam->buffer_length = sizeof(m_aStmtParameterExtras[Idx].i); + pParam->length = nullptr; + pParam->is_null = nullptr; + pParam->is_unsigned = false; + pParam->error = nullptr; } void CMysqlConnection::BindFloat(int Idx, float Value) { -#if defined(CONF_SQL) - m_pPreparedStmt->setDouble(Idx, (double)Value); m_NewQuery = true; -#endif + Idx -= 1; + dbg_assert(0 <= Idx && Idx < (int)m_aStmtParameters.size(), "index out of bounds"); + + m_aStmtParameterExtras[Idx].f = Value; + MYSQL_BIND *pParam = &m_aStmtParameters[Idx]; + pParam->buffer_type = MYSQL_TYPE_FLOAT; + pParam->buffer = &m_aStmtParameterExtras[Idx].f; + pParam->buffer_length = sizeof(m_aStmtParameterExtras[Idx].i); + pParam->length = nullptr; + pParam->is_null = nullptr; + pParam->is_unsigned = false; + pParam->error = nullptr; } bool CMysqlConnection::Step() { -#if defined(CONF_SQL) if(m_NewQuery) { m_NewQuery = false; - m_pResults.reset(m_pPreparedStmt->executeQuery()); + if(mysql_stmt_bind_param(m_pStmt.get(), &m_aStmtParameters[0])) + { + StoreErrorStmt("bind_param"); + dbg_msg("mysql", "error binding params %s", m_aErrorDetail); + throw std::runtime_error(m_aErrorDetail); + // TODO: error handling + } + if(mysql_stmt_execute(m_pStmt.get())) + { + StoreErrorStmt("execute"); + dbg_msg("mysql", "error executing query %s", m_aErrorDetail); + throw std::runtime_error(m_aErrorDetail); + // TODO: error handling + return false; + } } - return m_pResults->next(); -#else - return false; -#endif + int Result = mysql_stmt_fetch(m_pStmt.get()); + if(Result == 1) + { + StoreErrorStmt("fetch"); + dbg_msg("mysql", "error fetching row %s", m_aErrorDetail); + throw std::runtime_error(m_aErrorDetail); + // TODO: error handling + return false; + } + if(Result == MYSQL_NO_DATA) + { + return false; + } + // `Result` is now either `MYSQL_DATA_TRUNCATED` (which we ignore, we + // fetch our columns in a different way) or `0` aka success. + return true; } int CMysqlConnection::ExecuteUpdate() { -#if defined(CONF_SQL) if(m_NewQuery) { m_NewQuery = false; - return m_pPreparedStmt->executeUpdate(); + if(mysql_stmt_bind_param(m_pStmt.get(), &m_aStmtParameters[0])) + { + StoreErrorStmt("bind_param"); + dbg_msg("mysql", "error binding params %s", m_aErrorDetail); + throw std::runtime_error(m_aErrorDetail); + // TODO: error handling + return -1; + } + if(mysql_stmt_execute(m_pStmt.get())) + { + StoreErrorStmt("execute"); + dbg_msg("mysql", "error executing update %s", m_aErrorDetail); + throw std::runtime_error(m_aErrorDetail); + // TODO: error handling + return -1; + } + return mysql_stmt_affected_rows(m_pStmt.get()); } -#endif return -1; } -bool CMysqlConnection::IsNull(int Col) const +bool CMysqlConnection::IsNull(int Col) { -#if defined(CONF_SQL) - return m_pResults->isNull(Col); -#else - return false; -#endif + Col -= 1; + + MYSQL_BIND Bind; + my_bool IsNull; + mem_zero(&Bind, sizeof(Bind)); + Bind.buffer_type = MYSQL_TYPE_NULL; + Bind.buffer = nullptr; + Bind.buffer_length = 0; + Bind.length = nullptr; + Bind.is_null = &IsNull; + Bind.is_unsigned = false; + Bind.error = nullptr; + if(mysql_stmt_fetch_column(m_pStmt.get(), &Bind, Col, 0)) + { + StoreErrorStmt("fetch_column:null"); + dbg_msg("mysql", "error fetching column %s", m_aErrorDetail); + dbg_assert(0, "error in IsNull"); + } + return IsNull; } -float CMysqlConnection::GetFloat(int Col) const +float CMysqlConnection::GetFloat(int Col) { -#if defined(CONF_SQL) - return (float)m_pResults->getDouble(Col); -#else - return 0.0; -#endif + Col -= 1; + + MYSQL_BIND Bind; + float Value; + my_bool IsNull; + mem_zero(&Bind, sizeof(Bind)); + Bind.buffer_type = MYSQL_TYPE_FLOAT; + Bind.buffer = &Value; + Bind.buffer_length = sizeof(Value); + Bind.length = nullptr; + Bind.is_null = &IsNull; + Bind.is_unsigned = false; + Bind.error = nullptr; + if(mysql_stmt_fetch_column(m_pStmt.get(), &Bind, Col, 0)) + { + StoreErrorStmt("fetch_column:float"); + dbg_msg("mysql", "error fetching column %s", m_aErrorDetail); + dbg_assert(0, "error in GetFloat"); + } + if(IsNull) + { + dbg_assert(0, "error getting float: NULL"); + } + return Value; } -int CMysqlConnection::GetInt(int Col) const +int CMysqlConnection::GetInt(int Col) { -#if defined(CONF_SQL) - return m_pResults->getInt(Col); -#else - return 0; -#endif + Col -= 1; + + MYSQL_BIND Bind; + int Value; + my_bool IsNull; + mem_zero(&Bind, sizeof(Bind)); + Bind.buffer_type = MYSQL_TYPE_LONG; + Bind.buffer = &Value; + Bind.buffer_length = sizeof(Value); + Bind.length = nullptr; + Bind.is_null = &IsNull; + Bind.is_unsigned = false; + Bind.error = nullptr; + if(mysql_stmt_fetch_column(m_pStmt.get(), &Bind, Col, 0)) + { + StoreErrorStmt("fetch_column:int"); + dbg_msg("mysql", "error fetching column %s", m_aErrorDetail); + dbg_assert(0, "error in GetInt"); + } + if(IsNull) + { + dbg_assert(0, "error getting int: NULL"); + } + return Value; } -void CMysqlConnection::GetString(int Col, char *pBuffer, int BufferSize) const +void CMysqlConnection::GetString(int Col, char *pBuffer, int BufferSize) { -#if defined(CONF_SQL) - auto String = m_pResults->getString(Col); - str_copy(pBuffer, String.c_str(), BufferSize); -#endif + Col -= 1; + + for(int i = 0; i < BufferSize; i++) + { + pBuffer[i] = 'a'; + } + + MYSQL_BIND Bind; + unsigned long Length; + my_bool IsNull; + my_bool Error; + mem_zero(&Bind, sizeof(Bind)); + Bind.buffer_type = MYSQL_TYPE_STRING; + Bind.buffer = pBuffer; + Bind.buffer_length = BufferSize; + Bind.length = &Length; + Bind.is_null = &IsNull; + Bind.is_unsigned = false; + Bind.error = &Error; + if(mysql_stmt_fetch_column(m_pStmt.get(), &Bind, Col, 0)) + { + StoreErrorStmt("fetch_column:string"); + dbg_msg("mysql", "error fetching column %s", m_aErrorDetail); + dbg_assert(0, "error in GetString"); + } + if(IsNull) + { + dbg_assert(0, "error getting string: NULL"); + } + if(Error) + { + dbg_assert(0, "error getting string: truncation occured"); + } } -int CMysqlConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const +int CMysqlConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize) { -#if defined(CONF_SQL) - auto *Blob = m_pResults->getBlob(Col); - Blob->read((char *)pBuffer, BufferSize); - int NumRead = Blob->gcount(); - delete Blob; - return NumRead; -#else - return 0; -#endif + Col -= 1; + + MYSQL_BIND Bind; + unsigned long Length; + my_bool IsNull; + my_bool Error; + mem_zero(&Bind, sizeof(Bind)); + Bind.buffer_type = MYSQL_TYPE_BLOB; + Bind.buffer = pBuffer; + Bind.buffer_length = BufferSize; + Bind.length = &Length; + Bind.is_null = &IsNull; + Bind.is_unsigned = false; + Bind.error = &Error; + if(mysql_stmt_fetch_column(m_pStmt.get(), &Bind, Col, 0)) + { + StoreErrorStmt("fetch_column:blob"); + dbg_msg("mysql", "error fetching column %s", m_aErrorDetail); + dbg_assert(0, "error in GetBlob"); + } + if(IsNull) + { + dbg_assert(0, "error getting blob: NULL"); + } + if(Error) + { + dbg_assert(0, "error getting blob: truncation occured"); + } + return Length; } const char *CMysqlConnection::MedianMapTime(char *pBuffer, int BufferSize) const @@ -317,7 +648,7 @@ void CMysqlConnection::AddPoints(const char *pPlayer, int Points) str_format(aBuf, sizeof(aBuf), "INSERT INTO %s_points(Name, Points) " "VALUES (?, ?) " - "ON DUPLICATE KEY UPDATE Points=Points+?;", + "ON DUPLICATE KEY UPDATE Points=Points+?", GetPrefix()); PrepareStatement(aBuf); BindString(1, pPlayer); @@ -325,3 +656,35 @@ void CMysqlConnection::AddPoints(const char *pPlayer, int Points) BindInt(3, Points); Step(); } + +IDbConnection *CreateMysqlConnection( + const char *pDatabase, + const char *pPrefix, + const char *pUser, + const char *pPass, + const char *pIp, + int Port, + bool Setup) +{ + return new CMysqlConnection(pDatabase, pPrefix, pUser, pPass, pIp, Port, Setup); +} +#else +int MysqlInit() +{ + return 0; +} +void MysqlUninit() +{ +} +IDbConnection *CreateMysqlConnection( + const char *pDatabase, + const char *pPrefix, + const char *pUser, + const char *pPass, + const char *pIp, + int Port, + bool Setup) +{ + return nullptr; +} +#endif diff --git a/src/engine/server/databases/mysql.h b/src/engine/server/databases/mysql.h deleted file mode 100644 index 8e179ade3..000000000 --- a/src/engine/server/databases/mysql.h +++ /dev/null @@ -1,83 +0,0 @@ -#ifndef ENGINE_SERVER_DATABASES_MYSQL_H -#define ENGINE_SERVER_DATABASES_MYSQL_H - -#include -#include -#include - -class CLock; -namespace sql { -class Connection; -class PreparedStatement; -class ResultSet; -class Statement; -} /* namespace sql */ - -class CMysqlConnection : public IDbConnection -{ -public: - CMysqlConnection( - const char *pDatabase, - const char *pPrefix, - const char *pUser, - const char *pPass, - const char *pIp, - int Port, - bool Setup); - virtual ~CMysqlConnection(); - virtual void Print(IConsole *pConsole, const char *Mode); - - virtual CMysqlConnection *Copy(); - - virtual const char *BinaryCollate() const { return "utf8mb4_bin"; } - virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize); - virtual const char *InsertTimestampAsUtc() const { return "?"; } - virtual const char *CollateNocase() const { return "CONVERT(? USING utf8mb4) COLLATE utf8mb4_general_ci"; } - virtual const char *InsertIgnore() const { return "INSERT IGNORE"; }; - virtual const char *Random() const { return "RAND()"; }; - virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const; - - virtual Status Connect(); - virtual void Disconnect(); - - virtual void PrepareStatement(const char *pStmt); - - virtual void BindString(int Idx, const char *pString); - virtual void BindBlob(int Idx, unsigned char *pBlob, int Size); - virtual void BindInt(int Idx, int Value); - virtual void BindFloat(int Idx, float Value); - - virtual void Print() {} - virtual bool Step(); - virtual int ExecuteUpdate(); - - virtual bool IsNull(int Col) const; - virtual float GetFloat(int Col) const; - virtual int GetInt(int Col) const; - virtual void GetString(int Col, char *pBuffer, int BufferSize) const; - virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const; - - virtual void AddPoints(const char *pPlayer, int Points); - -private: -#if defined(CONF_SQL) - std::unique_ptr m_pConnection; - std::unique_ptr m_pPreparedStmt; - std::unique_ptr m_pStmt; - std::unique_ptr m_pResults; - bool m_NewQuery; -#endif - - // copy of config vars - char m_aDatabase[64]; - char m_aUser[64]; - char m_aPass[64]; - char m_aIp[64]; - int m_Port; - bool m_Setup; - - std::atomic_bool m_InUse; - static CLock m_SqlDriverLock; -}; - -#endif // ENGINE_SERVER_DATABASES_MYSQL_H diff --git a/src/engine/server/databases/sqlite.cpp b/src/engine/server/databases/sqlite.cpp index 6e5d6d292..c646dfbe3 100644 --- a/src/engine/server/databases/sqlite.cpp +++ b/src/engine/server/databases/sqlite.cpp @@ -1,11 +1,69 @@ -#include "sqlite.h" +#include "connection.h" + +#include #include #include -#include +#include #include +class CSqliteConnection : public IDbConnection +{ +public: + CSqliteConnection(const char *pFilename, bool Setup); + virtual ~CSqliteConnection(); + virtual void Print(IConsole *pConsole, const char *Mode); + + virtual CSqliteConnection *Copy(); + + virtual const char *BinaryCollate() const { return "BINARY"; } + virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize); + virtual const char *InsertTimestampAsUtc() const { return "DATETIME(?, 'utc')"; } + virtual const char *CollateNocase() const { return "? COLLATE NOCASE"; } + virtual const char *InsertIgnore() const { return "INSERT OR IGNORE"; }; + virtual const char *Random() const { return "RANDOM()"; }; + virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const; + + virtual Status Connect(); + virtual void Disconnect(); + + virtual void PrepareStatement(const char *pStmt); + + virtual void BindString(int Idx, const char *pString); + virtual void BindBlob(int Idx, unsigned char *pBlob, int Size); + virtual void BindInt(int Idx, int Value); + virtual void BindFloat(int Idx, float Value); + + virtual void Print(); + virtual bool Step(); + virtual int ExecuteUpdate(); + + virtual bool IsNull(int Col); + virtual float GetFloat(int Col); + virtual int GetInt(int Col); + virtual void GetString(int Col, char *pBuffer, int BufferSize); + // passing a negative buffer size is undefined behavior + virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize); + + virtual void AddPoints(const char *pPlayer, int Points); + +private: + // copy of config vars + char m_aFilename[512]; + bool m_Setup; + + sqlite3 *m_pDb; + sqlite3_stmt *m_pStmt; + bool m_Done; // no more rows available for Step + // returns true, if the query succeeded + bool Execute(const char *pQuery); + + void ExceptionOnError(int Result); + + std::atomic_bool m_InUse; +}; + CSqliteConnection::CSqliteConnection(const char *pFilename, bool Setup) : IDbConnection("record"), m_Setup(Setup), @@ -178,27 +236,27 @@ int CSqliteConnection::ExecuteUpdate() return sqlite3_changes(m_pDb); } -bool CSqliteConnection::IsNull(int Col) const +bool CSqliteConnection::IsNull(int Col) { return sqlite3_column_type(m_pStmt, Col - 1) == SQLITE_NULL; } -float CSqliteConnection::GetFloat(int Col) const +float CSqliteConnection::GetFloat(int Col) { return (float)sqlite3_column_double(m_pStmt, Col - 1); } -int CSqliteConnection::GetInt(int Col) const +int CSqliteConnection::GetInt(int Col) { return sqlite3_column_int(m_pStmt, Col - 1); } -void CSqliteConnection::GetString(int Col, char *pBuffer, int BufferSize) const +void CSqliteConnection::GetString(int Col, char *pBuffer, int BufferSize) { str_copy(pBuffer, (const char *)sqlite3_column_text(m_pStmt, Col - 1), BufferSize); } -int CSqliteConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const +int CSqliteConnection::GetBlob(int Col, unsigned char *pBuffer, int BufferSize) { int Size = sqlite3_column_bytes(m_pStmt, Col - 1); Size = minimum(Size, BufferSize); @@ -257,3 +315,8 @@ void CSqliteConnection::AddPoints(const char *pPlayer, int Points) BindInt(3, Points); Step(); } + +IDbConnection *CreateSqliteConnection(const char *pFilename, bool Setup) +{ + return new CSqliteConnection(pFilename, Setup); +} diff --git a/src/engine/server/databases/sqlite.h b/src/engine/server/databases/sqlite.h deleted file mode 100644 index fc43f1c30..000000000 --- a/src/engine/server/databases/sqlite.h +++ /dev/null @@ -1,66 +0,0 @@ -#ifndef ENGINE_SERVER_DATABASES_SQLITE_H -#define ENGINE_SERVER_DATABASES_SQLITE_H - -#include "connection.h" -#include - -struct sqlite3; -struct sqlite3_stmt; - -class CSqliteConnection : public IDbConnection -{ -public: - CSqliteConnection(const char *pFilename, bool Setup); - virtual ~CSqliteConnection(); - virtual void Print(IConsole *pConsole, const char *Mode); - - virtual CSqliteConnection *Copy(); - - virtual const char *BinaryCollate() const { return "BINARY"; } - virtual void ToUnixTimestamp(const char *pTimestamp, char *aBuf, unsigned int BufferSize); - virtual const char *InsertTimestampAsUtc() const { return "DATETIME(?, 'utc')"; } - virtual const char *CollateNocase() const { return "? COLLATE NOCASE"; } - virtual const char *InsertIgnore() const { return "INSERT OR IGNORE"; }; - virtual const char *Random() const { return "RANDOM()"; }; - virtual const char *MedianMapTime(char *pBuffer, int BufferSize) const; - - virtual Status Connect(); - virtual void Disconnect(); - - virtual void PrepareStatement(const char *pStmt); - - virtual void BindString(int Idx, const char *pString); - virtual void BindBlob(int Idx, unsigned char *pBlob, int Size); - virtual void BindInt(int Idx, int Value); - virtual void BindFloat(int Idx, float Value); - - virtual void Print(); - virtual bool Step(); - virtual int ExecuteUpdate(); - - virtual bool IsNull(int Col) const; - virtual float GetFloat(int Col) const; - virtual int GetInt(int Col) const; - virtual void GetString(int Col, char *pBuffer, int BufferSize) const; - // passing a negative buffer size is undefined behavior - virtual int GetBlob(int Col, unsigned char *pBuffer, int BufferSize) const; - - virtual void AddPoints(const char *pPlayer, int Points); - -private: - // copy of config vars - char m_aFilename[512]; - bool m_Setup; - - sqlite3 *m_pDb; - sqlite3_stmt *m_pStmt; - bool m_Done; // no more rows available for Step - // returns true, if the query succeeded - bool Execute(const char *pQuery); - - void ExceptionOnError(int Result); - - std::atomic_bool m_InUse; -}; - -#endif // ENGINE_SERVER_DATABASES_SQLITE_H diff --git a/src/engine/server/server.cpp b/src/engine/server/server.cpp index 65399a793..0c96fc0f3 100644 --- a/src/engine/server/server.cpp +++ b/src/engine/server/server.cpp @@ -3,6 +3,8 @@ #define _WIN32_WINNT 0x0501 +#include "server.h" + #include #include @@ -35,18 +37,15 @@ #include #include +#include "databases/connection.h" +#include "databases/connection_pool.h" #include "register.h" -#include "server.h" #if defined(CONF_FAMILY_WINDOWS) #define WIN32_LEAN_AND_MEAN #include #endif -#include -#include -#include - CSnapIDPool::CSnapIDPool() { Reset(); @@ -2358,7 +2357,7 @@ int CServer::Run() if(g_Config.m_SvSqliteFile[0] != '\0') { - auto pSqlServers = std::unique_ptr(new CSqliteConnection( + auto pSqlServers = std::unique_ptr(CreateSqliteConnection( g_Config.m_SvSqliteFile, true)); if(g_Config.m_SvUseSQL) @@ -2367,7 +2366,7 @@ int CServer::Run() } else { - auto pCopy = std::unique_ptr(pSqlServers->Copy()); + auto pCopy = std::unique_ptr(pSqlServers->Copy()); DbPool()->RegisterDatabase(std::move(pSqlServers), CDbConnectionPool::READ); DbPool()->RegisterDatabase(std::move(pCopy), CDbConnectionPool::WRITE); } @@ -3166,14 +3165,20 @@ void CServer::ConAddSqlServer(IConsole::IResult *pResult, void *pUserData) bool SetUpDb = pResult->NumArguments() == 8 ? pResult->GetInteger(7) : true; - auto pSqlServers = std::unique_ptr(new CMysqlConnection( + auto pSqlServers = std::unique_ptr(CreateMysqlConnection( pResult->GetString(1), pResult->GetString(2), pResult->GetString(3), pResult->GetString(4), pResult->GetString(5), pResult->GetInteger(6), SetUpDb)); + if(!pSqlServers) + { + pSelf->Console()->Print(IConsole::OUTPUT_LEVEL_STANDARD, "server", "can't add MySQL server: compiled without MySQL support"); + return; + } + char aBuf[512]; str_format(aBuf, sizeof(aBuf), - "Added new Sql%sServer: DB: '%s' Prefix: '%s' User: '%s' IP: <{'%s'}> Port: %d", + "Added new Sql%sServer: DB: '%s' Prefix: '%s' User: '%s' IP: <{%s}> Port: %d", ReadOnly ? "Read" : "Write", pResult->GetString(1), pResult->GetString(2), pResult->GetString(3), pResult->GetString(5), pResult->GetInteger(6)); @@ -3483,6 +3488,11 @@ int main(int argc, const char **argv) // ignore_convention dbg_msg("secure", "could not initialize secure RNG"); return -1; } + if(MysqlInit() != 0) + { + dbg_msg("mysql", "failed to initialize MySQL library"); + return -1; + } CServer *pServer = CreateServer(); IKernel *pKernel = IKernel::Create(); @@ -3556,6 +3566,8 @@ int main(int argc, const char **argv) // ignore_convention dbg_msg("server", "starting..."); int Ret = pServer->Run(); + MysqlUninit(); + // free delete pKernel;