From 098afeb4d83ab48f5ff924ac571afb9e6ab5fbfe Mon Sep 17 00:00:00 2001 From: DcruBro Date: Sun, 9 Nov 2025 01:35:19 +0100 Subject: [PATCH] Relatively graceful shutdowns --- README.md | 8 ++ .../columnlynx/client/net/tcp/tcp_client.hpp | 12 ++- .../server/net/tcp/tcp_connection.hpp | 2 + .../columnlynx/server/net/tcp/tcp_server.hpp | 7 +- .../columnlynx/server/net/udp/udp_server.hpp | 7 +- res/ColumnLynxFull.png | Bin 0 -> 22059 bytes res/ColumnLynxIcon.png | Bin 0 -> 17068 bytes src/client/main.cpp | 2 +- src/server/main.cpp | 52 ++++++++++++- src/server/server/net/tcp/tcp_server.cpp | 69 ++++++++++++++---- src/server/server/net/udp/udp_server.cpp | 25 ++++++- 11 files changed, 154 insertions(+), 30 deletions(-) create mode 100644 res/ColumnLynxFull.png create mode 100644 res/ColumnLynxIcon.png diff --git a/README.md b/README.md index 8f45336..066b5f8 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,14 @@ ColumnLynx makes use of both **TCP** and **UDP**. **TCP** is used for the initia It operates on port **48042** for both TCP and UDP. +### Handshake Procedure + +*wip* + +### Packet Exchange + +*wip* + ## Packet Structure These are the general packet structures for both the TCP and UDP sides of the protocol. Generally **headers** are **plain-text (unencrypted)** and do not contain any sensitive data. diff --git a/include/columnlynx/client/net/tcp/tcp_client.hpp b/include/columnlynx/client/net/tcp/tcp_client.hpp index d94eac0..f5c2b32 100644 --- a/include/columnlynx/client/net/tcp/tcp_client.hpp +++ b/include/columnlynx/client/net/tcp/tcp_client.hpp @@ -69,9 +69,11 @@ namespace ColumnLynx::Net::TCP { } } - void disconnect() { + void disconnect(bool echo = true) { if (mConnected && mHandler) { - mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye"); + if (echo) { + mHandler->sendMessage(ClientMessageType::GRACEFUL_DISCONNECT, "Goodbye"); + } asio::error_code ec; @@ -94,6 +96,10 @@ namespace ColumnLynx::Net::TCP { return mHandshakeComplete; } + bool isConnected() const { + return mConnected; + } + private: void mHandleMessage(ServerMessageType type, const std::string& data) { switch (type) { @@ -187,7 +193,7 @@ namespace ColumnLynx::Net::TCP { case ServerMessageType::GRACEFUL_DISCONNECT: Utils::log("Server is disconnecting: " + data); if (mConnected) { // Prevent Recursion - disconnect(); + disconnect(false); } break; default: diff --git a/include/columnlynx/server/net/tcp/tcp_connection.hpp b/include/columnlynx/server/net/tcp/tcp_connection.hpp index e321008..75c63bd 100644 --- a/include/columnlynx/server/net/tcp/tcp_connection.hpp +++ b/include/columnlynx/server/net/tcp/tcp_connection.hpp @@ -61,6 +61,8 @@ namespace ColumnLynx::Net::TCP { void disconnect() { std::string ip = mHandler->socket().remote_endpoint().address().to_string(); + mHandler->sendMessage(ServerMessageType::GRACEFUL_DISCONNECT, "Server initiated disconnect."); + asio::error_code ec; mHandler->socket().shutdown(asio::ip::tcp::socket::shutdown_both, ec); mHandler->socket().close(ec); diff --git a/include/columnlynx/server/net/tcp/tcp_server.hpp b/include/columnlynx/server/net/tcp/tcp_server.hpp index ee6397e..381ed39 100644 --- a/include/columnlynx/server/net/tcp/tcp_server.hpp +++ b/include/columnlynx/server/net/tcp/tcp_server.hpp @@ -21,19 +21,22 @@ namespace ColumnLynx::Net::TCP { class TCPServer { public: - TCPServer(asio::io_context& ioContext, uint16_t port, Utils::LibSodiumWrapper* sodiumWrapper) - : mIoContext(ioContext), mAcceptor(ioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), mSodiumWrapper(sodiumWrapper) + TCPServer(asio::io_context& ioContext, uint16_t port, Utils::LibSodiumWrapper* sodiumWrapper, bool* hostRunning) + : mIoContext(ioContext), mAcceptor(ioContext, asio::ip::tcp::endpoint(asio::ip::tcp::v4(), port)), mSodiumWrapper(sodiumWrapper), mHostRunning(hostRunning) { Utils::log("Started TCP server on port " + std::to_string(port)); mStartAccept(); } + void stop(); + private: void mStartAccept(); asio::io_context &mIoContext; asio::ip::tcp::acceptor mAcceptor; std::unordered_set mClients; Utils::LibSodiumWrapper *mSodiumWrapper; + bool* mHostRunning; }; } \ No newline at end of file diff --git a/include/columnlynx/server/net/udp/udp_server.hpp b/include/columnlynx/server/net/udp/udp_server.hpp index 95c25b7..ce894c3 100644 --- a/include/columnlynx/server/net/udp/udp_server.hpp +++ b/include/columnlynx/server/net/udp/udp_server.hpp @@ -12,13 +12,15 @@ namespace ColumnLynx::Net::UDP { class UDPServer { public: - UDPServer(asio::io_context& ioContext, uint16_t port) - : mSocket(ioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)) + UDPServer(asio::io_context& ioContext, uint16_t port, bool* hostRunning) + : mSocket(ioContext, asio::ip::udp::endpoint(asio::ip::udp::v4(), port)), mHostRunning(hostRunning) { Utils::log("Started UDP server on port " + std::to_string(port)); mStartReceive(); } + void stop(); + private: void mStartReceive(); void mHandlePacket(std::size_t bytes); @@ -26,5 +28,6 @@ namespace ColumnLynx::Net::UDP { asio::ip::udp::socket mSocket; asio::ip::udp::endpoint mRemoteEndpoint; std::array mRecvBuffer; // Adjust size as needed + bool* mHostRunning; }; } \ No newline at end of file diff --git a/res/ColumnLynxFull.png b/res/ColumnLynxFull.png new file mode 100644 index 0000000000000000000000000000000000000000..badb4463949757fa401a101719ba4395a56ba127 GIT binary patch literal 22059 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 literal 0 HcmV?d00001 diff --git a/res/ColumnLynxIcon.png b/res/ColumnLynxIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..7a2a7764f1517e0762e3b70edd9b842d2d67bb37 GIT binary patch literal 17068 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 zfBhisHandshakeComplete()) { + if (client->isHandshakeComplete() && client->isConnected()) { // Send a test UDP message every 5 seconds after handshake is complete static auto lastSendTime = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now(); diff --git a/src/server/main.cpp b/src/server/main.cpp index 392a21b..25a94c1 100644 --- a/src/server/main.cpp +++ b/src/server/main.cpp @@ -16,11 +16,25 @@ using namespace ColumnLynx::Utils; using namespace ColumnLynx::Net::TCP; using namespace ColumnLynx::Net::UDP; +volatile sig_atomic_t done = 0; + +/*void signalHandler(int signum) { + if (signum == SIGINT || signum == SIGTERM) { + log("Received termination signal. Shutting down server gracefully."); + done = 1; + } +}*/ + int main(int argc, char** argv) { PanicHandler::init(); try { - // TODO: Catch SIGINT and SIGTERM for graceful shutdown + // Catch SIGINT and SIGTERM for graceful shutdown + /*struct sigaction action; + memset(&action, 0, sizeof(struct sigaction)); + action.sa_handler = signalHandler; + sigaction(SIGINT, &action, nullptr); + sigaction(SIGTERM, &action, nullptr);*/ log("ColumnLynx Server, Version " + getVersion()); log("This software is licensed under the GPLv3. See LICENSE for details."); @@ -30,18 +44,48 @@ int main(int argc, char** argv) { log("Server public key: " + bytesToHexString(sodiumWrapper.getPublicKey(), crypto_sign_PUBLICKEYBYTES)); log("Server private key: " + bytesToHexString(sodiumWrapper.getPrivateKey(), crypto_sign_SECRETKEYBYTES)); // TEMP, remove later + bool hostRunning = true; + asio::io_context io; - auto server = std::make_shared(io, serverPort(), &sodiumWrapper); - auto udpServer = std::make_shared(io, serverPort()); + + auto server = std::make_shared(io, serverPort(), &sodiumWrapper, &hostRunning); + auto udpServer = std::make_shared(io, serverPort(), &hostRunning); + + asio::signal_set signals(io, SIGINT, SIGTERM); + signals.async_wait([&](const std::error_code&, int) { + log("Received termination signal. Shutting down server gracefully."); + done = 1; + asio::post(io, [&]() { + hostRunning = false; + server->stop(); + udpServer->stop(); + }); + }); // Run the IO context in a separate thread std::thread ioThread([&io]() { io.run(); }); - ioThread.join(); + //ioThread.detach(); log("Server started on port " + std::to_string(serverPort())); + + while (!done) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + log("Shutting down server..."); + /*hostRunning = false; + server->stop(); + udpServer->stop();*/ + + io.stop(); + if (ioThread.joinable()) { + ioThread.join(); + } + + log("Server stopped."); } catch (const std::exception& e) { error("Server error: " + std::string(e.what())); } diff --git a/src/server/server/net/tcp/tcp_server.cpp b/src/server/server/net/tcp/tcp_server.cpp index 73bb5d7..1a2b013 100644 --- a/src/server/server/net/tcp/tcp_server.cpp +++ b/src/server/server/net/tcp/tcp_server.cpp @@ -20,27 +20,66 @@ namespace ColumnLynx::Net::TCP { void TCPServer::mStartAccept() { + // A bit of a shotty implementation, might improve later + /*std::cout << "Host running pointer: " << *mHostRunning << std::endl; + + if (mHostRunning != nullptr && !(*mHostRunning)) { + Utils::log("Server is stopping, not accepting new connections."); + return; + }*/ + mAcceptor.async_accept( [this](asio::error_code ec, asio::ip::tcp::socket socket) { - if (!NetHelper::isExpectedDisconnect(ec)) { - auto client = TCPConnection::create(std::move(socket), - mSodiumWrapper, - [this](std::shared_ptr c) { - mClients.erase(c); - Utils::log("Client removed."); - }); - - mClients.insert(client); - client->start(); - - Utils::log("Accepted new client connection."); - } else { + if (ec) { + if (ec == asio::error::operation_aborted) { + // Acceptor was cancelled/closed during shutdown + return; + } Utils::error("Accept failed: " + ec.message()); + // Try again only if still running + if (mHostRunning && *mHostRunning && mAcceptor.is_open()) + mStartAccept(); + return; } - - TCPServer::mStartAccept(); // Accept next + + auto client = TCPConnection::create( + std::move(socket), + mSodiumWrapper, + [this](std::shared_ptr c) { + mClients.erase(c); + Utils::log("Client removed."); + } + ); + mClients.insert(client); + client->start(); + Utils::log("Accepted new client connection."); + + if (mHostRunning && *mHostRunning && mAcceptor.is_open()) + mStartAccept(); } ); } + void TCPServer::stop() { + // Stop accepting + if (mAcceptor.is_open()) { + asio::error_code ec; + mAcceptor.cancel(ec); + mAcceptor.close(ec); + Utils::log("TCP Acceptor closed."); + } + + // Snapshot to avoid iterator invalidation while callbacks erase() + std::vector> snapshot(mClients.begin(), mClients.end()); + for (auto& client : snapshot) { + try { + client->disconnect(); // should shutdown+close the socket + Utils::log("GRACEFUL_DISCONNECT sent to session: " + std::to_string(client->getSessionID())); + } catch (const std::exception& e) { + Utils::error(std::string("Error disconnecting client: ") + e.what()); + } + } + // Let the erase callback run as sockets close + // Do NOT destroy server while io handlers may still reference it + } } \ No newline at end of file diff --git a/src/server/server/net/udp/udp_server.cpp b/src/server/server/net/udp/udp_server.cpp index d12913f..3a9bdeb 100644 --- a/src/server/server/net/udp/udp_server.cpp +++ b/src/server/server/net/udp/udp_server.cpp @@ -10,13 +10,23 @@ namespace ColumnLynx::Net::UDP { void UDPServer::mStartReceive() { + // A bit of a shotty implementation, might improve later + /*if (mHostRunning != nullptr && !(*mHostRunning)) { + Utils::log("Server is stopping, not receiving new packets."); + return; + }*/ + mSocket.async_receive_from( asio::buffer(mRecvBuffer), mRemoteEndpoint, [this](asio::error_code ec, std::size_t bytes) { - if (!ec && bytes > 0) { - mHandlePacket(bytes); + if (ec) { + if (ec == asio::error::operation_aborted) return; // Socket closed + // Other recv error + if (mHostRunning && *mHostRunning) mStartReceive(); + return; } - mStartReceive(); // Continue receiving + if (bytes > 0) mHandlePacket(bytes); + if (mHostRunning && *mHostRunning) mStartReceive(); } ); } @@ -69,4 +79,13 @@ namespace ColumnLynx::Net::UDP { void UDPServer::mSendData(const uint64_t sessionID, const std::string& data) { // TODO: Implement } + + void UDPServer::stop() { + if (mSocket.is_open()) { + asio::error_code ec; + mSocket.cancel(ec); + mSocket.close(ec); + Utils::log("UDP Socket closed."); + } + } } \ No newline at end of file