diff --git a/.gitignore b/.gitignore index f2b235a..cfa208d 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,7 @@ vcpkg_installed/ out/ app.aps + +.idea/ + +external/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 1519ea6..2b13ec5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,10 @@ find_package(boost_program_options CONFIG REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(unofficial-minizip CONFIG REQUIRED) find_package(indicators CONFIG REQUIRED) +find_package(Drogon CONFIG REQUIRED) +find_package(libpqxx CONFIG REQUIRED) +find_package(yaml-cpp CONFIG REQUIRED) + # Add source to this project's executable. add_executable (${PROJECT_NAME} @@ -23,16 +27,24 @@ add_executable (${PROJECT_NAME} "src/sim_object.cpp" "src/tac_file.cpp" "src/utils.cpp" - "src/tac_file_load.cpp" + "src/tac_file_load.cpp" + "src/TAWAServer.cpp" + "src/data_service.cpp" + "src/server_configuration.cpp" app.rc ) target_include_directories(${PROJECT_NAME} PRIVATE "include") -target_link_libraries(${PROJECT_NAME} PRIVATE Boost::program_options) -target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) -target_link_libraries(${PROJECT_NAME} PRIVATE unofficial::minizip::minizip) -target_link_libraries(${PROJECT_NAME} PRIVATE indicators::indicators) +target_link_libraries(${PROJECT_NAME} PRIVATE + Boost::program_options + nlohmann_json::nlohmann_json + unofficial::minizip::minizip + indicators::indicators + Drogon::Drogon + libpqxx::pqxx + yaml-cpp::yaml-cpp +) include(InstallRequiredSystemLibraries) set(CPACK_PACKAGE_DIRECTORY ${CMAKE_BINARY_DIR}/package) diff --git a/docs/DB_schema.drawio b/docs/DB_schema.drawio new file mode 100644 index 0000000..3bbbb5b --- /dev/null +++ b/docs/DB_schema.drawio @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/include/TAWAServer.hpp b/include/TAWAServer.hpp new file mode 100644 index 0000000..0998ea0 --- /dev/null +++ b/include/TAWAServer.hpp @@ -0,0 +1,21 @@ +#pragma once +#include + +using namespace drogon; +namespace api { +namespace v1 { +class Pilot : public drogon::HttpController { + public: + METHOD_LIST_BEGIN + // use METHOD_ADD to add your custom processing function here; + METHOD_ADD(Pilot::getPilots, "/", Get); // path is /api/v1/Pilot/ + METHOD_LIST_END + // your declaration of processing function maybe like this: + void getPilots(const HttpRequestPtr &req, + std::function &&callback) const; + + public: + Pilot() = default; +}; +} // namespace v1 +} // namespace api \ No newline at end of file diff --git a/include/data_service.hpp b/include/data_service.hpp new file mode 100644 index 0000000..e5ad7f0 --- /dev/null +++ b/include/data_service.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class DataService { + private: + // Database connection parameters + std::string dbname; + std::string user; + std::string password; + std::string hostaddr; + uint16_t port; + std::vector> connection_pool; + std::queue connection_queue; + std::mutex connection_queue_lock; + + std::unique_ptr CreateConnection(); + + public: + DataService() = default; + DataService(std::string_view dbname, + std::string_view user, + std::string_view password, + std::string_view hostaddr, + uint16_t port); + ~DataService(); + + void Init(uint16_t count); + bool Test(); + void ReleaseConnection(pqxx::connection *conn); + pqxx::connection *GetConnection(); + std::unique_ptr ExecuteQuery(const std::string &query, const pqxx::params ¶ms = {}); +}; diff --git a/include/global.hpp b/include/global.hpp index 873aacb..28e7489 100644 --- a/include/global.hpp +++ b/include/global.hpp @@ -1,6 +1,14 @@ #pragma once + +#include "data_service.hpp" +#include "server_configuration.hpp" + #include #include +#include extern std::filesystem::path exe_path; extern std::filesystem::path exe_dir; + +extern std::unique_ptr data_service; +extern std::unique_ptr server_config; diff --git a/include/server_configuration.hpp b/include/server_configuration.hpp new file mode 100644 index 0000000..c0fa618 --- /dev/null +++ b/include/server_configuration.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +class ServerConfiguration { + public: + struct DatabaseConfig { + std::string host; + uint16_t port; + std::string username; + std::string password; + std::string database; + }; + + struct ServerConfig { + std::string host; + uint16_t port; + }; + + struct LogConfig { + std::filesystem::path log_directory; + int log_level; + }; + + ServerConfiguration() = default; + bool LoadYamlConfig(); + bool LoadYamlConfig(const std::filesystem::path &path); + DatabaseConfig database_config; + ServerConfig web_api_config; + LogConfig log_config; +}; diff --git a/sql_scripts/db_setup.sql b/sql_scripts/db_setup.sql new file mode 100644 index 0000000..b6be465 --- /dev/null +++ b/sql_scripts/db_setup.sql @@ -0,0 +1,58 @@ +CREATE TABLE Pilot ( + Id SERIAL PRIMARY KEY, + Username VARCHAR(255) NOT NULL +); + +CREATE UNIQUE INDEX idx_pilot_username ON Pilot(Username); + +CREATE TABLE ServerFile ( + Id SERIAL PRIMARY KEY, + Filename VARCHAR(255) NOT NULL, + FilePath VARCHAR(255) NOT NULL +); + +CREATE TABLE TacFile ( + Id SERIAL PRIMARY KEY, + StartTime TIMESTAMP NOT NULL, + File INT NOT NULL, + FOREIGN KEY (File) REFERENCES ServerFile(Id) +); + + +CREATE TABLE PilotRun ( + Id SERIAL PRIMARY KEY, + Team VARCHAR(10) CHECK (Team IN ('Red', 'Blue')), + PilotId INT NOT NULL, + StartTime TIMESTAMP NOT NULL, + StartTimeRel INTERVAL NOT NULL, + EndTime TIMESTAMP NOT NULL, + EndTimeRel INTERVAL NOT NULL, + TacFileId INT NOT NULL, + PositionFile INT, + FOREIGN KEY (PilotId) REFERENCES Pilot(Id), + FOREIGN KEY (TacFileId) REFERENCES TacFile(Id), + FOREIGN KEY (PositionFile) REFERENCES ServerFile(Id) +); + +CREATE INDEX idx_pilotrun_pilotid ON PilotRun(PilotId); +CREATE INDEX idx_pilotrun_tacfile ON PilotRun(TacFileId); + +CREATE TABLE Missile ( + Id SERIAL PRIMARY KEY, + Type VARCHAR(255) NOT NULL, + Team VARCHAR(10) CHECK (Team IN ('Red', 'Blue')), + LauncherId INT, + TargetId INT, + StartTime TIMESTAMP NOT NULL, + StartTimeRel INTERVAL NOT NULL, + EndTime TIMESTAMP NOT NULL, + EndTimeRel INTERVAL NOT NULL, + TacFileId INT NOT NULL, + PositionFile INT, + FOREIGN KEY (LauncherId) REFERENCES PilotRun(Id), + FOREIGN KEY (TargetId) REFERENCES PilotRun(Id), + FOREIGN KEY (TacFileId) REFERENCES TacFile(Id), + FOREIGN KEY (PositionFile) REFERENCES ServerFile(Id) +); + +CREATE INDEX idx_missile_tacfile ON Missile(TacFileId); \ No newline at end of file diff --git a/src/TAWAServer.cpp b/src/TAWAServer.cpp new file mode 100644 index 0000000..94b495c --- /dev/null +++ b/src/TAWAServer.cpp @@ -0,0 +1,42 @@ +#include "TAWAServer.hpp" + +#include "global.hpp" +#include "nlohmann/json.hpp" + +using json = nlohmann::json; + +void api::v1::Pilot::getPilots( + [[maybe_unused]] const HttpRequestPtr& req, + std::function&& callback) const { + auto qres = data_service->ExecuteQuery("SELECT t.* FROM main.pilot t LIMIT 501"); + if (!qres) { + auto resp = HttpResponse::newHttpResponse( + drogon::HttpStatusCode::k500InternalServerError, + drogon::CT_APPLICATION_JSON); + json res = { + {"result", "error"}, + {"message", "Internal Server Error"}, + }; + resp->setBody(res.dump()); + callback(resp); + return; + } else { + json pilotArray = {}; + for (auto row : *qres) { + json pilot = { + {"id", row["id"].as()}, + {"username", row["username"].as()} + }; + pilotArray.push_back(pilot); + } + json res = { + {"result", "ok"}, + {"pilots", pilotArray}, + }; + auto resp = HttpResponse::newHttpResponse(drogon::HttpStatusCode::k200OK, + drogon::CT_APPLICATION_JSON); + resp->setBody(res.dump()); + callback(resp); + return; + } +} diff --git a/src/data_service.cpp b/src/data_service.cpp new file mode 100644 index 0000000..13ca085 --- /dev/null +++ b/src/data_service.cpp @@ -0,0 +1,94 @@ +#include "data_service.hpp" + +#include "global.hpp" + +std::unique_ptr data_service; + +std::unique_ptr DataService::CreateConnection() { + std::string connectionStr = std::format( + "dbname={} user={} password={} " + "hostaddr={} port={}", + dbname, user, password, hostaddr, port); + std::unique_ptr cx = std::make_unique(connectionStr); + if (!cx->is_open()) { + return nullptr; + } + return cx; +} + +DataService::DataService(std::string_view dbname, + std::string_view user, + std::string_view password, + std::string_view hostaddr, + uint16_t port) + : dbname(dbname), user(user), password(password), hostaddr(hostaddr), port(port) {} + +DataService::~DataService() { + for (const auto &con : connection_pool) { + con->close(); + } +} + +void DataService::Init(uint16_t count) { + for (int i = 0; i < count; ++i) { + std::unique_ptr cx = CreateConnection(); + if (cx) { + connection_queue.push(cx.get()); + connection_pool.push_back(std::move(cx)); + } else { + std::cerr << "Failed to create connection." << std::endl; + return; + } + } +} + +bool DataService::Test() { + try { + std::unique_ptr cx(CreateConnection()); + if (cx) { + std::cout << "Connected to the database." << std::endl; + } else { + std::cerr << "Failed to connect to the database." << std::endl; + return false; + } + cx->close(); + return true; + } catch (const std::exception &e) { + std::cerr << "Exception: " << e.what() << std::endl; + return false; + } +} + +void DataService::ReleaseConnection(pqxx::connection *conn) { + std::scoped_lock lock(connection_queue_lock); + connection_queue.push(conn); +} + +pqxx::connection *DataService::GetConnection() { + std::scoped_lock lock(connection_queue_lock); + if (connection_queue.empty()) { + return nullptr; + } else { + pqxx::connection *conn = connection_queue.front(); + connection_queue.pop(); + return conn; + } +} + +std::unique_ptr DataService::ExecuteQuery(const std::string &query, const pqxx::params ¶ms) { + auto cx = GetConnection(); + if (!cx) { + std::cerr << "Failed to get a connection from the pool." << std::endl; + return nullptr; + } + std::unique_ptr res; + try { + pqxx::work tx{*cx}; + res = std::make_unique(tx.exec(query, params)); + tx.commit(); + } catch (const std::exception &e) { + std::cerr << "Query execution failed: " << e.what() << std::endl; + } + ReleaseConnection(cx); + return res; +} diff --git a/src/main.cpp b/src/main.cpp index 6a93bed..7632dde 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include @@ -6,6 +6,7 @@ #include #include "cache.hpp" +#include "data_service.hpp" #include "global.hpp" #include "tac_file.hpp" @@ -14,16 +15,51 @@ namespace po = boost::program_options; std::filesystem::path exe_path; std::filesystem::path exe_dir; -int main(int argc, char** argv) { +int main(int argc, char **argv) { exe_path = std::filesystem::path(argv[0]); exe_dir = exe_path.parent_path(); + server_config = std::make_unique(); + if (!server_config->LoadYamlConfig()) { + std::cerr << "Error loading configuration. Exiting..." << std::endl; + return 1; + } + + data_service = std::make_unique( + server_config->database_config.database, + server_config->database_config.username, + server_config->database_config.password, + server_config->database_config.host, server_config->database_config.port); + if (!data_service->Test()) { + std::cerr << "Database Connection is required. Exiting..." << std::endl; + return 1; + } + data_service->Init(10); + + std::filesystem::create_directory(server_config->log_config.log_directory); + std::filesystem::create_directory(server_config->log_config.log_directory / + "web"); + std::filesystem::create_directory(server_config->log_config.log_directory / + "server"); + drogon::app() + .setLogPath((server_config->log_config.log_directory / "web").string()) + .setLogLevel(static_cast( + server_config->log_config.log_level)) + .addListener(server_config->web_api_config.host, + server_config->web_api_config.port) + .setThreadNum(16) + .run(); + + while (true) { + Sleep(1000); + } + /*CacheObject cache; cache.LoadCache();*/ po::options_description opts_desc("Allowed options"); - opts_desc.add_options()("help", "")( - "input-file", po::value(), "TacView file to analyse"); + opts_desc.add_options()("help", "")("input-file", po::value(), + "TacView file to analyse"); po::positional_options_description pos_opts_desc; pos_opts_desc.add("input-file", 1); @@ -36,7 +72,7 @@ int main(int argc, char** argv) { .run(); po::store(parsed, vm); po::notify(vm); - } catch (const std::exception& e) { + } catch (const std::exception &e) { std::cerr << e.what() << "\n"; return 1; } @@ -60,7 +96,7 @@ int main(int argc, char** argv) { } try { tac_file->PrintPlayerRuns(std::cout, username); - } catch (const std::exception& e) { + } catch (const std::exception &e) { std::cerr << e.what() << "\n"; } } diff --git a/src/server_configuration.cpp b/src/server_configuration.cpp new file mode 100644 index 0000000..4e2c321 --- /dev/null +++ b/src/server_configuration.cpp @@ -0,0 +1,30 @@ +#include "server_configuration.hpp" + +#include "global.hpp" +#include "yaml-cpp/yaml.h" + +std::unique_ptr server_config; + +bool ServerConfiguration::LoadYamlConfig(const std::filesystem::path &path) { + try { + YAML::Node config = YAML::LoadFile(path.string()); + database_config.host = config["database"]["host"].as(); + database_config.port = config["database"]["port"].as(); + database_config.username = config["database"]["username"].as(); + database_config.password = config["database"]["password"].as(); + database_config.database = config["database"]["database"].as(); + web_api_config.host = config["server"]["host"].as(); + web_api_config.port = config["server"]["port"].as(); + log_config.log_directory = + exe_dir / config["logging"]["directory"].as(); + log_config.log_level = config["logging"]["level"].as(); + } catch (const YAML::Exception &e) { + std::cerr << "Error loading configuration: " << e.what() << std::endl; + return false; + } + return true; +} + +bool ServerConfiguration::LoadYamlConfig() { + return LoadYamlConfig(exe_dir / "config.yaml"); +} \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index 09336ec..5b0ea17 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -1,8 +1,11 @@ { "dependencies": [ "boost-program-options", + "drogon", "indicators", + "libpqxx", "minizip", - "nlohmann-json" + "nlohmann-json", + "yaml-cpp" ] }