From def1e9ee637d3a1ebfc3378879df5913c3496067 Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Sat, 30 May 2026 13:30:28 -0600 Subject: [PATCH 1/8] Format fixes and small residual mutex stuff --- src/chat_app.cc | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/chat_app.cc b/src/chat_app.cc index be940aa..ceb1ae6 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -14,7 +14,7 @@ Session::Session(const std::string &host, unsigned port) : send_timer (std::make_unique(ioc)), socket (ioc) { if ( port > 65535 ) { - std::cerr << "Port out of bounds (0-65535)" << std::endl; + std::cerr << "Port out of bounds (0-65535)\n"; throw std::out_of_range("Port out of bounds (0-65535)"); } send_timer->expires_at(asio::steady_timer::time_point::min()); @@ -26,14 +26,14 @@ Session::Session(const std::string &host, unsigned port) : auto endpoints = resolver.resolve(host, std::to_string(port), ec); if (ec) { std::cerr << "[error] resolve(): " << ec.message() - << std::endl << "Is the server running?" << std::endl; + << "\nIs the server running?\n"; throw std::runtime_error("Failed to resolve host"); } asio::connect(socket, endpoints, ec); if (ec) { std::cerr << "[error] connect(): " << ec.message() - << std::endl << "Is the server running?" << std::endl; + << "\nIs the server running?\n"; throw std::runtime_error("Failed to connect to server"); } @@ -71,7 +71,7 @@ awaitable Session::sender(tcp::socket& socket) { send_timer->expires_at(asio::steady_timer::time_point::min()); } } catch (const std::exception&) { - std::cout << std::endl << "[disconnected sender from server due to exception]" << std::endl; + std::cout << "\n[disconnected sender from server due to exception]\n"; } if ( socket.is_open() ) socket.close(); @@ -93,7 +93,8 @@ awaitable Session::receiver(tcp::socket& socket) { buf.consume(n); // Strip \r\n and make Glib valid - while (!line.empty() && (line[line.size() - 1] == '\n' || line[line.size() - 1] == '\r')) + while (!line.empty() && (line[line.size() - 1] == '\n' + || line[line.size() - 1] == '\r')) line.erase(line.size() - 1); if ( !line.size() ) continue; line.make_valid(); { @@ -103,7 +104,7 @@ awaitable Session::receiver(tcp::socket& socket) { poster(); } } catch (const std::exception&) { - std::cout << std::endl << "[disconnected receiver from server due to exception]" << std::endl; + std::cout << "\n[disconnected receiver from server due to exception]\n"; } if ( socket.is_open() ) socket.close(); @@ -207,7 +208,7 @@ awaitable Server::client_receiver(std::shared_ptr client) { } asio::error_code ec; if (client->socket.is_open()) client->socket.close(ec); - { clients.remove(client); } + clients.remove(client); broadcast(client->nickname + " disconnected!!!\n"); } @@ -242,13 +243,13 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { try { ref_builder->add_from_file("res/gtk/chat_app.ui"); } catch(const Glib::FileError& ex) { - std::cerr << "FileError: " << ex.what() << std::endl; + std::cerr << "FileError: " << ex.what() << "\n"; throw ex; } catch(const Glib::MarkupError& ex) { - std::cerr << "MarkupError: " << ex.what() << std::endl; + std::cerr << "MarkupError: " << ex.what() << "\n"; throw ex; } catch(const Gtk::BuilderError& ex) { - std::cerr << "BuilderError: " << ex.what() << std::endl; + std::cerr << "BuilderError: " << ex.what() << "\n"; throw ex; } @@ -302,7 +303,7 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { void Chat::on_realize() { Gtk::Box::on_realize(); status_label = dynamic_cast(get_parent()->get_parent()->get_parent()->get_parent()->get_last_child()); - if (!status_label) std::cout << "Status label not found" << std::endl; + if (!status_label) std::cout << "Status label not found\n"; if (status_label) status_label->set_text("Welcome to the LAN Chat!"); } @@ -335,7 +336,7 @@ inline void Chat::session_connection() { session = nullptr; // If not localhost, give up if ( !(host == std::string(LOCALHOST)) ) { - std::cerr << "[fatal] while creating session: " << e.what() << std::endl; + std::cerr << "[fatal] while creating session: " << e.what() << "\n"; status_label->set_label("Something went wrong starting client, check cerr"); return; } @@ -346,7 +347,7 @@ inline void Chat::session_connection() { session = std::make_unique(LOCALHOST, port); status_label->set_label("Hosting on port " + std::to_string(port) + " — waiting for peers!"); } catch (const std::exception& e2) { - std::cerr << "[fatal] could not start server: " << e2.what() << std::endl; + std::cerr << "[fatal] could not start server: " << e2.what() << "\n"; status_label->set_label("Could not start server, check cerr"); server = nullptr; session = nullptr; @@ -361,8 +362,9 @@ inline void Chat::session_connection() { footer_box->set_visible(true); message_entry->grab_focus(); - session->set_disconnecter([this]() - { Glib::signal_idle().connect_once([this]() { session_connection(); }); }); + session->set_disconnecter([this]() { + Glib::signal_idle().connect_once([this]() { session_connection(); }); + }); session->set_poster([this]() { dispatcher->emit(); }); dispatcher = std::make_unique(); @@ -372,7 +374,7 @@ inline void Chat::session_connection() { std::string message{std::move(session->message_queue.front())}; session->message_queue.pop_front(); - std::cout << message << std::endl; + std::cout << message << "\n"; auto bubble {Gtk::manage(new Gtk::Label())}; bubble->set_css_classes({ "bubble" }); From 6319a2de409bb6764e354171e6c9678ccba5156c Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Sun, 31 May 2026 13:47:04 -0600 Subject: [PATCH 2/8] Creation of session and child classes Client/Server WIP - Created unified Session - Created Client/Server - Need to check how to unify receiver and sender --- src/chat_app.cc | 186 +++++++++++++++++++++------------------ src/include/chat_app.hpp | 80 ++++++++--------- 2 files changed, 136 insertions(+), 130 deletions(-) diff --git a/src/chat_app.cc b/src/chat_app.cc index ceb1ae6..16ced9e 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -8,54 +8,20 @@ #include -Session::Session(const std::string &host, unsigned port) : - host (host), - port (port), - send_timer (std::make_unique(ioc)), - socket (ioc) { - if ( port > 65535 ) { - std::cerr << "Port out of bounds (0-65535)\n"; - throw std::out_of_range("Port out of bounds (0-65535)"); - } - send_timer->expires_at(asio::steady_timer::time_point::min()); - tcp::resolver resolver(ioc); - - std::cout << "Connecting to " << host << ":" << port << "...\n"; - - asio::error_code ec; - auto endpoints = resolver.resolve(host, std::to_string(port), ec); - if (ec) { - std::cerr << "[error] resolve(): " << ec.message() - << "\nIs the server running?\n"; - throw std::runtime_error("Failed to resolve host"); - } - - asio::connect(socket, endpoints, ec); - if (ec) { - std::cerr << "[error] connect(): " << ec.message() - << "\nIs the server running?\n"; - throw std::runtime_error("Failed to connect to server"); - } - - // Spawn receiver and sender - asio::co_spawn(ioc, receiver(socket), asio::detached); - asio::co_spawn(ioc, sender(socket), asio::detached); - - // Run the event loop in separate thread until ioc.stop() is called - ioc_thread = std::thread([this](){ ioc.run(); }); -} +Session::Session() : send_timer (std::make_unique(ioc)) {} Session::~Session() noexcept { if ( !ioc.stopped() ) ioc.stop(); if ( ioc_thread.joinable() ) ioc_thread.join(); - if ( socket.is_open() ) socket.close(); + if ( disconnecter ) disconnecter(); + std::cout << "[" << typeid(this).name() << "] stopped\n"; } awaitable Session::sender(tcp::socket& socket) { try { while (true) { // Wait until there's something to send - while (send_buf.empty()) { + while (sender_buf.empty()) { asio::error_code ec; co_await send_timer->async_wait( asio::redirect_error(use_awaitable, ec)); @@ -63,15 +29,18 @@ awaitable Session::sender(tcp::socket& socket) { } std::string data; - std::swap(data, send_buf); + { + std::lock_guard lock(sender_mutex); + std::swap(data, sender_buf); + } co_await asio::async_write(socket, asio::buffer(data), use_awaitable); // Reset timer so we wait again next iteration send_timer->expires_at(asio::steady_timer::time_point::min()); } - } catch (const std::exception&) { - std::cout << "\n[disconnected sender from server due to exception]\n"; + } catch (const std::exception& e) { + std::cout << "[" << typeid(this).name() << "] sender error: " << e.what() << "\n"; } if ( socket.is_open() ) socket.close(); @@ -97,41 +66,86 @@ awaitable Session::receiver(tcp::socket& socket) { || line[line.size() - 1] == '\r')) line.erase(line.size() - 1); if ( !line.size() ) continue; - line.make_valid(); { - std::lock_guard lock(queue_mutex); - message_queue.push_back(std::string(line.c_str())); - } - poster(); + line.make_valid(); + process_message(line.raw()); } - } catch (const std::exception&) { - std::cout << "\n[disconnected receiver from server due to exception]\n"; + } catch (const std::exception& e) { + std::cout << "[" << typeid(this).name() << "] receiver error: " << e.what() << "\n"; } if ( socket.is_open() ) socket.close(); if ( !ioc.stopped() ) ioc.stop(); - if ( disconnecter ) disconnecter(); } -void Session::add_to_buffer(std::string message) { - send_buf.append(message + "\n"); +void Session::add_to_send_buffer(std::string message) { + sender_buf.append(message + "\n"); send_timer->cancel(); return; } +void Session::process_message(std::string message) { + std::lock_guard lock(receiver_mutex); + receiver_queue.push_back(message.c_str()); + if (poster) poster(); +} + void Session::set_poster(std::function message_poster) { poster = std::move(message_poster); } void Session::set_disconnecter(std::function new_disconnecter) { disconnecter = std::move(new_disconnecter); } -Server::Client::Client(asio::io_context& ioc, unsigned id) : +Client::Client(const std::string &host, unsigned port) : + Session::Session(), + socket (ioc) { + this->host = host; + this->port = port; + + if ( port > 65535 ) { + std::cerr << "Port out of bounds (0-65535)\n"; + throw std::out_of_range("Port out of bounds (0-65535)"); + } + send_timer->expires_at(asio::steady_timer::time_point::min()); + tcp::resolver resolver(ioc); + + std::cout << "Connecting to " << host << ":" << port << "...\n"; + + asio::error_code ec; + auto endpoints = resolver.resolve(host, std::to_string(port), ec); + if (ec) { + std::cerr << "[error] resolve(): " << ec.message() + << "\nIs the server running?\n"; + throw std::runtime_error("Failed to resolve host"); + } + + asio::connect(socket, endpoints, ec); + if (ec) { + std::cerr << "[error] connect(): " << ec.message() + << "\nIs the server running?\n"; + throw std::runtime_error("Failed to connect to server"); + } + + // Spawn receiver and sender + asio::co_spawn(ioc, receiver(socket), asio::detached); + asio::co_spawn(ioc, sender(socket), asio::detached); + + // Run the event loop in separate thread until ioc.stop() is called + ioc_thread = std::thread([this](){ ioc.run(); }); +} + +Client::~Client() noexcept { + if ( socket.is_open() ) socket.close(); + Session::~Session(); +} + +Server::Client::Client(asio::io_context& ioc) : socket (ioc), timer (ioc), - nickname ("User" + std::to_string(id)) + nickname ("User" + std::to_string(++current_id)) { timer.expires_at(asio::steady_timer::time_point::min()); } -Server::Server(unsigned port) : - acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port))) { +Server::Server(unsigned port) : Session::Session(), + acceptor (tcp::acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port)))) { asio::co_spawn(ioc, accept_loop(), asio::detached); ioc_thread = std::thread([this](){ ioc.run(); }); std::cout << "[server] listening on port " << port << "\n"; @@ -146,9 +160,31 @@ Server::~Server() noexcept { client->timer.cancel(ec); } }); - if (!ioc.stopped()) ioc.stop(); - if (ioc_thread.joinable()) ioc_thread.join(); - std::cout << "[server] stopped\n"; + Session::~Session(); +} + +awaitable Server::accept_loop() { + while (true) { + auto client = std::make_shared(ioc); + asio::error_code ec; + co_await acceptor.async_accept(client->socket, asio::redirect_error(use_awaitable, ec)); + if (ec) { + std::cout << "[server] accept loop ending: " << ec.message() << "\n"; + co_return; + } + + try { + auto ep = client->socket.remote_endpoint(); + client->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); + } catch (...) { client->fingerprint = "unknown"; } + std::cout << "[server] new client: " << client->fingerprint << "\n"; + + clients.push_back(client); + broadcast(client->nickname + " connected!!!\n", client.get()); + + asio::co_spawn(ioc, client_receiver(client), asio::detached); + asio::co_spawn(ioc, client_sender(client), asio::detached); + } } void Server::broadcast(const std::string& line, Client* origin) { @@ -212,30 +248,6 @@ awaitable Server::client_receiver(std::shared_ptr client) { broadcast(client->nickname + " disconnected!!!\n"); } -awaitable Server::accept_loop() { - while (true) { - auto client = std::make_shared(ioc, ++current_id); - asio::error_code ec; - co_await acceptor.async_accept(client->socket, asio::redirect_error(use_awaitable, ec)); - if (ec) { - std::cout << "[server] accept loop ending: " << ec.message() << "\n"; - co_return; - } - - try { - auto ep = client->socket.remote_endpoint(); - client->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); - } catch (...) { client->fingerprint = "unknown"; } - std::cout << "[server] new client: " << client->fingerprint << "\n"; - - clients.push_back(client); - broadcast(client->nickname + " connected!!!\n", client.get()); - - asio::co_spawn(ioc, client_receiver(client), asio::detached); - asio::co_spawn(ioc, client_sender(client), asio::detached); - } -} - Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { // Load the GtkBuilder file and instantiate its widgets, check for errors @@ -369,10 +381,10 @@ inline void Chat::session_connection() { dispatcher = std::make_unique(); dispatcher->connect([this]() { - std::lock_guard lock(session->queue_mutex); - while (!session->message_queue.empty()) { - std::string message{std::move(session->message_queue.front())}; - session->message_queue.pop_front(); + std::lock_guard lock(session->receiver_mutex); + while (!session->receiver_queue.empty()) { + std::string message{std::move(session->receiver_queue.front())}; + session->receiver_queue.pop_front(); std::cout << message << "\n"; @@ -406,6 +418,6 @@ inline void Chat::session_connection() { inline void Chat::message_buffer () { if ( !message_entry->get_text_length() ) return; - session->add_to_buffer(message_entry->get_text()); + session->add_to_send_buffer(message_entry->get_text()); message_entry->delete_text(0, -1); }; diff --git a/src/include/chat_app.hpp b/src/include/chat_app.hpp index dee61a3..4dc608f 100644 --- a/src/include/chat_app.hpp +++ b/src/include/chat_app.hpp @@ -25,31 +25,14 @@ constexpr const char *LOCALHOST {"127.0.0.1"}; * It has a poster and disconnecter function so it can interact with GUI. */ class Session { - std::string send_buf; - std::string host; - unsigned port; - - asio::io_context ioc; - std::unique_ptr send_timer; - std::thread ioc_thread; - tcp::socket socket; - - std::function poster; - std::function disconnecter; - public: - std::deque message_queue; - std::mutex queue_mutex; - - /** - * @brief Connects the session as client and starts the asio thread. - * - * @throws out_of_range if the port is out of range. - * @throws RuntimeError on connection failure. - */ - Session(const std::string&, unsigned); + std::deque receiver_queue; + std::mutex receiver_mutex; + std::string sender_buf; + std::mutex sender_mutex; - /* Stops asio io context, joins asio thread and deletes sockets. */ + /* Deleted default constructor and destructor as it's not supposed to be used without Client or Server. */ + Session(); ~Session() noexcept; @@ -89,7 +72,33 @@ class Session { * * @param[in] message: Message to be buffered. */ - void add_to_buffer(std::string); + void add_to_send_buffer(std::string); + + virtual void process_message(std::string); + +protected: + std::string host; + unsigned port; + + asio::io_context ioc; + std::unique_ptr send_timer; + std::thread ioc_thread; + + std::function poster; + std::function disconnecter; + +}; + + +class Client : public Session { + tcp::socket socket; + + /* Queue line for the sender, then wake their sender. */ + void send_message(const std::string&); + +public: + explicit Client(const std::string &host, unsigned port); + ~Client() noexcept; }; @@ -100,42 +109,27 @@ class Session { * clients, and echoes it back to the sender tagged with "(you)". * Runs its own ioc on a dedicated thread — same pattern as Session. */ -class Server { +class Server : public Session { /* Client struct containing socket, timer, buffer and nickname for each client. */ struct Client { + inline static unsigned current_id{}; + tcp::socket socket; asio::steady_timer timer; std::string nickname{}; std::string fingerprint{}; std::string buf{}; - explicit Client(asio::io_context&, unsigned); + explicit Client(asio::io_context&); }; - asio::io_context ioc; tcp::acceptor acceptor; - std::thread ioc_thread{}; - unsigned current_id{}; std::list> clients{}; /* Queue line for every client except origin, then wake their sender. */ void broadcast(const std::string&, Client* = nullptr); - /** - * @brief Sender function to run so the session sends data. - * - * @param[in] client: Client struct to send data to. - */ - awaitable client_sender(std::shared_ptr); - - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] client: Client struct to receive data from. - */ - awaitable client_receiver(std::shared_ptr); - /* Accept loop function to run so the session accepts new clients. */ awaitable accept_loop(); From d030be24fb6ffdd923f1181da2e71ff92b3dc28d Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Mon, 1 Jun 2026 13:18:55 -0600 Subject: [PATCH 3/8] Session class and derivatives working --- src/chat_app.cc | 292 ++++++++++++++++++--------------------- src/include/chat_app.hpp | 112 +++++++++------ 2 files changed, 205 insertions(+), 199 deletions(-) diff --git a/src/chat_app.cc b/src/chat_app.cc index 16ced9e..715f870 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -8,85 +8,62 @@ #include -Session::Session() : send_timer (std::make_unique(ioc)) {} + +Connection::Connection(asio::io_context& ioc) : + socket (ioc), + send_timer (ioc), + nickname ("User" + std::to_string(++current_id)) + { send_timer.expires_at(asio::steady_timer::time_point::min()); } + +void Connection::add_to_send_buffer(std::string message) { + send_buffer.append(message + "\n"); + send_timer.cancel(); + return; +} + Session::~Session() noexcept { if ( !ioc.stopped() ) ioc.stop(); if ( ioc_thread.joinable() ) ioc_thread.join(); - if ( disconnecter ) disconnecter(); - std::cout << "[" << typeid(this).name() << "] stopped\n"; + std::cout << "[Server] stopped\n"; } -awaitable Session::sender(tcp::socket& socket) { +awaitable Session::sender(std::shared_ptr connection) { try { while (true) { - // Wait until there's something to send - while (sender_buf.empty()) { + while (true) { + if (!connection->send_buffer.empty()) break; asio::error_code ec; - co_await send_timer->async_wait( - asio::redirect_error(use_awaitable, ec)); + co_await connection->send_timer.async_wait(asio::redirect_error(use_awaitable, ec)); if (ec && ec != asio::error::operation_aborted) co_return; } - std::string data; - { - std::lock_guard lock(sender_mutex); - std::swap(data, sender_buf); - } - - co_await asio::async_write(socket, asio::buffer(data), use_awaitable); - - // Reset timer so we wait again next iteration - send_timer->expires_at(asio::steady_timer::time_point::min()); + std::swap(data, connection->send_buffer); + connection->send_timer.expires_at(asio::steady_timer::time_point::min()); + co_await asio::async_write(connection->socket, asio::buffer(data), use_awaitable); } } catch (const std::exception& e) { - std::cout << "[" << typeid(this).name() << "] sender error: " << e.what() << "\n"; + std::cout << "[Server] sender error: " << e.what() << "\n"; } - - if ( socket.is_open() ) socket.close(); - if ( !ioc.stopped() ) ioc.stop(); } -awaitable Session::receiver(tcp::socket& socket) { +awaitable Session::receiver(std::shared_ptr connection) { try { asio::streambuf buf; - while (true) { - // Receive until a '\n' - std::size_t n = co_await asio::async_read_until( - socket, buf, '\n', use_awaitable); + std::size_t n = co_await asio::async_read_until(connection->socket, buf, '\n', use_awaitable); - Glib::ustring line( + std::string line( asio::buffers_begin(buf.data()), - asio::buffers_begin(buf.data()) + static_cast(n)); + asio::buffers_begin(buf.data()) + static_cast(n) + ); buf.consume(n); - // Strip \r\n and make Glib valid - while (!line.empty() && (line[line.size() - 1] == '\n' - || line[line.size() - 1] == '\r')) - line.erase(line.size() - 1); - if ( !line.size() ) continue; - line.make_valid(); - process_message(line.raw()); + process_message(line, connection); } } catch (const std::exception& e) { - std::cout << "[" << typeid(this).name() << "] receiver error: " << e.what() << "\n"; + std::cout << "[Server] " << connection->nickname << " disconnected: " << e.what() << "\n"; } - - if ( socket.is_open() ) socket.close(); - if ( !ioc.stopped() ) ioc.stop(); -} - -void Session::add_to_send_buffer(std::string message) { - sender_buf.append(message + "\n"); - send_timer->cancel(); - return; -} - -void Session::process_message(std::string message) { - std::lock_guard lock(receiver_mutex); - receiver_queue.push_back(message.c_str()); - if (poster) poster(); } void Session::set_poster(std::function message_poster) @@ -97,7 +74,8 @@ void Session::set_disconnecter(std::function new_disconnecter) Client::Client(const std::string &host, unsigned port) : Session::Session(), - socket (ioc) { + connection (std::make_shared(ioc)) { + connection->nickname = "Server"; this->host = host; this->port = port; @@ -105,7 +83,7 @@ Client::Client(const std::string &host, unsigned port) : std::cerr << "Port out of bounds (0-65535)\n"; throw std::out_of_range("Port out of bounds (0-65535)"); } - send_timer->expires_at(asio::steady_timer::time_point::min()); + connection->send_timer.expires_at(asio::steady_timer::time_point::min()); tcp::resolver resolver(ioc); std::cout << "Connecting to " << host << ":" << port << "...\n"; @@ -118,7 +96,7 @@ Client::Client(const std::string &host, unsigned port) : throw std::runtime_error("Failed to resolve host"); } - asio::connect(socket, endpoints, ec); + asio::connect(connection->socket, endpoints, ec); if (ec) { std::cerr << "[error] connect(): " << ec.message() << "\nIs the server running?\n"; @@ -126,126 +104,125 @@ Client::Client(const std::string &host, unsigned port) : } // Spawn receiver and sender - asio::co_spawn(ioc, receiver(socket), asio::detached); - asio::co_spawn(ioc, sender(socket), asio::detached); + asio::co_spawn(ioc, receiver(connection), asio::detached); + asio::co_spawn(ioc, sender(connection), asio::detached); // Run the event loop in separate thread until ioc.stop() is called ioc_thread = std::thread([this](){ ioc.run(); }); } + Client::~Client() noexcept { - if ( socket.is_open() ) socket.close(); - Session::~Session(); + if ( connection->socket.is_open() ) + connection->socket.close(); +} + +awaitable Client::receiver(std::shared_ptr connection) { + co_await Session::receiver(connection); + if ( connection->socket.is_open() ) + connection->socket.close(); + if (disconnecter) disconnecter(); +} + +void Client::broadcast(const std::string& message, std::shared_ptr origin) { + (void) origin; + connection->send_buffer += message + "\n"; + connection->send_timer.cancel(); +} + +void Client::process_message(std::string &message, std::shared_ptr origin) { + while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) + message.erase(message.size()-1); + + if ( origin ) { + std::lock_guard lock(receive_mutex); + receive_queue.push_back(message); + if (poster) poster(); + } + else { + broadcast(message, origin); + } } -Server::Client::Client(asio::io_context& ioc) : - socket (ioc), - timer (ioc), - nickname ("User" + std::to_string(++current_id)) - { timer.expires_at(asio::steady_timer::time_point::min()); } Server::Server(unsigned port) : Session::Session(), acceptor (tcp::acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port)))) { + host = "Server"; asio::co_spawn(ioc, accept_loop(), asio::detached); ioc_thread = std::thread([this](){ ioc.run(); }); - std::cout << "[server] listening on port " << port << "\n"; + std::cout << "[Server] listening on port " << port << "\n"; } Server::~Server() noexcept { - asio::post(ioc, [this]() { - asio::error_code ec; - acceptor.close(ec); - for (auto& client : clients) { - if (client->socket.is_open()) client->socket.close(ec); - client->timer.cancel(ec); - } - }); - Session::~Session(); + acceptor.close(); + for (auto& connection : connections) { + if (connection->socket.is_open()) + connection->socket.close(); + connection->send_timer.cancel(); + } +} + +awaitable Server::receiver(std::shared_ptr connection) { + co_await Session::receiver(connection); + if ( connection->socket.is_open() ) + connection->socket.close(); + connections.remove(connection); + std::string farewell{connection->nickname + " has left the chat!!!"}; + process_message(farewell); } awaitable Server::accept_loop() { while (true) { - auto client = std::make_shared(ioc); + auto connection = std::make_shared(ioc); asio::error_code ec; - co_await acceptor.async_accept(client->socket, asio::redirect_error(use_awaitable, ec)); + co_await acceptor.async_accept(connection->socket, asio::redirect_error(use_awaitable, ec)); if (ec) { std::cout << "[server] accept loop ending: " << ec.message() << "\n"; co_return; } try { - auto ep = client->socket.remote_endpoint(); - client->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); - } catch (...) { client->fingerprint = "unknown"; } - std::cout << "[server] new client: " << client->fingerprint << "\n"; + auto ep = connection->socket.remote_endpoint(); + connection->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); + } catch (...) { connection->fingerprint = "unknown"; } + std::cout << "[server] new client: " << connection->nickname << " (" << connection->fingerprint << ")\n"; - clients.push_back(client); - broadcast(client->nickname + " connected!!!\n", client.get()); + std::string message {connection->nickname + " says Hi!!!\n"}; + process_message(message); - asio::co_spawn(ioc, client_receiver(client), asio::detached); - asio::co_spawn(ioc, client_sender(client), asio::detached); - } -} + asio::co_spawn(ioc, receiver(connection), asio::detached); + asio::co_spawn(ioc, sender(connection), asio::detached); -void Server::broadcast(const std::string& line, Client* origin) { - for (auto& client : clients) { - if (client.get() == origin) continue; - client->buf += line; - client->timer.cancel(); + connections.push_back(connection); } } -awaitable Server::client_sender(std::shared_ptr client) { - try { - while (true) { - while (true) { - if (!client->buf.empty()) break; - asio::error_code ec; - co_await client->timer.async_wait(asio::redirect_error(use_awaitable, ec)); - if (ec && ec != asio::error::operation_aborted) co_return; - } - std::string data; - std::swap(data, client->buf); - client->timer.expires_at(asio::steady_timer::time_point::min()); - co_await asio::async_write(client->socket, asio::buffer(data), use_awaitable); - } - } catch (const std::exception& e) { - std::cout << "[server] sender error: " << e.what() << "\n"; +void Server::broadcast(const std::string& message, std::shared_ptr origin) { + for (auto& connection : connections) { + std::lock_guard send_lock(connection->send_mutex); + if ( !origin ) + connection->send_buffer += message + "\n"; + else if ( connection->fingerprint == origin->fingerprint ) + connection->send_buffer += "(You): " + message + "\n"; + else + connection->send_buffer += "(" + origin->nickname + "): " + message + "\n"; + connection->send_timer.cancel(); } - asio::error_code ec; - if (client->socket.is_open()) client->socket.close(ec); } -awaitable Server::client_receiver(std::shared_ptr client) { - try { - asio::streambuf buf; - while (true) { - std::size_t n = co_await asio::async_read_until(client->socket, buf, '\n', use_awaitable); - - Glib::ustring line( - asio::buffers_begin(buf.data()), - asio::buffers_begin(buf.data()) + static_cast(n)); - buf.consume(n); +void Server::process_message(std::string &message, std::shared_ptr connection) { + (void) connection; + while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) + message.erase(message.size()-1); - while (!line.empty() && (line[line.size()-1] == '\n' || line[line.size()-1] == '\r')) - line.erase(line.size()-1); - if (line.empty()) continue; - line.make_valid(); + broadcast(message, connection); - std::string message { line.c_str() }; - std::cout << "[server] " << client->nickname << ": " << message << "\n"; + if ( connection ) + message = "(" + connection->nickname + "): " + message; - client->buf += "(You): " + message + "\n"; - client->timer.cancel(); - broadcast("(" + client->nickname + "): " + message + "\n", client.get()); - } - } catch (const std::exception& e) { - std::cout << "[server] " << client->nickname << " disconnected: " << e.what() << "\n"; - } - asio::error_code ec; - if (client->socket.is_open()) client->socket.close(ec); - clients.remove(client); - broadcast(client->nickname + " disconnected!!!\n"); + std::lock_guard lock(receive_mutex); + receive_queue.push_back(message); + if (poster) poster(); } @@ -315,24 +292,24 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { void Chat::on_realize() { Gtk::Box::on_realize(); status_label = dynamic_cast(get_parent()->get_parent()->get_parent()->get_parent()->get_last_child()); - if (!status_label) std::cout << "Status label not found\n"; if (status_label) status_label->set_text("Welcome to the LAN Chat!"); + else + std::cout << "Status label not found\n"; } inline void Chat::session_connection() { // Disconnect if connected if (!connect_button->get_label().compare("Disconnect")) { session = nullptr; - server = nullptr; - std::cout << "\n[disconnected from server due to user request]\n"; connect_button->set_label("Connect"); footer_box->set_visible(false); ip_entry->set_sensitive(true); port_entry->set_sensitive(true); home_button->set_sensitive(true); - status_label->set_label("Disconnected from server!"); + if (status_label) + status_label->set_label("Disconnected!"); return; } @@ -342,26 +319,25 @@ inline void Chat::session_connection() { // Try to connect as client try { - session = std::make_unique(host, port); + session = std::make_unique(host, port); status_label->set_label("Connected to server " + std::string(host) + ":" + std::to_string(port) + "!"); } catch (const std::exception& e) { session = nullptr; // If not localhost, give up if ( !(host == std::string(LOCALHOST)) ) { std::cerr << "[fatal] while creating session: " << e.what() << "\n"; - status_label->set_label("Something went wrong starting client, check cerr"); + if (status_label) + status_label->set_label("Something went wrong starting client, check cerr"); return; } - // Localhost and no server found — become the server, then connect to ourselves - std::cout << "[info] no server on localhost, starting one...\n"; try { - server = std::make_unique(port); - session = std::make_unique(LOCALHOST, port); - status_label->set_label("Hosting on port " + std::to_string(port) + " — waiting for peers!"); + session = std::make_unique(port); + if (status_label) + status_label->set_label("Hosting on port " + std::to_string(port) + " — waiting for peers!"); } catch (const std::exception& e2) { std::cerr << "[fatal] could not start server: " << e2.what() << "\n"; - status_label->set_label("Could not start server, check cerr"); - server = nullptr; + if (status_label) + status_label->set_label("Could not start server, check cerr"); session = nullptr; return; } @@ -377,14 +353,12 @@ inline void Chat::session_connection() { session->set_disconnecter([this]() { Glib::signal_idle().connect_once([this]() { session_connection(); }); }); - session->set_poster([this]() { dispatcher->emit(); }); - dispatcher = std::make_unique(); dispatcher->connect([this]() { - std::lock_guard lock(session->receiver_mutex); - while (!session->receiver_queue.empty()) { - std::string message{std::move(session->receiver_queue.front())}; - session->receiver_queue.pop_front(); + std::lock_guard lock(session->receive_mutex); + while (!session->receive_queue.empty()) { + std::string message{std::move(session->receive_queue.front())}; + session->receive_queue.pop_front(); std::cout << message << "\n"; @@ -400,7 +374,9 @@ inline void Chat::session_connection() { bubble->add_css_class("others"); } } - bubble->set_text(std::move(message)); + Glib::ustring valid_message{std::move(message)}; + valid_message.make_valid(); + bubble->set_text(std::move(valid_message)); bubble->set_hexpand(true); bubble->set_vexpand(false); bubble->set_wrap(true); @@ -413,11 +389,13 @@ inline void Chat::session_connection() { }); } }); + session->set_poster([this]() { dispatcher->emit(); }); return; } inline void Chat::message_buffer () { if ( !message_entry->get_text_length() ) return; - session->add_to_send_buffer(message_entry->get_text()); + std::string message{message_entry->get_text()}; + session->process_message(message); message_entry->delete_text(0, -1); }; diff --git a/src/include/chat_app.hpp b/src/include/chat_app.hpp index 4dc608f..362df69 100644 --- a/src/include/chat_app.hpp +++ b/src/include/chat_app.hpp @@ -16,6 +16,32 @@ constexpr const char *DEFAULT_PORT {"1234"}; constexpr const char *LOCALHOST {"127.0.0.1"}; + +/* Connection struct containing socket, timer, buffer and nickname for each client. */ +struct Connection { + inline static unsigned current_id{}; + + tcp::socket socket; + asio::steady_timer send_timer; + std::mutex send_mutex; + std::string nickname{}; + std::string fingerprint{}; + std::string send_buffer{}; + + explicit Connection(asio::io_context&); + + /** + * @brief Adds a string to the buffer. + * + * Adds a string to the buffer, appending the string with a '\n' at the end, + * canceling the timer for the message to be sent. + * + * @param[in] message: Message to be buffered. + */ + void add_to_send_buffer(std::string); +}; + + /** * @brief Session class containing everything needed to open an async client session. * @@ -26,14 +52,12 @@ constexpr const char *LOCALHOST {"127.0.0.1"}; */ class Session { public: - std::deque receiver_queue; - std::mutex receiver_mutex; - std::string sender_buf; - std::mutex sender_mutex; + std::deque receive_queue; + std::mutex receive_mutex; /* Deleted default constructor and destructor as it's not supposed to be used without Client or Server. */ - Session(); - ~Session() noexcept; + Session() = default; + virtual ~Session() noexcept; /** @@ -41,14 +65,14 @@ class Session { * * @param[in] socket: Socket connection to send data to. */ - awaitable sender(tcp::socket& socket); + virtual awaitable sender(std::shared_ptr); /** * @brief Receiver function to run so the session awaits for data. * * @param[in] socket: Socket connection to receive data from. */ - awaitable receiver(tcp::socket& socket); + virtual awaitable receiver(std::shared_ptr); /** * @brief Adds a function for the session to run on received message. @@ -64,26 +88,17 @@ class Session { */ void set_disconnecter(std::function); - /** - * @brief Adds a string to the buffer. - * - * Adds a string to the buffer, appending the string with a '\n' at the end, - * canceling the timer for the message to be sent. - * - * @param[in] message: Message to be buffered. - */ - void add_to_send_buffer(std::string); + /* Broadcasting method for Server and Client. */ + virtual void broadcast(const std::string&, std::shared_ptr = nullptr) = 0; - virtual void process_message(std::string); + virtual void process_message(std::string&, std::shared_ptr = nullptr) = 0; protected: std::string host; unsigned port; - asio::io_context ioc; - std::unique_ptr send_timer; - std::thread ioc_thread; - + asio::io_context ioc; + std::thread ioc_thread; std::function poster; std::function disconnecter; @@ -91,10 +106,24 @@ class Session { class Client : public Session { - tcp::socket socket; + std::shared_ptr connection; + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] socket: Socket connection to receive data from. + */ + awaitable receiver(std::shared_ptr) override; + + /** + * @brief Broadcasting method for Client, send message to Server. + * + * @param[in] message: Message to be broadcast. + * @param[in] origin: Not used as it's a Client transaction. + */ + void broadcast(const std::string&, std::shared_ptr = nullptr) override; - /* Queue line for the sender, then wake their sender. */ - void send_message(const std::string&); + virtual void process_message(std::string&, std::shared_ptr = nullptr) override; public: explicit Client(const std::string &host, unsigned port); @@ -110,29 +139,29 @@ class Client : public Session { * Runs its own ioc on a dedicated thread — same pattern as Session. */ class Server : public Session { - /* Client struct containing socket, timer, buffer and nickname for each client. */ - struct Client { - inline static unsigned current_id{}; - - tcp::socket socket; - asio::steady_timer timer; - std::string nickname{}; - std::string fingerprint{}; - std::string buf{}; - - explicit Client(asio::io_context&); - }; - tcp::acceptor acceptor; - std::list> clients{}; + std::list> connections{}; - /* Queue line for every client except origin, then wake their sender. */ - void broadcast(const std::string&, Client* = nullptr); + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] socket: Socket connection to receive data from. + */ + awaitable receiver(std::shared_ptr) override; /* Accept loop function to run so the session accepts new clients. */ awaitable accept_loop(); + /** + * @brief Broadcasting method for Server, send to all connected clients accordingly. + * + * @param[in] message: Message to be broadcast. + * @param[in] origin: Connection that sent the message. + */ + void broadcast(const std::string&, std::shared_ptr = nullptr) override; + void process_message(std::string&, std::shared_ptr = nullptr) override; + public: explicit Server(unsigned port); ~Server() noexcept; @@ -163,7 +192,6 @@ class Chat : public Gtk::Box { std::unique_ptr dispatcher{}; std::unique_ptr session{}; - std::unique_ptr server{}; // non-null when we are the host /** * @brief Get Main Application status label. From 1a8972ae8794093077fb792736385f97a70a9ca7 Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Mon, 1 Jun 2026 13:27:48 -0600 Subject: [PATCH 4/8] Sepparated Session class and derivatives to their own files --- makefile | 2 +- src/chat_app.cc | 223 +-------------------------------------- src/include/chat_app.hpp | 164 +--------------------------- src/include/session.hpp | 168 +++++++++++++++++++++++++++++ src/session.cc | 223 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 395 insertions(+), 385 deletions(-) create mode 100644 src/include/session.hpp create mode 100644 src/session.cc diff --git a/makefile b/makefile index b590dab..b8525e5 100644 --- a/makefile +++ b/makefile @@ -1,5 +1,5 @@ PROGRAM=multiapp -OBJECTS=build/main_application.o build/browser_app.o build/fractal_app.o build/chat_app.o +OBJECTS=build/main_application.o build/browser_app.o build/fractal_app.o build/chat_app.o build/session.o MAIN=build/main.o TESTS=test_build/main.o test_build/main_application_tests.o test_build/browser_app_tests.o test_build/fractal_app_tests.o test_build/chat_app_tests.o diff --git a/src/chat_app.cc b/src/chat_app.cc index 715f870..7d6693d 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -1,229 +1,8 @@ #include "include/chat_app.hpp" - -#include -#include -#include +#include "include/session.hpp" #include -#include - - - -Connection::Connection(asio::io_context& ioc) : - socket (ioc), - send_timer (ioc), - nickname ("User" + std::to_string(++current_id)) - { send_timer.expires_at(asio::steady_timer::time_point::min()); } - -void Connection::add_to_send_buffer(std::string message) { - send_buffer.append(message + "\n"); - send_timer.cancel(); - return; -} - - -Session::~Session() noexcept { - if ( !ioc.stopped() ) ioc.stop(); - if ( ioc_thread.joinable() ) ioc_thread.join(); - std::cout << "[Server] stopped\n"; -} - -awaitable Session::sender(std::shared_ptr connection) { - try { - while (true) { - while (true) { - if (!connection->send_buffer.empty()) break; - asio::error_code ec; - co_await connection->send_timer.async_wait(asio::redirect_error(use_awaitable, ec)); - if (ec && ec != asio::error::operation_aborted) co_return; - } - std::string data; - std::swap(data, connection->send_buffer); - connection->send_timer.expires_at(asio::steady_timer::time_point::min()); - co_await asio::async_write(connection->socket, asio::buffer(data), use_awaitable); - } - } catch (const std::exception& e) { - std::cout << "[Server] sender error: " << e.what() << "\n"; - } -} - -awaitable Session::receiver(std::shared_ptr connection) { - try { - asio::streambuf buf; - while (true) { - std::size_t n = co_await asio::async_read_until(connection->socket, buf, '\n', use_awaitable); - - std::string line( - asio::buffers_begin(buf.data()), - asio::buffers_begin(buf.data()) + static_cast(n) - ); - buf.consume(n); - - process_message(line, connection); - } - } catch (const std::exception& e) { - std::cout << "[Server] " << connection->nickname << " disconnected: " << e.what() << "\n"; - } -} - -void Session::set_poster(std::function message_poster) - { poster = std::move(message_poster); } - -void Session::set_disconnecter(std::function new_disconnecter) - { disconnecter = std::move(new_disconnecter); } - -Client::Client(const std::string &host, unsigned port) : - Session::Session(), - connection (std::make_shared(ioc)) { - connection->nickname = "Server"; - this->host = host; - this->port = port; - - if ( port > 65535 ) { - std::cerr << "Port out of bounds (0-65535)\n"; - throw std::out_of_range("Port out of bounds (0-65535)"); - } - connection->send_timer.expires_at(asio::steady_timer::time_point::min()); - tcp::resolver resolver(ioc); - - std::cout << "Connecting to " << host << ":" << port << "...\n"; - - asio::error_code ec; - auto endpoints = resolver.resolve(host, std::to_string(port), ec); - if (ec) { - std::cerr << "[error] resolve(): " << ec.message() - << "\nIs the server running?\n"; - throw std::runtime_error("Failed to resolve host"); - } - - asio::connect(connection->socket, endpoints, ec); - if (ec) { - std::cerr << "[error] connect(): " << ec.message() - << "\nIs the server running?\n"; - throw std::runtime_error("Failed to connect to server"); - } - - // Spawn receiver and sender - asio::co_spawn(ioc, receiver(connection), asio::detached); - asio::co_spawn(ioc, sender(connection), asio::detached); - - // Run the event loop in separate thread until ioc.stop() is called - ioc_thread = std::thread([this](){ ioc.run(); }); -} - - -Client::~Client() noexcept { - if ( connection->socket.is_open() ) - connection->socket.close(); -} - -awaitable Client::receiver(std::shared_ptr connection) { - co_await Session::receiver(connection); - if ( connection->socket.is_open() ) - connection->socket.close(); - if (disconnecter) disconnecter(); -} - -void Client::broadcast(const std::string& message, std::shared_ptr origin) { - (void) origin; - connection->send_buffer += message + "\n"; - connection->send_timer.cancel(); -} - -void Client::process_message(std::string &message, std::shared_ptr origin) { - while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) - message.erase(message.size()-1); - - if ( origin ) { - std::lock_guard lock(receive_mutex); - receive_queue.push_back(message); - if (poster) poster(); - } - else { - broadcast(message, origin); - } -} - - -Server::Server(unsigned port) : Session::Session(), - acceptor (tcp::acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port)))) { - host = "Server"; - asio::co_spawn(ioc, accept_loop(), asio::detached); - ioc_thread = std::thread([this](){ ioc.run(); }); - std::cout << "[Server] listening on port " << port << "\n"; -} - -Server::~Server() noexcept { - acceptor.close(); - for (auto& connection : connections) { - if (connection->socket.is_open()) - connection->socket.close(); - connection->send_timer.cancel(); - } -} -awaitable Server::receiver(std::shared_ptr connection) { - co_await Session::receiver(connection); - if ( connection->socket.is_open() ) - connection->socket.close(); - connections.remove(connection); - std::string farewell{connection->nickname + " has left the chat!!!"}; - process_message(farewell); -} - -awaitable Server::accept_loop() { - while (true) { - auto connection = std::make_shared(ioc); - asio::error_code ec; - co_await acceptor.async_accept(connection->socket, asio::redirect_error(use_awaitable, ec)); - if (ec) { - std::cout << "[server] accept loop ending: " << ec.message() << "\n"; - co_return; - } - - try { - auto ep = connection->socket.remote_endpoint(); - connection->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); - } catch (...) { connection->fingerprint = "unknown"; } - std::cout << "[server] new client: " << connection->nickname << " (" << connection->fingerprint << ")\n"; - - std::string message {connection->nickname + " says Hi!!!\n"}; - process_message(message); - - asio::co_spawn(ioc, receiver(connection), asio::detached); - asio::co_spawn(ioc, sender(connection), asio::detached); - - connections.push_back(connection); - } -} - -void Server::broadcast(const std::string& message, std::shared_ptr origin) { - for (auto& connection : connections) { - std::lock_guard send_lock(connection->send_mutex); - if ( !origin ) - connection->send_buffer += message + "\n"; - else if ( connection->fingerprint == origin->fingerprint ) - connection->send_buffer += "(You): " + message + "\n"; - else - connection->send_buffer += "(" + origin->nickname + "): " + message + "\n"; - connection->send_timer.cancel(); - } -} - -void Server::process_message(std::string &message, std::shared_ptr connection) { - (void) connection; - while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) - message.erase(message.size()-1); - - broadcast(message, connection); - - if ( connection ) - message = "(" + connection->nickname + "): " + message; - - std::lock_guard lock(receive_mutex); - receive_queue.push_back(message); - if (poster) poster(); -} Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { diff --git a/src/include/chat_app.hpp b/src/include/chat_app.hpp index 362df69..57ed643 100644 --- a/src/include/chat_app.hpp +++ b/src/include/chat_app.hpp @@ -1,171 +1,11 @@ #pragma once +#include "session.hpp" + #include #include - -#include - -#include #include -using asio::ip::tcp; -using asio::awaitable; -using asio::use_awaitable; - -constexpr const char *DEFAULT_PORT {"1234"}; -constexpr const char *LOCALHOST {"127.0.0.1"}; - - - -/* Connection struct containing socket, timer, buffer and nickname for each client. */ -struct Connection { - inline static unsigned current_id{}; - - tcp::socket socket; - asio::steady_timer send_timer; - std::mutex send_mutex; - std::string nickname{}; - std::string fingerprint{}; - std::string send_buffer{}; - - explicit Connection(asio::io_context&); - - /** - * @brief Adds a string to the buffer. - * - * Adds a string to the buffer, appending the string with a '\n' at the end, - * canceling the timer for the message to be sent. - * - * @param[in] message: Message to be buffered. - */ - void add_to_send_buffer(std::string); -}; - - -/** - * @brief Session class containing everything needed to open an async client session. - * - * Uses a string buffer for sending, appends received messages - * to a deque and runs asio in a different thread for the app to run other things. - * - * It has a poster and disconnecter function so it can interact with GUI. - */ -class Session { -public: - std::deque receive_queue; - std::mutex receive_mutex; - - /* Deleted default constructor and destructor as it's not supposed to be used without Client or Server. */ - Session() = default; - virtual ~Session() noexcept; - - - /** - * @brief Sender function to run so the session sends data. - * - * @param[in] socket: Socket connection to send data to. - */ - virtual awaitable sender(std::shared_ptr); - - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] socket: Socket connection to receive data from. - */ - virtual awaitable receiver(std::shared_ptr); - - /** - * @brief Adds a function for the session to run on received message. - * - * @param[in] function: Function to run on received message. - */ - void set_poster(std::function); - - /** - * @brief Adds a function for the session to run on error disconnection. - * - * @param[in] function: Function to run on error disconnection. - */ - void set_disconnecter(std::function); - - /* Broadcasting method for Server and Client. */ - virtual void broadcast(const std::string&, std::shared_ptr = nullptr) = 0; - - virtual void process_message(std::string&, std::shared_ptr = nullptr) = 0; - -protected: - std::string host; - unsigned port; - - asio::io_context ioc; - std::thread ioc_thread; - std::function poster; - std::function disconnecter; - -}; - - -class Client : public Session { - std::shared_ptr connection; - - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] socket: Socket connection to receive data from. - */ - awaitable receiver(std::shared_ptr) override; - - /** - * @brief Broadcasting method for Client, send message to Server. - * - * @param[in] message: Message to be broadcast. - * @param[in] origin: Not used as it's a Client transaction. - */ - void broadcast(const std::string&, std::shared_ptr = nullptr) override; - - virtual void process_message(std::string&, std::shared_ptr = nullptr) override; - -public: - explicit Client(const std::string &host, unsigned port); - ~Client() noexcept; -}; - - -/** - * @brief Server class that accepts clients and broadcasts messages between them. - * - * Listens on a given port, relays every incoming line to all other connected - * clients, and echoes it back to the sender tagged with "(you)". - * Runs its own ioc on a dedicated thread — same pattern as Session. - */ -class Server : public Session { - tcp::acceptor acceptor; - - std::list> connections{}; - - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] socket: Socket connection to receive data from. - */ - awaitable receiver(std::shared_ptr) override; - - /* Accept loop function to run so the session accepts new clients. */ - awaitable accept_loop(); - - /** - * @brief Broadcasting method for Server, send to all connected clients accordingly. - * - * @param[in] message: Message to be broadcast. - * @param[in] origin: Connection that sent the message. - */ - void broadcast(const std::string&, std::shared_ptr = nullptr) override; - void process_message(std::string&, std::shared_ptr = nullptr) override; - -public: - explicit Server(unsigned port); - ~Server() noexcept; -}; /** diff --git a/src/include/session.hpp b/src/include/session.hpp new file mode 100644 index 0000000..d8e774c --- /dev/null +++ b/src/include/session.hpp @@ -0,0 +1,168 @@ +#pragma once + +#include +#include +#include +#include + +#include + +using asio::ip::tcp; +using asio::awaitable; +using asio::use_awaitable; + +constexpr const char *DEFAULT_PORT {"1234"}; +constexpr const char *LOCALHOST {"127.0.0.1"}; + + + +/* Connection struct containing socket, timer, buffer and nickname for each client. */ +struct Connection { + inline static unsigned current_id{}; + + tcp::socket socket; + asio::steady_timer send_timer; + std::mutex send_mutex; + std::string nickname{}; + std::string fingerprint{}; + std::string send_buffer{}; + + explicit Connection(asio::io_context&); + + /** + * @brief Adds a string to the buffer. + * + * Adds a string to the buffer, appending the string with a '\n' at the end, + * canceling the timer for the message to be sent. + * + * @param[in] message: Message to be buffered. + */ + void add_to_send_buffer(std::string); +}; + + +/** + * @brief Session class containing everything needed to open an async client session. + * + * Uses a string buffer for sending, appends received messages + * to a deque and runs asio in a different thread for the app to run other things. + * + * It has a poster and disconnecter function so it can interact with GUI. + */ +class Session { +public: + std::deque receive_queue; + std::mutex receive_mutex; + + /* Deleted default constructor and destructor as it's not supposed to be used without Client or Server. */ + Session() = default; + virtual ~Session() noexcept; + + + /** + * @brief Sender function to run so the session sends data. + * + * @param[in] socket: Socket connection to send data to. + */ + virtual awaitable sender(std::shared_ptr); + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] socket: Socket connection to receive data from. + */ + virtual awaitable receiver(std::shared_ptr); + + /** + * @brief Adds a function for the session to run on received message. + * + * @param[in] function: Function to run on received message. + */ + void set_poster(std::function); + + /** + * @brief Adds a function for the session to run on error disconnection. + * + * @param[in] function: Function to run on error disconnection. + */ + void set_disconnecter(std::function); + + /* Broadcasting method for Server and Client. */ + virtual void broadcast(const std::string&, std::shared_ptr = nullptr) = 0; + + virtual void process_message(std::string&, std::shared_ptr = nullptr) = 0; + +protected: + std::string host; + unsigned port; + + asio::io_context ioc; + std::thread ioc_thread; + std::function poster; + std::function disconnecter; + +}; + + +class Client : public Session { + std::shared_ptr connection; + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] socket: Socket connection to receive data from. + */ + awaitable receiver(std::shared_ptr) override; + + /** + * @brief Broadcasting method for Client, send message to Server. + * + * @param[in] message: Message to be broadcast. + * @param[in] origin: Not used as it's a Client transaction. + */ + void broadcast(const std::string&, std::shared_ptr = nullptr) override; + + virtual void process_message(std::string&, std::shared_ptr = nullptr) override; + +public: + explicit Client(const std::string &host, unsigned port); + ~Client() noexcept; +}; + + +/** + * @brief Server class that accepts clients and broadcasts messages between them. + * + * Listens on a given port, relays every incoming line to all other connected + * clients, and echoes it back to the sender tagged with "(you)". + * Runs its own ioc on a dedicated thread — same pattern as Session. + */ +class Server : public Session { + tcp::acceptor acceptor; + + std::list> connections{}; + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] socket: Socket connection to receive data from. + */ + awaitable receiver(std::shared_ptr) override; + + /* Accept loop function to run so the session accepts new clients. */ + awaitable accept_loop(); + + /** + * @brief Broadcasting method for Server, send to all connected clients accordingly. + * + * @param[in] message: Message to be broadcast. + * @param[in] origin: Connection that sent the message. + */ + void broadcast(const std::string&, std::shared_ptr = nullptr) override; + void process_message(std::string&, std::shared_ptr = nullptr) override; + +public: + explicit Server(unsigned port); + ~Server() noexcept; +}; + diff --git a/src/session.cc b/src/session.cc new file mode 100644 index 0000000..6126d80 --- /dev/null +++ b/src/session.cc @@ -0,0 +1,223 @@ +#include "include/session.hpp" + +#include +#include +#include + + + +Connection::Connection(asio::io_context& ioc) : + socket (ioc), + send_timer (ioc), + nickname ("User" + std::to_string(++current_id)) + { send_timer.expires_at(asio::steady_timer::time_point::min()); } + +void Connection::add_to_send_buffer(std::string message) { + send_buffer.append(message + "\n"); + send_timer.cancel(); + return; +} + + +Session::~Session() noexcept { + if ( !ioc.stopped() ) ioc.stop(); + if ( ioc_thread.joinable() ) ioc_thread.join(); + std::cout << "[Server] stopped\n"; +} + +awaitable Session::sender(std::shared_ptr connection) { + try { + while (true) { + while (true) { + if (!connection->send_buffer.empty()) break; + asio::error_code ec; + co_await connection->send_timer.async_wait(asio::redirect_error(use_awaitable, ec)); + if (ec && ec != asio::error::operation_aborted) co_return; + } + std::string data; + std::swap(data, connection->send_buffer); + connection->send_timer.expires_at(asio::steady_timer::time_point::min()); + co_await asio::async_write(connection->socket, asio::buffer(data), use_awaitable); + } + } catch (const std::exception& e) { + std::cout << "[Server] sender error: " << e.what() << "\n"; + } +} + +awaitable Session::receiver(std::shared_ptr connection) { + try { + asio::streambuf buf; + while (true) { + std::size_t n = co_await asio::async_read_until(connection->socket, buf, '\n', use_awaitable); + + std::string line( + asio::buffers_begin(buf.data()), + asio::buffers_begin(buf.data()) + static_cast(n) + ); + buf.consume(n); + + process_message(line, connection); + } + } catch (const std::exception& e) { + std::cout << "[Server] " << connection->nickname << " disconnected: " << e.what() << "\n"; + } +} + +void Session::set_poster(std::function message_poster) + { poster = std::move(message_poster); } + +void Session::set_disconnecter(std::function new_disconnecter) + { disconnecter = std::move(new_disconnecter); } + +Client::Client(const std::string &host, unsigned port) : + Session::Session(), + connection (std::make_shared(ioc)) { + connection->nickname = "Server"; + this->host = host; + this->port = port; + + if ( port > 65535 ) { + std::cerr << "Port out of bounds (0-65535)\n"; + throw std::out_of_range("Port out of bounds (0-65535)"); + } + connection->send_timer.expires_at(asio::steady_timer::time_point::min()); + tcp::resolver resolver(ioc); + + std::cout << "Connecting to " << host << ":" << port << "...\n"; + + asio::error_code ec; + auto endpoints = resolver.resolve(host, std::to_string(port), ec); + if (ec) { + std::cerr << "[error] resolve(): " << ec.message() + << "\nIs the server running?\n"; + throw std::runtime_error("Failed to resolve host"); + } + + asio::connect(connection->socket, endpoints, ec); + if (ec) { + std::cerr << "[error] connect(): " << ec.message() + << "\nIs the server running?\n"; + throw std::runtime_error("Failed to connect to server"); + } + + // Spawn receiver and sender + asio::co_spawn(ioc, receiver(connection), asio::detached); + asio::co_spawn(ioc, sender(connection), asio::detached); + + // Run the event loop in separate thread until ioc.stop() is called + ioc_thread = std::thread([this](){ ioc.run(); }); +} + + +Client::~Client() noexcept { + if ( connection->socket.is_open() ) + connection->socket.close(); +} + +awaitable Client::receiver(std::shared_ptr connection) { + co_await Session::receiver(connection); + if ( connection->socket.is_open() ) + connection->socket.close(); + if (disconnecter) disconnecter(); +} + +void Client::broadcast(const std::string& message, std::shared_ptr origin) { + (void) origin; + connection->send_buffer += message + "\n"; + connection->send_timer.cancel(); +} + +void Client::process_message(std::string &message, std::shared_ptr origin) { + while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) + message.erase(message.size()-1); + + if ( origin ) { + std::lock_guard lock(receive_mutex); + receive_queue.push_back(message); + if (poster) poster(); + } + else { + broadcast(message, origin); + } +} + + +Server::Server(unsigned port) : Session::Session(), + acceptor (tcp::acceptor(ioc, tcp::endpoint(tcp::v4(), static_cast(port)))) { + host = "Server"; + asio::co_spawn(ioc, accept_loop(), asio::detached); + ioc_thread = std::thread([this](){ ioc.run(); }); + std::cout << "[Server] listening on port " << port << "\n"; +} + +Server::~Server() noexcept { + acceptor.close(); + for (auto& connection : connections) { + if (connection->socket.is_open()) + connection->socket.close(); + connection->send_timer.cancel(); + } +} + +awaitable Server::receiver(std::shared_ptr connection) { + co_await Session::receiver(connection); + if ( connection->socket.is_open() ) + connection->socket.close(); + connections.remove(connection); + std::string farewell{connection->nickname + " has left the chat!!!"}; + process_message(farewell); +} + +awaitable Server::accept_loop() { + while (true) { + auto connection = std::make_shared(ioc); + asio::error_code ec; + co_await acceptor.async_accept(connection->socket, asio::redirect_error(use_awaitable, ec)); + if (ec) { + std::cout << "[server] accept loop ending: " << ec.message() << "\n"; + co_return; + } + + try { + auto ep = connection->socket.remote_endpoint(); + connection->fingerprint = ep.address().to_string() + ":" + std::to_string(ep.port()); + } catch (...) { connection->fingerprint = "unknown"; } + std::cout << "[server] new client: " << connection->nickname << " (" << connection->fingerprint << ")\n"; + + std::string message {connection->nickname + " says Hi!!!\n"}; + process_message(message); + + asio::co_spawn(ioc, receiver(connection), asio::detached); + asio::co_spawn(ioc, sender(connection), asio::detached); + + connections.push_back(connection); + } +} + +void Server::broadcast(const std::string& message, std::shared_ptr origin) { + for (auto& connection : connections) { + std::lock_guard send_lock(connection->send_mutex); + if ( !origin ) + connection->send_buffer += message + "\n"; + else if ( connection->fingerprint == origin->fingerprint ) + connection->send_buffer += "(You): " + message + "\n"; + else + connection->send_buffer += "(" + origin->nickname + "): " + message + "\n"; + connection->send_timer.cancel(); + } +} + +void Server::process_message(std::string &message, std::shared_ptr connection) { + (void) connection; + while (!message.empty() && (message[message.size()-1] == '\n' || message[message.size()-1] == '\r')) + message.erase(message.size()-1); + + broadcast(message, connection); + + if ( connection ) + message = "(" + connection->nickname + "): " + message; + + std::lock_guard lock(receive_mutex); + receive_queue.push_back(message); + if (poster) poster(); +} From ca5fa782207d740762df013bddf881f61704904b Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Mon, 1 Jun 2026 21:16:25 -0600 Subject: [PATCH 5/8] Added new project AI chat --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bae650f..147de72 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ MultiApp/ // Core application files - 🎉 Local chat app - ❌ Maze generator/player/solver - ❌ Game of life +- ❌ Local AI chat with ollama - 🔬 Other goodies in the future ## 🏗️ Requirements From 0c2952c16b72a260c0701a958047b73da6a33817 Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Mon, 1 Jun 2026 21:16:47 -0600 Subject: [PATCH 6/8] Added new chat screenshot --- res/screenshots/chat.png | Bin 29443 -> 31508 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/res/screenshots/chat.png b/res/screenshots/chat.png index d6a6ca4f5f072f104029d7b8fd9b50ce6b54dfd4..0ce192bfd9a87a2d520f2d42a36e655113d284cf 100644 GIT binary patch literal 31508 zcmd43by!?W_b%A+oM16R@Fchg53Ye=K^h4j+#$Hji69|B&_;s0Cuop{BoM4|YrI47 zZXBA%nQC&*{mq@ZckX;Mf6VNMhi30xyLQ#8T5G-QU8@OGdoE9OgW?7R0wGd(CaVE~ zT;Ya5E_IbWmjnh&FkRsyKWu|fjodH$VzE?eOjIN^t7o(_xjxp@4#>x;AZ-1FE=U)UA>lDcLeLR9!Z==wd`k2DY7CeKVd zdMx4OJ1E}Xm5!Nyi`?1jF!Qvsv=ZknG4`A}I_bTyW8nPG;(l;Z@o9bD5*P;lAijI| z?r;Idway@-zA2&WqxnI@^*?tY1!4Zf&h1LVUttTGK8AyL?)B?K2_9B4+aaanq z#4c55i8D9>CkfnnsP(QUnpf39o_LMH;%yM3-ET~7I@&*a#0rp4LIN&lSo8+4;D zEN;9}{^xvCa0hq668oSsZzgfK`|_Ww{RYJAVJw9{vI;N%f)3W1>GFOQqt{T?CaMn;h*2u$uqc}R$%Vo6tq(lOckUMkGkmpQbj_WErNiDn#FuKd zf?YTGp-lXRJd^t|7ApuL;|@NRq&$|E$9{dqMp4ufnVlX$x({w%VM@<#Pis`Wg)kZk zuNBL~`i7g!wicVY=m>e-EjX39;2U4P&thmLhpANZM4wL149N9mChS~OJMYTvqx`0*9+IkFEcECnxcq{>S7t#BSjD!uv-V^J+H8vE4v_k zl5?5kl-_XWssjla_h4pAy#8w3`&6}2 zM3*h!upGy`$7LnEGFD96UutGXDvmZj{3Q*mY4k5%KNQt~!S`||PLD|HoeK41GDD;D zZR}^Ir?<4PiTQPQqPNFWCbZ*CC&2b%t7TqpIk0tY8>blImR|Rlu-hY0UBS{;E|l5@ z+@S^v9VDrx@65p7$U|ie?iAPkpZ5Kk#bw@7*GN~kv z{knJo0?}jT^>`bJNh-UNe!8z8G)^_yW0b`;x;I>SCq`4qc1mqE49aJ|13MH(^qZXJ z*e_rj$Y}dTQo5@5P`vJYLS)O4s(VyatjS*}JP#i`r0^Q2G^b~mOEcM^gEB6`zPJyp zK+Sd!QpuL%SgbI4eT1pbeWk(-GbJcLymEAarNW+!2oQyMNUC-hnJkQiB&Pdy73w{9 zDq}|sehrIV_6HgksVJvel@*u$p`_?*U)cp1yx>}XYv;%L(-m(O-&fH$D0~~Vn-Y3- z3zJ~!B{a0XgN#lwj4CTdv@-#cb=-3l-geF0BF_hHrUM2iaTW+n?*Q3OICgh9I=R}i z@V(!|GI=|RGs)tLshBCngI$gu5sX6W6MAXad!yWeGqZ%v*B}r{m1I^(Y12%RQEWtY zX)s~2V6o#bDE!*u1y3dzpYt|L@~iFnlgfj7{BXs!P5!S|c zT8v3vJ^K5R?^A7mm~bmeGtnW(m_}!mv8{9eU&ni19Nh z5MxCg-upbI&7VY_$Qt_z==Yt~6;DtJYBBxP-gvsAE)_7~grbIzcXbQgC8z!p*x=p+ z#`u;DdYyKUdq*CVn@6Pke0>Ct_Wbtmsr0XMY06aFePKx{cu+(^M*+o9;!_UdsvFg&>n`uoJ*xAf)=LT=KqEIjC|uUvg=^<*{p;wnF9=p| zS!1T^Cfrx~>n*yC{JA~bzhgF6hsyk)W2whe@6Ns;}%|I_~W|u|ElQ$N9(Q+ynbu1c_rMN;8^R2dbk;0$iNgQw9dq__4RS} z2c8M)vjyxc2np5Q`0FqAC`Uv0gDR!C;UBEIG*CcfQS&=~7ikrM5 zOggnF*Jf!M8UD4`JHroI6n-)==+!#DnP`>epYUipv#Gv)=Qo9ee}u(w=xqIb-_pKO zia?6JfPSn!n_ATOSz*WF>nhF57rh&4EG#TBr8tSVphawxkn{SrwAlVwtRCDW_4S+AeBM8v7`|FhH(w_y z6>xYy5IA!rIqA8oR)@&_`{Zb)o{ckhh`caAEl>f)ex#jXvC>T2Gx~)tc%@h0(Z0w~ znEjSXCi>&>X^@RkxlZkrdsMH)5_{?K788e2g|bzuxYp9?3JL$Zm1Z0?X5OW4xAz*Y z%~pkz){7S@qD$u2g=f|Y{}8a3~j>^yJ zqrdO@S88&?^jofy40hX)v9Wfslq2=c{Qt=I36IKc(|QBW(qo%=PEp*aOBcIk4*b^r z3=d#7^&X2B6Q8}q)BM)2U{^+8xt(y2Q^psHuA4+G%0;?n0Qv?FTsHrY)0)z1sFD~ z3mUk;GtJ7bvcnnu{Mk0@MQE#UtfXdOP~xOhGW_fB_Kv3JYnaT)R1007g@`ljxjc@; zNnHl^<^6BK^(Ep;5z)&~sNJK5FCTQ%M zt2`8k#DGo2#>_tWn*oKcNhyPt(@Zlc1fCAjR@-7JdXCU`U}&-%0s9i9q%dYtcFpDz z-Ta~FX*u2mWSw(c1tlfQg*FX*b1?)9TR!Hx2|uKz7e0LW$jkFXM_c#=X<| zJPUF72~%?07jVKi`(oG^<`-A8RmF$INX}$Pru|`5#34CvqNba6-Ml%J3`N}cRCraO z)Ab(O6TODQOHo#=^z<+EYaF$zcoi+mcvPOOT<$sbi}c7@)09$=mX>}!TxTXf?jVpV zZ1dW&VJU!!#_L0GBG0(z!5jaT6;?Km5qQ+G%;ITZHL^l;)}#1Av)UJK?vRo{HWngZ zrXbzDT)-^!`gL55E9`K0RToncskc4~7ZfwZNgTfAe6plkzmq^Y|LV!PX>e71iDQn- z3Eg~U?9|Dsg}gv?Sg{12R%a zGJ{IJ5<2^&ORn=t}ES4r3zIzMJE+ak$N{QtEA zE-Hf->`$INYv08bgGUP+8yml4#_Kt>h#L$+cN>^6SZu84H=f{!-l8*fMMxBSqVMgA zCk?8`to>au^&(ybo6z z7T`7#7*f;ri2OZKwRQ5dCd{Uyq4y6*kf9O#HZj;_Icuj^TU$Hf^z>Wy;rbD6d~ywx zQov#G_^B~sZfn?1?8_H!RyHo3iQeg$oOI-I(MHqRY|t8g>rb1Wa!g!&Z1(Z->%6g4 zMjd2;ocz-jU}kqG0%u0*iBhV##AHzZXW2Kx-;;O~qVMGz^#tdvNh>gYA3u853jayegCM1?(SVGdMopswzOA` zrzdUthbv1XU0t062@aEdp5JwvXlnkLC~FE850&mn>X_ZAqm2O(TECK|Gwl2K@54!N z-_B}+``Au}nVAop*}Ij`o1d7V3+su)e6Y*s_S=F@`!M04WthchHEGFxoL>+< zcB{o57%QaR^xrvUCwzo5I^AD2of=QlXuQ$nq@<+9`b)j&$zqp+(~YR_JxC+nC1(Oz zM3a7@1*$a8wEbvre$&Vwa1JnHrCFEiDmg3m9e8{CJcYf(iW zVev9UB8=5KAL*Wjg>Aie(I!5L40p(WyYEGMkCgh=rJN}70|Sx4L30JXb998y+M?8U znhji{fpB(zAj(;p1a;I0fb*Mu%P2zgM;Zm@jfdM3rC;C53at{4uL_{Dr!8Zr*FORX zXj!=tSk(zg5X%MEmdEDFmf-|MAZWoKRfGTsrdU1^`g025yC?lGk#r&A=D&26Bwg!2 zMAFUv>)ii5dYSJ(XzJ^KFL_TrlyHcVe9nn@rx;r~6a$1>I?xk-MA&!Xy=ovEV2Dd4-#4@n~J(idH?T>d%z`#+d@&&&(9 z3ijeJ+E*YKIv*1T!ayHE+Mzo(kWyW7uSZc%&NGG7`q4U32fCoq z^;Z*55fxP;4h=VUcH+jGvg>iHlge5bC3jdl`k+459$U{|cV9KWGgQI8uwYsIwcuH5 zy@$>VS@SpHfYTwncdPKZVFUiMcinh3@j+)#m!_0aS&T0(mY21jycoQ8;lq1vmyH1Y zXdJaIOA&(t7oSiJ*BVx_D+wi$vv&7Eqr@*+gQ@EE7Fc7Kc_rT+cz1PnKgmK^@qJ~= zLt?Z7p4a^TeTSma8Deb%{)kJ7<*?HHU@tKF5Z3t&mHpO3p~rw6vabpzYmu{2=)O-6 zW|pmBxRwPT)?O56sM&ZBhB3KleR6x!Q_b?w=xKY~h!>|J?CQHjtwI|Gg)hQ#Zg%~_ z7cv-bgoh5Vr-}f)0Pu;q&i?No7lRcpJ}G*VomFjyS=57NI$VCmS6FITa!8{)&bfCB zw|n6gU#OcL8@~m#bZO9CC~I~e@~SF}21lJp<=Z=|tgHWQ;>&bAATp5@7tj=p$EL<R2E;}Huk2MtueLbWtC3%=&ambt)0 zX;c>q9w}{miL!FB={PHdO*wkPM=f*{#FJ0L0;WG_+JuX@)+D=5LiC#3>?tomuDgM7 zLC3%oeu^8nI_11_{l#vpZ1``F1}$x)^#(U(UGF-=mX9s7n@%Q@m+VZ{ed? zFflRVA(8=4=UP~7`>@h7P-D}vrNDDA397b)5N~<1K$6v<)e)Sv`by1)WmUkBZXED`|x+P zsKl7x4{l3+_1ec{W4%u!rRVk2@G5@ykg`PXX;f~;m4{%a>frFM(D3hg#}J6Hu zSA!UbH66^1jMDD;!Cz#4|9-89jf)GZTC81YFv4BsC#p& z!s!lM)UV7FN%?yQHMK zP(~F^S+B2yygb;JC<#^(CjIprQ85#>$gw+Z%zX4YZENxI324LtL6vVGjnJw9c4b-~ zzfsoo4Y4JL%|>>kr$xVio4&hxt=KAxhJxEKvFUJ;(m`RIRRI8p^pRXwE0we-v!5fc z1bstlI?ScPkA@p`RDV{7jWoU^AW0;nqAHR26i~lW&C8?Vf3!P#NJP1S^&!1SQRK{& zwwyvsb1yHgQ>@CThe9`Dp}4%N#K4$Kw@95%-g>+i5w-jTaMG~PA0JoSO=`ysf04Ns zLB(xABC+?D#(&!ezH`VvzUE=*bD*puhSf|xX_x4zVE5k~X!kj4rezkRlNlHo5MGOm zbst%Pft?r+gJBkQjE7)5WCa-+YN;X~!?9;xFC6aN(OP#|$qj7I5^`NC2FipVD3OkF zsW{xX3xlL!_)T?ho!>qR0x z3;mO4Cx?6LrqZwRyY(05=ivg3pMu{-Heyo0HUQp6<@u>C?Xld-%8FjqJ|zPqV}WaX zr28los0D4HUT!RtvRX>webT#k3#PQZ$3wTw+KU=X47Rqm2$`Jlb2~P?%`& zG=`&@Iasu0iBN1VuCCRNb8-xx8|x`fx(Q9X^_pV7^u;n7F^yBDmmq2cEsx#s(nWah zR0~-YAMUNYe>?8c*Vr8_(ZTN1krevYI!;vE)xN($V38c3eK*Z#&%0FbBzzr?A^uro zlRZo=5`UAH3sJGVTiIvVQ{Ig33>3}!Iwo*9zr*Er!cmLv(J|7NmTqK66z8Y=O9>hW zraCsPFqR&;r>=W%J|tR28MjA>Ysa9gyDPa)l~};5yCl+eo;?et2|PS=+mMM`UTLR^ zHn|^C!p;3q3U_kye1Yt4f$BzD{ckSbQ5X=-GqbTZj5>I%Pw9eawbXB>F!uKE z-*NHrx!~TYuIkcrYT#a8Q+d35dzp=%{`B)6nIr;9z$M3c)tE8%_Qo zP=uLlahmCBylbaJ#QczMAthbW)J*SEV%1HEgTcz{2<|P<9<7gTX135POxJ50>GL*c zNV+a@^|#snDG=E?3__rhGUtFfvDW+7d@H-K`unS;4H=&$ zjSw0JDFRJ4!6wX{oZ90;LaglUaoC1~k+tat)na6cI$$eux>D>I+K@Et+9Xm$Egz?6 zH{TJvkMP=eyabV$e5J&hG_?o_!cGng4I65RCOjb=`p}w)Y8K#gj^7cC&qF)gU7SZ>7zX4O&=SKtR>i z%_1aaZkaxMnUuHEFlQwtWdfg>bCVO;7g%iiw-&%cK{A_%2ukBUk3AiI)3N4(Fr3wv z^?J({>iLAK>5lu*lkQhdAe#%)!j+{mQ#Z3SG9-;TUu2lbT(ui~y9E%&q4P*zS4T&I zf3t0afCDx5esE@VOGu$HN?flt;7yakXGy?CglcvrPN-;BM8$B^^oG+bvraxDL$Mv6RQJV|L+ic#`hz_YH*{yL+YT1l zIU#ZJ`{R-~qX&~DHqGf)3Os&8R31kU7issK^k84`-bd_c_|E(A2rP%m{a`>oRyz+Z zCfkW-2%J(%o&YXBD1D52uZvRS>lC2Rqucc#mFK&RnPZh6c@?F0Oj3s+?L|_bC*>8E ze4=un2uxK%Q~)(DjieT4=HZc!iAzl_-6W1j@7Tn5<=>F_qz8>LjN1 z=tm2`%l>2c)qW}C-t_WWnGohDPhJF@P~EhLSo&4)WlU*aRaZ8lVJp<7Azd z!;x7eB?k<#;Jb$P;kQ~B1$al3T7;h3*#QzX+K$1ZdunIYff0$DDz%G1RI4ZtkMMF0 zeW44%v^CFUBCNVMBQz&IRcN(q6WnAdlnJ39Ujy{Gf;ygR)lU)3ANDakYKb3iO0^e& zu8@O~fi;gb&+^^=2nRv>k z9l#Ybow>RBcwv6Nz+tlH1_1%A;Yaq=l)=xRKW&if(~2r8T7CvCwqKdU=c1lz{-3C>wXD$GBzw62x}+pvPg zymjlAye1_i!=0IfV_?Sr2oU~N9Q<0e0moqiPBSG;w7#+)4JR42XwRHn7K=#_Jh?MF zJFC~=2>%oveS?@-!(8@j8*u1-r#nyI1;1q$7k>`O$)R6Tmq-`f(7P!Rby2JwQs!qb*zaN}exP#qw*T}Wu6%DJ*PQTVBYsi|!l zH;)E@LD)If;<)G}mXPV$hHN14$|z+yQsN9-;dpd34Sa+~-}*HF=fFUPLA;I> zJH1^_nB34bk;?SJqI2_U0yU{)VkjImV@>YYZ%x?UC0!Y&i;^fTS+)P!DvPP=jP(bK zvGrr8cf=4kRW9+#x2Y|XJp0 z|CD2GR~N#rF)ZGM;s()QTUfcvL6TJ-BRC2kTjhuy8_Ek*4z{*rAldo!>ATU>Drx^~ zG=3+B5bM&>v+eAwgw$Gdb#4xT&QVkaTcfs!Hp;-UmUW7Bd%an$)+Vt+WEF7B@>7WG z5IqXOuE<|=w7m$yJ_X92SwoYs+azoE@_sN#togOb0Cp?_usQog62^bH*Z*0W^vqZWVypi)8Gk`cA2rHoy+$lGveD}sCr z7$q5^^P@DU57NIL2v|oTIti~xHtDmSEY5U-x~|$1Z&vcwKV~!rE*+VK1Pdp_%gHsq9l%aiWY7o8>w;6$&a-ZLYkc z2NVX7QY|eQuE=N^?38cO1vhw#X@t_R_EYdL*6rQ`T++|PJcTy|86$L!c=pHa(Ko;q zS(mu3&=GSNzj8UxvwWMK^P_;EM>mUY{^2ksc^~}O4*D-{=szJ`{{wFJ|83LCqXtM7 z=Q>YRP03X8@JLdZGRLOpA3`~U{HLdL5hBA2dw=|SPs6)zU-n=S7i8!wQ$$ zJhwf)tr@O!JGb88QK~+Mt0A+1GK z-%8^a$n~%X9&1MF*#k@17+&#VlttFNY+tI#XN!>+d;A@%6>oI$Tk2I@RelbZ3eN}s zV!nrVv;H7)5_bdSzFUXRn97lujqS)#kR-&ASQV*m z%rqf0A7MT72fDa?wV{~+d`h0^Cget01!jSK>kIOvr1z1Lqw`^R+{SlpfE@$F zs5AJMWM0oQ&Zb{}E?ONIGL2xnmPaP2Kw$hOln}ra^M?uGqjfVfNq~|AvA0*hTL^ryye=~{ z^9B)7Ar8Mm_G$5ZOp;)*Ql2E$`%u%+za@f#+di@PV2BpajwlSKfjB`=uSkER5t^q? zuI9s04mdSD|M7L~g~DL}!#5=8Y3s`I-EU%nI-q31V|^m`tZ}F8Gyrwtv)trMqE}Nx zLB9g4w!`-No{iKDYbt0sJBI-gvjKt|fKjtUlK$TeN_f^l;^erJN{X0>=7}0{NcJd} zrpviKa?oNZpZf6{5{tpE5^#;~r+xf*^WD4Pkt4i{97o~Piw`8nlv7wm)86Fyiqeel zT?@;zv0ePIBJ7H`IcHu@oILHeTmPzGM;Dvx#+%H?Mu}&QfzmWhAz5Kz)-IedGxPGt zN!2llsz#-y=b)0w2xjOWG3c?b0nj(`i?SMuz18XB$@RkqO9P)Uny*WaSFT)HsTMrT zCZZHPuuXcrP7lYf`zCvAJ=LqQuZtAfdvWF3H6@J*3_s!AM}B!ZS^P%5ri|$7EnIud z#z!yva3nY!iKPHDIBlQq1iSlUkjZ^TTh#Lu0SEEr$X`I+H70Aqt{J5B04k2xN*PH< zGL_G@*b8WjFCa0&meQjsX9$w{{2Q(t!L~N@<+KWL#=&HkMKVJ-&2Sm4WlC2N^$N}yNx7uI zo_f|)zBXBlh#BS;(#1`=hrGt&d>R+K*ycZc7`~%=mJXza%z=7Vsm z_XV|U&(<6P=R$ss(5c)3MWOVr-BnU`sNU#Y=Kt4$kIE3vI@vM;$t;c8ZHquHcl%yD zz&pwQ*Py}&Jq5G@dgdj2=@5s*Bc%-To+i&~PVYn6K-k~+%t0^&*lM8LJNkf6um!~> z{yw-pkQswOtS_94D##M5shI;xcznRv-S%_?MOP__!Cw5e+*&IKn(+|;ADH6UAX6yf zv5fZ?Z8p$j!`tAF-t}|1$44;4hutRVJ0BFJO48uJvW)*-u7BVGO)c`D?0nF@V9^Ui zAXK`_((8Qie{$<}4d5#ye}LD&ZvBbYECnVE}>zp+=;O`AP;=yC@o?!J zn-JbdDTGaDRUK9uy2NV&Y;^wqn;8ZSq#|~(>GN@W6P;?9$S4*XiCVccJ*{c{wYO>G|j!@Sen|P zuVk#Gp2+(G-)g`2c_8fFz25bWi~ZX|b29t~6H)*Ls@N^&+hE?~0b6P7{Ij3*@7Nku zKH3v+|LIAOE^4y)HxBH+2C(1sN>Z-g=?r786G8{4zNJW$nxU58g0eVA_d%8~T@zgxgFBM$XZ6aM zYU%yb?%F0SJ@jkvDDN8p+q;vT7QMkU@MA!XSf1Wh{_jQcT0Mp<3lzK@x|bN4ZoNDo z$g8^q?!tU8U1Hjlm9!P4>hO({M83{bMG`(_q$qh%scGD&m;d>GNSm^%Hi%6EVSiNV zqAWPU2T ziPvd55O0(i#^r&ih=Q;B(ggsHz)_&CmT34U&qM97#;?>cZgInJXxOyb>zY z@;`as{?YtlmKxW$Ab3J1AK19Syzz>E9^!DY;fjFhx0f&fyV3PRLF82Q%Dc*9!6*>+ z`d;YGJ0TqI?s*fqh(*r%^Vep~NT(9xbrHnF|1F-fwEg>bEFDPtpEt2K0VnZEdcQV! z@a8K^$~5N3|IvV2f7}$1I^{fHAxPC@d}IW4HMHlu{694!G~1W??xz2l00iwr1wQ+$Ko-jqej2<8(kq~RMK0ogCZ@bek(+w}>l0Xq$ki=} zmmLt06@p?E;OGELX;q}tM{|usUtK+ufHSp>e28-(XKN1BzpFQ&F6sjk_PXJ#nL3bl zqU}4C-3Z+M0>1b{N=;n~GE~bID?^~dtrY9?KM8=EHeXX9ZSWl58&XC5mlFC-&y7Nr z3m2S6iG@WUdpxAdLQgN-CyIMb<9+;>*x|1e_a1<~TH8@dC-m&mh8B|35MOKo$_6|6dVEIgyx~5xxiEgW%?DyT zXRFiRpN!o7ac4O9o|sO7zZ4iB(wozn{H6aw8X_f|@S0uA4qo`{YPGgO5gapZ>x@}6 znw)GfExj>j`-QaR^?UZLpl522gC6+ayPin+*EmeK41){tV3$Zxw!B@oLEpDD#~qdQ zNeC&F?iwuN*C>i$l8BCsjC63UEOmGhTwCpcD}g}bqyWiu|3_KfdcEJ-Att@LG4}v@ z#_P0J?_Re!f!QvMIyKx&6XsXg4t6z$c{VPo?hF@cB|Q!}CU6X#KE&3oA5v~dlNiO- zU0Y43_a4IZA%3Kvvg-tH_}2YWahh6B^7ZwFmLqvh_d`F}IQ1p&?KPpWRx>{6BtB=; z8SmN2!$%7slZb7cRF0W#39;ILb?Xk~Wx;L0rUrk8QMtO!xSXb>e_18NO~cNPRLPhW z(!73$+0QfzZTO!ZK}(N?_eapxySuyQJ;~LR9}-nXw`6_ns=L#wRZ$hT)yYDYxX$cw zG7{@W|E2Cuwmxw-r$C&q%W$;(Vs*K@J zNKo6I^HBi!ZMFD;yzj*xsM__HKw=c2P^kFHmnh5Qv+D`*iVSA$AI2B=Fj%rfhg_8F zsBLvqdhv+ujL-K2?*(0}#pbel;XvnsYpxS4OiPAN&7V;_H8nNjW=C9AJ;u#r`5X?8 z0O{SYXiTMoydY_PQx2_T%_Xj`#TlosQ1>(s(S&`2+=*GUaL3^|gyf zw)QQdr4vyK z5c|w5fmu?)UrJf_CN}l-fzk#>yu`@_EV~mw`D}L{to}^zbErk7_2#E_osftpIvJm& zS{YCuG^RT_^^xbNCj_4UhPj-*A+a1O(mLqUX--%`z?Q0@&jUseM{N0zx|5SA1NK|& zr|bFQaQH!+;2B%7j>L3rZ$&!|g2bs_h)uh!JD7;lWy8N&`Dl5-hI&-^m$$KX3TRz7 zUr|0Uf@-jQ$#e@66znP@1o_5UqER?c?Q}F(6-AS(>c2=^X^TaFpcY}wPxrBHuH70X z=eSJ6w4M@mvSw`6W#lWibb7ka5ttYic(}3Cg-naxnrpN8eoW{3 zW2;@l3|{VkB2nSB^?NvWRm9grd>6KX_>7u*B;N0x?t~xC%F3FGlSCE=1Y%L-cR-L{ zOv|^?QB_ryQ&(5FaTItqH9Q=*xVXs2&yV%jDjsRt0P~MmJWCI0?e`q_4T#eT@G{D7 z2j&kFoN;!znbo+(%f#nH82nMhsbPHW+)GG~%bqiO8P<=^os}9NSPgJ6C;$1f)LoT7 zEbx0qQ=SL#>1w$s{9QtBpNp3mz!gdzPktY~KYSTFaOXbrt=J3RJ_l|8U89O=VaqD3 zpDh?lF}%}x+*;!04P3cGd`4h;^D4lg_3e=Vzgb^cdup2c7Zx31gB+{(o8(6TdV93<`T zfbPsHrs>PkHFMci7y^CB#D#VSQTz4=tc>d+ejvyQlJKLfbOQ~+S-zDWM|)bOadx}O z^*h|;pZ6kXg|=Q_s$5xFFTA>y{ckP6)bT>dv>tvuySmkGGLEO)E#tdxp3fV9eXH^$ zblQDJX!3~h%^){iEFbP_Bv(a(=z)oT@-5R{FU6zx&yM@M?N|8QHXEN{ zkt=HvG~o}+>IN=7-mUf|?89I5b$pS~P;PiG1S8jW%fZ!wakH+D4^+tHynArGpZ0hP zR?^;JIzAuf)crmLxfFIZnp0QJ5|S=7#QS*ML{`}&@r`P7!ig~t0||#8kJ3H&#U)fg z$p9T51#o3WlCLG5b@}sHeK&V{XccmsdpuKbBAEyGwlK-}$FN)!-s67b{2_LZ#3cv* zZ||^k{Pq-Y^}s@8h2cFq2 z^`7!`C4DH^4`db4M~h;D?1*V3r;@6UQ(Gi;3LG~hLMI=oY-eXzq(v5ppYkfJZM9x~ zeSL-VTw958rMSdNx%~Fd250BP{O5@8Oe%J}UaOBhtXmC`b%3onmDCok7oeu5PI}^O zs;8&tG0%yAJ@d0_Jr#X@{fUVQo`B|UFV`!mbj#&U0UJc-BReGone@HxfC#-=ll1Qw_4(FYFBo!n&rlcEs(lu z6(Za1o|@u12U}OE9*r5Vw6Yp5P!}<+#J`Nr<imI+Jv}{1LavtJ&Xmu=r3jjkhxYxqcG4&lEPiI&@w%fm zFW?PPkd?KgV=u@@SNTAa5pAyr`W<;wtJLRH&+~g|=t#ccI zl8acD)PBULSvOw;x$=0djA#_Eah(mM5FaehZaXGHi;8{P!Vb^E{*RE^!$%bMmeg-FanYb^@K zcF$<91iNmuH)OD%i&9l}!aYyQ*2eUJM@4A&58oZMg!gspH*Xdlw|Gj#X<>926#~Aj zMjjtD8-7&L7u@wtNpD`ia{$9gU`rO7y;ggVXP5dEJ)MxmDS^JCO|}Z+{v%JlF%oBv zoL~35kALc*VleBc+A@~j53BW@IO~r~>*m`6f!)RrlJA8po#$z#yi-rCszie4Cl7VH z_Kw3+%=Ys8qaE}Wr;ZPiLb}b~hNsxPzQq{IGwwj2QDdw1nGlQLiJb>CS!_GL1T+2p zy4TF?a0z~g=Hoa9<&kBn z5dut_jcfdDp3u%l32wB3TtZ`g%9@w->nsQ2b*nRZvqPeKwvN(2A*oIlHQ7eoa2^Zq zCpi%uBnE(oE29!VDQG<1Cm0epwa2f(c7*5B2!T<$2o7zTTTz>vg+6mj?RA3!nZ|)qIPfIcB zY2hi+J@?~d)BLL*+gSZk%dM_!JWg%ulh`^7Rk0YhdjSkiLMwgSXT4`WGyaDmF5iqo z^7Ia{nSLjuTnhv7B=cE1Xa3@Q^;~)RwIXLOE%_S7$e829dwb>6$M5*GQntyOkHQ+= z)>rx_+P6oPcHS5p#AoCDb&E#w=sTwhL-HDG*`i9mo(bA%K??~_cOC3z$TZ54JJl%? z>BgcS@-zNz^4nXk&2c9iyR-DRh}Diz6S|$D5GS7*>qhv}#@o8`j@RQ% z^Hxz)pL|fl*s0Uy9wF28xxv9H;f!X5T#e4p2_`mnHk8YS__X3nvVqTR+_b*7&PDNy z%H)D(4dOb*xICls{8D#Wx$0`G;k0HrsZUFSd)>k*W_zacl+!mdHJM)Mr~|p+RDL`p z3U}6Oi=ax+N&73CX>n=}HA*HPYb^0*tGvuhmBtr6FbwTOkO=dZe+qO&Z2cu6ze+4(5?1CMgcEqG$VhCy(kN6(wUh}XSiTni+7&0h zO8$ji0W4?Z&Mt|$9dc2$ZgI=+$m%SuFJPm5yG+OL$%rC9_?eS)pD)C+9g1O0c%ypl z?lm5eBWR4AjZfF|!fWkjmMUG=?yQs|c^r<#{SmD3wt1ebvF$n>0lj@xkFouJ$BkT8 z1HaSf4}HtJ1f9;1$6JMg(RI)zqWxp{s*NPw*gXVw)JzPR@q8IWtyNU?9Ui;i0lI?+ z{Xu7pEqW6?B^Ip&>E?JV;NeXao~Yj-m|tB7aB;4aE;N?~UOi(~s$Y!FTM@iaqF1K!CYE;#vSc`3hN^Gfh>J_fm zb@A5Hd_D>4(-l{vWZP;RwZK@D9tZM8MLcE?nl@@l3mYHgV>g5w`uACuXtE|fv2?jt z?zgGi_r}ls=cS~_mKwohr@80Rj%wiD71Y%Fya$ElOct z$Kkh4!VDmw9KD zQ4wZF5kY}bM3gGhq>l|mnh=VF5)lFELI_9;I-_6%lwPDuFhD>82~uMlI)oNlfJhAx zAP58qA;~>K=Y8j{`>pleyY4^lUH5(dP}a$Fp69e@pZ(i=pWl{`m>+L&WrQjiYfHBD zeNm}mGOrfIM*tU?mpaA_V{@cWCz_~r(%k2I8cdO(EySL0kWY>HkQ4W%OLX5^#P*tx z#MdnnALaO1(|7Gb$!Mh(n30AQ>dU*IMVZBNW0{e)b8t=0JxFg)UwW7%ERjw@TpkPj zP-}^{TAfO7S{Z~GgH3>awhnPgf}IvT$|?o#17+8WN5fwiygsZa@tgXuSw+myMIq6!0M zH`CCh&{Yjvj>mdhWn`x=$LD)J{`}!af!kvgWP`5E(YsL2stoKZYq?xhBOu(jjvi)L z?G_TN%}~n1aj=Dls3~pqR`A>&uyJIKy_L09+#XoapY8%k&E2Z^9o56{NXGZ5qdPNy zI?RNaMbPy7Ubhj7(ExW|UC#`^(HYWWzlG4_DF}mVYVtRibdfeSVKu9p^95;|{$}yF z<~Kz{=ys6_e)4tlksBTQ#(o$LNTVq+!)aYc)2*|OrN_$D=1R5MDwyfEti=vhF9(mM z^rK*u*zKB0?z#M718NW0Z_WLnnP4yiwmF3jpXH^ZV%mu!lw`l{sd(?jp>*&Iym^hu zWV;aQVc#?RvoIk6jGnrc<^gFVVCAU$JUYU@hPK+RO6RT(=M#xl>Ex9(dia?Pym#~3 zW7dp0af@amRxwb~?^CKiT^P50%2vKk)xNgsRm3L=&6_jIAsxzoOJNezzfsXvvYKv` zlxaAHdmD^+91E|J>au`g=uw5sdEfGalPrw$fjZCMp?y|CLBYe|A z;G*h6TB;FmA%lIU^o7(~+X#AfrDXutGYhz=z-Cp|*CqEgjt_tNpSb()%Hds$K2Lzb z-~A-Lqcbk&&CbtzuKg%r@fE!*J&8G@p&t4})Ttd5e^2DkEf659oL>+6^ut#?wGcQu zI%-$p_BU|Nd$smxYtsLWrgyaOOwW%|`{x!G+5r?X{t9Y-k`Dnwy_*ZHzfmfn;x$*j1eD zz?f=ox2I*@ZxK|PeN;VLdKvu+E9J&(;f)_@sohyjC9(`aWb20lf?l7$+^!M6Re-iB zMq7Oar8lU{K|9{5{|)=c-{;}q6rKKMyn({=`ELvW6r}MDzN6qRH3o4!Z1c{nUpZe_ zWd?DJXHoI+8a~bXNqNY5soL3YsUgTNv?f&L<`T-}To?pe7+B!y5G<>}Fo@$x?RSEf z^wL*h2;-XMD!0jx87sY9Xi9cuu#uo90xGEIF)T>Qe$SBm^qPQo!`J%6Bp zJ&5!0rTeU12an)eIFXbb3Y9?=AS`VS#2b$3($cX@{mKBQj>!}J5V5-M1YixJIJ1By zC9$)B7QjQB#5XZRs~yEX>Ji5IhP+w$e7S#QEW^JL)A{Iobyt%0X!T4*27F={KguPP zB{7x8j8&)(pn z)X*rO_E&J5S6X@fJH{ZsQ@k!)dQnlQzft@ z-382Bi@6WYQH8}Ci%q3IUy;+n5-@N);CgXi_R+qeOD1@7{N+qh3Pf&&or-z#QTFX~8r`f;ZLoj@pVmUMrVHpC47hf5p>D$7jV;6p7xr z{1cpP-DE<@>Hs$6V?Q!-MozN!7>xZ%D61Z$EkVrA{PJ)ukRj-Ka;vc4zO9KnOJi=i zBL}-QzrCkxRNv{Pa9jVBYnj#w_lCLG@-=fWO2WNODRjxOH!4Dkp*n^2&Pds$)|_=5 zeH_wTZX1TY^QYq<;u#|L1@Zm(&y`QxSAsWup~#+1QKxNOx|aiD<%7=~y^5{vmUYA#{s|S~4L=-gIL0r`)1P zX!npIZIrq>uaG}8;##*8))&bfE{j5G{cay>IuR~tj)S(8yB|&EZZ=6yCvp+j)9uq& z=7ODGq`^1Zy~g~vb7Bj?@}N`)I{Olq?g#oUc8uhss2gKQ2Lrlt9V|OC*M*$LjuGok zHh0>LX%Cw#dAU_1wbc*1yU1oPNmo(x+MH6Z$`ZA`z*JBOwAQ+bm{s@&=ILq$O|rd( z5P5yc?(GSF#nh}`${B)RM)3CMT!)Y^3e3;8oyIuyR^94=7bAQt)0Q{1=bri^RX4Z` z<&~AVDQWli4!N;Ft0GdXI?=fxzEZ%;jJDk9cFA^U?JT&dmyX7}3YE0q--nVhl1;j5 z7RKHlSxtb!GOHIjNp_*!CojW0$HBf@y)t3H@=iQ%I*v0h9Kw~3d_EMtko1%iiO6y) z&!w4sST}|N|9r&+o1)Q_7YDp`$Zz-3pRph%=K1_>bPyxSB142F!q#?Ww8(I z(B}Q4|;pNOmo6U2M)%v zD`gIgYmQq-zls=nWm1W1aiXc*>PiJL)6CT4BU+;;)PL$)ABrM4&e!mxf>P2mK^SDyRXqg=i z?H>0ye|3IeT-zF3#tMLV?cxh?ydaSCUzq*BobZQ=#WaXE+ zU$=*kZ8jt%oG{ldSI0qh!#dKts++Hbj$YpS(H2;BJwYEVCov9{KSb?!)hCa%A-tBSDGUDF}=a1>o% zIhay+F2Vg7v3_5Q+ronvX$_Q5I_uZm32vkGWpqSUh)m1i9w`-CPK#Y&Z+@Cqt(4IA z_7~MEb=UP=$ja1&H9MUf**Ncsi+A=>n43d0XsPS7hm(K+5m+_Zai}<%IA+xLYL(2{ zDJ_VqATPN|>S*RS3YwE2Q7#pW)XA}!e*9%5Pa;&k1fvOVW$98MS$sSj(zxj^Z(r-V z>c_P(PfstZUUqRWh}eEaF|$VB>PtoH9Ds+#`5a*t+sJ8df6}a;w%?eV(m$v3)Hn1* zbmkdt63Ua{?&BNbR$2@c>Xm8M%y1+@s*vD}T&i*P7u0eyfJUorgmF+aC@Ksi39klD zKd>Q)IJUh(1qL(LN0Zt5)^Iidqxq`i4c zluaJ%JpN6TGzL9uts!OfkXZ}G(p;_qzG^zPmpW6VG?om$iS`=nBda`lT$o(e@;hYR z7%ZJcyGb}zNb>4He^y_p7js;L-U%L%Rh|h1d!e?58(80#G|V&)WR`A^DEy%n`@&^7 zV5)^R)B4n}C8A2HBfU`l*3chvtCPj_#et_0c!c-&LzLI$K56jT#BgfQ!!(_UIFpgE zg&cQr<@-t9|LA&eW?t1HXo%BJ6$8g9G%$s$*n9u%|W}kaOOhzcpiPGpiJjn2Zm6u zyZvycY(r{l-Mw^d44O4L$EQ@(nnGBv-a5Y;Yhr9{oSu=vYT#}V0r~>C3SADSK+I(|FYvR*T z+8I|9U*@_}&{l&4bnTLjVwjdpHhj~JGEnNa8ildrG((siTY9s%TM)Obq^mqwesjuz%s*w$vK1vn1vN50h)tmax_rj-JzX8=7 zz&l>8paVc{#JCuQsq5xczGXy)Oa-(CjRaoMUXc8)R{ycGv~cb!-$*jcv`xupLm*Ziuo@* z=2?IR6J>Un)Vti}c{8oDB{F3QIzu~dOTLAJfAeZK;>?@N6o*ub%S5T)e!d29;l124 zUfnQi9Zz!}iCDyz)M9j{W_(*84Q2Fb704+})&MA}5xOH-0k%yLpW*p#xhW0x)fqAF++oxO6o*rJUBL`4!OM4(kba{b5e6>Fk|4T&uZ;s^tS2z6sLooC| zjpzGk{KPISNgu?c74mWf{I?FjzoV-EDiq^CYvA8HNZ>Cs%zxJZzt;bM);!<8cVx?d z7VZBBl6IMk3#N2iCo#pEvgJSmljtKNp-PbuHaEf;c+kr2T9C;4XxG z^W}mckN^7X6Q4_lZ*uNl7X9Ie-QxQOe|_L&aN)<(GL_Ha>ujm1=g%+hef6@N?vAOy zvW_IOIO|St`t5}1NElGM{*U-M|8LKL|LxGw(ZcL2M8@ou`xuNU6MNJw4dl5AXjWAa zO_uIeHJ3;h=uLD^Ju8r}?z!WR1jlm)6+LGGM_@seMKXnk_Zhfz`Jox{NW`(3@2YH% z1iQ&nmz6{I>YOz-PX2iMqpE8fc!caBC$Y08AQj3lUyY=EeB6-rrNrMa{4Q*(Vd-e! zwDrM+j88(GxhTqyBsmBB#`Igs=v-h-%*$!vvy_4ZuUJNrixKEF*;C7}O?wYZs}~h- zS-cO>$^=p6;0Ru_D^svnH6k+oq{>&H5Z^uSB^DfzA88mx-a`XAr;9jYEX!;8NP>GZ)g}hg&t=fFZ;#jZZvW} z{Iq)ee(nJ#Q%gxNZ!4P@3kt4#_xdWG6IbFSY~Nc{eUQq#MJ^&q=^VcrJzD+n*i1v+ z4I@hvaX053g@=!iV7uq%WN%WZ@04q~kNpWA(JAlx2lU~^cE!{6MHM_`&AXP{EXd!w z$M>V^eFjvsEP-bd2iK3c+rI(<5VH5X`1J$IX^+32e&_r3%|DOkyjEVU4TP{1M`an{ zi2c`!e1+w55w~33Jw{yOtaE%^8&6&O8Y2rF5-;Up<(6!}YR5L)l9FW3&^P`}zIS#l zt)2a^s3>duyLJ!$NoKo8cqlWRDI%vfSM31;trh(nB2t#D)eU}j;+?tUUDrI`pEt8h zww7{H{_9xWJCD}vlS8;w;ZW+tQ#G&7qxFO-7YehC;9%$Ka?ttYj?xH5wBnbaB~boC zBV}l!iIs_o2`=X1zuFnN82<23!xlEwaVQ_UCG*!v-095A%Tx8=K0xZx*hweBYs0DB zv)a)EKm4ozdrA%_4D6Wjz^{M*wlN=$&7=#r75{7epQi0ZZ-V>&A$Ektdm6Y-y%&75 z*wPW@+$3$37F~~X*gqKjH8tkjG;WVAfNwto6MYbThI{<|Td#0& zcwH}C9zd63 zTu8t6BsNW2IIs-@YO#qS%lFn7Mr`sK527B*sh2i<$j@`N>qq|tUo{78=hr=NYEB96 z?^Y(~Mnu#4Zu@_0zo$)abN*hqgiT3O=~{<mU^c$9jCH7iMTqv0;$h;Ix5$t zDfYBB?YVq!qQ@b#yg*ftoa>gBk)F#^&QBcN+#XgeyO3PAd4U|f z)bshf;O&!*d#s}4BuG=Idn?`8p%E*bld4PED%x1(VEScHt&4Z{3@J?!TO~d%q-_uCbl-;^J-^ zDFm(>GVz0B&&+YsQ?xyK+Ilb@K`RiuzFdcD+;d4gnBL#@bAD3D>@_qunAq0VyKM`z zr6(R_`@9b~O*?0y4|udhE8L9ZEo*{%?aLY*CQ_F-VbcM-id~<&6*L0*PVBgQzIJ1= z_hlN9_7DxJ)Ks&sfTh!`@U3_yI;m3lBBz5?Q-kwu>dxDo1j^sSHZF_Jk@OI}j zk0MB6EIpj8F2X|UWo4Iztirmv5$n~lM|F#)Y>dQo!&9NsIFNpU^v+p|#5EtFJ++3- z7e_|q1M*$R$PSX2&N5(#v&&VFPs|3@m6W=AT-y>Ob-<~i`PVFiILp95D`)YhI4{oc z18Z|D)2$r{+O|QAs>cK*T|4rAvI0zzjAyjc{9cC*D24`o*bHE`A!^lJw^pGQzL^fk za%;Y9(WU~=vGruYeN$Y$$>5jRtT+PlQ2oaJLk+c8=J*9qfg|j;jK%!i;Qfgn>8||? zfRp0(=BfP>cWP}gc7?9lk*EyT#z2u8EwLv*W9jX*8|9p_z9Wj)S_&oHG|Jhkh-lAQ z5!wHijkm-_a@o5b!>sjjx5pfW^j2;^_HuT1PS+1Z(Kn*@i_J!Pm34<1>%{|s0n)R4 z`0(Ls_{?IkNHI+~lazyGTe^trgs;6^=j@{H4Y>L!ec4s*N|HCi4-`|~8%{;Ldd|Qhh((eu+V-kY zM7bvet&YbC7uytvRVf1cxzfPw0S6M1uJV>sbh{XB-kkq7T3$tBbz!iq1)rX zFwBfzxZ1_<9k`WwE~JRlRaG?%3qk|l>~g5U!?zcv0v=l3_T~wU8;2swPkR}l2#m8K zqrkoM^64LGX<9)Gsp)Dyp^fU*zEygx1$fL+t?QTsrE3?$>voU%XNdKcl>wRe1xM>= z?q$p+J$7 zKc5GTg0mIV*9l0AF;03 z3I00;r>fYPu>mj8&G(DnqA`bAk<@(lhjjQ&H<*AFn zUVT?ZnP;`r{1g&DI?v5;BG<`(iNGyh}9Q;by`~>C#$-ZEAL{FK9yrM3>< zCGqw+%lTvlcM`SwPVK7dQn^fDmAg5jVSB}L03Hyq_NB0GZT8|uw!aBEY-T7HpRHyj zM{dx{oa2BrLcNoc126KMz`0W2`mGszv5mn4Kw92q)5!F-I3*j8^))Iu`})`}_40)a z4z9kIV34kj9z}UsvlRyZ}y&_8uEof)XuZx4#7e!!$!+sgq)ocjG^racs%jl4; z%@J}kV4!s~Nqd!=nhKFR9F>=!SE-3;YoWA>qb{%ud{4jp&NEe7JMd*Vt}0J`Dqm6avw_>WV+_`oKtrxao(merkUHNEm<7`SCFj=0`+|(DtfgU`xCQe3eLCHg z^Z0;P(Dv)&Qpbta5s1@t&gA6838Tj`#DS0)vH8wpA8}qpr+U^H|1dtHcFT!n2(u^S z5ybC>&=+fi($(iY9@bpA&Qs|7WGN=?Di6~NU19*dxF&qz!iB!29z1mWy&*{Nk#ga} z)v~IA*A^E6RZyjmz3>tH!@u*gx)_m*%w%AHltQRbMQ(?@xwm@7$x&&-iQGNDPLzSr6#XW>vmSB9P4(9lp#@Pbs$=7(D` z5_;`)`8IHlX|7&Kzvmv=_r=HYb1pCteVE)UTGH32P_q`V9{;KXZGWrTD&ZucePYdD zd%;rbge(kEA~#NnYo6`e2s?lN{u1&=Nt8ehM4XIE0pyQ$5E;PKZf(y7s=~DL)Bald z@T9tD7uy?o`%2Rpf&HSYzX9HDu?WidP{y`WP@8jd97@^i+ zRh}NL^=MqE9)9!1aFMk^z|FU*wIXVP!v4#|wF z48%It@}2~T+2u$@$0aiBuEs!Gs1;sB3C#6sD66*@Fsc`3Bbk>07tG$hg0fXJ*h?VY z4HoaCMxVCrt2A;^(<`%hnB`^N(%Rae$}a&_mBg*}d30Z;Do>M|=~K`OpXyU#hhH9F z0>TfKxLMaU*_|^k1vXB3Bg@RLYdm>9wCZFzjQ1IiPey_HLmO6=KKW5dpSnJc_6GYr zw-(Ij(!@p*-Y4U%ka!P~;N4RxszSnsz`@&Jfc}wGmAivasCyib{+RpV!PeC)k5a%} zwr!nAhLR~x?m)56L7jWz=6p!P{VwL^@-KXRR)2hIj8O~j7wCX3r=K*-k_FtfDQyMn zB95>1UkZ#zQPi&N`~BkDyN_|LnKTJedaEYO7EqiLd5d17QN*&kw zT;h_4vOUtpg1PnSI_!{9&AX`=92FUaIRnIEPj$N%I|kCf`h^E+OVHCT>w`6!cj-zQzTRwn;ZAmU_1+ve@f`EmDX!CZx#$L#A`=`vfe8oG7!4Gxk| zQAdVT3ePUK)pAp75IDC{t&R|nhFA&xQjpHdu^XKqwO_(&9Cu}(4xn`_+Lr}9sj_XG z>tsJ~fWBPqu<{T=!){`sATT$Hq2!D2ezx1(3b9uB@RyB)llrH-^On5`M0R+jFj?*| zYfkO0bc2@9J@noBvegitf1G#q9jw@tHzBAfKkaCgzqzUT&C0+>dM__=;}DFBy_MP` f|CgNuH_83(z&dGn+qe&fSJ~*Y*`?A8H-7&gi4#1} literal 29443 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 Date: Mon, 1 Jun 2026 21:17:01 -0600 Subject: [PATCH 7/8] Standarized format and logic --- src/browser_app.cc | 65 +++++++------ src/chat_app.cc | 9 +- src/fractal_app.cc | 30 +++--- src/include/browser_app.hpp | 73 ++++++++------- src/include/chat_app.hpp | 21 ++--- src/include/fractal_app.hpp | 102 ++++++++++---------- src/include/main_application.hpp | 25 +++-- src/include/session.hpp | 156 +++++++++++++++++++++---------- src/session.cc | 17 ++-- 9 files changed, 282 insertions(+), 216 deletions(-) diff --git a/src/browser_app.cc b/src/browser_app.cc index 8304e67..ec470f9 100644 --- a/src/browser_app.cc +++ b/src/browser_app.cc @@ -7,39 +7,6 @@ #include -std::string Browser::get_uri_root(const std::string &uri) { - std::string result {std::regex_replace(uri, std::regex("(https?://|www\\.)"), "")}; - return result; -} - -void Browser::entry_uri_load(std::string uri) const { - if(uri == "") [[unlikely]] - // Get entry text - uri = uri_entry->get_text(); - - // Is current URI a web page. - if(uri.find(' ') > uri.size() && - std::regex_search(uri, std::regex("^(http(s)?://)?(www\\.)?[A-Za-z0-9.]+\\.[A-Za-z0-9/+-_?=#]+$"))) [[likely]] { - // Add missing parts of the URL - if(uri.find("http") > uri.size()) [[likely]] { - if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[likely]] - uri = "www." + uri; - uri = "https://" + uri; - }else if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[unlikely]] - std::regex_replace(uri, std::regex("https?://"), "https://www."); - } else [[unlikely]] { - // Add as a google search - std::replace(uri.begin(), uri.end(), ' ', '+'); - uri = "https://www.google.com/search?q=" + uri; - } - - // Reload if the requested URI is the same as current - if(get_uri_root(webkit_web_view_get_uri(web_view)) != get_uri_root(uri)) [[likely]] - webkit_web_view_load_uri(web_view, uri.c_str()); - else [[unlikely]] - webkit_web_view_reload(web_view); -} - Browser::Browser() : Gtk::Box(Gtk::Orientation::VERTICAL) { // Load the GtkBuilder file and instantiate its widgets, check for errors auto ref_builder {Gtk::Builder::create()}; @@ -123,6 +90,38 @@ void Browser::on_realize() { status_label->set_text("Welcome to the Browser!"); } +std::string Browser::get_uri_root(const std::string &uri) { + std::string result { std::regex_replace(uri, std::regex("(https?://|www\\.)"), "") }; + return result; +} + +void Browser::entry_uri_load(std::string uri) const { + if(uri == "") [[unlikely]] + // Get entry text + uri = uri_entry->get_text(); + + // Is current URI a web page. + if(uri.find(' ') > uri.size() && + std::regex_search(uri, std::regex("^(http(s)?://)?(www\\.)?[A-Za-z0-9.]+\\.[A-Za-z0-9/+-_?=#]+$"))) [[likely]] { + // Add missing parts of the URL + if(uri.find("http") > uri.size()) [[likely]] { + if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[likely]] + uri = "www." + uri; + uri = "https://" + uri; + }else if(uri.find("www.") > uri.size() && get_uri_root(uri) != get_uri_root(HOME_URL)) [[unlikely]] + std::regex_replace(uri, std::regex("https?://"), "https://www."); + } else [[unlikely]] { + // Add as a google search + std::replace(uri.begin(), uri.end(), ' ', '+'); + uri = "https://www.google.com/search?q=" + uri; + } + + // Reload if the requested URI is the same as current + if(get_uri_root(webkit_web_view_get_uri(web_view)) != get_uri_root(uri)) [[likely]] + webkit_web_view_load_uri(web_view, uri.c_str()); + else [[unlikely]] + webkit_web_view_reload(web_view); +} GtkWidget *Browser::on_create_cb(WebKitWebView *web_view, WebKitNavigationAction *action, diff --git a/src/chat_app.cc b/src/chat_app.cc index 7d6693d..121861a 100644 --- a/src/chat_app.cc +++ b/src/chat_app.cc @@ -2,6 +2,8 @@ #include "include/session.hpp" #include +#include +#include @@ -57,10 +59,10 @@ Chat::Chat() : Gtk::Box(Gtk::Orientation::VERTICAL) { append(*chat_scrolled); append(*footer_box); + // Add css to the default display auto css_provider = Gtk::CssProvider::create(); css_provider->load_from_path("./res/gtk/chat_app.css"); - // Add to the default display Gtk::StyleContext::add_provider_for_display( Gdk::Display::get_default(), css_provider, @@ -79,7 +81,7 @@ void Chat::on_realize() { inline void Chat::session_connection() { // Disconnect if connected - if (!connect_button->get_label().compare("Disconnect")) { + if ( session ) { session = nullptr; connect_button->set_label("Connect"); @@ -137,7 +139,7 @@ inline void Chat::session_connection() { std::lock_guard lock(session->receive_mutex); while (!session->receive_queue.empty()) { std::string message{std::move(session->receive_queue.front())}; - session->receive_queue.pop_front(); + session->receive_queue.pop_back(); std::cout << message << "\n"; @@ -169,7 +171,6 @@ inline void Chat::session_connection() { } }); session->set_poster([this]() { dispatcher->emit(); }); - return; } inline void Chat::message_buffer () { diff --git a/src/fractal_app.cc b/src/fractal_app.cc index 8d76825..1b7ced2 100644 --- a/src/fractal_app.cc +++ b/src/fractal_app.cc @@ -2,10 +2,11 @@ #include #include +#include +#include #include #include -#include int mandelbrot(double cr, double ci, int max_iter, const std::vector) { @@ -20,6 +21,10 @@ int mandelbrot(double cr, double ci, int max_iter, const std::vector) } int julia(double cr, double ci, int max_iter, const std::vector consts) { + if (consts.size() != 2) { + std::cerr << "Julia fractal requires two constants, using defaults." << std::endl; + return julia(cr, ci, max_iter, {-0.5125, 0.5213}); + } double zr {cr}, zi {ci}; for (int i{}; i < max_iter; ++i) { double zr2 {zr*zr - zi*zi + std::any_cast(consts[0])}, @@ -68,14 +73,14 @@ void FractalArea::iter_to_rgb(int iter, int max_iter, uint8_t &r, uint8_t &g, ui FractalArea::FractalArea(const std::string selection) : Gtk::DrawingArea(), - selection (selection), - cx (-0.5), - cy (0.0), - range (3.5), - drag_start_cx (), - drag_start_cy (), - surface (), - fractal_map ({ { "Julia", julia }, { "Mandelbrot", mandelbrot } }) { + selection (selection), + cx (-0.5), + cy (0.0), + range (3.5), + drag_start_cx (), + drag_start_cy (), + surface (), + fractal_map ({ { "Julia", julia }, { "Mandelbrot", mandelbrot } }) { set_expand(true); set_draw_func(sigc::mem_fun(*this, &FractalArea::on_draw)); @@ -124,7 +129,7 @@ void FractalArea::on_draw(const Cairo::RefPtr& cr, int w, int h) } }}; - int band {h/NTHREADS}; + int band { h/NTHREADS }; for (int t{}; t < NTHREADS; ++t) { int y0 {t*band}, y1 {(t == NTHREADS-1) ? h : y0 + band}; @@ -171,10 +176,8 @@ FractalBox::FractalBox() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { throw ex; } - // Get the GtkBuilder-instantiated nav and header: - menu = Gtk::manage(ref_builder->get_widget("menu_bar")); - // Get the GtkBuilder-instantiated buttons, and connect a signal handler + menu = Gtk::manage(ref_builder->get_widget("menu_bar")); reset_button = ref_builder->get_widget("reset_button"); save_button = ref_builder->get_widget("save_button"); fractal_dropdown = ref_builder->get_widget("fractal_dropdown"); @@ -211,6 +214,7 @@ FractalBox::FractalBox() : Gtk::Box(Gtk::Orientation::HORIZONTAL) { } ); + // Iterations scale and color scales iter_scale = ref_builder->get_widget("iter_scale"); if(iter_scale) [[likely]] { iter_scale->signal_value_changed().connect([this]{ diff --git a/src/include/browser_app.hpp b/src/include/browser_app.hpp index acdf327..6f9dfbd 100644 --- a/src/include/browser_app.hpp +++ b/src/include/browser_app.hpp @@ -16,6 +16,43 @@ class Browser : public Gtk::Box { friend class BrowserTest; friend class BrowserTest_BrowserFunctionalTest_Test; +public: + /** + * @brief Creates Browser object with all needed Widgets and signals. + * + * Creates Browser object using res/gtk/browser_app.ui file, + * Checks for file issues, gets Widgets and connects needed signals. + */ + Browser(); + + /** + * @brief Function for webView load events. + * + * Detect load event type and modify Browser widgets accordingly + * for stopping load and disabling input. + * + * @param[in] web_view: webView that fired event. + * @param[in] load_event: Load event detected. + * @param[in] user_data: User provided data, will use to pass browser object. + */ + static void web_view_load_changed(WebKitWebView *web_view, + const WebKitLoadEvent load_event, + gpointer user_data); + /** + * @brief Function for webView load in new tab events. + * + * Detect load _blank event type and modify Browser widgets accordingly + * for stopping load and disabling input. + * + * @param[in] web_view: webView that fired event. + * @param[in] action: Action detected. + * @param[in] user_data: User provided data, will use to pass browser object. + */ + static GtkWidget *on_create_cb(WebKitWebView *web_view, + WebKitNavigationAction *action, + gpointer user_data); + +private: Gtk::Label *status_label{}; WebKitWebView *web_view{}; @@ -56,40 +93,4 @@ class Browser : public Gtk::Box { * the box needing to be contained. */ void on_realize() override; - -public: - /** - * @brief Creates Browser object with all needed Widgets and signals. - * - * Creates Browser object using res/gtk/browser_app.ui file, - * Checks for file issues, gets Widgets and connects needed signals. - */ - Browser(); - - /** - * @brief Function for webView load events. - * - * Detect load event type and modify Browser widgets accordingly - * for stopping load and disabling input. - * - * @param[in] web_view: webView that fired event. - * @param[in] load_event: Load event detected. - * @param[in] user_data: User provided data, will use to pass browser object. - */ - static void web_view_load_changed(WebKitWebView *web_view, - const WebKitLoadEvent load_event, - gpointer user_data); - /** - * @brief Function for webView load in new tab events. - * - * Detect load _blank event type and modify Browser widgets accordingly - * for stopping load and disabling input. - * - * @param[in] web_view: webView that fired event. - * @param[in] action: Action detected. - * @param[in] user_data: User provided data, will use to pass browser object. - */ - static GtkWidget *on_create_cb(WebKitWebView *web_view, - WebKitNavigationAction *action, - gpointer user_data); }; diff --git a/src/include/chat_app.hpp b/src/include/chat_app.hpp index 57ed643..2e543c2 100644 --- a/src/include/chat_app.hpp +++ b/src/include/chat_app.hpp @@ -2,9 +2,7 @@ #include "session.hpp" -#include #include -#include @@ -18,6 +16,16 @@ class Chat : public Gtk::Box { friend class ChatTest; friend class ChatTest_ChatFunctionalTest_Test; +public: + /** + * @brief Creates Chat object with all needed Widgets and signals. + * + * Creates Chat object using res/gtk/chat_app.ui file, + * Checks for file issues, gets Widgets and connects needed signals. + */ + Chat(); + +private: Gtk::Button *home_button{}; Gtk::Button *connect_button{}; Gtk::Button *message_button{}; @@ -57,13 +65,4 @@ class Chat : public Gtk::Box { * connects to it. */ inline void session_connection(); - -public: - /** - * @brief Creates Chat object with all needed Widgets and signals. - * - * Creates Chat object using res/gtk/chat_app.ui file, - * Checks for file issues, gets Widgets and connects needed signals. - */ - Chat(); }; diff --git a/src/include/fractal_app.hpp b/src/include/fractal_app.hpp index 924b2fb..2d75b75 100644 --- a/src/include/fractal_app.hpp +++ b/src/include/fractal_app.hpp @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include #include #include @@ -46,37 +47,17 @@ class FractalArea : public Gtk::DrawingArea { friend class FractalTest; friend class FractalTest_FractalFunctionalTest_Test; - std::string selection; - double cx{}; - double cy{}; - double range{}; - double drag_start_cx{}; - double drag_start_cy{}; - double const_r{}; - double const_i{}; - double r_lower_level{}; - double r_upper_level{}; - double g_lower_level{}; - double g_upper_level{}; - double b_lower_level{}; - double b_upper_level{}; - int max_iter{}; - - Cairo::RefPtr surface; - - std::unordered_map)>> fractal_map; - /** - * @brief Uses custom RGB intervals to draw spectrum + * @brief Draws the fractal needed into the Cairo Context. * - * @param[in] iter: Iteration to compute the RGB spectrum with. - * @param[in] max_iter: Maximum iterations available for fractal resolution. - * @param[in] r: reference to the R data to write on. - * @param[in] g: reference to the G data to write on. - * @param[in] b: reference to the B data to write on. + * Whenever the drawing area needs to redraw, this slot will be called. + * It will draw the fractal using the current settings into the provided Cairo Context with threading support. + * + * @param[in] cr: Cairo Context to draw to. + * @param[in] w: Width of the drawing area. + * @param[in] h: Height of the drawing area. */ - void iter_to_rgb(int iter, int max_iter, uint8_t &r, uint8_t &g, uint8_t &b); - + void on_draw(const Cairo::RefPtr&, int, int); public: /** @@ -88,18 +69,6 @@ class FractalArea : public Gtk::DrawingArea { */ FractalArea(const std::string selection = "Julia"); - /** - * @brief Draws the fractal needed into the Cairo Context. - * - * Whenever the drawing area needs to redraw, this slot will be called. - * It will draw the fractal using the current settings into the provided Cairo Context with threading support. - * - * @param[in] cr: Cairo Context to draw to. - * @param[in] w: Width of the drawing area. - * @param[in] h: Height of the drawing area. - */ - void on_draw(const Cairo::RefPtr&, int, int); - /** * @brief Sets the selection string. * @@ -156,6 +125,38 @@ class FractalArea : public Gtk::DrawingArea { * Resets Drawing area to it's original positions. */ void reset(); + +private: + std::string selection; + double cx{}; + double cy{}; + double range{}; + double drag_start_cx{}; + double drag_start_cy{}; + double const_r{}; + double const_i{}; + double r_lower_level{}; + double r_upper_level{}; + double g_lower_level{}; + double g_upper_level{}; + double b_lower_level{}; + double b_upper_level{}; + int max_iter{}; + + Cairo::RefPtr surface; + + std::unordered_map)>> fractal_map; + + /** + * @brief Uses custom RGB intervals to draw spectrum + * + * @param[in] iter: Iteration to compute the RGB spectrum with. + * @param[in] max_iter: Maximum iterations available for fractal resolution. + * @param[in] r: reference to the R data to write on. + * @param[in] g: reference to the G data to write on. + * @param[in] b: reference to the B data to write on. + */ + void iter_to_rgb(int iter, int max_iter, uint8_t &r, uint8_t &g, uint8_t &b); }; /** @@ -168,6 +169,16 @@ class FractalBox : public Gtk::Box { friend class FractalTest; friend class FractalTest_FractalFunctionalTest_Test; +public: + /** + * @brief Creates FractalBox object with all needed Widgets and signals. + * + * Creates FractalBox object using res/gtk/fractal_app.ui file, + * Checks for file issues, gets Widgets and connects needed signals. + */ + FractalBox(); + +private: std::string selection{}; Gtk::Box *menu{}; Gtk::Box *consts_box{}; @@ -193,13 +204,4 @@ class FractalBox : public Gtk::Box { * the box needing to be contained. */ void on_realize() override; - -public: - /** - * @brief Creates FractalBox object with all needed Widgets and signals. - * - * Creates FractalBox object using res/gtk/fractal_app.ui file, - * Checks for file issues, gets Widgets and connects needed signals. - */ - FractalBox(); }; diff --git a/src/include/main_application.hpp b/src/include/main_application.hpp index 534eee2..a88b35e 100644 --- a/src/include/main_application.hpp +++ b/src/include/main_application.hpp @@ -13,9 +13,19 @@ class MainApplication : public Gtk::Application { friend class MainApplicationTest; friend class MainApplicationTest_MainApplicationStructureTest_Test; - Gtk::ApplicationWindow *main_window{}; +public: + Gtk::Label *status_label{}; - Gtk::Box *selected_app{}; + /** + * @brief Creates RefPtr using class constructor. + * + * @return New MainApplication object RefPtr. + */ + static Glib::RefPtr create(); + +private: + Gtk::ApplicationWindow *main_window{}; + Gtk::Box *selected_app{}; /** * @brief Creates ApplicationWindow to show. @@ -27,7 +37,6 @@ class MainApplication : public Gtk::Application { */ Gtk::ApplicationWindow* create_window(); -protected: /** * @brief Creates MainApplication object with application id and handles. */ @@ -37,14 +46,4 @@ class MainApplication : public Gtk::Application { * @brief Presents main window. */ void on_activate() override; - -public: - Gtk::Label *status_label{}; - - /** - * @brief Creates RefPtr using class constructor. - * - * @return New MainApplication object RefPtr. - */ - static Glib::RefPtr create(); }; diff --git a/src/include/session.hpp b/src/include/session.hpp index d8e774c..89cd8c4 100644 --- a/src/include/session.hpp +++ b/src/include/session.hpp @@ -1,10 +1,9 @@ #pragma once -#include -#include -#include +#include #include +#include #include using asio::ip::tcp; @@ -16,7 +15,12 @@ constexpr const char *LOCALHOST {"127.0.0.1"}; -/* Connection struct containing socket, timer, buffer and nickname for each client. */ +/** + * @brief Connection struct containing important information for a connection. + * + * Contains the socket, timer, nickname, fingerprint and send buffer for a connection. + * Also contains a function to add messages to the send buffer using a mutex in case needed. + */ struct Connection { inline static unsigned current_id{}; @@ -27,6 +31,7 @@ struct Connection { std::string fingerprint{}; std::string send_buffer{}; + /* Constructor for Connection, initializes the socket and timer, and sets the default nickname. */ explicit Connection(asio::io_context&); /** @@ -37,44 +42,35 @@ struct Connection { * * @param[in] message: Message to be buffered. */ - void add_to_send_buffer(std::string); + void add_to_send_buffer(std::string&); }; /** - * @brief Session class containing everything needed to open an async client session. + * @brief Session class containing basic structure for Client/Server. * - * Uses a string buffer for sending, appends received messages - * to a deque and runs asio in a different thread for the app to run other things. - * - * It has a poster and disconnecter function so it can interact with GUI. + * Uses a vector to store received messages and runs connection in its own thread. + * Contains a sender and receiver asio functions and has a poster and disconnecter function so it can interact with GUI. */ class Session { public: - std::deque receive_queue; - std::mutex receive_mutex; + std::vector receive_queue; + std::mutex receive_mutex; - /* Deleted default constructor and destructor as it's not supposed to be used without Client or Server. */ + /* Default constructor for Session. */ Session() = default; - virtual ~Session() noexcept; - - /** - * @brief Sender function to run so the session sends data. - * - * @param[in] socket: Socket connection to send data to. - */ - virtual awaitable sender(std::shared_ptr); + /* Virtual destructor for Session. Stops ioc and joins thread. */ + virtual ~Session() noexcept; - /** - * @brief Receiver function to run so the session awaits for data. - * - * @param[in] socket: Socket connection to receive data from. - */ - virtual awaitable receiver(std::shared_ptr); + /* Non copyable, non movable */ + Session(const Session&) = delete; + Session& operator=(const Session&) = delete; + Session(Session&&) = delete; + Session& operator=(Session&&) = delete; /** - * @brief Adds a function for the session to run on received message. + * @brief Adds a function for the session to run when a message was processed. * * @param[in] function: Function to run on received message. */ @@ -87,9 +83,12 @@ class Session { */ void set_disconnecter(std::function); - /* Broadcasting method for Server and Client. */ - virtual void broadcast(const std::string&, std::shared_ptr = nullptr) = 0; - + /** + * @brief Processes a message depending on its type. + * + * @param[in] message: Message to be processed. + * @param[in] origin: Connection that sent the message. + */ virtual void process_message(std::string&, std::shared_ptr = nullptr) = 0; protected: @@ -101,32 +100,75 @@ class Session { std::function poster; std::function disconnecter; + /** + * @brief Sender function to run so the session sends data. + * + * @param[in] connection: Connection struct to send data to. + */ + awaitable sender(std::shared_ptr); + + /** + * @brief Receiver function to run so the session awaits for data. + * + * @param[in] connection: Connection struct to receive data from. + */ + virtual awaitable receiver(std::shared_ptr); + + /** + * @brief Broadcasting method for Server and Client. + * + * @param[in] message: Message to be broadcasted. + * @param[in] origin: Connection that sent the message. + */ + virtual void broadcast(const std::string&, std::shared_ptr = nullptr) = 0; }; +/** + * @brief Client class that connects to a server and sends messages. + * + * Connects to a server on a given host and port, sends messages to the server, + * and receives messages from the server. + */ class Client : public Session { +public: + /* Constructor for Client, initializes the connection and connects to the server. */ + explicit Client(const std::string &host, unsigned port); + + /* Destructor for Client. Closes socket if needed. */ + ~Client() noexcept; + + /** + * @brief Processes a message for Client. + * + * Client posts the message and sends it to the server in case there's no origin. + * + * @param[in] message: Message to be processed. + * @param[in] origin: Connection that sent the message. + */ + void process_message(std::string&, std::shared_ptr = nullptr) override; + +private: std::shared_ptr connection; /** - * @brief Receiver function to run so the session awaits for data. + * @brief Receiver function to run so the connection awaits for data. + * + * It also removes closed connections. * - * @param[in] socket: Socket connection to receive data from. + * @param[in] connection: Connection struct to receive data from. */ awaitable receiver(std::shared_ptr) override; /** - * @brief Broadcasting method for Client, send message to Server. + * @brief Broadcasting method for Client. + * + * Broadcasts a message to the server, sending it to the server with the origin connection as nullptr. * - * @param[in] message: Message to be broadcast. - * @param[in] origin: Not used as it's a Client transaction. + * @param[in] message: Message to be broadcasted. + * @param[in] origin: nullptr. */ void broadcast(const std::string&, std::shared_ptr = nullptr) override; - - virtual void process_message(std::string&, std::shared_ptr = nullptr) override; - -public: - explicit Client(const std::string &host, unsigned port); - ~Client() noexcept; }; @@ -135,9 +177,26 @@ class Client : public Session { * * Listens on a given port, relays every incoming line to all other connected * clients, and echoes it back to the sender tagged with "(you)". - * Runs its own ioc on a dedicated thread — same pattern as Session. */ class Server : public Session { +public: + /* Constructor for Server, initializes the acceptor and starts the accept loop. */ + explicit Server(unsigned port); + + /* Destructor for Server. Closes the acceptor and all connections. */ + ~Server() noexcept; + + /** + * @brief Processes a message for the Server. + * + * Always posts and broadcasts the message to all clients. + * + * @param[in] message: Message to be processed. + * @param[in] origin: Connection that sent the message. + */ + void process_message(std::string&, std::shared_ptr = nullptr) override; + +private: tcp::acceptor acceptor; std::list> connections{}; @@ -153,16 +212,13 @@ class Server : public Session { awaitable accept_loop(); /** - * @brief Broadcasting method for Server, send to all connected clients accordingly. + * @brief Broadcasting method for Server. + * + * Broadcasts a message to all clients. * - * @param[in] message: Message to be broadcast. + * @param[in] message: Message to be broadcasted. * @param[in] origin: Connection that sent the message. */ void broadcast(const std::string&, std::shared_ptr = nullptr) override; - void process_message(std::string&, std::shared_ptr = nullptr) override; - -public: - explicit Server(unsigned port); - ~Server() noexcept; }; diff --git a/src/session.cc b/src/session.cc index 6126d80..62ad0ed 100644 --- a/src/session.cc +++ b/src/session.cc @@ -1,25 +1,29 @@ #include "include/session.hpp" -#include #include +#include +#include + #include +#include Connection::Connection(asio::io_context& ioc) : - socket (ioc), - send_timer (ioc), - nickname ("User" + std::to_string(++current_id)) + socket (ioc), + send_timer (ioc), + nickname ("User" + std::to_string(++current_id)) { send_timer.expires_at(asio::steady_timer::time_point::min()); } -void Connection::add_to_send_buffer(std::string message) { +void Connection::add_to_send_buffer(std::string& message) { send_buffer.append(message + "\n"); send_timer.cancel(); - return; } Session::~Session() noexcept { + poster = nullptr; + disconnecter = nullptr; if ( !ioc.stopped() ) ioc.stop(); if ( ioc_thread.joinable() ) ioc_thread.join(); std::cout << "[Server] stopped\n"; @@ -166,6 +170,7 @@ awaitable Server::receiver(std::shared_ptr connection) { connections.remove(connection); std::string farewell{connection->nickname + " has left the chat!!!"}; process_message(farewell); + if (disconnecter) disconnecter(); } awaitable Server::accept_loop() { From 223c4b3c16f4a355b8758e5c7b7e5d5d08a3021f Mon Sep 17 00:00:00 2001 From: David Xchel Morales Hurtado Date: Mon, 1 Jun 2026 21:28:16 -0600 Subject: [PATCH 8/8] Fixed Server crashing on app change --- src/session.cc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/session.cc b/src/session.cc index 62ad0ed..39b4e7c 100644 --- a/src/session.cc +++ b/src/session.cc @@ -156,11 +156,6 @@ Server::Server(unsigned port) : Session::Session(), Server::~Server() noexcept { acceptor.close(); - for (auto& connection : connections) { - if (connection->socket.is_open()) - connection->socket.close(); - connection->send_timer.cancel(); - } } awaitable Server::receiver(std::shared_ptr connection) {