/* +------------------------------------+ * | Inspire Internet Relay Chat Daemon | * +------------------------------------+ * * InspIRCd: (C) 2002-2011 InspIRCd Development Team * See: http://wiki.inspircd.org/Credits * * This program is free but copyrighted software; see * the file COPYING for details. * * --------------------------------------------------- */ /* Stop mysql wanting to use long long */ #define NO_CLIENT_LONG_LONG #include "inspircd.h" #include #include "sql.h" #ifdef WINDOWS #pragma comment(lib, "mysqlclient.lib") #endif namespace m_mysql { /* VERSION 3 API: With nonblocking (threaded) requests */ /* $ModDesc: SQL Service Provider module for all other m_sql* modules */ /* $CompileFlags: exec("mysql_config --include") */ /* $LinkerFlags: exec("mysql_config --libs_r") rpath("mysql_config --libs_r") */ /* $ModDep: m_sqlv2.h */ /* THE NONBLOCKING MYSQL API! * * MySQL provides no nonblocking (asyncronous) API of its own, and its developers recommend * that instead, you should thread your program. This is what i've done here to allow for * asyncronous SQL requests via mysql. The way this works is as follows: * * The module spawns a thread via class Thread, and performs its mysql queries in this thread, * using a queue with priorities. There is a mutex on either end which prevents two threads * adjusting the queue at the same time, and crashing the ircd. Every 50 milliseconds, the * worker thread wakes up, and checks if there is a request at the head of its queue. * If there is, it processes this request, blocking the worker thread but leaving the ircd * thread to go about its business as usual. During this period, the ircd thread is able * to insert futher pending requests into the queue. * * Once the processing of a request is complete, it is removed from the incoming queue to * an outgoing queue, and initialized as a 'response'. The worker thread then signals the * ircd thread (via a loopback socket) of the fact a result is available, by sending the * connection ID through the connection. * * The ircd thread then mutexes the queue once more, reads the outbound response off the head * of the queue, and sends it on its way to the original calling module. * * XXX: You might be asking "why doesnt he just send the response from within the worker thread?" * The answer to this is simple. The majority of InspIRCd, and in fact most ircd's are not * threadsafe. This module is designed to be threadsafe and is careful with its use of threads, * however, if we were to call a module's OnRequest even from within a thread which was not the * one the module was originally instantiated upon, there is a chance of all hell breaking loose * if a module is ever put in a re-enterant state (stack corruption could occur, crashes, data * corruption, and worse, so DONT think about it until the day comes when InspIRCd is 100% * gauranteed threadsafe!) * * For a diagram of this system please see http://wiki.inspircd.org/Mysql2 */ class SQLConnection; class MySQLresult; typedef std::map ConnMap; /** MySQL module * */ class ModuleSQL : public Module { public: ConnMap connections; // main thread only ModuleSQL(); void init(); ~ModuleSQL(); void ReadConfig(ConfigReadStatus&); Version GetVersion(); }; #if !defined(MYSQL_VERSION_ID) || MYSQL_VERSION_ID<32224 #define mysql_field_count mysql_num_fields #endif /** Represents a mysql result set */ class MySQLresult : public SQLResult { public: SQLerror err; int currentrow; int rows; std::vector colnames; std::vector fieldlists; MySQLresult(MYSQL_RES* res, int affected_rows) : err(SQL_NO_ERROR), currentrow(0), rows(0) { if (affected_rows >= 1) { rows = affected_rows; fieldlists.resize(rows); } unsigned int field_count = 0; if (res) { MYSQL_ROW row; int n = 0; while ((row = mysql_fetch_row(res))) { if (fieldlists.size() < (unsigned int)rows+1) { fieldlists.resize(fieldlists.size()+1); } field_count = 0; MYSQL_FIELD *fields = mysql_fetch_fields(res); if(mysql_num_fields(res) == 0) break; if (fields && mysql_num_fields(res)) { colnames.clear(); while (field_count < mysql_num_fields(res)) { std::string a = (fields[field_count].name ? fields[field_count].name : ""); if (row[field_count]) fieldlists[n].push_back(SQLEntry(row[field_count])); else fieldlists[n].push_back(SQLEntry()); colnames.push_back(a); field_count++; } n++; } rows++; } mysql_free_result(res); res = NULL; } } MySQLresult(SQLerror& e) : err(e) { } ~MySQLresult() { } virtual int Rows() { return rows; } virtual void GetCols(std::vector& result) { result.assign(colnames.begin(), colnames.end()); } virtual SQLEntry GetValue(int row, int column) { if ((row >= 0) && (row < rows) && (column >= 0) && (column < (int)fieldlists[row].size())) { return fieldlists[row][column]; } return SQLEntry(); } virtual bool GetRow(SQLEntries& result) { if (currentrow < rows) { result.assign(fieldlists[currentrow].begin(), fieldlists[currentrow].end()); currentrow++; return true; } else { result.clear(); return false; } } }; /** Represents a connection to a mysql database */ class SQLConnection : public SQLProvider { public: reference config; MYSQL *connection; Mutex lock; // This constructor creates an SQLConnection object with the given credentials, but does not connect yet. SQLConnection(Module* p, ConfigTag* tag) : SQLProvider(p, "SQL/" + tag->getString("id")), config(tag), connection(NULL) { } ~SQLConnection() { Close(); } // This method connects to the database using the credentials supplied to the constructor, and returns // true upon success. bool Connect() { unsigned int timeout = 1; connection = mysql_init(connection); mysql_options(connection,MYSQL_OPT_CONNECT_TIMEOUT,(char*)&timeout); std::string host = config->getString("host"); std::string user = config->getString("user"); std::string pass = config->getString("pass"); std::string dbname = config->getString("name"); int port = config->getInt("port"); bool rv = mysql_real_connect(connection, host.c_str(), user.c_str(), pass.c_str(), dbname.c_str(), port, NULL, 0); if (!rv) return rv; std::string initquery; if (config->readString("initialquery", initquery)) { mysql_query(connection,initquery.c_str()); } return true; } ModuleSQL* Parent() { return (ModuleSQL*)(Module*)creator; } MySQLresult* DoBlockingQuery(const std::string& query) { /* Parse the command string and dispatch it to mysql */ if (CheckConnection() && !mysql_real_query(connection, query.data(), query.length())) { /* Successfull query */ MYSQL_RES* res = mysql_use_result(connection); unsigned long rows = mysql_affected_rows(connection); return new MySQLresult(res, rows); } else { /* XXX: See /usr/include/mysql/mysqld_error.h for a list of * possible error numbers and error messages */ SQLerror e(SQL_QREPLY_FAIL, ConvToStr(mysql_errno(connection)) + std::string(": ") + mysql_error(connection)); return new MySQLresult(e); } } bool CheckConnection() { if (!connection || mysql_ping(connection) != 0) return Connect(); return true; } std::string GetError() { return mysql_error(connection); } void Close() { mysql_close(connection); } void submit(SQLQuery*, const std::string&); void submit(SQLQuery*, const std::string& q, const ParamL& p); void submit(SQLQuery*, const std::string& q, const ParamM& p); }; class QueryJob : public Job { protected: SQLQuery* const query; SQLConnection* conn; private: MySQLresult* result; public: QueryJob(SQLQuery* Q, SQLConnection* C) : Job(C->creator), query(Q), conn(C), result(NULL) { } ~QueryJob() { } virtual MySQLresult* exec() = 0; void run() { Mutex::Lock lock(conn->lock); if (!IsCancelled()) result = exec(); } void finish() { if (!result) { SQLerror err(SQL_BAD_DBID, "Query cancelled"); query->OnError(err); } else if (result->err.id == SQL_NO_ERROR) query->OnResult(*result); else query->OnError(result->err); delete query; delete result; } bool BlocksUnload(Module* m) { if (m == owner || m == query->creator) return true; return false; } }; class QueryJobStatic : public QueryJob { const std::string query_str; public: QueryJobStatic(SQLQuery* Q, SQLConnection* C, const std::string& S) : QueryJob(Q, C), query_str(S) {} MySQLresult* exec() { return conn->DoBlockingQuery(query_str); } }; class QueryJobList : public QueryJob { public: const std::string format; const ParamL p; QueryJobList(SQLQuery* Q, SQLConnection* C, const std::string& F, const ParamL& P) : QueryJob(Q, C), format(F), p(P) {} MySQLresult* exec() { std::string res; unsigned int param = 0; for(std::string::size_type i = 0; i < format.length(); i++) { if (format[i] != '?') res.push_back(format[i]); else { if (param < p.size()) { std::string parm = p[param++]; char buffer[MAXBUF]; mysql_real_escape_string(conn->connection, buffer, parm.data(), parm.length()); res.append(buffer); } } } return conn->DoBlockingQuery(res); } }; class QueryJobMap : public QueryJob { public: const std::string format; const ParamM p; QueryJobMap(SQLQuery* Q, SQLConnection* C, const std::string& F, const ParamM& P) : QueryJob(Q, C), format(F), p(P), subst(this) {} class FormatSubstFn : public FormatSubstitute { public: QueryJobMap* me; FormatSubstFn(QueryJobMap* Me) : me(Me) {} std::string lookup(const std::string& key) { char buffer[MAXBUF]; ParamM::const_iterator it = me->p.find(key); if (it == me->p.end()) return ""; mysql_real_escape_string(me->conn->connection, buffer, it->second.data(), it->second.length()); return buffer; } } subst; MySQLresult* exec() { return conn->DoBlockingQuery(subst.format(format)); } }; void SQLConnection::submit(SQLQuery* call, const std::string& qs) { ServerInstance->Threads->Submit(new QueryJobStatic(call, this, qs)); } void SQLConnection::submit(SQLQuery* call, const std::string& format, const ParamL& p) { ServerInstance->Threads->Submit(new QueryJobList(call, this, format, p)); } void SQLConnection::submit(SQLQuery* call, const std::string& format, const ParamM& p) { ServerInstance->Threads->Submit(new QueryJobMap(call, this, format, p)); } ModuleSQL::ModuleSQL() { } void ModuleSQL::init() { } ModuleSQL::~ModuleSQL() { for(ConnMap::iterator i = connections.begin(); i != connections.end(); i++) { delete i->second; } } class CleanupJob : public Job { public: SQLConnection* conn; CleanupJob(SQLConnection* c) : Job(c->creator), conn(c) {} void run() { conn->lock.lock(); // TODO wait for any not-yet-started pending queries to finish conn->lock.unlock(); } void finish() { delete conn; } }; void ModuleSQL::ReadConfig(ConfigReadStatus&) { ConnMap conns; ConfigTagList tags = ServerInstance->Config->GetTags("database"); for(ConfigIter i = tags.first; i != tags.second; i++) { if (i->second->getString("module", "mysql") != "mysql") continue; std::string id = i->second->getString("id"); ConnMap::iterator curr = connections.find(id); if (curr == connections.end()) { SQLConnection* conn = new SQLConnection(this, i->second); conns.insert(std::make_pair(id, conn)); ServerInstance->Modules->AddService(*conn); } else { conns.insert(*curr); connections.erase(curr); } } // now clean up the deleted databases for(ConnMap::iterator i = connections.begin(); i != connections.end(); i++) { ServerInstance->Modules->DelService(*i->second); ServerInstance->Threads->Submit(new CleanupJob(i->second)); } connections.swap(conns); } Version ModuleSQL::GetVersion() { return Version("MySQL support", VF_VENDOR); } } using m_mysql::ModuleSQL; MODULE_INIT(ModuleSQL)