diff options
| author | Dark1-dev <shansarkar272@gmail.com> | 2023-03-01 21:32:53 +0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-03-01 21:32:53 +0600 |
| commit | de57bc38217c09a0ae4a143f631896652368ecc3 (patch) | |
| tree | 635e3f429a5a6e00744b6817533615e41c6db5e2 | |
| parent | 60a301a93b6057bb2c54ac04a7c38c38389037b3 (diff) | |
| download | Masterserver-Qt5-de57bc38217c09a0ae4a143f631896652368ecc3.tar.gz Masterserver-Qt5-de57bc38217c09a0ae4a143f631896652368ecc3.zip | |
Add files via upload
38 files changed, 1245 insertions, 0 deletions
diff --git a/src/Core/CoreObject/coreobject.cpp b/src/Core/CoreObject/coreobject.cpp new file mode 100644 index 0000000..11d1ab6 --- /dev/null +++ b/src/Core/CoreObject/coreobject.cpp @@ -0,0 +1,5 @@ +#include "coreobject.h" + +CoreObject::CoreObject() +{ +} diff --git a/src/Core/CoreObject/coreobject.h b/src/Core/CoreObject/coreobject.h new file mode 100644 index 0000000..91bd33b --- /dev/null +++ b/src/Core/CoreObject/coreobject.h @@ -0,0 +1,30 @@ +#ifndef COREOBJECT_H +#define COREOBJECT_H + +#include "Core/CoreObject/serverinfostructure.h" +#include "Core/GameInfo/gameinfostructure.h" +#include "Logger/logger.h" +#include "Settings/settingstructure.h" + +class CoreObject +{ +public: + CoreObject(); + + // struct with internal and external settings + SettingStructure Settings; + + // list of game details: gamename, cipher + QHash<QString, GameInfo> SupportedGames; + + // logging functions + Logger Log; + + // server address list acquired through third party masterservers + QList<ServerInfo> PendingServers; + + // generate our session/identification string, to prevent self-syncing + QString masterserverIdentity; // msid value +}; + +#endif // COREOBJECT_H diff --git a/src/Core/CoreObject/serverinfostructure.h b/src/Core/CoreObject/serverinfostructure.h new file mode 100644 index 0000000..4f86adb --- /dev/null +++ b/src/Core/CoreObject/serverinfostructure.h @@ -0,0 +1,31 @@ +#ifndef SERVERINFOSTRUCTURE_H +#define SERVERINFOSTRUCTURE_H + +#include <QDateTime> +#include <QHostAddress> + +struct ServerInfo +{ + // server address + QHostAddress ip; + + // server port + unsigned short port = 0; + + // gamename + QString gamename = ""; + + // date that the serverinfo was added or last updated + qint64 time = QDateTime::currentSecsSinceEpoch(); +}; + +// compare operator +inline bool operator== (const ServerInfo serverInfo1, const ServerInfo serverInfo2) +{ + // compare address, port and gamename. ignore time. + return ( serverInfo1.ip.isEqual(serverInfo2.ip) and + serverInfo1.port == serverInfo2.port and + serverInfo1.gamename == serverInfo2.gamename ); +} + +#endif // SERVERINFOSTRUCTURE_H diff --git a/src/Core/GameInfo/gameinfostructure.h b/src/Core/GameInfo/gameinfostructure.h new file mode 100644 index 0000000..b73e9ef --- /dev/null +++ b/src/Core/GameInfo/gameinfostructure.h @@ -0,0 +1,24 @@ +#ifndef STRUCTGAMEINFO_H +#define STRUCTGAMEINFO_H + +#include <QString> + +struct GameInfo +{ + // gamename is the unique identifier + QString gamename; + + // 6-byte GameSpy identifier + QString cipher; + + // game label + QString label; + + // default port + unsigned short port = 0; + + // known protocol + QString protocol; +}; + +#endif // STRUCTGAMEINFO_H diff --git a/src/Core/GameInfo/loadsupportedgames.cpp b/src/Core/GameInfo/loadsupportedgames.cpp new file mode 100644 index 0000000..9a73947 --- /dev/null +++ b/src/Core/GameInfo/loadsupportedgames.cpp @@ -0,0 +1,97 @@ +#include "loadsupportedgames.h" + +QHash<QString, GameInfo> loadSupportedGames (const QString &applicationPath) +{ + // return hash + QHash<QString, GameInfo> supportedGames; + + // supported games file path (following README structure) + const QString supportPath = applicationPath + "/" + _supportedPath; + + // determine if file exists + if ( ! QFile(supportPath).exists() ) + { + // no game info file exists + logPrimitive() << "No games file found at " << supportPath << endl + << "Please provide the correct file . " << endl; + return supportedGames; + } + + // load config file + QFile supportedFile(supportPath); + if ( ! supportedFile.open(QIODevice::ReadOnly) ) + { + // error occurred. report and quit. + logPrimitive() << "Unable to open the game file. Is the file open or in use?" << endl; + return supportedGames; + } + + // stats + int total = 0; + + // load as json object + QJsonDocument supportedJsonData(QJsonDocument::fromJson(supportedFile.readAll())); + + /* Optimisation for web interface: add games to the database. + * + * If database/json data do not match, replace database table + * with json data. This should only occur during database creation + * or when manual changes were made to the json file (which is rare). + */ + bool dbInsert = supportedJsonData.array().count() != getNumGames(0).value("numTotal", 0); + QSqlQuery q; + if (dbInsert) + { + // no game info file exists + logPrimitive() << "Game info mismatch in database. Reloading json data. (" + << supportedJsonData.array().count() << "/" + << getNumGames(0).value("numTotal", 0) << ")" << endl; + + // void existing data + q.prepare("DELETE FROM gameinfo"); + if ( ! q.exec() ) + reportQuery(q); + } + + for (int i = 0; i < supportedJsonData.array().count(); i++) + { + // get the game object + QJsonObject thisGame = supportedJsonData.array().at(i).toObject(); + + GameInfo gameInfo; + gameInfo.gamename = thisGame.value("gamename").toString(""); + gameInfo.cipher = thisGame.value("cipher").toString(""); + gameInfo.port = static_cast<unsigned short>(thisGame.value("port").toDouble(0)); + gameInfo.label = thisGame.value("label").toString(""); + + // insert in db if needed + if (dbInsert) + { + QString insertString = "INSERT INTO gameinfo (gamename, label) " + "VALUES (:gamename, :label)"; + + q.prepare(insertString); + q.bindValue(":gamename", gameInfo.gamename ); + q.bindValue(":label", gameInfo.label); + + if ( ! q.exec() ) + reportQuery(q); + } + + // add to list + supportedGames.insert(gameInfo.gamename, gameInfo); + total++; + } + + // no games found? report this! + if (total <= 0) + { + // no game info exists + logPrimitive() << "No game info found in file at " << supportPath << endl + << "Please provide the correct file . " << endl; + return supportedGames; + } + + // all games parsed. done. + return supportedGames; +} diff --git a/src/Core/GameInfo/loadsupportedgames.h b/src/Core/GameInfo/loadsupportedgames.h new file mode 100644 index 0000000..333dd0b --- /dev/null +++ b/src/Core/GameInfo/loadsupportedgames.h @@ -0,0 +1,16 @@ +#ifndef LOADSUPPORTEDGAMES_H +#define LOADSUPPORTEDGAMES_H + +#include <QFile> +#include <QJsonObject> +#include <QJsonArray> +#include <QJsonDocument> +#include "Database/Common/commonactions.h" +#include "gameinfostructure.h" + +// load supported games from json file + +const QString _supportedPath = "../data/SupportedGames.json"; +QHash<QString, GameInfo> loadSupportedGames (const QString &applicationPath); + +#endif // LOADSUPPORTEDGAMES_H diff --git a/src/Core/core.cpp b/src/Core/core.cpp new file mode 100644 index 0000000..04c7e53 --- /dev/null +++ b/src/Core/core.cpp @@ -0,0 +1,15 @@ +#include "core.h" + +Core::Core(QString applicationPath) +{ + _applicationPath = applicationPath; +} + +// TODO +void Core::shutdown() +{ + logPrimitive() << "[stop] quitting masterserver application" << endl; + + // end application + exit(0); +} diff --git a/src/Core/core.h b/src/Core/core.h new file mode 100644 index 0000000..bde3a04 --- /dev/null +++ b/src/Core/core.h @@ -0,0 +1,43 @@ +#ifndef CORE_H +#define CORE_H + +#include "Core/CoreObject/coreobject.h" +#include "Core/GameInfo/loadsupportedgames.h" +#include "Core/version.h" +#include "Database/databaseinterface.h" +#include "Settings/loadsettings.h" +#include "Maintenance/maintenance.h" +#include "Protocols/GameSpy0/securevalidate.h" +#include "UdpTasks/BeaconServer/beaconserver.h" +#include "UdpTasks/StatusChecker/statuschecker.h" +#include "TcpTasks/ListenServer/listenserver.h" +#include "TcpTasks/Updater/syncupdater.h" + +class Core : public QObject +{ + Q_OBJECT +public: + Core(QString applicationPath); + void run(); + void shutdown(); + +private: + // root path + QString _applicationPath = ""; + + // internal dataobject + QSharedPointer<CoreObject> _coreObject = QSharedPointer<CoreObject>(new CoreObject); + + // Networking services + QSharedPointer<BeaconServer> _udpBeaconServer = QSharedPointer<BeaconServer>(new BeaconServer(_coreObject)); + QSharedPointer<ListenServer> _tcpListenServer = QSharedPointer<ListenServer>(new ListenServer(_coreObject)); + + // Updaters + QSharedPointer<SyncUpdater> _syncUpdater = QSharedPointer<SyncUpdater> (new SyncUpdater (_coreObject)); + QSharedPointer<StatusChecker> _statusChecker = QSharedPointer<StatusChecker>(new StatusChecker(_coreObject)); + + // Maintenance + QSharedPointer<Maintenance> _maintenance = QSharedPointer<Maintenance>(new Maintenance(_coreObject)); +}; + +#endif // CORE_H diff --git a/src/Core/corerun.cpp b/src/Core/corerun.cpp new file mode 100644 index 0000000..c978026 --- /dev/null +++ b/src/Core/corerun.cpp @@ -0,0 +1,75 @@ +#include "core.h" + +void Core::run() +{ + // randomize + qsrand(static_cast<unsigned int>(QDateTime::currentMSecsSinceEpoch())); + + // announce startup + logPrimitive() << "*** Starting 333networks Master Server v" << BUILD_VERSION << " ***" << endl; + + // can not set file paths + if (_applicationPath.length() <= 0) + this->shutdown(); + + // debug info (hardcoded disable for releases) + if ( false ) + logPrimitive() << "Using Qt " << qVersion() << endl; + + // set our own 12-byte identifier + _coreObject->masterserverIdentity = genChallengeString(12, true); + + // load config settings from file + _coreObject->Settings = loadSettings(_applicationPath); + if ( ! _coreObject->Settings.init ) + this->shutdown(); + + // initialise database + if ( ! initDatabase(_applicationPath) ) + this->shutdown(); + + // load game info from file and into database + _coreObject->SupportedGames = loadSupportedGames(_applicationPath); + if ( _coreObject->SupportedGames.count() <= 0 ) + this->shutdown(); + + // logger init + if ( ! _coreObject->Log.init(_applicationPath, _coreObject->Settings) ) + this->shutdown(); + + /* + * enter runmode + */ + + // udp beacon server + if ( ! _udpBeaconServer->listen() ) + this->shutdown(); + + // tcp listen server + if ( ! _tcpListenServer->listen() ) + this->shutdown(); + + /* + * advanced functionality + */ + + // maintenance and statistics + if ( _coreObject->Settings.MaintenanceSettings.doMaintenance ) + _maintenance->scheduleMaintenance(); + + // udp uplink broadcast every X minutes + if ( _coreObject->Settings.BeaconServerSettings.doUplink ) + _udpBeaconServer->uplink(); + + // syncing with other masterservers + if ( _coreObject->Settings.SyncerSettings.doSync ) + _syncUpdater->scheduleUpdater(); + + // server checker + if ( _coreObject->Settings.CheckerSettings.doCheck ) + _statusChecker->startTicker(); + + /* + * all services running + */ +} diff --git a/src/Core/version.h b/src/Core/version.h new file mode 100644 index 0000000..6dbc808 --- /dev/null +++ b/src/Core/version.h @@ -0,0 +1,43 @@ +#ifndef VERSION_H +#define VERSION_H + +#include <QString> + +/* + * Version and author information + * + * Only when you make (significant) modifications to the master server source + * code, you should edit these variables to reflect your changes. + * + * For example, + * if you limit functionality to only one game, you should change the variable + * BUILD_TYPE and SHORT_VER to something that reflects the change in function. + * + * In addition, if you have the actual interest to go through all this source + * code to end up here, consider sending Darkelarious (the original author) a + * postcard or (e)mail with your compliments. Or buy us a coffee. We like the + * appreciation. + */ + +// gamename for the 333networks-type MasterServer +#define TYPE_GAMENAME QString("333networks") + +// build type: type of software +#define BUILD_TYPE QString("MasterServer Qt5") + +// software version (of this particular type) +#define BUILD_VERSION QString("0.27") + +// short version (in query) -- Qt v0.n +#define SHORT_VER QString("Qt-" + BUILD_VERSION) + +// build time/date +#define BUILD_TIME QStringLiteral("%1 %2").arg(__DATE__).arg(__TIME__) + +// software author, contact +#define BUILD_AUTHOR QString("Darkelarious <darkelarious@333networks.com>") + +// minimum required database version +#define DATABASE_VERSION 0.27 + +#endif // VERSION_H diff --git a/src/Database/Common/commonactions.h b/src/Database/Common/commonactions.h new file mode 100644 index 0000000..3bce0b6 --- /dev/null +++ b/src/Database/Common/commonactions.h @@ -0,0 +1,31 @@ +#ifndef COMMONACTIONS_H +#define COMMONACTIONS_H + +#include <QDateTime> +#include "Database/databaseinterface.h" +#include "Logger/logprimitive.h" + +// insert, update or delete a server from the list +bool insertServer(const QString &serverAddress, + const unsigned short &serverPort, + const QString &gamename, + const bool &directBeacon); + +bool updateServer(const QString &serverAddress, + const unsigned short &serverPort, + const QString &gamename, + const bool &directBeacon, + const bool &authenticated); + +bool existServer(const QString &serverAddress, + const unsigned short &serverPort); + +QSqlQuery selectServerList(const QString &gamename, + const int &serverAge_s, + const bool &withSyncData); + +QStringList getGamenames(const int &serverAge_s); + +QHash<QString, int> getNumGames(const int &serverAge_s); + +#endif // COMMONACTIONS_H diff --git a/src/Database/Common/existserver.cpp b/src/Database/Common/existserver.cpp new file mode 100644 index 0000000..9609948 --- /dev/null +++ b/src/Database/Common/existserver.cpp @@ -0,0 +1,21 @@ +#include "commonactions.h" + +bool existServer(const QString &serverAddress, + const unsigned short &serverPort) +{ + // find existing entry + QSqlQuery q; + QString selectString = "SELECT id FROM serverlist " + "WHERE ip = :ip AND queryport = :queryport "; + + // bind values and execute + q.prepare(selectString); + q.bindValue(":ip", serverAddress); + q.bindValue(":queryport", serverPort); + + if ( ! q.exec() ) + return reportQuery(q); + + // was a row/server found? + return q.next(); +} diff --git a/src/Database/Common/getgamenames.cpp b/src/Database/Common/getgamenames.cpp new file mode 100644 index 0000000..8093f7c --- /dev/null +++ b/src/Database/Common/getgamenames.cpp @@ -0,0 +1,28 @@ +#include "commonactions.h" + +QStringList getGamenames(const int &serverAge_s) +{ + // init output + QStringList gamenameList; + + // retrieve active gamenames from database + QSqlQuery q; + QString selectString = "SELECT DISTINCT gamename FROM serverlist " + "WHERE dt_updated > :timestamp " + "ORDER BY gamename ASC"; + + // bind and execute + q.prepare(selectString); + q.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-serverAge_s ).toSecsSinceEpoch()); + + if ( ! q.exec() ) + reportQuery(q); + + // parse to stringlist + while ( q.next() ) + { + gamenameList.append( q.value(0).toString() ); + } + + return gamenameList; +} diff --git a/src/Database/Common/getnumgames.cpp b/src/Database/Common/getnumgames.cpp new file mode 100644 index 0000000..1425427 --- /dev/null +++ b/src/Database/Common/getnumgames.cpp @@ -0,0 +1,30 @@ +#include "commonactions.h" + +QHash<QString, int> getNumGames(const int &serverAge_s) +{ + QSqlQuery q; + QHash<QString, int> numGames; + + // number of games + q.prepare("SELECT count(gamename) FROM gameinfo"); + if ( ! q.exec() ) + reportQuery(q); + if (q.next()) + numGames["numTotal"] = q.value(0).toInt(); + + // number of active games + QString selectString = "SELECT count(gamename) FROM gameinfo " + "WHERE num_direct > 0 " + "OR num_total > 0 "; + q.prepare(selectString); + q.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-serverAge_s ).toSecsSinceEpoch()); + if ( ! q.exec() ) + reportQuery(q); + if (q.next()) + numGames["numActive"] = q.value(0).toInt(); + + + // TODO: get more relevant stats + + return numGames; +} diff --git a/src/Database/Common/insertserver.cpp b/src/Database/Common/insertserver.cpp new file mode 100644 index 0000000..05b2eaa --- /dev/null +++ b/src/Database/Common/insertserver.cpp @@ -0,0 +1,26 @@ +#include "commonactions.h" + +// insert a server into the list +bool insertServer(const QString &serverAddress, + const unsigned short &serverPort, + const QString &gamename, + const bool &directBeacon) +{ + // insert query string + QSqlQuery q; + QString insertString = "INSERT INTO serverlist (ip, queryport, gamename, f_direct) " + "VALUES (:ip, :queryport, :gamename, :directbeacon)"; + + // bind values and execute + q.prepare(insertString); + q.bindValue(":ip", serverAddress); + q.bindValue(":queryport", serverPort); + q.bindValue(":gamename", gamename); + q.bindValue(":directbeacon", ( directBeacon ? 1 : 0 ) ); // bool to int + + if ( ! q.exec() ) + return reportQuery(q); + + // was a row inserted? + return (q.numRowsAffected() > 0); +} diff --git a/src/Database/Common/selectserverlist.cpp b/src/Database/Common/selectserverlist.cpp new file mode 100644 index 0000000..c4797ed --- /dev/null +++ b/src/Database/Common/selectserverlist.cpp @@ -0,0 +1,31 @@ +#include "commonactions.h" + +QSqlQuery selectServerList(const QString &gamename, + const int &serverAge_s, + const bool &withSyncData) +{ + // retrieve servers from database + QSqlQuery q; + QString selectString = "SELECT ip, queryport FROM serverlist " + "WHERE gamename = :gamename "; + + if ( withSyncData ) + { + // relies on sync data to be accurate (if checker is not enabled) + selectString += "AND (dt_updated > :timestamp OR dt_sync > :timestamp)"; + } + else + { + // sync data may not be (sufficiently) accurate, only use data that we verified directly + selectString += "AND dt_updated > :timestamp"; + } + + // bind values and execute + q.prepare(selectString); + q.bindValue(":gamename", gamename); + q.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-serverAge_s ).toSecsSinceEpoch()); + if ( ! q.exec() ) + reportQuery(q); + + return q; +} diff --git a/src/Database/Common/updateserver.cpp b/src/Database/Common/updateserver.cpp new file mode 100644 index 0000000..24fa26f --- /dev/null +++ b/src/Database/Common/updateserver.cpp @@ -0,0 +1,47 @@ +#include "commonactions.h" + +bool updateServer(const QString &serverAddress, + const unsigned short &serverPort, + const QString &gamename, + const bool &directBeacon, + const bool &authenticated + ) +{ + // update existing entry, but do not insert. + QSqlQuery q; + QString updateString; + + // update with available values + updateString = "UPDATE serverlist SET "; + + /* + * Note that direct/auth set to 'false' will NOT override a previous value in the database + */ + + // is this a direct beacon? + if (directBeacon) + { + updateString += "f_direct = 1, "; + updateString += "dt_beacon = :timestamp, "; + } + + // did the server authenticate? + if (authenticated) updateString += "f_auth = 1, "; + + updateString += "gamename = :gamename, " + "dt_updated = :timestamp " + "WHERE ip = :ip AND queryport = :queryport"; + + // bind values and execute + q.prepare(updateString); + q.bindValue(":ip", serverAddress); + q.bindValue(":queryport", serverPort); + q.bindValue(":gamename", gamename); + q.bindValue(":timestamp", QDateTime::currentSecsSinceEpoch() ); + + if ( ! q.exec() ) + return reportQuery(q); + + // was a row updated? + return (q.numRowsAffected() > 0); +} diff --git a/src/Database/closedatabase.cpp b/src/Database/closedatabase.cpp new file mode 100644 index 0000000..6f3ff89 --- /dev/null +++ b/src/Database/closedatabase.cpp @@ -0,0 +1,7 @@ +#include "databaseinterface.h" + +void closeDatabase() +{ + QSqlDatabase dbi; + dbi.close(); +} diff --git a/src/Database/createtables.cpp b/src/Database/createtables.cpp new file mode 100644 index 0000000..b437163 --- /dev/null +++ b/src/Database/createtables.cpp @@ -0,0 +1,129 @@ +#include "databaseinterface.h" + +bool createTables() +{ + { // create serverlist + QSqlQuery q; + QString createServerlist = QStringLiteral( + "CREATE TABLE serverlist(" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "gamename TEXT NOT NULL DEFAULT ''," + "ip TEXT NOT NULL ," + "queryport INTEGER NOT NULL DEFAULT 0," + "t_protocol INTEGER NOT NULL DEFAULT 0," + "f_blacklist INTEGER NOT NULL DEFAULT 0," + "f_auth INTEGER NOT NULL DEFAULT 0," + "f_direct INTEGER NOT NULL DEFAULT 0," + "dt_added BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))," + "dt_beacon BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))," + "dt_sync BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))," + "dt_updated BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))" + ")"); + + // bind values and execute (not all db-interfaces support prepare(create table), exec directly) + if ( ! q.exec(createServerlist) ) + return reportQuery(q); + } + + { // create serverinfo + QSqlQuery q; + QString createServerinfo = QStringLiteral( + "CREATE TABLE serverinfo(" + "sid INTEGER NOT NULL DEFAULT 0," + "hostport INTEGER NOT NULL DEFAULT 0," + "hostname TEXT," + "gamever TEXT," + "minnetver TEXT," + "country TEXT," + "location TEXT," + "listenserver TEXT," + "adminname TEXT," + "adminemail TEXT," + "password TEXT," + "gametype TEXT," + "gamestyle TEXT," + "changelevels TEXT," + "maptitle TEXT," + "mapname TEXT," + "numplayers INTEGER DEFAULT 0," + "maxplayers INTEGER DEFAULT 0," + "minplayers INTEGER DEFAULT 0," + "botskill TEXT," + "balanceteams TEXT," + "playersbalanceteams TEXT," + "friendlyfire TEXT," + "maxteams TEXT," + "timelimit TEXT," + "goalteamscore TEXT," + "fraglimit TEXT," + "mutators TEXT DEFAULT 'None'," + "misc TEXT," + "dt_serverinfo BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))" + ")"); + + // bind values and execute + if ( ! q.exec(createServerinfo) ) + return reportQuery(q); + } + + { // create playerinfo + QSqlQuery q; + QString createPlayerlist = QStringLiteral( + "CREATE TABLE playerinfo(" + "sid INTEGER NOT NULL DEFAULT 0," + "name TEXT DEFAULT 'Player'," + "team TEXT," + "frags INTEGER DEFAULT 0," + "mesh TEXT," + "skin TEXT," + "face TEXT," + "ping INTEGER DEFAULT 0," + "misc TEXT," + "dt_player BIGINT DEFAULT (CAST(strftime('%s','now') AS BIGINT))" + ")"); + + // bind values and execute + if ( ! q.exec(createPlayerlist) ) + return reportQuery(q); + } + + { // create gameinfo + QSqlQuery q; + QString createGamelist = QStringLiteral( + "CREATE TABLE gameinfo(" + "gamename TEXT NOT NULL DEFAULT ''," + "label TEXT NOT NULL DEFAULT ''," + "num_total INTEGER NOT NULL DEFAULT 0," + "num_direct INTEGER NOT NULL DEFAULT 0" + ")"); + + // bind values and execute + if ( ! q.exec(createGamelist) ) + return reportQuery(q); + } + + { // create version info + QSqlQuery q; + QString createVersion = QStringLiteral( + "CREATE TABLE versioninfo(" + "type TEXT NOT NULL DEFAULT ''," + "version TEXT NOT NULL DEFAULT ''" + ")"); + + // bind values and execute + if ( ! q.exec(createVersion) ) + return reportQuery(q); + + // insert current version directly into the db during creation + q.prepare("INSERT INTO versioninfo (type, version) VALUES (\"database\", :currentver)"); + q.bindValue(":currentver", BUILD_VERSION); + + if ( ! q.exec() ) + reportQuery(q); + } + + // TODO: optional: write an sql file to /data/tables.sql for easy access on generated tables. + + // no errors + return true; +} diff --git a/src/Database/databaseinterface.h b/src/Database/databaseinterface.h new file mode 100644 index 0000000..f80c93a --- /dev/null +++ b/src/Database/databaseinterface.h @@ -0,0 +1,24 @@ +#ifndef DATABASEINTERFACE_H +#define DATABASEINTERFACE_H + +#include <QSqlDatabase> +#include <QSqlQuery> +#include <QSqlError> +#include <QDataStream> +#include "Core/version.h" +#include "Logger/logprimitive.h" + +// database file path (following README structure) +const QString _sqlitePath = "../data/masterserver.db"; + +// init and close +bool initDatabase(const QString applicationPath); +void closeDatabase(); + +// report execution errors (terminal/display only) +bool reportQuery(const QSqlQuery &q); // always returns false! + +// generate tables in first run +bool createTables(); + +#endif // DATABASEINTERFACE_H diff --git a/src/Database/initdatabase.cpp b/src/Database/initdatabase.cpp new file mode 100644 index 0000000..06cd37c --- /dev/null +++ b/src/Database/initdatabase.cpp @@ -0,0 +1,77 @@ +#include "databaseinterface.h" + +bool initDatabase(const QString applicationPath) +{ + // open SQLite database + QSqlDatabase dbi = QSqlDatabase::addDatabase( "QSQLITE" ); + dbi.setDatabaseName(applicationPath + "/" + _sqlitePath); + + // open database + if ( ! dbi.open() ) + { + QSqlError sqlError = dbi.lastError(); + logPrimitive() << "Error opening database: " << sqlError.text() << endl; + return false; + } + + // speed up SQLite with keeping journals in memory and asynchronous writing + dbi.exec("PRAGMA synchronous = OFF"); + dbi.exec("PRAGMA journal_mode = MEMORY"); + + { // check if the database was generated with this version of the software + + // serverlist exists (and thus the other tables exist) but not version + if ( dbi.tables().contains("serverlist") and ! dbi.tables().contains("versioninfo")) + { + // outdated because the "version" table does not even exist yet (pre-0.14) + logPrimitive() << "The database tables are outdated (pre-0.14). Please remove the current database." << endl; + return false; + } + + // does the version table exist, and does it contain the correct version? + if ( dbi.tables().contains("versioninfo") ) + { + float dbVersion = 0.0; + QSqlQuery versionQuery; + versionQuery.prepare("SELECT version FROM versioninfo WHERE type = \"database\""); + + // failed query? (unlikely) + if ( ! versionQuery.exec() ) + return reportQuery(versionQuery); + + if (versionQuery.next()) + dbVersion = versionQuery.value("version").toFloat(); + + // version check is intentionally hardcoded since the last change in database structure (currently v0.15) + if ( dbVersion < DATABASE_VERSION ) + { + logPrimitive() << "The database tables are outdated (" << QString::number(dbVersion) << "). Please remove the current database." << endl; + return false; + } + + // tables up to date + return true; + } + } + + // specifying AUTO_CREATE_DB=y in the environment allows bypassing of the interactive prompt for non-interactive environments + if (qgetenv("AUTO_CREATE_DB") != "y") + { + // relevant tables do not exist. ask to generate. + logPrimitive() << "The database tables do not exist. Do you want to generate these? [y/N]" << endl; + + // if no, do not proceed. + if ( ! QTextStream(stdin).readLine().startsWith("y") ) + { + // do not generate. failed to load database. + return false; + } + } + else + { + logPrimitive() << "The database tables do not exist and will now be created." << endl; + } + + // create tables and proceed + return createTables(); +} diff --git a/src/Database/reportquery.cpp b/src/Database/reportquery.cpp new file mode 100644 index 0000000..2a5d7f7 --- /dev/null +++ b/src/Database/reportquery.cpp @@ -0,0 +1,11 @@ +#include "databaseinterface.h" + +bool reportQuery(const QSqlQuery &q) +{ + logPrimitive() << "Database query error." << endl << "" << endl + << "Technical info: " << q.lastQuery() << endl << "" << endl + << "Reported error: " << q.lastError().text() << endl; + + // always return false, so we can report error and return false in one line. + return false; +} diff --git a/src/Logger/cyclelogfile.cpp b/src/Logger/cyclelogfile.cpp new file mode 100644 index 0000000..cb0266e --- /dev/null +++ b/src/Logger/cyclelogfile.cpp @@ -0,0 +1,48 @@ +#include "logger.h" + +// if "now" matches the previous logfile name, no cycling needed. otherwise, determine new logfile name. +bool Logger::cycleLogFile() +{ + // calculate the expected log filename + QString newFilename = _logLabel; + + // get date to see if we need to cycle + QDate date(QDate::currentDate()); + + // when is it necessary to cycle? + switch ( _cyclePeriod ) + { + case 1: // yearly + newFilename += date.toString("-yyyy"); + break; + case 2: // monthly + newFilename += date.toString("-yyyy-MM"); + break; + case 3: // weekly + newFilename += date.toString("-yyyy") + "-week" + QString("%1").arg(date.weekNumber(), 2, 10, QChar('0')); + break; + case 4: // daily + newFilename += date.toString("-yyyy-MM-dd"); + break; + default:; // 0 = never + } + + // add log file extension + newFilename += ".log"; + + // if current and new filename are not the same, cycle log file + if (_logFileName != newFilename) + { + // cycle log file + _logFileName = newFilename; + + // close the existing/opened log file + closeLogFile(); + + // open the new log file (pass possible error to caller) + return openLogFile(); + } + + // else: no cycling necessary + return true; +} diff --git a/src/Logger/initlog.cpp b/src/Logger/initlog.cpp new file mode 100644 index 0000000..d852c86 --- /dev/null +++ b/src/Logger/initlog.cpp @@ -0,0 +1,37 @@ +#include "logger.h" + +bool Logger::init(const QString &applicationPath, + const SettingStructure &settings) +{ + // test if log directory exists + _logPath = applicationPath + "/" + _logDirectory; + QDir logDir(_logPath); + if ( ! logDir.mkpath(".") ) + { + QTextStream(stdout) << "Failed to access log directory at " + _logPath << endl + << "Please create the director and/or apply relevant permissions." << endl; + return false; + } + + // log cycling -- determine log cycling setting + const QHash<QString, int> logCycling = { + {"never", 0}, + {"yearly", 1}, + {"monthly", 2}, + {"weekly", 3}, + {"daily", 4} + }; + + // load log-specific settings + _cyclePeriod = logCycling.value( settings.LoggingSettings.cycle ); + _suppressLog = settings.LoggingSettings.suppressLog; + _suppressDisplay = settings.LoggingSettings.suppressDisplay; + + // cycling initialisation ("never" is also a cycle) + if ( ! cycleLogFile() ) + { + QTextStream(stdout) << "Failed to access log file " + _logFileName + " at " + _logPath << endl; + return false; + } + return true; +} diff --git a/src/Logger/logevent.cpp b/src/Logger/logevent.cpp new file mode 100644 index 0000000..9d1858c --- /dev/null +++ b/src/Logger/logevent.cpp @@ -0,0 +1,20 @@ +#include "logger.h" + +void Logger::logEvent(const QString &messageType, + const QString &message) +{ + // printing to display suppressed? + if ( ! _suppressDisplay.contains(messageType) and ! _suppressDisplay.contains("all") ) + { + QString dateTimeStr(QDateTime::currentDateTime().toString("dd-MM-yyyy HH:mm:ss:zzz")); + logPrimitive() << QStringLiteral("[%1][%2]\t%3").arg(dateTimeStr, messageType, message.toLatin1()) << endl; + } + + // printing to logfile suppressed? + if ( ! _suppressLog.contains(messageType) and ! _suppressLog.contains("all") ) + { + // write message to log + QString dateTimeStr(QDateTime::currentDateTime().toString("dd-MM-yyyy HH:mm:ss:zzz")); + writeLogFile( QStringLiteral("[%1][%2]\t%3").arg(dateTimeStr, messageType, message.toLatin1()) ); + } +} diff --git a/src/Logger/logger.cpp b/src/Logger/logger.cpp new file mode 100644 index 0000000..3bf7def --- /dev/null +++ b/src/Logger/logger.cpp @@ -0,0 +1,10 @@ +#include "logger.h" + +Logger::Logger() +{ +} + +void Logger::stop() +{ + closeLogFile(); +} diff --git a/src/Logger/logger.h b/src/Logger/logger.h new file mode 100644 index 0000000..9880c44 --- /dev/null +++ b/src/Logger/logger.h @@ -0,0 +1,46 @@ +#ifndef LOGGER_H +#define LOGGER_H + +#include <QDir> +#include <QFile> +#include <QDateTime> +#include <QTextStream> + +#include "Settings/settingstructure.h" +#include "Core/version.h" +#include "logprimitive.h" + +class Logger +{ +public: + Logger(); + bool init(const QString &applicationPath, + const SettingStructure &settings); + void stop(); + void logEvent(const QString &messageType, + const QString &message); + +private: + // path variables + const QString _logDirectory = "../log"; + const QString _logLabel = "MasterServer-" + SHORT_VER; + QString _logPath = ""; + + // local variables + int _cyclePeriod; + QString _logFileName; + QString _suppressLog; + QString _suppressDisplay; + + // file i/o + QFile _logFile; + +private: + bool cycleLogFile(); + bool openLogFile(); + void closeLogFile(); + bool writeLogFile(const QString message); + +}; + +#endif // LOGGER_H diff --git a/src/Logger/logprimitive.h b/src/Logger/logprimitive.h new file mode 100644 index 0000000..1630566 --- /dev/null +++ b/src/Logger/logprimitive.h @@ -0,0 +1,12 @@ +#ifndef LOGPRIMITIVE_H +#define LOGPRIMITIVE_H + +#include <QTextStream> + +inline QTextStream& logPrimitive() +{ + static QTextStream r{stdout}; + return r; +} + +#endif // LOGPRIMITIVE_H diff --git a/src/Logger/openlogfile.cpp b/src/Logger/openlogfile.cpp new file mode 100644 index 0000000..349d058 --- /dev/null +++ b/src/Logger/openlogfile.cpp @@ -0,0 +1,21 @@ +#include "logger.h" + +bool Logger::openLogFile() +{ + // verify that there is no open logfile already + if ( _logFile.openMode() == QIODevice::NotOpen) + { + // set new log file + _logFile.setFileName(_logPath + "/" + _logFileName); + return _logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text); + } + // else: + // file already open, can not get a lock + return false; +} + +void Logger::closeLogFile() +{ + // close file if open. if already closed, ignore. + _logFile.close(); +} diff --git a/src/Logger/writelogfile.cpp b/src/Logger/writelogfile.cpp new file mode 100644 index 0000000..954b5b2 --- /dev/null +++ b/src/Logger/writelogfile.cpp @@ -0,0 +1,18 @@ +#include "logger.h" + +bool Logger::writeLogFile(const QString message) +{ + // first see if we need to cycle the log file already + cycleLogFile(); + + // write to file + if ( _logFile.isOpen() ) + { + QTextStream logStream(&_logFile); + logStream << message << endl; + return true; + } + + logPrimitive() << "[log error]" << message; + return false; +} diff --git a/src/Maintenance/maintenance.cpp b/src/Maintenance/maintenance.cpp new file mode 100644 index 0000000..8869f99 --- /dev/null +++ b/src/Maintenance/maintenance.cpp @@ -0,0 +1,7 @@ +#include "maintenance.h" + +Maintenance::Maintenance(const QSharedPointer<CoreObject> &coreObject) +{ + // create local access + this->_coreObject = coreObject; +} diff --git a/src/Maintenance/maintenance.h b/src/Maintenance/maintenance.h new file mode 100644 index 0000000..b7a63a2 --- /dev/null +++ b/src/Maintenance/maintenance.h @@ -0,0 +1,33 @@ +#ifndef MAINTENANCE_H +#define MAINTENANCE_H + +#include <QTimer> + +#include "Core/CoreObject/coreobject.h" +#include "Database/databaseinterface.h" + +class Maintenance: public QObject +{ + Q_OBJECT + +public: + Maintenance(const QSharedPointer<CoreObject> &coreObject); + bool scheduleMaintenance(); + +private: + QSharedPointer<CoreObject> _coreObject; + + // schedule timer + QTimer _maintenanceTimer; + +private: + int updateStats(); + int pruneBeacons(); + int pruneServerInfo(); + int prunePlayers(); + +private slots: + void onMaintenanceTimerAction(); +}; + +#endif // MAINTENANCE_H diff --git a/src/Maintenance/onmaintenancetimeraction.cpp b/src/Maintenance/onmaintenancetimeraction.cpp new file mode 100644 index 0000000..97e5c51 --- /dev/null +++ b/src/Maintenance/onmaintenancetimeraction.cpp @@ -0,0 +1,28 @@ +#include "maintenance.h" + +// perform maintenance actions +void Maintenance::onMaintenanceTimerAction() +{ + // announce + _coreObject->Log.logEvent("main", QStringLiteral("performing maintenance")); + + // update statistics + int numUpdated = updateStats(); + if (numUpdated > 0) + _coreObject->Log.logEvent("stat", QStringLiteral("updated %1 game stats").arg(QString::number(numUpdated))); + + // prune direct beacons + int numPrunedBeacons = pruneBeacons(); + if (numPrunedBeacons > 0) + _coreObject->Log.logEvent("prune", QStringLiteral("pruned %1 direct beacons").arg(QString::number(numPrunedBeacons))); + + // prune serverinfo, serverinfo from which the serverlist entry is gone already + int numPrunedInfo = pruneServerInfo(); + if (numPrunedInfo > 0) + _coreObject->Log.logEvent("prune", QStringLiteral("pruned %1 server info entries").arg(QString::number(numPrunedInfo))); + + // prune orphaned players from which the serverinfo entry is gone already OR updateinfo is outdated + int numPrunedPlayers = prunePlayers(); + if (numPrunedPlayers > 0) + _coreObject->Log.logEvent("prune", QStringLiteral("pruned %1 players").arg(QString::number(numPrunedPlayers))); +} diff --git a/src/Maintenance/prunebeacons.cpp b/src/Maintenance/prunebeacons.cpp new file mode 100644 index 0000000..10e4630 --- /dev/null +++ b/src/Maintenance/prunebeacons.cpp @@ -0,0 +1,16 @@ +#include "maintenance.h" + +int Maintenance::pruneBeacons() +{ + QString updateString = "UPDATE serverlist " + "SET f_direct = 0 " + "WHERE f_direct = 1 AND dt_beacon < :timestamp"; + QSqlQuery updateQuery; + updateQuery.prepare(updateString); + updateQuery.bindValue(":timestamp", QDateTime::currentDateTime().addSecs(-600).toSecsSinceEpoch()); + + if ( ! updateQuery.exec() ) + return reportQuery(updateQuery); + + return updateQuery.numRowsAffected(); +} diff --git a/src/Maintenance/pruneplayers.cpp b/src/Maintenance/pruneplayers.cpp new file mode 100644 index 0000000..0bae4f9 --- /dev/null +++ b/src/Maintenance/pruneplayers.cpp @@ -0,0 +1,18 @@ +#include "maintenance.h" + +int Maintenance::prunePlayers() +{ + QString deleteString = "DELETE FROM playerinfo " + "WHERE playerinfo.sid NOT IN ( " + "SELECT serverinfo.sid FROM serverinfo) " + "OR dt_player < :timestamp"; + QSqlQuery deleteQuery; + deleteQuery.prepare(deleteString); + deleteQuery.bindValue(":timestamp", QDateTime::currentDateTime() + .addSecs(-7200).toSecsSinceEpoch()); // 2 hours + + if ( ! deleteQuery.exec() ) + return reportQuery(deleteQuery); + + return deleteQuery.numRowsAffected(); +} diff --git a/src/Maintenance/pruneserverinfo.cpp b/src/Maintenance/pruneserverinfo.cpp new file mode 100644 index 0000000..e3f2bd8 --- /dev/null +++ b/src/Maintenance/pruneserverinfo.cpp @@ -0,0 +1,15 @@ +#include "maintenance.h" + +int Maintenance::pruneServerInfo() +{ + QString deleteString = "DELETE FROM serverinfo " + "WHERE sid NOT IN ( " + "SELECT id FROM serverlist)"; + QSqlQuery deleteQuery; + deleteQuery.prepare(deleteString); + + if ( ! deleteQuery.exec() ) + return reportQuery(deleteQuery); + + return deleteQuery.numRowsAffected(); +} diff --git a/src/Maintenance/schedulemaintenance.cpp b/src/Maintenance/schedulemaintenance.cpp new file mode 100644 index 0000000..3a2decd --- /dev/null +++ b/src/Maintenance/schedulemaintenance.cpp @@ -0,0 +1,16 @@ +#include "maintenance.h" + +bool Maintenance::scheduleMaintenance() +{ + // set update timer + connect(&_maintenanceTimer, &QTimer::timeout, this, &Maintenance::onMaintenanceTimerAction); + + _maintenanceTimer.setInterval( _coreObject->Settings.MaintenanceSettings.timeMaintenanceInterval_s * 1000); + _maintenanceTimer.start(); + + // complete startup + _coreObject->Log.logEvent("info", QStringLiteral("performing maintenance every %1 seconds") + .arg(_coreObject->Settings.MaintenanceSettings.timeMaintenanceInterval_s)); + + return true; +} diff --git a/src/Maintenance/updatestats.cpp b/src/Maintenance/updatestats.cpp new file mode 100644 index 0000000..a19e7d9 --- /dev/null +++ b/src/Maintenance/updatestats.cpp @@ -0,0 +1,59 @@ +#include "maintenance.h" + +int Maintenance::updateStats() +{ + // result + int numOfUpdatedStats = 0; + + // get list of gamenames in database + QString selectGamenames = "SELECT DISTINCT gamename FROM serverlist"; + QSqlQuery gamenameQuery; + gamenameQuery.prepare(selectGamenames); + if ( ! gamenameQuery.exec() ) + return reportQuery(gamenameQuery); + + // update stats for every gamename + while ( gamenameQuery.next() ) + { + // get next gamename + QString gamename = gamenameQuery.value(0).toString(); + + // determine beacon and server counts + QString selectStats = "SELECT COUNT(CASE WHEN f_direct THEN 1 END) AS num_direct, " + "count(*) AS num_total " + "FROM serverlist " + "WHERE gamename = :gamename AND dt_updated > :timestamp"; + QSqlQuery statQuery; + statQuery.prepare(selectStats); + statQuery.bindValue(":gamename", gamename); + statQuery.bindValue(":timestamp", QDateTime::currentDateTime() + .addSecs(-_coreObject->Settings.ListenServerSettings.serverttl_s).toSecsSinceEpoch()); + if ( ! statQuery.exec() ) + return reportQuery(statQuery); + + // get values + int num_direct = -1; + int num_total = -1; + if ( statQuery.next() ) + { + num_direct = statQuery.value("num_direct").toInt(); + num_total = statQuery.value("num_total").toInt(); + } + + // write to db + QString updateStatQuery = "UPDATE gameinfo " + "SET num_direct = :num_direct, num_total = :num_total " + "WHERE gamename = :gamename "; + statQuery.prepare(updateStatQuery); + statQuery.bindValue(":num_direct", num_direct); + statQuery.bindValue(":num_total", num_total); + statQuery.bindValue(":gamename", gamename); + if ( ! statQuery.exec() ) + return reportQuery(statQuery); + + // update counter + numOfUpdatedStats += statQuery.numRowsAffected(); + } + + return numOfUpdatedStats; +} |
