Skip to content
61 changes: 21 additions & 40 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ configure_file("generated/version.hpp.in" ${CMAKE_BINARY_DIR}/generated/version.
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

option(ENABLE_XMPP_JID "Build XMPP JID support" ON)

# Find Packages - vcpkg provides standard CMake target configs
find_package(asio CONFIG REQUIRED)
find_package(OpenSSL REQUIRED)
Expand All @@ -43,24 +45,6 @@ find_package(UserModules QUIET)
# option(ENABLE_STORAGE_SQLITE "Include SQLite storage backend" OFF)
# option(ENABLE_STORAGE_POSTGRES "Include PostgreSQL storage backend" OFF)

# Core Server Target
# add_library(xtrpg_core STATIC
# )
# target_include_directories(xtrpg_core PUBLIC include)

# Conditional Storage Compilation
# if(ENABLE_STORAGE_SQLITE)
# find_package(SQLite3 REQUIRED)
# target_sources(xtrpg_core PRIVATE src/storage/SQLiteBackend.cpp)
# target_compile_definitions(xtrpg_core PUBLIC HAS_STORAGE_SQLITE)
# target_link_libraries(xtrpg_core PRIVATE SQLite::SQLite3)
# endif()

# if(ENABLE_STORAGE_MEMORY)
# target_sources(xtrpg_core PRIVATE src/storage/MemoryBackend.cpp)
# target_compile_definitions(xtrpg_core PUBLIC HAS_STORAGE_MEMORY)
# endif()

# Include discovered modules into the build
if(UserModules_FOUND)
message(STATUS "Integrating User Modules:")
Expand All @@ -73,19 +57,27 @@ if(UserModules_FOUND)
endforeach()
endif()

# Config LIB
add_library(xtrpg_config STATIC
src/config/ConfigManager.cpp
# Single executable target
add_executable(xtrpg_cpp_server
apps/main.cpp
)
target_include_directories(xtrpg_cpp_server PRIVATE
include
${CMAKE_BINARY_DIR}/generated
)
target_include_directories(xtrpg_config PUBLIC include)

# Network LIB
add_library(xtrpg_network STATIC
target_sources(xtrpg_cpp_server PRIVATE
src/config/ConfigManager.cpp
src/network/SocketConnectionListener.cpp
src/network/TcpConnection.cpp
src/xml/tokenizer/XmlStreamTokenizer.cpp
src/xmpp/ClientConnectionManager.cpp
src/xmpp/session/ClientSession.cpp
src/xmpp/Jid.cpp
)
target_include_directories(xtrpg_network PUBLIC include)
target_link_libraries(xtrpg_network PUBLIC


target_link_libraries(xtrpg_cpp_server PRIVATE
asio::asio
OpenSSL::SSL
OpenSSL::Crypto
Expand All @@ -94,27 +86,16 @@ if(WIN32)
# Windows-specific OS network primitives for raw sockets
# Define minimum Windows version (0x0601 = Windows 7, 0x0A00 = Windows 10)
# 0x0A00 unlocks modern Windows socket features for Asio
target_compile_definitions(xtrpg_network PUBLIC
target_compile_definitions(xtrpg_cpp_server PRIVATE
_WIN32_WINNT=0x0A00
WINVER=0x0A00
)
target_link_libraries(xtrpg_network PUBLIC ws2_32 wsock32 crypt32)
target_link_libraries(xtrpg_cpp_server PRIVATE ws2_32 wsock32 crypt32)
elseif(UNIX AND NOT APPLE)
# Platform-specific OS network primitives for raw sockets
target_link_libraries(xtrpg_network PUBLIC pthread dl)
target_link_libraries(xtrpg_cpp_server PRIVATE pthread dl)
endif()

# XMPP LIB
add_library(xtrpg_xmpp STATIC
src/xmpp/Jid.cpp
)
target_include_directories(xtrpg_xmpp PUBLIC include)

# Executable Target
add_executable(xtrpg_cpp_server apps/main.cpp)
target_link_libraries(xtrpg_cpp_server PRIVATE xtrpg_config xtrpg_network xtrpg_xmpp)
target_include_directories(xtrpg_cpp_server PRIVATE ${CMAKE_BINARY_DIR}/generated)

# If modules produce targets registered via xmpp_register_user_module
if(USER_MODULE_TARGETS)
target_link_libraries(xtrpg_cpp_server PRIVATE ${USER_MODULE_TARGETS})
Expand Down
90 changes: 90 additions & 0 deletions apps/main.cpp
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
#include <atomic>
#include <fstream>
#include <iostream>
#include <memory>
#include <signal.h>
#include <thread>
#include <vector>

#include <asio.hpp>

#include "version.hpp"
#include "xtrpg/config/ConfigManager.hpp"
#include "xtrpg/interface/Observer.hpp"
#include "xtrpg/network/SocketConnectionListener.hpp"
#include "xtrpg/xml/tokenizer/XmlStreamTokenizer.hpp"
#include "xtrpg/xmpp/ClientConnectionManager.hpp"
#include "xtrpg/xmpp/session/ClientSession.hpp"

#ifdef _WIN32
#define _WINSOCKAPI_
#include <windows.h>
#endif

// Global flag for signal handling
std::atomic<bool> g_shouldShutdown{false};

// Signal handler for graceful shutdown
void signalHandler(int signal) {
std::cout << "\nReceived signal " << signal
<< ", initiating graceful shutdown..." << std::endl;
g_shouldShutdown = true;
}

int main(int argc, char *argv[]) {
#ifdef _WIN32
// Set console codepages to UTF-8 (65001)
Expand Down Expand Up @@ -45,11 +67,79 @@ int main(int argc, char *argv[]) {
}
configManager.parseCLI(argc, argv);

// Initialize Asio IO context for async I/O operations
asio::io_context ioContext;

// Create temporary connection handler to listen for incoming connections
// and create ClientSession instances
xtrpg::xmpp::ClientConnectionManager connectionManager(ioContext);

// Get the port from configuration (default 5222 for XMPP C2S)
auto portValue = configManager.get<int64_t>("c2s", "port").value_or(5222);
uint16_t listeningPort = static_cast<uint16_t>(portValue);

// Initialize socket connection listener
std::cout << "[INFO] Starting XMPP Client-to-Server (C2S) listener on port "
<< listeningPort << std::endl;
xtrpg::network::SocketConnectionListener listener(ioContext, listeningPort);

// Register connection handler as observer for incoming connections
listener.setObserver(&connectionManager);

// Start accepting connections
listener.start();

// Set up signal handlers for graceful shutdown (SIGINT and SIGTERM)
#ifdef _WIN32
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
#else
signal(SIGINT, signalHandler);
signal(SIGTERM, signalHandler);
signal(SIGHUP, signalHandler);
#endif

std::cout << "[INFO] Server running. Press Ctrl+C to shutdown."
<< std::endl;

// Run IO context in a thread pool for handling async operations
std::vector<std::thread> ioThreads;
const size_t threadCount = std::thread::hardware_concurrency();
for (size_t i = 0; i < threadCount; ++i) {
ioThreads.emplace_back([&ioContext]() { ioContext.run(); });
}

// Main thread: wait for shutdown signal
while (!g_shouldShutdown) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

// Graceful shutdown: stop listener and wait for pending operations
std::cout << "[INFO] Stopping listener..." << std::endl;
listener.stop();
listener.setObserver(nullptr);

std::cout << "[INFO] Shutting down IO context..." << std::endl;
ioContext.stop();

// Wait for all IO threads to complete
for (auto &thread : ioThreads) {
if (thread.joinable()) {
thread.join();
}
}

std::cout << "[INFO] Server shutdown complete." << std::endl;

} catch (const std::exception &e) {
std::cerr << "EXCEPTION OCCURRED" << std::endl
<< "Application closing die to \"" << e.what() << "\"."
<< std ::endl;
return 1;
} catch (...) {
std::cerr << "UNEXPECTED EXCEPTION OCCURRED" << std::endl
<< "Application closing." << std ::endl;
return 1;
}

return 0;
Expand Down
48 changes: 13 additions & 35 deletions include/xtrpg/interface/Observable.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,26 @@
namespace xtrpg::interface {
template <typename TContext> class Observable {
public:
virtual ~Observable() = default;

void addObserver(const std::shared_ptr<Observer<TContext>> &ptr) {
if (ptr) {
this->_observers.push_back(ptr);
}
}

void eraseObserver(const Observer<TContext> *pTarget) {
if (!pTarget) {
return;
virtual ~Observable() {
if (nullptr != this->_ptrObserver) {
std::cerr << "Observable not removed from an instance. This may lead to "
"memory leaks."
<< std::endl;
}
};

std::erase_if(this->_observers,
[pTarget](const std::weak_ptr<Observer<TContext>> &wp) {
auto sp = wp.lock();
return !sp || sp.get() == pTarget;
});
}

void clearObservers() { this->_observers.clear(); }
void setObserver(Observer<TContext> *ptr) { this->_ptrObserver = ptr; }

protected:
void dispatchObservation(TContext &ctx) {
std::erase_if(this->_observers,
[&ctx](const std::weak_ptr<Observer<TContext>> &wp) {
// Attempt to gain temporary ownership of the current
// observer.
if (auto observer = wp.lock()) {
// Dispatch the observation.
observer->onObservation(ctx);

// Pointer is still valid, keep it in the vector.
return false;
}
if (nullptr == this->_ptrObserver) {
return;
}

// Pointer has expired, remove it from the observers vector.
return true;
});
this->_ptrObserver->onObservation(ctx);
}

private:
std::vector<std::weak_ptr<Observer<TContext>>> _observers;
Observer<TContext> *_ptrObserver;
};
} // namespace xtrpg
} // namespace xtrpg::interface
8 changes: 7 additions & 1 deletion include/xtrpg/network/TcpConnection.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ enum class ConnectionState {
* Represents a TCP connection that can be upgraded to TLS. It provides methods
* to write data to the connection, read data from the connection and close it.
*/
class TcpConnection : public std::enable_shared_from_this<TcpConnection> {
class TcpConnection {

public:
/**
Expand All @@ -53,6 +53,12 @@ class TcpConnection : public std::enable_shared_from_this<TcpConnection> {
*/
void upgrade(asio::ssl::context &ssl_ctx);

/**
* Async read from the underlying tcp connection, calling the provided lambda
* function with a new istream of the incoming stream data.
*/
void read(std::function<void(std::istream &)> callback);

/**
* Writes data to the connection. If the connection is closed, it will throw
* an exception.
Expand Down
8 changes: 2 additions & 6 deletions include/xtrpg/xml/node/TagNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
#include <vector>

#include "xtrpg/xml/node/IAttributes.hpp"
#include "xtrpg/xml/node/INode.hpp"
#include "xtrpg/xml/node/ITagname.hpp"
#include "xtrpg/xml/node/NodeContainer.hpp"
#include "xtrpg/xml/node/NodeType.hpp"
Expand All @@ -23,16 +22,13 @@ namespace xtrpg::xml::node {
/**
* Represents an XML element with a tag name, attributes, and child nodes.
*/
class TagNode : public INode,
public ITagname,
public IAttributes,
public NodeContainer {
class TagNode : public ITagname, public IAttributes, public NodeContainer {
public:
/**
* Inline constructor that accepts a tag name.
*/
explicit TagNode(std::string name)
: INode(NodeType::TAG), ITagname(name), IAttributes(), NodeContainer() {}
: ITagname(name), IAttributes(), NodeContainer(NodeType::TAG) {}

/**
* Explicitly defaulted copy constructor.
Expand Down
24 changes: 17 additions & 7 deletions include/xtrpg/xml/tokenizer/XmlStreamTokenizer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <vector>

#include "xtrpg/xml/tokenizer/TokenizationError.hpp"
#include "xtrpg/xml/tokenizer/XmlToken.hpp"
#include "xtrpg/xml/tokenizer/XmlTokenListener.hpp"

#ifndef __TOKENIZER_MAX_BUFFER_SIZE_IN_CHARS
Expand All @@ -20,19 +21,25 @@ namespace xtrpg::xml::tokenizer {
*/
class XmlStreamTokenizer {
public:
~XmlStreamTokenizer() {
if (nullptr != this->_ptrObserver) {
std::cerr << "Observable not removed from an instance of "
"XmlStreamTokenizer. This may lead to "
"memory leaks."
<< std::endl;
}
};

/**
* Consumes the data on the provided stream until it's exhausted.
*/
void process(std::istream &stream);

/**
* Defines a instance to act as the listener for this class.
*/
void setListener(std::shared_ptr<XmlTokenListener> listener) {
_listener = listener;
}
void setObserver(XmlTokenListener *ptr) { this->_ptrObserver = ptr; }

private:
XmlTokenListener *_ptrObserver = nullptr;

enum class State {
TEXT,
AFTER_OPEN,
Expand Down Expand Up @@ -69,7 +76,10 @@ class XmlStreamTokenizer {

TokenizationError _error{TokenizationError::NONE};

std::weak_ptr<XmlTokenListener> _listener;
/**
* The current token being parsed.
*/
XmlToken _currentToken{};
};

} // namespace xtrpg::xml::tokenizer
Loading
Loading