aboutsummaryrefslogtreecommitdiff
/*       +------------------------------------+
 *       | 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 <mysql.h>
#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<std::string, SQLConnection*> 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<std::string> colnames;
	std::vector<SQLEntries> 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<std::string>& 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<ConfigTag> 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)