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"
]
}