#ifdef _WINDOWS #include #endif #include "misc_functions.h" #include "eqemu_logsys.h" #include "timer.h" #include "dbcore.h" #include #include #include #include #include "strings.h" #ifdef _WINDOWS #define snprintf _snprintf #define strncasecmp _strnicmp #define strcasecmp _stricmp #include #else #include "unix.h" #include #endif #ifdef _EQDEBUG #define DEBUG_MYSQL_QUERIES 0 #else #define DEBUG_MYSQL_QUERIES 0 #endif DBcore::DBcore() { mysql = mysql_init(nullptr); mysqlOwner = true; pHost = nullptr; pUser = nullptr; pPassword = nullptr; pDatabase = nullptr; pCompress = false; pSSL = false; pStatus = Closed; m_mutex = new Mutex; } DBcore::~DBcore() { /** * This prevents us from doing a double free in multi-tenancy setups where we * are re-using the default database connection pointer when we dont have an * external configuration setup ex: (content_database) */ if (mysqlOwner) { mysql_close(mysql); } safe_delete_array(pHost); safe_delete_array(pUser); safe_delete_array(pPassword); safe_delete_array(pDatabase); } // Sends the MySQL server a keepalive void DBcore::ping() { if (!m_mutex->trylock()) { // well, if's it's locked, someone's using it. If someone's using it, it doesnt need a keepalive return; } mysql_ping(mysql); m_mutex->unlock(); } MySQLRequestResult DBcore::QueryDatabase(const std::string& query, bool retryOnFailureOnce) { auto r = QueryDatabase(query.c_str(), query.length(), retryOnFailureOnce); return r; } bool DBcore::DoesTableExist(const std::string& table_name) { auto results = QueryDatabase(fmt::format("SHOW TABLES LIKE '{}'", table_name)); return results.RowCount() > 0; } MySQLRequestResult DBcore::QueryDatabase(const char *query, uint32 querylen, bool retryOnFailureOnce) { BenchTimer timer; timer.reset(); LockMutex lock(m_mutex); // Reconnect if we are not connected before hand. if (pStatus != Connected) { Open(); } // request query. != 0 indicates some kind of error. if (mysql_real_query(mysql, query, querylen) != 0) { unsigned int errorNumber = mysql_errno(mysql); if (errorNumber == CR_SERVER_GONE_ERROR) { pStatus = Error; } // error appears to be a disconnect error, may need to try again. if (errorNumber == CR_SERVER_LOST || errorNumber == CR_SERVER_GONE_ERROR) { if (retryOnFailureOnce) { LogInfo("Database Error: Lost connection, attempting to recover"); MySQLRequestResult requestResult = QueryDatabase(query, querylen, false); if (requestResult.Success()) { LogInfo("Reconnection to database successful"); return requestResult; } } pStatus = Error; auto errorBuffer = new char[MYSQL_ERRMSG_SIZE]; snprintf(errorBuffer, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(mysql), mysql_error(mysql)); return MySQLRequestResult(nullptr, 0, 0, 0, 0, (uint32) mysql_errno(mysql), errorBuffer); } auto errorBuffer = new char[MYSQL_ERRMSG_SIZE]; snprintf(errorBuffer, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(mysql), mysql_error(mysql)); /** * Error logging */ if (mysql_errno(mysql) > 0 && query[0] != '\0') { LogMySQLError("MySQL Error ({}) [{}] Query [{}]", mysql_errno(mysql), mysql_error(mysql), query); } return MySQLRequestResult(nullptr, 0, 0, 0, 0, mysql_errno(mysql), errorBuffer); } // successful query. get results. MYSQL_RES *res = mysql_store_result(mysql); uint32 rowCount = 0; if (res != nullptr) { rowCount = (uint32) mysql_num_rows(res); } MySQLRequestResult requestResult( res, (uint32) mysql_affected_rows(mysql), rowCount, (uint32) mysql_field_count(mysql), (uint32) mysql_insert_id(mysql) ); if (LogSys.log_settings[Logs::MySQLQuery].is_category_enabled == 1) { if ((strncasecmp(query, "select", 6) == 0)) { LogMySQLQuery( "{0} -- ({1} row{2} returned) ({3}s)", query, requestResult.RowCount(), requestResult.RowCount() == 1 ? "" : "s", std::to_string(timer.elapsed()) ); } else { LogMySQLQuery( "{0} -- ({1} row{2} affected) ({3}s)", query, requestResult.RowsAffected(), requestResult.RowsAffected() == 1 ? "" : "s", std::to_string(timer.elapsed()) ); } } return requestResult; } void DBcore::TransactionBegin() { QueryDatabase("START TRANSACTION"); } void DBcore::TransactionCommit() { QueryDatabase("COMMIT"); } void DBcore::TransactionRollback() { QueryDatabase("ROLLBACK"); } uint32 DBcore::DoEscapeString(char *tobuf, const char *frombuf, uint32 fromlen) { // No good reason to lock the DB, we only need it in the first place to check char encoding. // LockMutex lock(&MDatabase); return mysql_real_escape_string(mysql, tobuf, frombuf, fromlen); } bool DBcore::Open( const char *iHost, const char *iUser, const char *iPassword, const char *iDatabase, uint32 iPort, uint32 *errnum, char *errbuf, bool iCompress, bool iSSL ) { LockMutex lock(m_mutex); safe_delete_array(pHost); safe_delete_array(pUser); safe_delete_array(pPassword); safe_delete_array(pDatabase); pHost = strcpy(new char[strlen(iHost) + 1], iHost); pUser = strcpy(new char[strlen(iUser) + 1], iUser); pPassword = strcpy(new char[strlen(iPassword) + 1], iPassword); pDatabase = strcpy(new char[strlen(iDatabase) + 1], iDatabase); pCompress = iCompress; pPort = iPort; pSSL = iSSL; return Open(errnum, errbuf); } bool DBcore::Open(uint32 *errnum, char *errbuf) { if (errbuf) { errbuf[0] = 0; } LockMutex lock(m_mutex); if (GetStatus() == Connected) { return true; } if (GetStatus() == Error) { mysql_close(mysql); mysql_init(mysql); // Initialize structure again } if (!pHost) { return false; } /* Added CLIENT_FOUND_ROWS flag to the connect otherwise DB update calls would say 0 rows affected when the value already equalled what the function was tring to set it to, therefore the function would think it failed */ uint32 flags = CLIENT_FOUND_ROWS; if (pCompress) { flags |= CLIENT_COMPRESS; } if (pSSL) { flags |= CLIENT_SSL; } if (mysql_real_connect(mysql, pHost, pUser, pPassword, pDatabase, pPort, 0, flags)) { pStatus = Connected; std::string connected_origin_host = pHost; SetOriginHost(connected_origin_host); return true; } else { if (errnum) { *errnum = mysql_errno(mysql); } if (errbuf) { snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(mysql), mysql_error(mysql)); } pStatus = Error; return false; } } const std::string &DBcore::GetOriginHost() const { return origin_host; } void DBcore::SetOriginHost(const std::string &origin_host) { DBcore::origin_host = origin_host; } std::string DBcore::Escape(const std::string& s) { const std::size_t s_len = s.length(); std::vector temp((s_len * 2) + 1, '\0'); mysql_real_escape_string(mysql, temp.data(), s.c_str(), s_len); return temp.data(); } void DBcore::SetMutex(Mutex *mutex) { safe_delete(m_mutex); DBcore::m_mutex = mutex; } // executes multiple statements in one query // do not use this in application logic // this was built and maintained for database migrations only MySQLRequestResult DBcore::QueryDatabaseMulti(const std::string &query) { SetMultiStatementsOn(); BenchTimer timer; timer.reset(); LockMutex lock(m_mutex); // Reconnect if we are not connected before hand. if (pStatus != Connected) { Open(); } auto r = MySQLRequestResult{}; int status = mysql_real_query(mysql, query.c_str(), query.length()); // process single result if (status != 0) { unsigned int error_number = mysql_errno(mysql); if (error_number == CR_SERVER_GONE_ERROR) { pStatus = Error; } // error logging if (mysql_errno(mysql) > 0 && query.length() > 0 && mysql_errno(mysql) != 1065) { std::string error_raw = fmt::format("{}", mysql_error(mysql)); std::string mysql_err = Strings::Trim(error_raw); std::string clean_query = Strings::Replace(query, "\n", ""); LogMySQLError("[{}] ({}) query [{}]", mysql_err, mysql_errno(mysql), clean_query); MYSQL_RES *res = mysql_store_result(mysql); uint32 row_count = 0; if (res) { row_count = (uint32) mysql_num_rows(res); } r = MySQLRequestResult( res, (uint32) mysql_affected_rows(mysql), row_count, (uint32) mysql_field_count(mysql), (uint32) mysql_insert_id(mysql) ); std::string error_message = mysql_error(mysql); r.SetErrorMessage(error_message); r.SetErrorNumber(mysql_errno(mysql)); if (res) { mysql_free_result(res); } SetMultiStatementsOff(); return r; } } int index = 0; // there could be a query with a semicolon in the actual data, this is best effort for // logging / display purposes // rare that we see this when this is only used in DDL statements auto pieces = Strings::Split(query, ";"); // process each statement result do { uint32 row_count = 0; MYSQL_RES *res = mysql_store_result(mysql); r = MySQLRequestResult( res, (uint32) mysql_affected_rows(mysql), row_count, (uint32) mysql_field_count(mysql), (uint32) mysql_insert_id(mysql) ); if (pieces.size() >= index) { auto piece = pieces[index]; LogMySQLQuery( "{} -- ({} row{} affected) ({}s)", piece, r.RowsAffected(), r.RowsAffected() == 1 ? "" : "s", std::to_string(timer.elapsed()) ); } if (res) { row_count = (uint32) mysql_num_rows(res); } // more results? -1 = no, >0 = error, 0 = yes (keep looping) if ((status = mysql_next_result(mysql)) > 0) { if (mysql_errno(mysql) > 0) { LogMySQLError("[{}] [{}]", mysql_errno(mysql), mysql_error(mysql)); } mysql_free_result(res); // error logging std::string error_message = mysql_error(mysql); r.SetErrorMessage(error_message); r.SetErrorNumber(mysql_errno(mysql)); SetMultiStatementsOff(); // we handle errors elsewhere return r; } if (res) { mysql_free_result(res); } index++; } while (status == 0); SetMultiStatementsOff(); return r; }