From d642de9462127a10bd83885610adebd34f9c69ec Mon Sep 17 00:00:00 2001 From: Jett <55758076+Jettford@users.noreply.github.com> Date: Sun, 10 Jul 2022 20:40:26 +0100 Subject: [PATCH] Implement a migration runner --- CMakeLists.txt | 17 ++++- README.md | 2 +- dCommon/GeneralUtils.cpp | 50 +++++++++++++++ dCommon/GeneralUtils.h | 2 + dDatabase/Database.cpp | 45 +++++++------ dDatabase/Database.h | 11 +++- dDatabase/MigrationRunner.cpp | 78 +++++++++++++++++++++++ dDatabase/MigrationRunner.h | 19 ++++++ dMasterServer/MasterServer.cpp | 8 +++ migrations/cdserver/3_plunger_gun_fix.sql | 1 - migrations/dlu/2_reporter_id.sql | 2 +- 11 files changed, 208 insertions(+), 27 deletions(-) create mode 100644 dDatabase/MigrationRunner.cpp create mode 100644 dDatabase/MigrationRunner.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 6cf311d2..5373dfe4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,8 @@ cmake_minimum_required(VERSION 3.14) project(Darkflame) include(CTest) +set (CMAKE_CXX_STANDARD 17) + # Read variables from file FILE(READ "${CMAKE_SOURCE_DIR}/CMakeVariables.txt" variables) @@ -88,8 +90,6 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DPROJECT_VERSION=${PROJECT_VERSION}") # Echo the version message(STATUS "Version: ${PROJECT_VERSION}") -set(CMAKE_CXX_STANDARD 17) - if(WIN32) add_compile_definitions(_CRT_SECURE_NO_WARNINGS) endif(WIN32) @@ -138,6 +138,19 @@ configure_file("${CMAKE_SOURCE_DIR}/vanity/INFO.md" "${CMAKE_BINARY_DIR}/vanity/ configure_file("${CMAKE_SOURCE_DIR}/vanity/TESTAMENT.md" "${CMAKE_BINARY_DIR}/vanity/TESTAMENT.md" COPYONLY) configure_file("${CMAKE_SOURCE_DIR}/vanity/NPC.xml" "${CMAKE_BINARY_DIR}/vanity/NPC.xml" COPYONLY) +# Move our migrations for MasterServer to run +file(MAKE_DIRECTORY ${PROJECT_BINARY_DIR}/migrations/) +file(GLOB SQL_FILES ${CMAKE_SOURCE_DIR}/migrations/dlu/*.sql) +foreach (file ${SQL_FILES}) + get_filename_component(file ${file} NAME) + if (NOT EXISTS ${PROJECT_BINARY_DIR}/migrations/${file}) + configure_file( + ${CMAKE_SOURCE_DIR}/migrations/dlu/${file} ${PROJECT_BINARY_DIR}/migrations/${file} + COPYONLY + ) + endif() +endforeach() + # 3rdparty includes include_directories(${PROJECT_SOURCE_DIR}/thirdparty/raknet/Source/) if(APPLE) diff --git a/README.md b/README.md index 6b82e1b0..d029ed93 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ Darkflame Universe utilizes a MySQL/MariaDB database for account and character i Initial setup can vary drastically based on which operating system or distribution you are running; there are instructions out there for most setups, follow those and come back here when you have a database up and running. * Create a database for Darkflame Universe to use -* Run each SQL file in the order at which they appear [here](migrations/dlu/) on the database +* Run the migrations by running `./MasterServer -m` to automatically run them ### Resources diff --git a/dCommon/GeneralUtils.cpp b/dCommon/GeneralUtils.cpp index b3461e8a..01306226 100644 --- a/dCommon/GeneralUtils.cpp +++ b/dCommon/GeneralUtils.cpp @@ -188,3 +188,53 @@ std::u16string GeneralUtils::ReadWString(RakNet::BitStream *inStream) { return string; } + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include + +std::vector GeneralUtils::GetFileNamesFromFolder(const std::string& folder) +{ + std::vector names; + std::string search_path = folder + "/*.*"; + WIN32_FIND_DATA fd; + HANDLE hFind = ::FindFirstFile(search_path.c_str(), &fd); + if (hFind != INVALID_HANDLE_VALUE) { + do { + if (!(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { + names.push_back(fd.cFileName); + } + } while (::FindNextFile(hFind, &fd)); + ::FindClose(hFind); + } + return names; +} +#else +#include +#include +#include +#include +#include +#include + +std::vector GeneralUtils::GetFileNamesFromFolder(const std::string& folder) { + std::vector names; + struct dirent* entry; + DIR* dir = opendir(folder.c_str()); + if (dir == NULL) { + return names; + } + + while ((entry = readdir(dir)) != NULL) { + std::string value(entry->d_name, strlen(entry->d_name)); + if (value == "." || value == "..") { + continue; + } + names.push_back(value); + } + + closedir(dir); + + return names; +} +#endif \ No newline at end of file diff --git a/dCommon/GeneralUtils.h b/dCommon/GeneralUtils.h index 58b9e962..4973201e 100644 --- a/dCommon/GeneralUtils.h +++ b/dCommon/GeneralUtils.h @@ -128,6 +128,8 @@ namespace GeneralUtils { std::vector SplitString(const std::string& str, char delimiter); + std::vector GetFileNamesFromFolder(const std::string& folder); + template T Parse(const char* value); diff --git a/dDatabase/Database.cpp b/dDatabase/Database.cpp index ef4faa52..26a45359 100644 --- a/dDatabase/Database.cpp +++ b/dDatabase/Database.cpp @@ -8,6 +8,8 @@ using namespace std; sql::Driver * Database::driver; sql::Connection * Database::con; +sql::Properties Database::props; +std::string Database::database; void Database::Connect(const string& host, const string& database, const string& username, const string& password) { @@ -25,14 +27,26 @@ void Database::Connect(const string& host, const string& database, const string& properties["user"] = szUsername; properties["password"] = szPassword; properties["autoReconnect"] = "true"; - con = driver->connect(properties); - con->setSchema(szDatabase); -} //Connect -void Database::Destroy(std::string source) { + Database::props = properties; + Database::database = database; + + Database::Connect(); +} + +void Database::Connect() { + con = driver->connect(Database::props); + con->setSchema(Database::database); +} + +void Database::Destroy(std::string source, bool log) { if (!con) return; - if (source != "") Game::logger->Log("Database", "Destroying MySQL connection from %s!\n", source.c_str()); - else Game::logger->Log("Database", "Destroying MySQL connection!\n"); + + if (log) { + if (source != "") Game::logger->Log("Database", "Destroying MySQL connection from %s!\n", source.c_str()); + else Game::logger->Log("Database", "Destroying MySQL connection!\n"); + } + con->close(); delete con; } //Destroy @@ -48,13 +62,7 @@ sql::PreparedStatement* Database::CreatePreppedStmt(const std::string& query) { sql::SQLString str(test, size); if (!con) { - //Connect to the MySQL Database - std::string mysql_host = Game::config->GetValue("mysql_host"); - std::string mysql_database = Game::config->GetValue("mysql_database"); - std::string mysql_username = Game::config->GetValue("mysql_username"); - std::string mysql_password = Game::config->GetValue("mysql_password"); - - Connect(mysql_host, mysql_database, mysql_username, mysql_password); + Connect(); Game::logger->Log("Database", "Trying to reconnect to MySQL\n"); } @@ -64,13 +72,7 @@ sql::PreparedStatement* Database::CreatePreppedStmt(const std::string& query) { con = nullptr; - //Connect to the MySQL Database - std::string mysql_host = Game::config->GetValue("mysql_host"); - std::string mysql_database = Game::config->GetValue("mysql_database"); - std::string mysql_username = Game::config->GetValue("mysql_username"); - std::string mysql_password = Game::config->GetValue("mysql_password"); - - Connect(mysql_host, mysql_database, mysql_username, mysql_password); + Connect(); Game::logger->Log("Database", "Trying to reconnect to MySQL from invalid or closed connection\n"); } @@ -79,3 +81,6 @@ sql::PreparedStatement* Database::CreatePreppedStmt(const std::string& query) { return stmt; } //CreatePreppedStmt +void Database::Commit() { + Database::con->commit(); +} \ No newline at end of file diff --git a/dDatabase/Database.h b/dDatabase/Database.h index e972b0ca..ece62a95 100644 --- a/dDatabase/Database.h +++ b/dDatabase/Database.h @@ -13,10 +13,17 @@ class Database { private: static sql::Driver *driver; static sql::Connection *con; - + static sql::Properties props; + static std::string database; public: static void Connect(const std::string& host, const std::string& database, const std::string& username, const std::string& password); - static void Destroy(std::string source=""); + static void Connect(); + static void Destroy(std::string source = "", bool log = true); + static sql::Statement* CreateStmt(); static sql::PreparedStatement* CreatePreppedStmt(const std::string& query); + static void Commit(); + + static std::string GetDatabase() { return database; } + static sql::Properties GetProperties() { return props; } }; diff --git a/dDatabase/MigrationRunner.cpp b/dDatabase/MigrationRunner.cpp new file mode 100644 index 00000000..a058b85e --- /dev/null +++ b/dDatabase/MigrationRunner.cpp @@ -0,0 +1,78 @@ +#include "MigrationRunner.h" + +#include "GeneralUtils.h" + +#include +#include +#include + +void MigrationRunner::RunMigrations() { + auto stmt = Database::CreatePreppedStmt("CREATE TABLE IF NOT EXISTS migration_history (name TEXT NOT NULL, date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP());"); + stmt->executeQuery(); + delete stmt; + + sql::SQLString finalSQL = ""; + Migration checkMigration{}; + + for (const auto& entry : GeneralUtils::GetFileNamesFromFolder("./migrations/")) { + auto migration = LoadMigration(entry); + + if (migration.data.empty()) { + continue; + } + + checkMigration = migration; + + stmt = Database::CreatePreppedStmt("SELECT name FROM migration_history WHERE name = ?;"); + stmt->setString(1, migration.name); + auto res = stmt->executeQuery(); + bool doExit = res->next(); + delete res; + delete stmt; + if (doExit) continue; + + Game::logger->Log("MigrationRunner", "Running migration: " + migration.name + "\n"); + + finalSQL.append(migration.data); + finalSQL.append('\n'); + + stmt = Database::CreatePreppedStmt("INSERT INTO migration_history (name) VALUES (?);"); + stmt->setString(1, entry); + stmt->execute(); + delete stmt; + } + + if (!finalSQL.empty()) { + try { + auto simpleStatement = Database::CreateStmt(); + simpleStatement->execute(finalSQL); + delete simpleStatement; + } + catch (sql::SQLException e) { + Game::logger->Log("MigrationRunner", std::string("Encountered error running migration: ") + e.what() + "\n"); + } + } +} + +Migration MigrationRunner::LoadMigration(std::string path) { + Migration migration{}; + std::ifstream file("./migrations/" + path); + + if (file.is_open()) { + std::hash hash; + + std::string line; + std::string total = ""; + + while (std::getline(file, line)) { + total += line; + } + + file.close(); + + migration.name = path; + migration.data = total; + } + + return migration; +} diff --git a/dDatabase/MigrationRunner.h b/dDatabase/MigrationRunner.h new file mode 100644 index 00000000..343b252d --- /dev/null +++ b/dDatabase/MigrationRunner.h @@ -0,0 +1,19 @@ +#pragma once + +#include "Database.h" + +#include "dCommonVars.h" +#include "Game.h" +#include "dCommonVars.h" +#include "dLogger.h" + +struct Migration { + std::string data; + std::string name; +}; + +class MigrationRunner { +public: + static void RunMigrations(); + static Migration LoadMigration(std::string path); +}; \ No newline at end of file diff --git a/dMasterServer/MasterServer.cpp b/dMasterServer/MasterServer.cpp index ac49b1a1..1cfd7d4a 100644 --- a/dMasterServer/MasterServer.cpp +++ b/dMasterServer/MasterServer.cpp @@ -19,6 +19,7 @@ #include "CDClientDatabase.h" #include "CDClientManager.h" #include "Database.h" +#include "MigrationRunner.h" #include "Diagnostics.h" #include "dCommonVars.h" #include "dConfig.h" @@ -127,6 +128,13 @@ int main(int argc, char** argv) { return EXIT_FAILURE; } + if (argc > 1 && (strcmp(argv[1], "-m") == 0 || strcmp(argv[1], "--migrations") == 0)) { + MigrationRunner::RunMigrations(); + Game::logger->Log("MigrationRunner", "Finished running migrations\n"); + + return EXIT_SUCCESS; + } + //If the first command line argument is -a or --account then make the user //input a username and password, with the password being hidden. if (argc > 1 && diff --git a/migrations/cdserver/3_plunger_gun_fix.sql b/migrations/cdserver/3_plunger_gun_fix.sql index 35654e8b..3d33592e 100644 --- a/migrations/cdserver/3_plunger_gun_fix.sql +++ b/migrations/cdserver/3_plunger_gun_fix.sql @@ -1,2 +1 @@ --- File added April 9th, 2022 UPDATE ItemComponent SET itemType = 5 where id = 7082; diff --git a/migrations/dlu/2_reporter_id.sql b/migrations/dlu/2_reporter_id.sql index 26103342..dc2a9a7e 100644 --- a/migrations/dlu/2_reporter_id.sql +++ b/migrations/dlu/2_reporter_id.sql @@ -1 +1 @@ -ALTER TABLE bug_reports ADD reporter_id INT NOT NULL DEFAULT 0; +ALTER TABLE bug_reports ADD (reporter_id) INT NOT NULL DEFAULT 0;