573 lines
16 KiB
C++
573 lines
16 KiB
C++
// Copyright (C) 2007 EQ2EMulator Development Team - GPL v3 License
|
|
|
|
#pragma once
|
|
|
|
#include <iostream>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <cstring>
|
|
#include <cstdarg>
|
|
#include <cerrno>
|
|
#include <mysql.h>
|
|
#include <errmsg.h>
|
|
|
|
#include "database_result.hpp"
|
|
#include "../types.hpp"
|
|
#include "../misc_functions.hpp"
|
|
#include "../log.hpp"
|
|
|
|
using namespace std;
|
|
|
|
// Database configuration file names based on build target
|
|
#ifdef LOGIN
|
|
#define DB_INI_FILE "login_db.ini"
|
|
#elif defined WORLD
|
|
#define DB_INI_FILE "world_db.ini"
|
|
#elif defined PARSER
|
|
#define DB_INI_FILE "parser_db.ini"
|
|
#elif defined PATCHER
|
|
#define DB_INI_FILE "patcher_db.ini"
|
|
#endif
|
|
|
|
// Initial buffer size for query allocation
|
|
#define QUERY_INITIAL_SIZE 512
|
|
|
|
// Core database class providing MySQL connection management and query execution
|
|
class DatabaseCore
|
|
{
|
|
public:
|
|
// Database connection status enumeration
|
|
enum eStatus { Closed, Connected, Error };
|
|
|
|
// Constructor - initializes MySQL connection and member variables
|
|
DatabaseCore()
|
|
{
|
|
mysql_init(&mysql);
|
|
int timeout = 10;
|
|
mysql_options(&mysql, MYSQL_OPT_CONNECT_TIMEOUT, &timeout);
|
|
pHost = nullptr;
|
|
pPort = 0;
|
|
pUser = nullptr;
|
|
pPassword = nullptr;
|
|
pDatabase = nullptr;
|
|
pCompress = false;
|
|
pSSL = false;
|
|
pStatus = Closed;
|
|
MMysql.SetName("DatabaseCore::mysql");
|
|
}
|
|
|
|
// Destructor - closes connection and frees allocated memory
|
|
virtual ~DatabaseCore()
|
|
{
|
|
pStatus = Closed;
|
|
mysql_close(&mysql);
|
|
#if MYSQL_VERSION_ID >= 50003
|
|
mysql_library_end();
|
|
#else
|
|
mysql_server_end();
|
|
#endif
|
|
safe_delete_array(pHost);
|
|
safe_delete_array(pUser);
|
|
safe_delete_array(pPassword);
|
|
safe_delete_array(pDatabase);
|
|
}
|
|
|
|
// Get current database connection status
|
|
eStatus GetStatus() { return pStatus; }
|
|
|
|
// Get MySQL error number from last operation
|
|
unsigned int GetError() { return mysql_errno(&mysql); }
|
|
|
|
// Get MySQL error message from last operation
|
|
const char* GetErrorMsg() { return mysql_error(&mysql); }
|
|
|
|
// Connect to database using configuration file settings
|
|
bool Connect()
|
|
{
|
|
char line[256], *key, *val;
|
|
char host[256], user[64], password[64], database[64], port[64];
|
|
bool found_section = false;
|
|
FILE* f;
|
|
|
|
if ((f = fopen(DB_INI_FILE, "r")) == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unable to read %s\n", DB_INI_FILE);
|
|
return false;
|
|
}
|
|
|
|
memset(host, 0, sizeof(host));
|
|
memset(user, 0, sizeof(user));
|
|
memset(password, 0, sizeof(password));
|
|
memset(database, 0, sizeof(database));
|
|
memset(port, 0, sizeof(port));
|
|
|
|
while (fgets(line, sizeof(line), f) != NULL) {
|
|
if (line[0] == '#' || line[0] == '\n' || line[0] == '\r')
|
|
continue;
|
|
|
|
if (!found_section) {
|
|
if (strncasecmp(line, "[Database]", 10) == 0)
|
|
found_section = true;
|
|
}
|
|
else {
|
|
if ((key = strtok(line, "=")) != NULL) {
|
|
if ((val = strtok(NULL, "\r\n")) != NULL) {
|
|
if (strncasecmp(line, "host", 4) == 0)
|
|
strncpy(host, val, sizeof(host) - 1);
|
|
else if (strncasecmp(line, "user", 4) == 0)
|
|
strncpy(user, val, sizeof(user) - 1);
|
|
else if (strncasecmp(line, "password", 8) == 0)
|
|
strncpy(password, val, sizeof(password) - 1);
|
|
else if (strncasecmp(line, "database", 8) == 0)
|
|
strncpy(database, val, sizeof(database) - 1);
|
|
else if (strncasecmp(line, "port", 4) == 0)
|
|
strncpy(port, val, sizeof(port) - 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fclose(f);
|
|
|
|
if (host[0] == '\0') {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'host' in '%s'\n", DB_INI_FILE);
|
|
return false;
|
|
}
|
|
if (user[0] == '\0') {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'user' in '%s'\n", DB_INI_FILE);
|
|
return false;
|
|
}
|
|
if (password[0] == '\0') {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'password' in '%s'\n", DB_INI_FILE);
|
|
return false;
|
|
}
|
|
if (database[0] == '\0') {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unknown 'database' in '%s'\n", DB_INI_FILE);
|
|
return false;
|
|
}
|
|
|
|
unsigned int portnum = atoul(port);
|
|
return Connect(host, user, password, database, portnum);
|
|
}
|
|
|
|
// Connect to database with explicit connection parameters
|
|
bool Connect(const char* host, const char* user, const char* password, const char* database, unsigned int port = 3306)
|
|
{
|
|
LockMutex lock(&MMysql);
|
|
safe_delete_array(pHost);
|
|
safe_delete_array(pUser);
|
|
safe_delete_array(pPassword);
|
|
safe_delete_array(pDatabase);
|
|
|
|
pHost = new char[strlen(host) + 1];
|
|
strcpy(pHost, host);
|
|
pUser = new char[strlen(user) + 1];
|
|
strcpy(pUser, user);
|
|
pPassword = new char[strlen(password) + 1];
|
|
strcpy(pPassword, password);
|
|
pDatabase = new char[strlen(database) + 1];
|
|
strcpy(pDatabase, database);
|
|
pPort = port;
|
|
|
|
if (mysql_real_connect(&mysql, pHost, pUser, pPassword, pDatabase, pPort, NULL, CLIENT_FOUND_ROWS) == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unable to connect to MySQL server at %s:%u: %s\n", host, port, mysql_error(&mysql));
|
|
pStatus = Error;
|
|
return false;
|
|
}
|
|
|
|
pStatus = Connected;
|
|
return true;
|
|
}
|
|
|
|
// Comprehensive query execution method used by Query class and async operations
|
|
bool RunQuery(const char* query, int32 querylen, char* errbuf = 0, MYSQL_RES** result = 0, int32* affected_rows = 0, int32* last_insert_id = 0, int32* errnum = 0, bool retry = true)
|
|
{
|
|
if (errnum)
|
|
*errnum = 0;
|
|
if (errbuf)
|
|
errbuf[0] = 0;
|
|
bool ret = false;
|
|
LockMutex lock(&MMysql);
|
|
if (pStatus != Connected)
|
|
Connect();
|
|
|
|
LogWrite(DATABASE__QUERY, 0, "Database", query);
|
|
if (mysql_real_query(&mysql, query, querylen)) {
|
|
if (mysql_errno(&mysql) == CR_SERVER_GONE_ERROR)
|
|
pStatus = Error;
|
|
if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) {
|
|
if (retry) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover...");
|
|
ret = RunQuery(query, querylen, errbuf, result, affected_rows, last_insert_id, errnum, false);
|
|
}
|
|
else {
|
|
pStatus = Error;
|
|
if (errnum)
|
|
*errnum = mysql_errno(&mysql);
|
|
if (errbuf)
|
|
snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(&mysql), mysql_error(&mysql));
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "#%i: %s\nQuery:\n%s", mysql_errno(&mysql), mysql_error(&mysql), query);
|
|
ret = false;
|
|
}
|
|
}
|
|
else {
|
|
if (errnum)
|
|
*errnum = mysql_errno(&mysql);
|
|
if (errbuf)
|
|
snprintf(errbuf, MYSQL_ERRMSG_SIZE, "#%i: %s", mysql_errno(&mysql), mysql_error(&mysql));
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "#%i: %s\nQuery:\n%s", mysql_errno(&mysql), mysql_error(&mysql), query);
|
|
ret = false;
|
|
}
|
|
}
|
|
else {
|
|
if (result && mysql_field_count(&mysql)) {
|
|
*result = mysql_store_result(&mysql);
|
|
}
|
|
else if (result)
|
|
*result = 0;
|
|
if (affected_rows)
|
|
*affected_rows = mysql_affected_rows(&mysql);
|
|
if (last_insert_id)
|
|
*last_insert_id = mysql_insert_id(&mysql);
|
|
if (result) {
|
|
if (*result) {
|
|
ret = true;
|
|
}
|
|
else {
|
|
if (errnum)
|
|
*errnum = UINT_MAX;
|
|
if (errbuf) {
|
|
if ((!affected_rows || (affected_rows && *affected_rows == 0)) && (!last_insert_id || (last_insert_id && *last_insert_id == 0)))
|
|
LogWrite(DATABASE__RESULT, 1, "Database", "No Result.");
|
|
}
|
|
ret = false;
|
|
}
|
|
}
|
|
else {
|
|
ret = true;
|
|
}
|
|
}
|
|
|
|
if (ret) {
|
|
char tmp1[200] = { 0 };
|
|
char tmp2[200] = { 0 };
|
|
if (result && (*result))
|
|
snprintf(tmp1, sizeof(tmp1), ", %i rows returned", (int)mysql_num_rows(*result));
|
|
if (affected_rows)
|
|
snprintf(tmp2, sizeof(tmp2), ", %i rows affected", (*affected_rows));
|
|
|
|
LogWrite(DATABASE__DEBUG, 0, "Database", "Query Successful%s%s", tmp1, tmp2);
|
|
}
|
|
else
|
|
LogWrite(DATABASE__DEBUG, 0, "Database", "Query returned no results in %s!\n%s", __FUNCTION__, query);
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Execute non-SELECT query with printf-style formatting and automatic reconnection
|
|
bool Query(const char* query, ...)
|
|
{
|
|
char* buf;
|
|
size_t size = QUERY_INITIAL_SIZE;
|
|
int num_chars;
|
|
va_list args;
|
|
bool ret = true;
|
|
|
|
LockMutex lock(&MMysql);
|
|
while (true) {
|
|
if ((buf = (char*)malloc(size)) == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate database query of %u bytes\n", size);
|
|
return false;
|
|
}
|
|
|
|
va_start(args, query);
|
|
num_chars = vsnprintf(buf, size, query, args);
|
|
va_end(args);
|
|
|
|
if (num_chars > -1 && (size_t)num_chars < size)
|
|
break;
|
|
|
|
if (num_chars > -1)
|
|
size = num_chars + 1;
|
|
else
|
|
size *= 2;
|
|
|
|
free(buf);
|
|
}
|
|
|
|
if (pStatus != Connected)
|
|
Connect();
|
|
|
|
LogWrite(DATABASE__QUERY, 0, "Database", query);
|
|
if (mysql_real_query(&mysql, buf, num_chars) != 0) {
|
|
if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover and retry query...");
|
|
Connect();
|
|
if (mysql_real_query(&mysql, buf, num_chars) != 0) {
|
|
ret = false;
|
|
}
|
|
}
|
|
else if (!IsIgnoredErrno(mysql_errno(&mysql))) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Error %i running MySQL query: %s\n%s\n", mysql_errno(&mysql), mysql_error(&mysql), buf);
|
|
ret = false;
|
|
}
|
|
}
|
|
free(buf);
|
|
return ret;
|
|
}
|
|
|
|
// Execute SELECT query and store results in DatabaseResult object
|
|
bool Select(DatabaseResult* result, const char* query, ...)
|
|
{
|
|
char* buf;
|
|
size_t size = QUERY_INITIAL_SIZE;
|
|
int num_chars;
|
|
va_list args;
|
|
MYSQL_RES* res;
|
|
bool ret = true;
|
|
|
|
LockMutex lock(&MMysql);
|
|
while (true) {
|
|
if ((buf = (char*)malloc(size)) == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate database query of %u bytes\n", size);
|
|
return false;
|
|
}
|
|
|
|
va_start(args, query);
|
|
num_chars = vsnprintf(buf, size, query, args);
|
|
va_end(args);
|
|
|
|
if (num_chars > -1 && (size_t)num_chars < size)
|
|
break;
|
|
|
|
if (num_chars > -1)
|
|
size = num_chars + 1;
|
|
else
|
|
size *= 2;
|
|
|
|
free(buf);
|
|
}
|
|
|
|
if (pStatus != Connected)
|
|
Connect();
|
|
|
|
if (mysql_real_query(&mysql, buf, (unsigned long)num_chars) != 0) {
|
|
if (mysql_errno(&mysql) == CR_SERVER_LOST || mysql_errno(&mysql) == CR_SERVER_GONE_ERROR) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Lost connection, attempting to recover and retry query...");
|
|
mysql_close(&mysql);
|
|
Connect();
|
|
if (mysql_real_query(&mysql, buf, (unsigned long)num_chars) != 0) {
|
|
ret = false;
|
|
}
|
|
}
|
|
else if (!IsIgnoredErrno(mysql_errno(&mysql))) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Error %i running MySQL query: %s\n%s\n", mysql_errno(&mysql), mysql_error(&mysql), buf);
|
|
ret = false;
|
|
}
|
|
}
|
|
|
|
if (ret && !IsIgnoredErrno(mysql_errno(&mysql))) {
|
|
res = mysql_store_result(&mysql);
|
|
if (res != NULL) {
|
|
uint8 num_rows = mysql_affected_rows(&mysql);
|
|
uint8 num_fields = mysql_field_count(&mysql);
|
|
ret = result->StoreResult(res, num_fields, num_rows);
|
|
}
|
|
else {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Error storing MySql query result (%d): %s\n%s", mysql_errno(&mysql), mysql_error(&mysql), buf);
|
|
ret = false;
|
|
}
|
|
}
|
|
|
|
free(buf);
|
|
return ret;
|
|
}
|
|
|
|
// Get the auto-increment ID from the last INSERT operation
|
|
int32 LastInsertID()
|
|
{
|
|
return (int32)mysql_insert_id(&mysql);
|
|
}
|
|
|
|
// Get number of rows affected by the last operation
|
|
long AffectedRows()
|
|
{
|
|
return mysql_affected_rows(&mysql);
|
|
}
|
|
|
|
// Escape string for safe SQL usage - caller must free() returned pointer
|
|
char* Escape(const char* str, size_t len)
|
|
{
|
|
char* buf = (char*)malloc(len * 2 + 1);
|
|
if (buf == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", len * 2 + 1, __FUNCTION__, __LINE__);
|
|
return NULL;
|
|
}
|
|
mysql_real_escape_string(&mysql, buf, str, len);
|
|
return buf;
|
|
}
|
|
|
|
// Escape null-terminated string for safe SQL usage - caller must free() returned pointer
|
|
char* Escape(const char* str)
|
|
{
|
|
return Escape(str, strlen(str));
|
|
}
|
|
|
|
// Escape string and return as std::string - no manual memory management required
|
|
string EscapeStr(const char* str, size_t len)
|
|
{
|
|
char* buf = (char*)malloc(len * 2 + 1);
|
|
string ret;
|
|
if (buf == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", len * 2 + 1, __FUNCTION__, __LINE__);
|
|
return ret;
|
|
}
|
|
mysql_real_escape_string(&mysql, buf, str, len);
|
|
ret.append(buf);
|
|
free(buf);
|
|
return ret;
|
|
}
|
|
|
|
// Escape null-terminated string and return as std::string
|
|
string EscapeStr(const char* str)
|
|
{
|
|
return EscapeStr(str, strlen(str));
|
|
}
|
|
|
|
// Escape std::string and return as std::string
|
|
string EscapeStr(string str)
|
|
{
|
|
return EscapeStr(str.c_str(), str.length());
|
|
}
|
|
|
|
// Legacy function for compatibility - use EscapeStr instead
|
|
string getSafeEscapeString(const char* from_string)
|
|
{
|
|
return EscapeStr(from_string);
|
|
}
|
|
|
|
// Execute multiple SQL statements from a file with proper result processing
|
|
bool QueriesFromFile(const char* file)
|
|
{
|
|
bool success = true;
|
|
long size;
|
|
char* buf;
|
|
int ret;
|
|
MYSQL_RES* res;
|
|
FILE* f;
|
|
|
|
f = fopen(file, "rb");
|
|
if (f == NULL) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Unable to open '%s' for reading: %s", file, strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
fseek(f, 0, SEEK_END);
|
|
size = ftell(f);
|
|
fseek(f, 0, SEEK_SET);
|
|
|
|
buf = (char*)malloc(size + 1);
|
|
if (buf == NULL) {
|
|
fclose(f);
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Out of memory trying to allocate %u bytes in %s:%u\n", size + 1, __FUNCTION__, __LINE__);
|
|
return false;
|
|
}
|
|
|
|
if (fread(buf, sizeof(*buf), size, f) != (size_t)size) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Failed to read from '%s': %s", file, strerror(errno));
|
|
fclose(f);
|
|
free(buf);
|
|
return false;
|
|
}
|
|
|
|
buf[size] = '\0';
|
|
fclose(f);
|
|
|
|
mysql_set_server_option(&mysql, MYSQL_OPTION_MULTI_STATEMENTS_ON);
|
|
ret = mysql_real_query(&mysql, buf, size);
|
|
free(buf);
|
|
|
|
if (ret != 0) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Error running MySQL queries from file '%s' (%d): %s", file, mysql_errno(&mysql), mysql_error(&mysql));
|
|
success = false;
|
|
}
|
|
else {
|
|
do {
|
|
res = mysql_store_result(&mysql);
|
|
if (res != NULL)
|
|
mysql_free_result(res);
|
|
ret = mysql_next_result(&mysql);
|
|
if (ret > 0) {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "Error running MySQL queries from file '%s' (%d): %s", file, mysql_errno(&mysql), mysql_error(&mysql));
|
|
success = false;
|
|
}
|
|
} while (ret == 0);
|
|
}
|
|
mysql_set_server_option(&mysql, MYSQL_OPTION_MULTI_STATEMENTS_OFF);
|
|
|
|
return success;
|
|
}
|
|
|
|
// Add MySQL error number to ignored list for suppressing specific error logging
|
|
void SetIgnoredErrno(unsigned int db_errno)
|
|
{
|
|
for (auto& errno_val : ignored_errnos) {
|
|
if (errno_val == db_errno)
|
|
return;
|
|
}
|
|
ignored_errnos.push_back(db_errno);
|
|
}
|
|
|
|
// Remove MySQL error number from ignored list
|
|
void RemoveIgnoredErrno(unsigned int db_errno)
|
|
{
|
|
for (auto itr = ignored_errnos.begin(); itr != ignored_errnos.end(); itr++) {
|
|
if ((*itr) == db_errno) {
|
|
ignored_errnos.erase(itr);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if MySQL error number should be ignored for logging
|
|
bool IsIgnoredErrno(unsigned int db_errno)
|
|
{
|
|
for (auto& errno_val : ignored_errnos) {
|
|
if (errno_val == db_errno)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Send keepalive ping to MySQL server and handle connection errors
|
|
void Ping()
|
|
{
|
|
if (!MMysql.trylock()) {
|
|
return; // If locked, someone's using it and it doesn't need a keepalive
|
|
}
|
|
mysql_ping(&mysql);
|
|
|
|
int32 errnum = mysql_errno(&mysql);
|
|
switch (errnum) {
|
|
case CR_COMMANDS_OUT_OF_SYNC:
|
|
case CR_SERVER_GONE_ERROR:
|
|
case CR_UNKNOWN_ERROR: {
|
|
LogWrite(DATABASE__ERROR, 0, "Database", "[Database] We lost connection to the database., errno: %i", errnum);
|
|
break;
|
|
}
|
|
}
|
|
MMysql.unlock();
|
|
}
|
|
|
|
protected:
|
|
MYSQL mysql; // MySQL connection handle
|
|
Mutex MMysql; // Thread safety mutex for database operations
|
|
eStatus pStatus; // Current connection status
|
|
char* pHost; // Database host address
|
|
char* pUser; // Database username
|
|
char* pPassword; // Database password
|
|
char* pDatabase; // Database name
|
|
bool pCompress; // Enable compression flag
|
|
int32 pPort; // Database port number
|
|
bool pSSL; // Enable SSL flag
|
|
vector<unsigned int> ignored_errnos; // List of MySQL error numbers to ignore in logging
|
|
}; |