From 6f7b994684032060f0a2d7af82f4f3572293b8d6 Mon Sep 17 00:00:00 2001 From: littleblack111 Date: Thu, 12 Jun 2025 21:41:41 +0800 Subject: internal!: organized internal structure, using cmake now --- CMakeLists.txt | 54 ++++++++ Makefile | 189 +++------------------------- src/FileDescriptor.cpp | 86 ------------- src/FileDescriptor.hpp | 35 ------ src/IOManager.cpp | 34 ----- src/IOManager.hpp | 33 ----- src/chatManager.cpp | 36 ------ src/chatManager.hpp | 25 ---- src/chatServer.cpp | 68 ---------- src/chatServer.hpp | 20 --- src/command/commandHandler.cpp | 101 +++++++++++++++ src/command/commandHandler.hpp | 71 +++++++++++ src/command/commands.cpp | 95 ++++++++++++++ src/command/commands.hpp | 1 + src/commandHandler.cpp | 101 --------------- src/commandHandler.hpp | 71 ----------- src/commands.cpp | 95 -------------- src/commands.hpp | 1 - src/core/chatManager.cpp | 36 ++++++ src/core/chatManager.hpp | 25 ++++ src/core/chatServer.cpp | 68 ++++++++++ src/core/chatServer.hpp | 20 +++ src/core/server.cpp | 39 ++++++ src/core/server.hpp | 19 +++ src/core/session.cpp | 273 +++++++++++++++++++++++++++++++++++++++++ src/core/session.hpp | 88 +++++++++++++ src/core/sessionManager.cpp | 141 +++++++++++++++++++++ src/core/sessionManager.hpp | 16 +-- src/debug/format.hpp | 51 ++++++++ src/debug/log.hpp | 37 ++++++ src/format.hpp | 51 -------- src/inputManager.cpp | 70 ----------- src/inputManager.hpp | 25 ---- src/io/IOManager.cpp | 34 +++++ src/io/IOManager.hpp | 33 +++++ src/io/inputManager.cpp | 70 +++++++++++ src/io/inputManager.hpp | 25 ++++ src/io/renderManager.cpp | 38 ++++++ src/io/renderManager.hpp | 23 ++++ src/io/scroller.cpp | 85 +++++++++++++ src/io/scroller.hpp | 15 +++ src/log.hpp | 37 ------ src/main.cpp | 4 +- src/misc/FileDescriptor.cpp | 86 +++++++++++++ src/misc/FileDescriptor.hpp | 35 ++++++ src/renderManager.cpp | 38 ------ src/renderManager.hpp | 23 ---- src/scroller.cpp | 85 ------------- src/scroller.hpp | 15 --- src/server.cpp | 39 ------ src/server.hpp | 19 --- src/session.cpp | 273 ----------------------------------------- src/session.hpp | 88 ------------- src/sessionManager.cpp | 141 --------------------- src/sessionManager.hpp | 40 ------ 55 files changed, 1593 insertions(+), 1728 deletions(-) create mode 100644 CMakeLists.txt delete mode 100644 src/FileDescriptor.cpp delete mode 100644 src/FileDescriptor.hpp delete mode 100644 src/IOManager.cpp delete mode 100644 src/IOManager.hpp delete mode 100644 src/chatManager.cpp delete mode 100644 src/chatManager.hpp delete mode 100644 src/chatServer.cpp delete mode 100644 src/chatServer.hpp create mode 100644 src/command/commandHandler.cpp create mode 100644 src/command/commandHandler.hpp create mode 100644 src/command/commands.cpp create mode 100644 src/command/commands.hpp delete mode 100644 src/commandHandler.cpp delete mode 100644 src/commandHandler.hpp delete mode 100644 src/commands.cpp delete mode 100644 src/commands.hpp create mode 100644 src/core/chatManager.cpp create mode 100644 src/core/chatManager.hpp create mode 100644 src/core/chatServer.cpp create mode 100644 src/core/chatServer.hpp create mode 100644 src/core/server.cpp create mode 100644 src/core/server.hpp create mode 100644 src/core/session.cpp create mode 100644 src/core/session.hpp create mode 100644 src/core/sessionManager.cpp create mode 100644 src/debug/format.hpp create mode 100644 src/debug/log.hpp delete mode 100644 src/format.hpp delete mode 100644 src/inputManager.cpp delete mode 100644 src/inputManager.hpp create mode 100644 src/io/IOManager.cpp create mode 100644 src/io/IOManager.hpp create mode 100644 src/io/inputManager.cpp create mode 100644 src/io/inputManager.hpp create mode 100644 src/io/renderManager.cpp create mode 100644 src/io/renderManager.hpp create mode 100644 src/io/scroller.cpp create mode 100644 src/io/scroller.hpp delete mode 100644 src/log.hpp create mode 100644 src/misc/FileDescriptor.cpp create mode 100644 src/misc/FileDescriptor.hpp delete mode 100644 src/renderManager.cpp delete mode 100644 src/renderManager.hpp delete mode 100644 src/scroller.cpp delete mode 100644 src/scroller.hpp delete mode 100644 src/server.cpp delete mode 100644 src/server.hpp delete mode 100644 src/session.cpp delete mode 100644 src/session.hpp delete mode 100644 src/sessionManager.cpp delete mode 100644 src/sessionManager.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6e3dc35 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,54 @@ +cmake_minimum_required(VERSION 4.0.2) + +project( + chat-server + VERSION 1.0.0 + DESCRIPTION "chat server via socket" + LANGUAGES CXX) + +include(GNUInstallDirs) + +set(CMAKE_CXX_STANDARD 26) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +if(NOT CMAKE_BUILD_TYPE) + set(CMAKE_BUILD_TYPE Release) +endif() + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + message(STATUS "Configuring chat-server in Debug with CMake") + add_compile_definitions(DEBUG) +else() + message(STATUS "Configuring chat-server in Release with CMake") +endif() + +message(STATUS "Checking dependencies...") + +find_package(PkgConfig REQUIRED) +pkg_check_modules(FTXUI REQUIRED IMPORTED_TARGET ftxui) + +file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "src/*.cpp") + +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_26) + +add_compile_options(-Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion + -Wno-unused-parameter) + +if(CMAKE_BUILD_TYPE MATCHES Debug OR CMAKE_BUILD_TYPE MATCHES DEBUG) + message(STATUS "Setting debug flags") + add_compile_options(-g3 -O0 -fno-omit-frame-pointer) + add_compile_options(-fsanitize=address,undefined) + target_link_options(${PROJECT_NAME} PRIVATE -fsanitize=address,undefined) +else() + add_compile_options(-O3 -flto -DNDEBUG) + target_link_options(${PROJECT_NAME} PRIVATE -flto -Wl,--gc-sections) +endif() + +target_link_libraries(${PROJECT_NAME} PRIVATE PkgConfig::FTXUI) +target_include_directories(${PROJECT_NAME} PRIVATE src) + +install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/Makefile b/Makefile index 7b9623d..3abc4f2 100644 --- a/Makefile +++ b/Makefile @@ -1,179 +1,30 @@ -BIN = chat-server -SRCS = $(wildcard src/*.cpp) -HEADERS = $(wildcard src/*.hpp) +PREFIX ?= /usr/local -PREFIX ?= /usr -BINDIR ?= $(PREFIX)/bin -DATADIR ?= $(PREFIX)/share -BUILDDIR ?= build +stub: + @echo "Use 'make release' or 'make debug' to build the project." -OBJS = $(patsubst src/%, $(BUILDDIR)/%, $(SRCS:.cpp=.o)) +release: + cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Release -DCMAKE_INSTALL_PREFIX:STRING=${PREFIX} -S . -B ./build + cmake --build ./build --config Release --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` -CXXFLAGS += -std=c++26 -Oz -s -Wall -Wextra -flto -fPIC -Wpedantic -LDFLAGS += -Wl,--as-needed,-z,now,-z,pack-relative-relocs - -# Debug flags -DEBUG_CXXFLAGS = -std=c++26 -g3 -O0 -Wall -Wextra -fno-lto -fPIC -DDEBUG -fsanitize=thread -fno-omit-frame-pointer -pthread -DEBUG_LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=thread -lpthread - -# Address Sanitizer Debug flags (run with make asan) -ASAN_CXXFLAGS = -std=c++26 -g3 -O0 -Wall -Wextra -fno-lto -fPIC -DDEBUG -fsanitize=address -fno-omit-frame-pointer -pthread -ASAN_LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=address -lpthread - -# Memory Sanitizer Debug flags (run with make msan) -MSAN_CXXFLAGS = -std=c++26 -g3 -O0 -Wall -Wextra -fno-lto -fPIC -DDEBUG -fsanitize=memory -fno-omit-frame-pointer -fsanitize-memory-track-origins=2 -pthread -MSAN_LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=memory -lpthread - -# UB Sanitizer Debug flags (run with make ubsan) -UBSAN_CXXFLAGS = -std=c++26 -g3 -O0 -Wall -Wextra -fno-lto -fPIC -DDEBUG -fsanitize=undefined -fno-omit-frame-pointer -pthread -UBSAN_LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=undefined -lpthread - -CXXFLAGS += $(shell pkg-config --cflags ftxui) -LDFLAGS += $(shell pkg-config --libs ftxui) -DEBUG_CXXFLAGS += $(shell pkg-config --cflags ftxui) -DEBUG_LDFLAGS += $(shell pkg-config --libs ftxui) - -$(shell mkdir -p $(BUILDDIR)) -JOB_COUNT := $(BIN) $(OBJS) -JOBS_DONE := $(shell ls -l $(JOB_COUNT) 2> /dev/null | wc -l) - -NPROC := $(shell nproc 2>/dev/null || getconf NPROCESSORS_CONF || echo 2) -MAKEFLAGS += -j$(NPROC) - -define progress - $(eval JOBS_DONE := $(shell echo $$(($(JOBS_DONE) + 1)))) - @printf "[$(JOBS_DONE)/$(shell echo $(JOB_COUNT) | wc -w)] %s %s\n" $(1) $(2) -endef - -all: $(BIN) - -install: $(all) - @echo "Installing..." - @install -D -t $(DESTDIR)$(BINDIR) $(BUILDDIR)/$(BIN) +debug: + cmake --no-warn-unused-cli -DCMAKE_BUILD_TYPE:STRING=Debug -DCMAKE_INSTALL_PREFIX:STRING=${PREFIX} -S . -B ./build + cmake --build ./build --config Debug --target all -j`nproc 2>/dev/null || getconf NPROCESSORS_CONF` clean: - @echo "Cleaning up" - -@rm -rf $(OBJS) - -clean-all: clean - @echo "Cleaning up all" - -@rm -rf $(BUILDDIR)/$(BIN) - -$(BUILDDIR)/$(BIN): $(OBJS) - $(call progress, Linking $@) - @$(CXX) -o $@ \ - $(OBJS) \ - $(CXXFLAGS) \ - $(LDFLAGS) - -$(BIN): $(BUILDDIR)/$(BIN) - - -$(BUILDDIR)/%.o: src/%.cpp - $(call progress, Compiling $@) - @$(CXX) -c $< -o $@ \ - $(CXXFLAGS) - -clang-format: - @echo "Formatting code..." - @clang-format -i $(SRCS) $(HEADERS) - -clang-tidy: - @echo "Running clang-tidy..." - @clang-tidy -fix-errors $(SRCS) -- $(CXXFLAGS) - -.PHONY: all install clean clean-all $(BIN) debug asan msan ubsan helgrind valgrind drd threadcheck compile_commands dchecks dthread live_tsan llvm_libfuzzer stack_combo deadlock pgo-gen pgo-use perf-analysis clang-format clang-tidy - -debug: CXXFLAGS = $(DEBUG_CXXFLAGS) -debug: LDFLAGS = $(DEBUG_LDFLAGS) -# debug: CC = afl-g++ -# debug: CXX = afl-g++ -# debug: export AFL_USE_ASAN = 1 -debug: $(BIN) - @echo "Debug build complete (Thread Sanitizer enabled)" - -asan: CXXFLAGS = $(ASAN_CXXFLAGS) -asan: LDFLAGS = $(ASAN_LDFLAGS) -asan: $(BIN) - @echo "Debug build complete (Address Sanitizer enabled)" - -msan: CXXFLAGS = $(MSAN_CXXFLAGS) -msan: LDFLAGS = $(MSAN_LDFLAGS) -msan: $(BIN) - @echo "Debug build complete (Memory Sanitizer enabled)" - -ubsan: CXXFLAGS = $(UBSAN_CXXFLAGS) -ubsan: LDFLAGS = $(UBSAN_LDFLAGS) -ubsan: $(BIN) - @echo "Debug build complete (Undefined Behavior Sanitizer enabled)" - -dthread: CXXFLAGS = $(DEBUG_CXXFLAGS) -dthread: LDFLAGS = $(DEBUG_LDFLAGS) -dthread: $(BIN) - @echo "Debug build complete (Enhanced Thread Sanitizer enabled)" - TSAN_OPTIONS="history_size=10 report_thread_leaks=1 report_signal_unsafe=1 halt_on_error=1 second_deadlock_stack=1" $(BUILDDIR)/$(BIN) $(ARGS) - -helgrind: debug - @echo "Running with Valgrind Helgrind thread checker" - valgrind --tool=helgrind --read-var-info=yes --track-lockorders=yes $(BUILDDIR)/$(BIN) $(ARGS) - -valgrind: debug - @echo "Running with Valgrind memory checker" - valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes $(BUILDDIR)/$(BIN) $(ARGS) - -drd: debug - @echo "Running with Valgrind DRD (Data Race Detector)" - valgrind --tool=drd --check-stack-var=yes --first-race-only=yes $(BUILDDIR)/$(BIN) $(ARGS) - -dchecks: CXXFLAGS = -std=c++26 -g3 -O0 -Wall -Wextra -fno-lto -fPIC -DDEBUG -fsanitize=thread,undefined -fno-omit-frame-pointer -pthread -dchecks: LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=thread,undefined -lpthread -dchecks: $(BIN) - @echo "Debug build complete (Thread & UB Sanitizers enabled)" - -threadcheck: debug - @echo "Running with Thread Sanitizer" - TSAN_OPTIONS="history_size=7 halt_on_error=1 second_deadlock_stack=1" $(BUILDDIR)/$(BIN) $(ARGS) - -live_tsan: CXXFLAGS = $(DEBUG_CXXFLAGS) -live_tsan: LDFLAGS = $(DEBUG_LDFLAGS) -live_tsan: $(BIN) - @echo "Running with live Thread Sanitizer (continues after errors)" - TSAN_OPTIONS="history_size=15 report_thread_leaks=1 report_signal_unsafe=1 halt_on_error=0 second_deadlock_stack=1" $(BUILDDIR)/$(BIN) $(ARGS) - -llvm_libfuzzer: CXXFLAGS = -std=c++26 -g -O1 -Wall -Wextra -fno-lto -fPIC -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION -fsanitize=fuzzer,address -fno-omit-frame-pointer -llvm_libfuzzer: LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=fuzzer,address -llvm_libfuzzer: $(BIN) - @echo "Fuzzer build complete (note: requires clang compiler)" - -stack_combo: CXXFLAGS = -std=c++26 -g3 -O0 -Wall -Wextra -fno-lto -fPIC -DDEBUG -fsanitize=address,undefined -fno-omit-frame-pointer -pthread -stack_combo: LDFLAGS = -Wl,--no-as-needed,-z,now,-z,pack-relative-relocs -fsanitize=address,undefined -lpthread -stack_combo: $(BIN) - @echo "Debug build complete (Combined ASan and UBSan)" - ASAN_OPTIONS="detect_leaks=1:check_initialization_order=1:detect_stack_use_after_return=1" $(BUILDDIR)/$(BIN) $(ARGS) - -compile_commands: - compiledb -n make + rm -rf build +all: + $(MAKE) clean + $(MAKE) release -deadlock: CXXFLAGS = $(DEBUG_CXXFLAGS) -DENABLE_DEADLOCK_DETECTION -deadlock: LDFLAGS = $(DEBUG_LDFLAGS) -deadlock: $(BIN) - @echo "Debug build complete (with deadlock detection)" - TSAN_OPTIONS="detect_deadlocks=1:second_deadlock_stack=1:report_thread_leaks=1:halt_on_error=0" $(BUILDDIR)/$(BIN) $(ARGS) +install: + cmake --install ./build -pgo-gen: CXXFLAGS = -std=c++26 -O2 -Wall -Wextra -fprofile-generate -fPIC -pgo-gen: LDFLAGS = -Wl,--as-needed,-z,now,-z,pack-relative-relocs -fprofile-generate -pgo-gen: $(BIN) - @echo "Profile generation build complete (run app with typical workload)" +uninstall: + xargs rm < ./build/install_manifest.txt -pgo-use: CXXFLAGS = -std=c++26 -Oz -Wall -Wextra -fprofile-use -fprofile-correction -fPIC -pgo-use: LDFLAGS = -Wl,--as-needed,-z,now,-z,pack-relative-relocs -fprofile-use -pgo-use: $(BIN) - @echo "Optimized build using profile data complete" +format: + find src -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i -perf-analysis: CXXFLAGS = -std=c++26 -g -O2 -Wall -Wextra -fno-lto -fno-omit-frame-pointer -perf-analysis: LDFLAGS = -Wl,--as-needed,-z,now,-z,pack-relative-relocs -perf-analysis: $(BIN) - @echo "Performance analysis build complete" - valgrind --tool=callgrind --callgrind-out-file=callgrind.out $(BUILDDIR)/$(BIN) $(ARGS) - @echo "Run 'callgrind_annotate callgrind.out' to view results" +.PHONY: stub release debug clean all install uninstall format diff --git a/src/FileDescriptor.cpp b/src/FileDescriptor.cpp deleted file mode 100644 index 92505f5..0000000 --- a/src/FileDescriptor.cpp +++ /dev/null @@ -1,86 +0,0 @@ -#include "FileDescriptor.hpp" -#include -#include -#include -#include -#include - -CFileDescriptor::CFileDescriptor(int const fd) - : m_fd(fd) {} - -CFileDescriptor::CFileDescriptor(CFileDescriptor &&other) noexcept - : m_fd(std::exchange(other.m_fd, -1)) {} - -CFileDescriptor &CFileDescriptor::operator=(CFileDescriptor &&other) noexcept { - if (this == &other) // Shit will go haywire if there is duplicate ownership - abort(); - - reset(); - m_fd = std::exchange(other.m_fd, -1); - return *this; -} - -CFileDescriptor::~CFileDescriptor() { - reset(); -} - -bool CFileDescriptor::isValid() const { - return m_fd != -1; -} - -int CFileDescriptor::get() const { - return m_fd; -} - -int CFileDescriptor::getFlags() const { - return fcntl(m_fd, F_GETFD); -} - -bool CFileDescriptor::setFlags(int flags) { - return fcntl(m_fd, F_SETFD, flags) != -1; -} - -int CFileDescriptor::take() { - return std::exchange(m_fd, -1); -} - -void CFileDescriptor::reset() { - if (m_fd != -1) { - close(m_fd); - m_fd = -1; - } -} - -CFileDescriptor CFileDescriptor::duplicate(int flags) const { - if (m_fd == -1) - return {}; - - return CFileDescriptor{fcntl(m_fd, flags, 0)}; -} - -bool CFileDescriptor::isClosed() const { - return isClosed(m_fd); -} - -bool CFileDescriptor::isReadable() const { - return isReadable(m_fd); -} - -bool CFileDescriptor::isClosed(int fd) { - pollfd pfd = { - .fd = fd, - .events = POLLIN, - .revents = 0, - }; - - if (poll(&pfd, 1, 0) < 0) - return true; - - return pfd.revents & (POLLHUP | POLLERR); -} - -bool CFileDescriptor::isReadable(int fd) { - pollfd pfd = {.fd = fd, .events = POLLIN, .revents = 0}; - - return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); -} diff --git a/src/FileDescriptor.hpp b/src/FileDescriptor.hpp deleted file mode 100644 index 75aeb73..0000000 --- a/src/FileDescriptor.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include -class CFileDescriptor { - public: - CFileDescriptor() = default; - explicit CFileDescriptor(int const fd); - CFileDescriptor(CFileDescriptor &&) noexcept; - CFileDescriptor &operator=(CFileDescriptor &&) noexcept; - ~CFileDescriptor(); - - CFileDescriptor(const CFileDescriptor &) = delete; - CFileDescriptor &operator=(const CFileDescriptor &) = delete; - - bool operator==(const CFileDescriptor &rhs) const { - return m_fd == rhs.m_fd; - } - - bool isValid() const; - int get() const; - int getFlags() const; - bool setFlags(int flags); - int take(); - void reset(); - CFileDescriptor duplicate(int flags = F_DUPFD_CLOEXEC) const; - - bool isReadable() const; - bool isClosed() const; - - static bool isReadable(int fd); - static bool isClosed(int fd); - - private: - int m_fd = -1; -}; diff --git a/src/IOManager.cpp b/src/IOManager.cpp deleted file mode 100644 index eb648e5..0000000 --- a/src/IOManager.cpp +++ /dev/null @@ -1,34 +0,0 @@ -#include "IOManager.hpp" -#include "inputManager.hpp" -#include "log.hpp" - -CIOManager::CIOManager() { - log(LOG, "IOManager: initialized"); -} - -CIOManager::~CIOManager() { - m_vIO.clear(); - log(SYS, "IOManager: bye"); -} - -const std::vector &CIOManager::getIO() const { - return m_vIO; -} - -void CIOManager::addLog(const SLog &log) { - m_vIO.push_back({.log = log}); - if (g_pInputManager) - g_pInputManager->updateIO(); -} - -void CIOManager::addMessage(const CChatManager::SMessage &msg) { - m_vIO.push_back({.msg = msg}); - if (g_pInputManager) - g_pInputManager->updateIO(); -} - -void CIOManager::addCustom(const std::pair &custom) { - m_vIO.push_back({.custom = custom}); - if (g_pInputManager) - g_pInputManager->updateIO(); -} diff --git a/src/IOManager.hpp b/src/IOManager.hpp deleted file mode 100644 index 670e597..0000000 --- a/src/IOManager.hpp +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#include "chatManager.hpp" -#include "format.hpp" -#include - -class CIOManager { - public: - CIOManager(); - ~CIOManager(); - - struct SLog { - eFormatType type; - std::string log; - }; - struct SIO { - // once a time - std::optional msg = std::nullopt; - std::optional log = std::nullopt; - std::optional> custom = std::nullopt; - }; - - const std::vector &getIO() const; - void addLog(const SLog &log); - void addMessage(const CChatManager::SMessage &msg); - void addCustom(const std::pair &custom); - - private: - std::vector m_vIO; - - friend class CChatManager; -}; -inline std::unique_ptr g_pIOManager; diff --git a/src/chatManager.cpp b/src/chatManager.cpp deleted file mode 100644 index 8439673..0000000 --- a/src/chatManager.cpp +++ /dev/null @@ -1,36 +0,0 @@ -#include "chatManager.hpp" -#include "IOManager.hpp" -#include "inputManager.hpp" -#include "log.hpp" -#include "sessionManager.hpp" - -CChatManager::CChatManager() { - log(LOG, "ChatManager: initialized"); -} - -CChatManager::~CChatManager() { - log(SYS, "ChatManager: bye"); -} - -void CChatManager::newMessage(const SMessage &msg) { - log(LOG, "{}: {}", msg.username, msg.msg); - const auto session = msg.sender ? msg.sender->lock() : g_pSessionManager->getByName(msg.username); - if (session && session->isMuted()) { - session->write("You are muted"); - return; - } - g_pSessionManager->broadcast(msg); -} - -std::string CChatManager::fmtBroadcastMessage(const SMessage &msg) { - return NFormatter::fmt(NONE, "{}: {}", msg.username, msg.msg); -} - -std::vector CChatManager::getChat() const { - std::vector chat; - - for (const auto &[msg, _, _] : g_pIOManager->getIO()) - if (msg) - chat.push_back(*msg); - return chat; -} diff --git a/src/chatManager.hpp b/src/chatManager.hpp deleted file mode 100644 index bd305de..0000000 --- a/src/chatManager.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -class CSession; - -class CChatManager { - public: - CChatManager(); - ~CChatManager(); - struct SMessage { - std::string msg; - std::string username; - std::optional> sender = std::nullopt; - bool admin = false; - }; - void newMessage(const SMessage &msg); - std::string fmtBroadcastMessage(const SMessage &msg); - - std::vector getChat() const; -}; -inline std::unique_ptr g_pChatManager; diff --git a/src/chatServer.cpp b/src/chatServer.cpp deleted file mode 100644 index 458fc09..0000000 --- a/src/chatServer.cpp +++ /dev/null @@ -1,68 +0,0 @@ -#include "chatServer.hpp" -#include "IOManager.hpp" -#include "chatManager.hpp" -#include "commandHandler.hpp" -#include "commands.hpp" -#include "inputManager.hpp" -#include "log.hpp" -#include "renderManager.hpp" -#include "server.hpp" -#include "sessionManager.hpp" - -CChatServer::CChatServer(uint16_t port) - : m_port(port) { - if (std::atexit([]() { g_pChatServer->cleanup(); })) - log(ERR, "Failed to register CChatServer atexit handler, dtors will not be called"); - - log(LOG, "ChatServer: initialized"); -}; - -CChatServer::~CChatServer() { - cleanup(); - log(SYS, "ChatServer: bye"); -} - -void CChatServer::cleanup() { - if (g_pSessionManager) { - g_pSessionManager->shutdownSessions(); - g_pSessionManager.reset(); - } - - g_pChatManager.reset(); - g_pServer.reset(); - g_pCommandHandler.reset(); - g_pInputManager.reset(); - g_pIOManager.reset(); - g_pRenderManager.reset(); -} - -void CChatServer::initManagers() { - log(LOG, "ChatServer: initializing managers"); - - log(LOG, "Creating RenderManager"); - g_pRenderManager = std::make_unique(); - log(LOG, "Creating InputManager"); - g_pInputManager = std::make_unique(); - log(LOG, "Creating Server"); - g_pServer = std::make_unique(m_port); - log(LOG, "Creating IOManager"); - g_pIOManager = std::make_unique(); - log(LOG, "Creating SessionManager"); - g_pSessionManager = std::make_shared(); - log(LOG, "Creating ChatManager"); - g_pChatManager = std::make_unique(); -} - -void CChatServer::start() { - initManagers(); - - log(LOG, "Creating CommandHandler"); - g_pCommandHandler = std::make_unique(); - registerCommands(); - - // will await while waiting for accept() to return a new client - m_sessionManagerThread = std::jthread(&CSessionManager::run, g_pSessionManager.get()); - m_sessionManagerThread.detach(); - - g_pInputManager->inputLoop(); -} diff --git a/src/chatServer.hpp b/src/chatServer.hpp deleted file mode 100644 index 00d2979..0000000 --- a/src/chatServer.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#include -#include - -class CChatServer { - public: - CChatServer(uint16_t port); - ~CChatServer(); - void start(); - void cleanup(); - - private: - uint16_t m_port; - - void initManagers(); - void initServer(); - - std::jthread m_sessionManagerThread; -}; - -inline std::unique_ptr g_pChatServer; diff --git a/src/command/commandHandler.cpp b/src/command/commandHandler.cpp new file mode 100644 index 0000000..4aa6723 --- /dev/null +++ b/src/command/commandHandler.cpp @@ -0,0 +1,101 @@ +#include "commandHandler.hpp" +#include "../debug/log.hpp" +#include "../core/session.hpp" +#include +#include + +CCommandHandler::CCommandHandler() { + log(LOG, "CommandHandler: initialized"); +} + +CCommandHandler::~CCommandHandler() { + log(SYS, "CommandHandler: bye"); + m_vCommands.clear(); +} + +bool CCommandHandler::isCommand(const std::string &str) const { + return !str.empty() && str.front() == '/'; +}; + +bool CCommandHandler::validCommand(const std::string &command) const { + return std::ranges::any_of(m_vCommands, [&command](const std::shared_ptr &cmd) { + return cmd->name == command; + }); +} + +bool CCommandHandler::registerCommand(const SCommand &command) { + if (command.name.empty() || !command.exe || !command.parser) + return false; + + m_vCommands.push_back(std::make_shared(command)); + + log(TRACE, "CommandHandler: registered command: {}", command.name); + + return true; +} + +std::shared_ptr CCommandHandler::getCommand(const std::string &command) const { + auto it = std::ranges::find_if(m_vCommands, [&command](const std::shared_ptr &cmd) { return cmd->name == command; }); + + if (it != m_vCommands.end()) + return *it; + + return nullptr; +} + +CCommandHandler::SResult CCommandHandler::exeCommand(const std::shared_ptr command, const std::string &args) const { + const auto [parsed, good] = command->parser(args); + if (!good) + try { + return {.result = std::any_cast(parsed), .good = false}; + } catch (const std::bad_any_cast &) { + return {.result = "", .good = false}; + } + + log(TRACE, "CommandHandler: executing command: {}", command->name); + return command->exe(parsed); +} + +CCommandHandler::SResult CCommandHandler::newCommand(const std::string &command, const std::string &args, std::shared_ptr session) const { + if (!validCommand(command)) + return {.result = "Invalid command", .good = false}; + + auto cmd = getCommand(command); + if (!cmd) + return {.result = "Command not found", .good = false}; + + if (cmd->requireAdmin && !session->isAdmin()) + return {.result = "Permission denied", .good = false}; + + return exeCommand(cmd, args); +} + +void CCommandHandler::handleCommand(std::string input, std::shared_ptr user) const { + input = input.substr(1); + + std::string command; + std::string args; + + size_t spacePos = input.find(' '); + if (spacePos == std::string::npos) { + command = input; + args = ""; + } else { + command = input.substr(0, spacePos); + // skip extra spaces + size_t startPos = input.find_first_not_of(' ', spacePos); + if (startPos != std::string::npos) + args = input.substr(startPos); + } + + args.empty() ? log(LOG, "Spawning Command: {}", command) : log(LOG, "Spawning Command: {}, Args: {}", command, args); + const auto exe = newCommand(command, args, user); + + std::function outputter = [](eFormatType type, const std::string &msg) { log(type, "{}", msg); }; + + if (!user->getIp().empty()) + outputter = [user](eFormatType, const std::string &msg) { user->write(msg); }; + + exe.good ? exe.result.empty() ? outputter(LOG, "Command succeeded") : outputter(LOG, exe.result) : exe.result.empty() ? outputter(ERR, "Command failed") + : outputter(ERR, exe.result); +}; diff --git a/src/command/commandHandler.hpp b/src/command/commandHandler.hpp new file mode 100644 index 0000000..7fc20d5 --- /dev/null +++ b/src/command/commandHandler.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "../core/session.hpp" +#include +#include +#include +#include +#include +#include + +class CCommandHandler { + public: + CCommandHandler(); + ~CCommandHandler(); + + struct SResult { + std::string result; + bool good = false; + }; + + struct SParseResult { + std::any data; + bool good = false; + }; + + struct SCommand { + bool requireAdmin = true; + std::string name; + std::function parser; + std::function exe; + }; + + bool isCommand(const std::string &str) const; + bool validCommand(const std::string &command) const; + // return false if command struct is invalid + bool registerCommand(const SCommand &command); + SResult newCommand(const std::string &command, const std::string &args, std::shared_ptr session) const; + void handleCommand(std::string input, std::shared_ptr user = nullptr) const; + + template + struct SMakeCommandArgs { + std::string name; + std::function(const std::string &args)> parser; + std::function exe; + }; + + template + static SCommand makeCommand(SMakeCommandArgs args) { + return { + .name = std::move(args.name), + .parser = [parser = std::move(args.parser)](const std::string &args) -> SParseResult { + auto [data, good] = parser(args); + return {.data = std::any(std::move(data)), .good = good}; + }, + .exe = [exe = std::move(args.exe)](const std::any &parsed) -> SResult { + try { + return exe(std::any_cast(parsed)); + } catch (const std::bad_any_cast &) { + return {.result = "Invalid data type", .good = false}; + } + } + }; + } + + private: + std::shared_ptr getCommand(const std::string &command) const; + SResult exeCommand(const std::shared_ptr command, const std::string &args) const; + + std::vector> m_vCommands; +}; +inline std::unique_ptr g_pCommandHandler; diff --git a/src/command/commands.cpp b/src/command/commands.cpp new file mode 100644 index 0000000..6f3db68 --- /dev/null +++ b/src/command/commands.cpp @@ -0,0 +1,95 @@ +#include "commands.hpp" +#include "commandHandler.hpp" +#include "../debug/format.hpp" +#include "../debug/log.hpp" +#include "../core/sessionManager.hpp" + +#define NOREASON "No reason specified"; + +struct SKickArgs { + std::string target; + std::optional reason; +}; + +struct SMuteArgs { + std::string target; + std::optional reason; +}; + +void registerCommands() { + g_pCommandHandler->registerCommand({.name = "raw", .parser = [](const std::string &args) -> CCommandHandler::SParseResult { return {.data = args, .good = true}; }, .exe = [](const std::any &parsed) -> CCommandHandler::SResult { + const std::string &data = std::any_cast(parsed); + g_pSessionManager->broadcast(data); + return {.result = "Broadcasted: " + data, .good = true}; }}); + + g_pCommandHandler->registerCommand(CCommandHandler::makeCommand({.name = "kick", .parser = [](const std::string &args) -> std::pair { + size_t pos = args.find(' '); + std::string target = args; + std::optional reason = std::nullopt; + + if (pos != std::string::npos) { + target = args.substr(0, pos); + reason = args.substr(pos + 1); + } + + if (target.empty()) + return {SKickArgs{}, false}; + + return { + SKickArgs{.target=target, .reason=reason}, true}; }, .exe = [](const SKickArgs &data) -> CCommandHandler::SResult { + const auto session = g_pSessionManager->getByName(data.target); + if (!session) + return {.result = "User '" + data.target + "' not found", .good = false}; + + const auto reason = data.reason ? *data.reason : NOREASON; + g_pSessionManager->kick(session->self, true, reason); + return {.result = "Kicked '" + data.target + "': " + reason, .good = true}; }})); + + g_pCommandHandler->registerCommand({.requireAdmin = false, .name = "list", .parser = [](const std::string &args) -> CCommandHandler::SParseResult { + (void)args; + return {.data = std::any(), .good = true}; }, .exe = [](const std::any &parsed) -> CCommandHandler::SResult { + (void)parsed; + std::string result = "ID \t Name \t IP \t Muted\n"; + for (const auto &session : g_pSessionManager->getSessions()) + result += NFormatter::fmt(NONEWLINE, "{} \t {} \t {} \t {}\n", (uintptr_t)session.get(), session->getName(), session->getIp(), (session->isMuted() ? "[MUTED]" : "[UNMUTED]")); + + + return {.result = result, .good = true}; }}); + + g_pCommandHandler->registerCommand(CCommandHandler::makeCommand({.name = "mute", .parser = [](const std::string &args) -> std::pair { + size_t pos = args.find(' '); + std::string target = args; + std::optional reason; + + if (pos != std::string::npos) { + target = args.substr(0, pos); + reason = args.substr(pos + 1); + } + + if (target.empty()) + return {SMuteArgs{}, false}; + + return {SMuteArgs{.target=target, .reason=reason}, true}; }, .exe = [](const SMuteArgs &data) -> CCommandHandler::SResult { + const auto session = g_pSessionManager->getByName(data.target); + if (!session) + return {.result = "User '" + data.target + "' not found", .good = false}; + + const auto reason = data.reason ? *data.reason : NOREASON; + + session->setMuted(true); + session->write("You have been muted for: {}", reason); + return {.result = "Muted '" + data.target + "': " + reason, .good = true}; }})); // TODO: use fmt + + g_pCommandHandler->registerCommand(CCommandHandler::makeCommand({.name = "unmute", .parser = [](const std::string &args) -> std::pair { + if (args.empty()) + return {SMuteArgs{}, false}; + + return {SMuteArgs{.target=args, .reason="Unmuted"}, true}; }, .exe = [](const SMuteArgs &data) -> CCommandHandler::SResult { + const auto session = g_pSessionManager->getByName(data.target); + if (!session) + return {.result = "User '" + data.target + "' not found", .good = false}; + + session->setMuted(false); + session->write("You have been unmuted"); + return {.result = "Unmuted '" + data.target + "'", .good = true}; }})); +} diff --git a/src/command/commands.hpp b/src/command/commands.hpp new file mode 100644 index 0000000..b49b6dc --- /dev/null +++ b/src/command/commands.hpp @@ -0,0 +1 @@ +void registerCommands(); diff --git a/src/commandHandler.cpp b/src/commandHandler.cpp deleted file mode 100644 index e459ee0..0000000 --- a/src/commandHandler.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "commandHandler.hpp" -#include "log.hpp" -#include "session.hpp" -#include -#include - -CCommandHandler::CCommandHandler() { - log(LOG, "CommandHandler: initialized"); -} - -CCommandHandler::~CCommandHandler() { - log(SYS, "CommandHandler: bye"); - m_vCommands.clear(); -} - -bool CCommandHandler::isCommand(const std::string &str) const { - return !str.empty() && str.front() == '/'; -}; - -bool CCommandHandler::validCommand(const std::string &command) const { - return std::ranges::any_of(m_vCommands, [&command](const std::shared_ptr &cmd) { - return cmd->name == command; - }); -} - -bool CCommandHandler::registerCommand(const SCommand &command) { - if (command.name.empty() || !command.exe || !command.parser) - return false; - - m_vCommands.push_back(std::make_shared(command)); - - log(TRACE, "CommandHandler: registered command: {}", command.name); - - return true; -} - -std::shared_ptr CCommandHandler::getCommand(const std::string &command) const { - auto it = std::ranges::find_if(m_vCommands, [&command](const std::shared_ptr &cmd) { return cmd->name == command; }); - - if (it != m_vCommands.end()) - return *it; - - return nullptr; -} - -CCommandHandler::SResult CCommandHandler::exeCommand(const std::shared_ptr command, const std::string &args) const { - const auto [parsed, good] = command->parser(args); - if (!good) - try { - return {.result = std::any_cast(parsed), .good = false}; - } catch (const std::bad_any_cast &) { - return {.result = "", .good = false}; - } - - log(TRACE, "CommandHandler: executing command: {}", command->name); - return command->exe(parsed); -} - -CCommandHandler::SResult CCommandHandler::newCommand(const std::string &command, const std::string &args, std::shared_ptr session) const { - if (!validCommand(command)) - return {.result = "Invalid command", .good = false}; - - auto cmd = getCommand(command); - if (!cmd) - return {.result = "Command not found", .good = false}; - - if (cmd->requireAdmin && !session->isAdmin()) - return {.result = "Permission denied", .good = false}; - - return exeCommand(cmd, args); -} - -void CCommandHandler::handleCommand(std::string input, std::shared_ptr user) const { - input = input.substr(1); - - std::string command; - std::string args; - - size_t spacePos = input.find(' '); - if (spacePos == std::string::npos) { - command = input; - args = ""; - } else { - command = input.substr(0, spacePos); - // skip extra spaces - size_t startPos = input.find_first_not_of(' ', spacePos); - if (startPos != std::string::npos) - args = input.substr(startPos); - } - - args.empty() ? log(LOG, "Spawning Command: {}", command) : log(LOG, "Spawning Command: {}, Args: {}", command, args); - const auto exe = newCommand(command, args, user); - - std::function outputter = [](eFormatType type, const std::string &msg) { log(type, "{}", msg); }; - - if (!user->getIp().empty()) - outputter = [user](eFormatType, const std::string &msg) { user->write(msg); }; - - exe.good ? exe.result.empty() ? outputter(LOG, "Command succeeded") : outputter(LOG, exe.result) : exe.result.empty() ? outputter(ERR, "Command failed") - : outputter(ERR, exe.result); -}; diff --git a/src/commandHandler.hpp b/src/commandHandler.hpp deleted file mode 100644 index 89d19d6..0000000 --- a/src/commandHandler.hpp +++ /dev/null @@ -1,71 +0,0 @@ -#pragma once - -#include "session.hpp" -#include -#include -#include -#include -#include -#include - -class CCommandHandler { - public: - CCommandHandler(); - ~CCommandHandler(); - - struct SResult { - std::string result; - bool good = false; - }; - - struct SParseResult { - std::any data; - bool good = false; - }; - - struct SCommand { - bool requireAdmin = true; - std::string name; - std::function parser; - std::function exe; - }; - - bool isCommand(const std::string &str) const; - bool validCommand(const std::string &command) const; - // return false if command struct is invalid - bool registerCommand(const SCommand &command); - SResult newCommand(const std::string &command, const std::string &args, std::shared_ptr session) const; - void handleCommand(std::string input, std::shared_ptr user = nullptr) const; - - template - struct SMakeCommandArgs { - std::string name; - std::function(const std::string &args)> parser; - std::function exe; - }; - - template - static SCommand makeCommand(SMakeCommandArgs args) { - return { - .name = std::move(args.name), - .parser = [parser = std::move(args.parser)](const std::string &args) -> SParseResult { - auto [data, good] = parser(args); - return {.data = std::any(std::move(data)), .good = good}; - }, - .exe = [exe = std::move(args.exe)](const std::any &parsed) -> SResult { - try { - return exe(std::any_cast(parsed)); - } catch (const std::bad_any_cast &) { - return {.result = "Invalid data type", .good = false}; - } - } - }; - } - - private: - std::shared_ptr getCommand(const std::string &command) const; - SResult exeCommand(const std::shared_ptr command, const std::string &args) const; - - std::vector> m_vCommands; -}; -inline std::unique_ptr g_pCommandHandler; diff --git a/src/commands.cpp b/src/commands.cpp deleted file mode 100644 index e7894db..0000000 --- a/src/commands.cpp +++ /dev/null @@ -1,95 +0,0 @@ -#include "commands.hpp" -#include "commandHandler.hpp" -#include "format.hpp" -#include "log.hpp" -#include "sessionManager.hpp" - -#define NOREASON "No reason specified"; - -struct SKickArgs { - std::string target; - std::optional reason; -}; - -struct SMuteArgs { - std::string target; - std::optional reason; -}; - -void registerCommands() { - g_pCommandHandler->registerCommand({.name = "raw", .parser = [](const std::string &args) -> CCommandHandler::SParseResult { return {.data = args, .good = true}; }, .exe = [](const std::any &parsed) -> CCommandHandler::SResult { - const std::string &data = std::any_cast(parsed); - g_pSessionManager->broadcast(data); - return {.result = "Broadcasted: " + data, .good = true}; }}); - - g_pCommandHandler->registerCommand(CCommandHandler::makeCommand({.name = "kick", .parser = [](const std::string &args) -> std::pair { - size_t pos = args.find(' '); - std::string target = args; - std::optional reason = std::nullopt; - - if (pos != std::string::npos) { - target = args.substr(0, pos); - reason = args.substr(pos + 1); - } - - if (target.empty()) - return {SKickArgs{}, false}; - - return { - SKickArgs{.target=target, .reason=reason}, true}; }, .exe = [](const SKickArgs &data) -> CCommandHandler::SResult { - const auto session = g_pSessionManager->getByName(data.target); - if (!session) - return {.result = "User '" + data.target + "' not found", .good = false}; - - const auto reason = data.reason ? *data.reason : NOREASON; - g_pSessionManager->kick(session->self, true, reason); - return {.result = "Kicked '" + data.target + "': " + reason, .good = true}; }})); - - g_pCommandHandler->registerCommand({.requireAdmin = false, .name = "list", .parser = [](const std::string &args) -> CCommandHandler::SParseResult { - (void)args; - return {.data = std::any(), .good = true}; }, .exe = [](const std::any &parsed) -> CCommandHandler::SResult { - (void)parsed; - std::string result = "ID \t Name \t IP \t Muted\n"; - for (const auto &session : g_pSessionManager->getSessions()) - result += NFormatter::fmt(NONEWLINE, "{} \t {} \t {} \t {}\n", (uintptr_t)session.get(), session->getName(), session->getIp(), (session->isMuted() ? "[MUTED]" : "[UNMUTED]")); - - - return {.result = result, .good = true}; }}); - - g_pCommandHandler->registerCommand(CCommandHandler::makeCommand({.name = "mute", .parser = [](const std::string &args) -> std::pair { - size_t pos = args.find(' '); - std::string target = args; - std::optional reason; - - if (pos != std::string::npos) { - target = args.substr(0, pos); - reason = args.substr(pos + 1); - } - - if (target.empty()) - return {SMuteArgs{}, false}; - - return {SMuteArgs{.target=target, .reason=reason}, true}; }, .exe = [](const SMuteArgs &data) -> CCommandHandler::SResult { - const auto session = g_pSessionManager->getByName(data.target); - if (!session) - return {.result = "User '" + data.target + "' not found", .good = false}; - - const auto reason = data.reason ? *data.reason : NOREASON; - - session->setMuted(true); - session->write("You have been muted for: {}", reason); - return {.result = "Muted '" + data.target + "': " + reason, .good = true}; }})); // TODO: use fmt - - g_pCommandHandler->registerCommand(CCommandHandler::makeCommand({.name = "unmute", .parser = [](const std::string &args) -> std::pair { - if (args.empty()) - return {SMuteArgs{}, false}; - - return {SMuteArgs{.target=args, .reason="Unmuted"}, true}; }, .exe = [](const SMuteArgs &data) -> CCommandHandler::SResult { - const auto session = g_pSessionManager->getByName(data.target); - if (!session) - return {.result = "User '" + data.target + "' not found", .good = false}; - - session->setMuted(false); - session->write("You have been unmuted"); - return {.result = "Unmuted '" + data.target + "'", .good = true}; }})); -} diff --git a/src/commands.hpp b/src/commands.hpp deleted file mode 100644 index b49b6dc..0000000 --- a/src/commands.hpp +++ /dev/null @@ -1 +0,0 @@ -void registerCommands(); diff --git a/src/core/chatManager.cpp b/src/core/chatManager.cpp new file mode 100644 index 0000000..e981e7f --- /dev/null +++ b/src/core/chatManager.cpp @@ -0,0 +1,36 @@ +#include "chatManager.hpp" +#include "../io/IOManager.hpp" +#include "../io/inputManager.hpp" +#include "../debug/log.hpp" +#include "sessionManager.hpp" + +CChatManager::CChatManager() { + log(LOG, "ChatManager: initialized"); +} + +CChatManager::~CChatManager() { + log(SYS, "ChatManager: bye"); +} + +void CChatManager::newMessage(const SMessage &msg) { + log(LOG, "{}: {}", msg.username, msg.msg); + const auto session = msg.sender ? msg.sender->lock() : g_pSessionManager->getByName(msg.username); + if (session && session->isMuted()) { + session->write("You are muted"); + return; + } + g_pSessionManager->broadcast(msg); +} + +std::string CChatManager::fmtBroadcastMessage(const SMessage &msg) { + return NFormatter::fmt(NONE, "{}: {}", msg.username, msg.msg); +} + +std::vector CChatManager::getChat() const { + std::vector chat; + + for (const auto &[msg, _, _] : g_pIOManager->getIO()) + if (msg) + chat.push_back(*msg); + return chat; +} diff --git a/src/core/chatManager.hpp b/src/core/chatManager.hpp new file mode 100644 index 0000000..bd305de --- /dev/null +++ b/src/core/chatManager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include +#include + +class CSession; + +class CChatManager { + public: + CChatManager(); + ~CChatManager(); + struct SMessage { + std::string msg; + std::string username; + std::optional> sender = std::nullopt; + bool admin = false; + }; + void newMessage(const SMessage &msg); + std::string fmtBroadcastMessage(const SMessage &msg); + + std::vector getChat() const; +}; +inline std::unique_ptr g_pChatManager; diff --git a/src/core/chatServer.cpp b/src/core/chatServer.cpp new file mode 100644 index 0000000..4a16c56 --- /dev/null +++ b/src/core/chatServer.cpp @@ -0,0 +1,68 @@ +#include "chatServer.hpp" +#include "../io/IOManager.hpp" +#include "chatManager.hpp" +#include "../command/commandHandler.hpp" +#include "../command/commands.hpp" +#include "../io/inputManager.hpp" +#include "../debug/log.hpp" +#include "../io/renderManager.hpp" +#include "server.hpp" +#include "sessionManager.hpp" + +CChatServer::CChatServer(uint16_t port) + : m_port(port) { + if (std::atexit([]() { g_pChatServer->cleanup(); })) + log(ERR, "Failed to register CChatServer atexit handler, dtors will not be called"); + + log(LOG, "ChatServer: initialized"); +}; + +CChatServer::~CChatServer() { + cleanup(); + log(SYS, "ChatServer: bye"); +} + +void CChatServer::cleanup() { + if (g_pSessionManager) { + g_pSessionManager->shutdownSessions(); + g_pSessionManager.reset(); + } + + g_pChatManager.reset(); + g_pServer.reset(); + g_pCommandHandler.reset(); + g_pInputManager.reset(); + g_pIOManager.reset(); + g_pRenderManager.reset(); +} + +void CChatServer::initManagers() { + log(LOG, "ChatServer: initializing managers"); + + log(LOG, "Creating RenderManager"); + g_pRenderManager = std::make_unique(); + log(LOG, "Creating InputManager"); + g_pInputManager = std::make_unique(); + log(LOG, "Creating Server"); + g_pServer = std::make_unique(m_port); + log(LOG, "Creating IOManager"); + g_pIOManager = std::make_unique(); + log(LOG, "Creating SessionManager"); + g_pSessionManager = std::make_shared(); + log(LOG, "Creating ChatManager"); + g_pChatManager = std::make_unique(); +} + +void CChatServer::start() { + initManagers(); + + log(LOG, "Creating CommandHandler"); + g_pCommandHandler = std::make_unique(); + registerCommands(); + + // will await while waiting for accept() to return a new client + m_sessionManagerThread = std::jthread(&CSessionManager::run, g_pSessionManager.get()); + m_sessionManagerThread.detach(); + + g_pInputManager->inputLoop(); +} diff --git a/src/core/chatServer.hpp b/src/core/chatServer.hpp new file mode 100644 index 0000000..00d2979 --- /dev/null +++ b/src/core/chatServer.hpp @@ -0,0 +1,20 @@ +#include +#include + +class CChatServer { + public: + CChatServer(uint16_t port); + ~CChatServer(); + void start(); + void cleanup(); + + private: + uint16_t m_port; + + void initManagers(); + void initServer(); + + std::jthread m_sessionManagerThread; +}; + +inline std::unique_ptr g_pChatServer; diff --git a/src/core/server.cpp b/src/core/server.cpp new file mode 100644 index 0000000..a78c217 --- /dev/null +++ b/src/core/server.cpp @@ -0,0 +1,39 @@ +#include "server.hpp" +#include "../debug/log.hpp" +#include +#include +#include + +CServer::CServer(uint16_t port) + : m_sockfd(std::make_shared(socket(AF_INET, SOCK_STREAM, 0))) { + if (!m_sockfd->isValid()) + throw std::runtime_error("Failed to create socket"); + + constexpr int opt = 1; + constexpr int size = sizeof(opt); + if (setsockopt(m_sockfd->get(), SOL_SOCKET, SO_REUSEADDR, &opt, size) < 0) + log(stderr, WARN, "setsockopt(SO_REUSEADDR) failed"); + + if (setsockopt(m_sockfd->get(), SOL_SOCKET, SO_KEEPALIVE, &opt, size) < 0) + log(stderr, WARN, "setsockopt(SO_KEEPALIVE) failed"); + + memset(&m_addr, 0, sizeof(m_addr)); + m_addr.sin_family = AF_INET; + m_addr.sin_port = htons(port); + m_addr.sin_addr.s_addr = INADDR_ANY; + bind(m_sockfd->get(), reinterpret_cast(&m_addr), sizeof(m_addr)) || + listen(m_sockfd->get(), 5) + ? throw std::runtime_error("Failed to bind or listen") + : log(LOG, "Server started on port: {}", port); + + log(LOG, "Server: initialized"); +}; + +CServer::~CServer() { + m_sockfd.reset(); + log(SYS, "Server: bye"); +} + +std::shared_ptr CServer::getSocket() const { + return m_sockfd; +} diff --git a/src/core/server.hpp b/src/core/server.hpp new file mode 100644 index 0000000..c42192b --- /dev/null +++ b/src/core/server.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "../misc/FileDescriptor.hpp" +#include +#include + +class CServer { + public: + CServer(uint16_t port); + ~CServer(); + std::shared_ptr getSocket() const; + + private: + std::shared_ptr m_sockfd; + sockaddr_in m_addr; + + mutable std::mutex m_mutex; +}; +inline std::unique_ptr g_pServer; diff --git a/src/core/session.cpp b/src/core/session.cpp new file mode 100644 index 0000000..7b0b459 --- /dev/null +++ b/src/core/session.cpp @@ -0,0 +1,273 @@ +#include "session.hpp" +#include "chatManager.hpp" +#include "../command/commandHandler.hpp" +#include "../debug/format.hpp" +#include "../debug/log.hpp" +#include "server.hpp" +#include "sessionManager.hpp" +#include +#include +#include +#include +#include +#include +#include + +CSession::CSession() { + m_sockfd = std::make_shared(accept(g_pServer->getSocket()->get(), reinterpret_cast(&m_addr), &m_addrLen)); // if this is in the init list, it will run before m_addrLen, so it won't work :/ + + if (!m_sockfd->isValid()) + throw std::runtime_error("session: Failed to create socket"); + + m_ip.resize(INET_ADDRSTRLEN); + inet_ntop(AF_INET, &m_addr.sin_addr, &m_ip[0], INET_ADDRSTRLEN); + m_ip.resize(strlen(m_ip.c_str())); + m_port = ntohs(m_addr.sin_port); + m_isAdmin = std::ranges::any_of(m_adminIps, [this](const char *ip) { return m_ip == ip; }); + + log(LOG, "Client {} connected on port {}", m_ip, m_port); + + log(TRACE, "session: initialized"); +} + +CSession::~CSession() { +#ifdef DEBUG + onDisconnect(); +#endif + m_sockfd.reset(); + + log(LOG, "Client {}@{} disconnected", m_name, m_ip); + + log(TRACE, "session({}@{}): bye", m_name, m_ip); +} + +#ifdef DEBUG +void CSession::onConnect() { + log(TRACE, "Client {}@{} connected", m_name, m_ip); +} + +void CSession::onDisconnect() { + log(TRACE, "Client {}@{} disconnected", m_name, m_ip); +} +#endif + +void CSession::onErrno(eEventType eventType) { + char errbuf[256]; + const char *err = strerror_r(errno, errbuf, sizeof(errbuf)); + + switch (eventType) { + case READ: + log(WARN, "Error while reading: {}", err); + break; + case WRITE: + log(WARN, "Error while writing: {}", err); + break; + } +} + +#ifdef DEBUG +void CSession::onRecv(const SRecvData &data) { + log(TRACE, "Received: {}", data.data); +} + +void CSession::onSend(const std::string &msg) { + log(LOG, "Sent: {}", msg); +} +#endif + +void CSession::recvLoop() { + while (true) { + auto recvData = read(NFormatter::fmt(NONEWLINE, "{}: ", m_name)); + if (!recvData->good) + break; + + recvData->sanitize(); + if (recvData->isEmpty()) { + log(TRACE, "Empty message from {}", m_name); + continue; + } + + const auto isCommand = g_pCommandHandler->isCommand(recvData->data); + if (isCommand) + g_pCommandHandler->handleCommand(recvData->data, self.lock()); + + g_pChatManager->newMessage({.msg = recvData->data, .username = m_name, .sender = self, .admin = isCommand}); + } +} + +bool CSession::SRecvData::isEmpty() const { + return data.empty() || std::ranges::all_of(data, [](char c) { return std::isspace(c); }); +} + +void CSession::SRecvData::sanitize() { + if (!data.empty() && data.back() == '\n') + data.pop_back(); + + data.erase(std::remove(data.begin(), data.end(), asciiEscape), data.end()); // don't accept ASCII code, might mess up terminal // NOLINT + + if (const auto start = data.find_first_not_of(" \t\r\n"); start != std::string::npos) { + const auto end = data.find_last_not_of(" \t\r\n"); + data = data.substr(start, end - start + 1); + } else + data.clear(); +} + +std::unique_ptr CSession::read() { + auto recvData = std::make_unique(); + + recvData->data.resize(recvData->size); + ssize_t size = recv(m_sockfd->get(), &recvData->data[0], recvData->size, 0); + + if (size < 0) { + recvData->good = false; + onErrno(READ); + } else if (size == 0) + recvData->good = false; + else { + recvData->data.resize(size); + recvData->good = true; + } + +#ifdef DEBUG + onRecv(*recvData); +#endif + m_szReading.reset(); + return recvData; +} + +std::unique_ptr CSession::read(const std::string &msg, bool bypassDeaf) { + if (bypassDeaf && m_bDeaf) + m_bDeaf = false; + write(NONEWLINE, "{}", msg); + if (bypassDeaf && !m_bDeaf) + m_bDeaf = true; + m_szReading = msg; + return read(); +} + +void CSession::run() { +#ifdef DEBUG + onConnect(); +#endif + if (registerSession()) { + for (const auto &chat : g_pChatManager->getChat()) + writeChat(chat); + recvLoop(); + } else + log(LOG, "Client {} exited without even registering", m_ip); + + g_pSessionManager->kick(self); +} + +bool CSession::isMuted() const { + return m_bMuted; +} + +void CSession::setMuted(bool mute) { + m_bMuted = mute; +} + +bool CSession::isDeaf() const { + return m_bDeaf; +} + +void CSession::setDeaf(bool deaf) { + m_bDeaf = deaf; +} + +bool CSession::registerSession() { + auto recvData = read("Name: ", true); + if (!recvData->good) + return false; + + recvData->sanitize(); + if (recvData->isEmpty()) { + m_bDeaf = false; + write("Name cannot be empty"); + m_bDeaf = true; + return registerSession(); + } + + if (g_pSessionManager->nameExists(recvData->data)) { + m_bDeaf = false; + write("Name already exists"); + m_bDeaf = true; + return registerSession(); + } + + this->m_name = recvData->data; + + log(LOG, "Client {} registered as: {}", m_ip, m_name); + + g_pSessionManager->broadcast(NFormatter::fmt(NONEWLINE, "{} has joined the chat", m_name), self); + + m_bDeaf = false; + return true; +} + +bool CSession::isValid() { + if (!m_sockfd->isValid() || m_name.empty()) + return false; + + int err = 0; + socklen_t size = sizeof(err); + return getsockopt(m_sockfd->get(), SOL_SOCKET, SO_ERROR, &err, &size) >= 0 && err == 0; +} + +bool CSession::write(const std::string &msg) { + return write("{}", msg); +} + +bool CSession::writeChat(const CChatManager::SMessage &msg) { + const auto sender = msg.sender ? msg.sender->lock() : nullptr; + if (sender ? sender != self.lock() : msg.username != m_name) { + if (msg.admin && !m_isAdmin) + return false; + + return write(g_pChatManager->fmtBroadcastMessage(msg)); + } + return false; +} + +const std::string &CSession::getName() const { + return m_name; +} + +const std::string &CSession::getIp() const { + return m_ip; +} + +bool CSession::isAdmin() const { + return m_isAdmin; +} + +template +bool CSession::write(std::format_string fmt, Args &&...args) { + if (m_bDeaf) + return false; + return write(NONE, fmt, std::forward(args)...); +} + +template +bool CSession::write(eFormatType type, std::format_string fmt, Args &&...args) { + if (m_bDeaf) + return false; + + std::string msg = NFormatter::fmt(type, fmt, std::forward(args)...); + if (m_szReading) { + msg.insert(0, "\r"); + msg.append(*m_szReading); + } + + if (send(m_sockfd->get(), msg.c_str(), msg.size(), 0) < 0) { + onErrno(WRITE); + return false; + } +#ifdef DEBUG + onSend(msg); +#endif + return true; +} + +template bool CSession::write(std::format_string, const std::string &); +template bool CSession::write(eFormatType, std::format_string, const std::string &); diff --git a/src/core/session.hpp b/src/core/session.hpp new file mode 100644 index 0000000..ca951d1 --- /dev/null +++ b/src/core/session.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "../misc/FileDescriptor.hpp" +#include "chatManager.hpp" +#include "../debug/format.hpp" +#include +#include +#include +#include +#include +#include +#include + +class CSession { + public: + CSession(); + ~CSession(); + + struct SRecvData { + std::string data; + const size_t size = 1024; + bool good = false; + + bool isEmpty() const; + void sanitize(); + + static constexpr char asciiEscape = 0x1B; //  + }; + + const std::string &getName() const; + const std::string &getIp() const; + bool isAdmin() const; + + std::unique_ptr read(); + std::unique_ptr read(const std::string &msg, bool bypassDeaf = false); + bool write(const std::string &msg); + template + bool write(std::format_string fmt, Args &&...args); + template + bool write(eFormatType type, std::format_string fmt, Args &&...args); + bool writeChat(const CChatManager::SMessage &msg); + + bool isMuted() const; + void setMuted(bool mute); + bool isDeaf() const; + void setDeaf(bool deaf); + + void run(); + + bool isValid(); + + private: + constexpr static char m_adminIps[1][INET_ADDRSTRLEN] = {"127.0.0.1"}; + + enum eEventType : std::uint8_t { + READ, + WRITE + }; + + std::weak_ptr self; + std::shared_ptr m_sockfd; + sockaddr_in m_addr; + socklen_t m_addrLen = sizeof(m_addr); + std::string m_name; + std::string m_ip; + int m_port; + bool m_isAdmin = false; + + std::optional m_szReading = std::nullopt; + bool m_bMuted = false; + bool m_bDeaf = true; // init as true as we don't want anything during registerSession + + void recvLoop(); + bool registerSession(); + +#ifdef DEBUG + void onConnect(); + void onDisconnect(); +#endif + void onErrno(eEventType); +#ifdef DEBUG + void onRecv(const SRecvData &data); + void onSend(const std::string &msg); +#endif + + friend class CSessionManager; + friend void registerCommands(); +}; diff --git a/src/core/sessionManager.cpp b/src/core/sessionManager.cpp new file mode 100644 index 0000000..6c002a5 --- /dev/null +++ b/src/core/sessionManager.cpp @@ -0,0 +1,141 @@ +#include "sessionManager.hpp" +#include "../io/IOManager.hpp" +#include "chatServer.hpp" +#include "../debug/format.hpp" +#include "../debug/log.hpp" +#include "session.hpp" +#include +#include +#include +#include + +void CSessionManager::shutdownSessions() { + if (m_vSessions.empty()) + return; + + for (auto &[thread, session] : m_vSessions) { + if (session) { + session->setDeaf(false); + session->write("Server shutting down"); + if (!session->getName().empty()) + g_pSessionManager->broadcast(NFormatter::fmt(NONEWLINE, "{} has left the chat", session->getName()), std::weak_ptr(session)); + } + } + + for (auto &[thread, session] : m_vSessions) { + if (thread.joinable()) + thread.detach(); + session.reset(); + } + + m_vSessions.clear(); +} + +CSessionManager::CSessionManager() { + if (std::atexit([]() { + if (g_pSessionManager) + g_pSessionManager->shutdownSessions(); + })) + log(ERR, "Failed to register SessionManager atexit handler, will fallback to quick_exit after hang"); + + log(LOG, "SessionManager: initialized"); +}; + +CSessionManager::~CSessionManager() { + for (auto &[thread, session] : m_vSessions) { + if (thread.joinable()) + thread.detach(); + kick(std::weak_ptr(session)); + } + + log(SYS, "SessionManager: bye"); +} + +void CSessionManager::newSession() { + std::shared_ptr session = std::make_shared(); + auto &instance = m_vSessions.emplace_back(std::jthread([session]() { session->run(); }), session); + instance.second->self = std::weak_ptr(instance.second); +} + +void CSessionManager::run() { + while (true) + newSession(); +} + +void CSessionManager::broadcast(const std::string &msg, std::optional> self) const { + for (const auto &[thread, session] : m_vSessions) + if (!self || (self && session != self->lock())) + session->write(msg); + + g_pIOManager->addCustom({"", msg}); +} + +void CSessionManager::broadcast(const CChatManager::SMessage &msg) const { + for (const auto &[thread, session] : m_vSessions) { + if (!session) + continue; + + session->writeChat(msg); + } + + g_pIOManager->addMessage(msg); +} + +bool CSessionManager::nameExists(const std::string &name) { + auto it = std::ranges::find_if(m_vSessions, [&name](const auto &s) { return s.second->getName() == name; }); + + if (it != m_vSessions.end()) { + if (!it->second->isValid()) { + kick(std::weak_ptr(it->second), true, "Connection lost"); + return false; + } + return true; + } + return false; +} + +std::shared_ptr CSessionManager::getByName(const std::string &name) const { + auto it = std::ranges::find_if(m_vSessions, [&name](const auto &s) { return s.second->getName() == name; }); + return it != m_vSessions.end() ? it->second : nullptr; +} + +std::shared_ptr CSessionManager::getByIp(const std::string &ip) const { + auto it = std::ranges::find_if(m_vSessions, [&ip](const auto &s) { log(LOG, s.second->getIp()); return s.second->getIp() == ip; }); + return it != m_vSessions.end() ? it->second : nullptr; +} + +std::vector> CSessionManager::getSessions() const { + return m_vSessions | std::views::transform([](const auto &s) { return s.second; }) | std::ranges::to(); +} + +void CSessionManager::kick(std::weak_ptr session_weak, const bool kill, const std::string &reason) { + auto session = session_weak.lock(); + if (!session) + return; + + for (auto it = m_vSessions.begin(); it != m_vSessions.end(); ++it) { + if (it->second == session) { + if (session) + // this only exist when the session is registered + // not sure why the second is null, but m_name definately is since it's setted during register + // but we don't need to notify people if the session didn't even "join"/register anyways + g_pSessionManager->broadcast(NFormatter::fmt(NONEWLINE, "{} has left the chat", session->getName()), session_weak); + + if (!reason.empty()) { + session->setDeaf(false); + session->write(reason); + } + const auto native_handle = it->first.native_handle(); + if (it->first.joinable()) + it->first.detach(); // .detach the thread since it's removing itself + it->second.reset(); + m_vSessions.erase(it); + if (kill && native_handle != 0) + // cancel(terminal/kill) the thread if it doesn't exit on its own + // pthread_cancel cuz it's the safest https://stackoverflow.com/a/3438576 + // need a condition because it might crash if the thread is already dead + pthread_cancel(native_handle); + return; + } + } +} diff --git a/src/core/sessionManager.hpp b/src/core/sessionManager.hpp index e84226c..71bcc5f 100644 --- a/src/core/sessionManager.hpp +++ b/src/core/sessionManager.hpp @@ -1,9 +1,9 @@ #pragma once #include "../debug/format.hpp" -#include "../misc/memory.hpp" #include "chatManager.hpp" #include "session.hpp" +#include #include #include @@ -15,22 +15,22 @@ class CSessionManager { void newSession(); void run(); - void broadcast(const std::string &msg, std::optional> self = std::nullopt) const; // second param only specified when we want to exclude the sender - void kick(WP session, const bool kill = false, const std::string &reason = ""); + void broadcast(const std::string &msg, std::optional> self = std::nullopt) const; // second param only specified when we want to exclude the sender + void kick(std::weak_ptr session, const bool kill = false, const std::string &reason = ""); void addSession(const CSession &session); bool nameExists(const std::string &name); void shutdownSessions(); - SP getByName(const std::string &name) const; - SP getByIp(const std::string &ip) const; - std::vector> getSessions() const; + std::shared_ptr getByName(const std::string &name) const; + std::shared_ptr getByIp(const std::string &ip) const; + std::vector> getSessions() const; private: void broadcast(const CChatManager::SMessage &msg) const; - std::vector>> m_vSessions; + std::vector>> m_vSessions; friend class CChatManager; }; -inline SP g_pSessionManager; +inline std::shared_ptr g_pSessionManager; diff --git a/src/debug/format.hpp b/src/debug/format.hpp new file mode 100644 index 0000000..f857f96 --- /dev/null +++ b/src/debug/format.hpp @@ -0,0 +1,51 @@ +#pragma once +#include +#include + +enum eFormatType : int8_t { + NONE, + TRACE, + LOG, + WARN, + ERR, + SYS, + NONEWLINE +}; + +namespace NFormatter { +inline std::string fmt(eFormatType type, std::string str) { + std::string prefix; + switch (type) { + case LOG: + prefix = "[LOG] "; + break; + case WARN: + prefix = "[WARN] "; + break; + case ERR: + prefix = "[ERROR] "; + break; + case SYS: + prefix = "[SYSTEM] "; + break; +#ifdef DEBUG + case TRACE: + prefix = "[DEBUG] "; + break; +#endif + default: + prefix = ""; + break; + } + + if (!str.empty() && str.back() != '\n' && type != NONEWLINE) + str += '\n'; + + return prefix + str; +} + +template +std::string fmt(eFormatType type, std::format_string fmt, Args &&...args) { + return NFormatter::fmt(type, std::format(fmt, std::forward(args)...)); +} +} // namespace NFormatter diff --git a/src/debug/log.hpp b/src/debug/log.hpp new file mode 100644 index 0000000..71a958e --- /dev/null +++ b/src/debug/log.hpp @@ -0,0 +1,37 @@ +#pragma once +#include "../io/IOManager.hpp" +#include "format.hpp" +#include +#include + +inline void log(FILE *_stream, eFormatType type, std::string str) { +#ifndef DEBUG + if (type == TRACE) + return; +#endif + if (g_pIOManager) + g_pIOManager->addLog({.type = type, .log = str}); + else + std::println(_stream, "{}", NFormatter::fmt(type, str)); +} + +template +void log(FILE *_stream, eFormatType type, std::format_string fmt, Args &&...args) { +#ifndef DEBUG + if (type == TRACE) + return; +#endif + if (g_pIOManager) + g_pIOManager->addLog({.type = type, .log = NFormatter::fmt(NONEWLINE, fmt, std::forward(args)...)}); + else + std::println(_stream, "{}", NFormatter::fmt(type, fmt, std::forward(args)...)); +} + +inline void log(eFormatType type, std::string str) { + log(type == ERR ? stderr : stdout, type, str); +} + +template +void log(eFormatType type, std::format_string fmt, Args &&...args) { + log(type == ERR ? stderr : stdout, type, fmt, std::forward(args)...); +} diff --git a/src/format.hpp b/src/format.hpp deleted file mode 100644 index f857f96..0000000 --- a/src/format.hpp +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once -#include -#include - -enum eFormatType : int8_t { - NONE, - TRACE, - LOG, - WARN, - ERR, - SYS, - NONEWLINE -}; - -namespace NFormatter { -inline std::string fmt(eFormatType type, std::string str) { - std::string prefix; - switch (type) { - case LOG: - prefix = "[LOG] "; - break; - case WARN: - prefix = "[WARN] "; - break; - case ERR: - prefix = "[ERROR] "; - break; - case SYS: - prefix = "[SYSTEM] "; - break; -#ifdef DEBUG - case TRACE: - prefix = "[DEBUG] "; - break; -#endif - default: - prefix = ""; - break; - } - - if (!str.empty() && str.back() != '\n' && type != NONEWLINE) - str += '\n'; - - return prefix + str; -} - -template -std::string fmt(eFormatType type, std::format_string fmt, Args &&...args) { - return NFormatter::fmt(type, std::format(fmt, std::forward(args)...)); -} -} // namespace NFormatter diff --git a/src/inputManager.cpp b/src/inputManager.cpp deleted file mode 100644 index d392081..0000000 --- a/src/inputManager.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "inputManager.hpp" -#include "IOManager.hpp" -#include "chatManager.hpp" -#include "commandHandler.hpp" -#include "log.hpp" -#include "renderManager.hpp" -#include "scroller.hpp" - -CInputManager::CInputManager() - : m_input(ftxui::Input(&m_szInput, "Type here...", {.transform = [](ftxui::InputState state) { - state.element |= ftxui::bgcolor(ftxui::Color::HSVA(0, 0, 0, 0)); - state.element |= ftxui::color(ftxui::Color::White); - return state.element; }, .multiline = false, .on_enter = [&] { - if (m_szInput.empty()) - return; - - g_pCommandHandler->isCommand(m_szInput) ? g_pCommandHandler->handleCommand(m_szInput) : g_pChatManager->newMessage({.msg = m_szInput, .username = "Server"}); - m_szInput.clear(); }}) // NOLINT - ) - , inputComponent(ftxui::Renderer(m_input, [&] { - return ftxui::hbox({ - ftxui::text("> "), - m_input->Render(), - }); - })) - , logComponent(ftxui::Renderer([] { - std::vector messages; - for (const auto &[chat, log, custom] : g_pIOManager->getIO()) { - if (chat && !chat->msg.empty()) { - std::string username = chat->username; - if (username.back() == '\n') - username.pop_back(); - messages.push_back(ftxui::window(ftxui::text(username), ftxui::paragraph(chat->msg))); - } else if (log && !log->log.empty()) - messages.push_back(ftxui::window(ftxui::text(NFormatter::fmt(log->type, "")), ftxui::paragraph(log->log))); - else if (custom) - messages.push_back(ftxui::window(ftxui::text(custom->first), ftxui::paragraph(custom->second))); - } - - return ftxui::vbox(messages); - })) { - - log(LOG, "InputManager: initialized"); -} - -CInputManager::~CInputManager() { - log(SYS, "InputManager: bye"); -} - -void CInputManager::updateIO() { - logComponent->OnEvent(ftxui::Event::Custom); - g_pRenderManager->screen.PostEvent(ftxui::Event::Custom); -} - -void CInputManager::inputLoop() { - container = logComponent |= ftxui::Scroller; - container = ftxui::ResizableSplitBottom(inputComponent, container, &bottomSize); - - // FIXME: this shit spams TakeFocus(not ideal for power etc reasons) - // consider: custom loop, or just only allow focus to be inputComponent somehow - container |= ftxui::CatchEvent([&](ftxui::Event) { - inputComponent->TakeFocus(); - return false; - }); - g_pRenderManager->setRenderer(ftxui::Renderer(container, [this] { - return container->Render() | ftxui::border; - })); - inputComponent->TakeFocus(); - g_pRenderManager->enterLoop(); -} diff --git a/src/inputManager.hpp b/src/inputManager.hpp deleted file mode 100644 index a81e39d..0000000 --- a/src/inputManager.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include -#include - -class CInputManager { - public: - CInputManager(); - ~CInputManager(); - - void inputLoop(); - - void updateIO(); - - private: - std::string m_szInput; - - ftxui::Component m_input; - ftxui::Component inputComponent; - ftxui::Component logComponent; - ftxui::Component container; - - int bottomSize = 1; -}; -inline std::unique_ptr g_pInputManager; diff --git a/src/io/IOManager.cpp b/src/io/IOManager.cpp new file mode 100644 index 0000000..be29adf --- /dev/null +++ b/src/io/IOManager.cpp @@ -0,0 +1,34 @@ +#include "IOManager.hpp" +#include "inputManager.hpp" +#include "../debug/log.hpp" + +CIOManager::CIOManager() { + log(LOG, "IOManager: initialized"); +} + +CIOManager::~CIOManager() { + m_vIO.clear(); + log(SYS, "IOManager: bye"); +} + +const std::vector &CIOManager::getIO() const { + return m_vIO; +} + +void CIOManager::addLog(const SLog &log) { + m_vIO.push_back({.log = log}); + if (g_pInputManager) + g_pInputManager->updateIO(); +} + +void CIOManager::addMessage(const CChatManager::SMessage &msg) { + m_vIO.push_back({.msg = msg}); + if (g_pInputManager) + g_pInputManager->updateIO(); +} + +void CIOManager::addCustom(const std::pair &custom) { + m_vIO.push_back({.custom = custom}); + if (g_pInputManager) + g_pInputManager->updateIO(); +} diff --git a/src/io/IOManager.hpp b/src/io/IOManager.hpp new file mode 100644 index 0000000..d1d891d --- /dev/null +++ b/src/io/IOManager.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "../core/chatManager.hpp" +#include "../debug/format.hpp" +#include + +class CIOManager { + public: + CIOManager(); + ~CIOManager(); + + struct SLog { + eFormatType type; + std::string log; + }; + struct SIO { + // once a time + std::optional msg = std::nullopt; + std::optional log = std::nullopt; + std::optional> custom = std::nullopt; + }; + + const std::vector &getIO() const; + void addLog(const SLog &log); + void addMessage(const CChatManager::SMessage &msg); + void addCustom(const std::pair &custom); + + private: + std::vector m_vIO; + + friend class CChatManager; +}; +inline std::unique_ptr g_pIOManager; diff --git a/src/io/inputManager.cpp b/src/io/inputManager.cpp new file mode 100644 index 0000000..09a7729 --- /dev/null +++ b/src/io/inputManager.cpp @@ -0,0 +1,70 @@ +#include "inputManager.hpp" +#include "IOManager.hpp" +#include "../core/chatManager.hpp" +#include "../command/commandHandler.hpp" +#include "../debug/log.hpp" +#include "renderManager.hpp" +#include "scroller.hpp" + +CInputManager::CInputManager() + : m_input(ftxui::Input(&m_szInput, "Type here...", {.transform = [](ftxui::InputState state) { + state.element |= ftxui::bgcolor(ftxui::Color::HSVA(0, 0, 0, 0)); + state.element |= ftxui::color(ftxui::Color::White); + return state.element; }, .multiline = false, .on_enter = [&] { + if (m_szInput.empty()) + return; + + g_pCommandHandler->isCommand(m_szInput) ? g_pCommandHandler->handleCommand(m_szInput) : g_pChatManager->newMessage({.msg = m_szInput, .username = "Server"}); + m_szInput.clear(); }}) // NOLINT + ) + , inputComponent(ftxui::Renderer(m_input, [&] { + return ftxui::hbox({ + ftxui::text("> "), + m_input->Render(), + }); + })) + , logComponent(ftxui::Renderer([] { + std::vector messages; + for (const auto &[chat, log, custom] : g_pIOManager->getIO()) { + if (chat && !chat->msg.empty()) { + std::string username = chat->username; + if (username.back() == '\n') + username.pop_back(); + messages.push_back(ftxui::window(ftxui::text(username), ftxui::paragraph(chat->msg))); + } else if (log && !log->log.empty()) + messages.push_back(ftxui::window(ftxui::text(NFormatter::fmt(log->type, "")), ftxui::paragraph(log->log))); + else if (custom) + messages.push_back(ftxui::window(ftxui::text(custom->first), ftxui::paragraph(custom->second))); + } + + return ftxui::vbox(messages); + })) { + + log(LOG, "InputManager: initialized"); +} + +CInputManager::~CInputManager() { + log(SYS, "InputManager: bye"); +} + +void CInputManager::updateIO() { + logComponent->OnEvent(ftxui::Event::Custom); + g_pRenderManager->screen.PostEvent(ftxui::Event::Custom); +} + +void CInputManager::inputLoop() { + container = logComponent |= ftxui::Scroller; + container = ftxui::ResizableSplitBottom(inputComponent, container, &bottomSize); + + // FIXME: this shit spams TakeFocus(not ideal for power etc reasons) + // consider: custom loop, or just only allow focus to be inputComponent somehow + container |= ftxui::CatchEvent([&](ftxui::Event) { + inputComponent->TakeFocus(); + return false; + }); + g_pRenderManager->setRenderer(ftxui::Renderer(container, [this] { + return container->Render() | ftxui::border; + })); + inputComponent->TakeFocus(); + g_pRenderManager->enterLoop(); +} diff --git a/src/io/inputManager.hpp b/src/io/inputManager.hpp new file mode 100644 index 0000000..a81e39d --- /dev/null +++ b/src/io/inputManager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include + +class CInputManager { + public: + CInputManager(); + ~CInputManager(); + + void inputLoop(); + + void updateIO(); + + private: + std::string m_szInput; + + ftxui::Component m_input; + ftxui::Component inputComponent; + ftxui::Component logComponent; + ftxui::Component container; + + int bottomSize = 1; +}; +inline std::unique_ptr g_pInputManager; diff --git a/src/io/renderManager.cpp b/src/io/renderManager.cpp new file mode 100644 index 0000000..a8dff68 --- /dev/null +++ b/src/io/renderManager.cpp @@ -0,0 +1,38 @@ +#include "renderManager.hpp" +#include "../debug/log.hpp" + +CRenderManager::CRenderManager() + : screen(ftxui::ScreenInteractive::TerminalOutput()) { + if (std::atexit([]() { + if (g_pRenderManager) + g_pRenderManager->exitLoop(); + })) + log(ERR, "Failed to register RenderManager atexit handler, will fallback to quick_exit after hang"); + log(LOG, "RenderManager: initialized"); +}; + +CRenderManager::~CRenderManager() { + exitLoop(); + log(SYS, "RenderManager: bye"); +} + +void CRenderManager::exitLoop() { + if (renderer) { + try { + screen.ExitLoopClosure()(); + screen.Exit(); + } catch (const std::exception &e) { + log(WARN, "Exception in exitLoop: {}", e.what()); + } catch (...) { + log(WARN, "Unknown exception in exitLoop"); + } + } +} + +void CRenderManager::enterLoop() { + screen.Loop(renderer); +} + +void CRenderManager::setRenderer(ftxui::Component component) { + renderer = std::move(component); +} diff --git a/src/io/renderManager.hpp b/src/io/renderManager.hpp new file mode 100644 index 0000000..7a14cc0 --- /dev/null +++ b/src/io/renderManager.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +class CRenderManager { + public: + CRenderManager(); + ~CRenderManager(); + + void enterLoop(); + + void setRenderer(ftxui::Component component); + + private: + void exitLoop(); + + ftxui::ScreenInteractive screen; + ftxui::Component renderer; + + friend class CInputManager; +}; +inline std::unique_ptr g_pRenderManager; diff --git a/src/io/scroller.cpp b/src/io/scroller.cpp new file mode 100644 index 0000000..c595a39 --- /dev/null +++ b/src/io/scroller.cpp @@ -0,0 +1,85 @@ +#include "scroller.hpp" + +#include "../debug/log.hpp" +#include // for max, min +#include // for Component, ComponentBase +#include // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp +#include // for shared_ptr, allocator, __shared_ptr_access +#include // for move + +#include "ftxui/component/component.hpp" // for Make +#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::WheelDown, Mouse::WheelUp +#include "ftxui/dom/deprecated.hpp" // for text +#include "ftxui/dom/elements.hpp" // for operator|, Element, size, vbox, EQUAL, HEIGHT, dbox, reflect, focus, inverted, nothing, select, vscroll_indicator, yflex, yframe +#include "ftxui/dom/node.hpp" // for Node +#include "ftxui/dom/requirement.hpp" // for Requirement +#include "ftxui/screen/box.hpp" // for Box + +namespace ftxui { // NOLINT + +class CScrollerBase : public ComponentBase { + public: + CScrollerBase(Component child) { Add(child); } + + private: + Element OnRender() final { // NOLINT + auto focused = Focused() ? focus : ftxui::select; + auto style = Focused() ? inverted : nothing; + + Element background = ComponentBase::Render(); + background->ComputeRequirement(); + size_ = background->requirement().min_y; + return dbox({ + std::move(background), + vbox({ + text(L"") | size(HEIGHT, EQUAL, selected_), + text(L"") | style | focused, + }), + }) | + vscroll_indicator | yframe | yflex | reflect(box_); + } + + bool OnEvent(Event event) final { // NOLINT + if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) + TakeFocus(); + + int selected_old = selected_; + if (event == Event::ArrowUp || event == Event::Character('k') || + (event.is_mouse() && event.mouse().button == Mouse::WheelUp)) + selected_--; + if ((event == Event::ArrowDown || event == Event::Character('j') || + (event.is_mouse() && event.mouse().button == Mouse::WheelDown))) + selected_++; + if (event == Event::PageDown) + selected_ += box_.y_max - box_.y_min; + if (event == Event::PageUp) + selected_ -= box_.y_max - box_.y_min; + if (event == Event::Home) + selected_ = 0; + if (event == Event::End) + selected_ = size_; + + if (event == Event::Custom && sticky_select) + selected_ = size_; + + selected_ = std::max(0, std::min(size_, selected_)); + sticky_select = selected_ >= size_ - 15; + return selected_old != selected_; + } + + bool Focusable() const final { return true; } // NOLINT + + bool sticky_select = true; + int selected_ = 0; + int size_ = 0; + Box box_; +}; + +Component Scroller(Component child) { // NOLINT + return Make(std::move(child)); +} +} // namespace ftxui + +// Copyright 2021 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/io/scroller.hpp b/src/io/scroller.hpp new file mode 100644 index 0000000..b8b39b1 --- /dev/null +++ b/src/io/scroller.hpp @@ -0,0 +1,15 @@ +#ifndef SCROLLER_H +#define SCROLLER_H + +#include + +#include "ftxui/component/component_base.hpp" // for Component + +namespace ftxui { // NOLINT +Component Scroller(Component child); // NOLINT +} +#endif /* end of include guard: SCROLLER_H */ + +// Copyright 2021 Arthur Sonzogni. All rights reserved. +// Use of this source code is governed by the MIT license that can be found in +// the LICENSE file. diff --git a/src/log.hpp b/src/log.hpp deleted file mode 100644 index a4654c4..0000000 --- a/src/log.hpp +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once -#include "IOManager.hpp" -#include "format.hpp" -#include -#include - -inline void log(FILE *_stream, eFormatType type, std::string str) { -#ifndef DEBUG - if (type == TRACE) - return; -#endif - if (g_pIOManager) - g_pIOManager->addLog({.type = type, .log = str}); - else - std::println(_stream, "{}", NFormatter::fmt(type, str)); -} - -template -void log(FILE *_stream, eFormatType type, std::format_string fmt, Args &&...args) { -#ifndef DEBUG - if (type == TRACE) - return; -#endif - if (g_pIOManager) - g_pIOManager->addLog({.type = type, .log = NFormatter::fmt(NONEWLINE, fmt, std::forward(args)...)}); - else - std::println(_stream, "{}", NFormatter::fmt(type, fmt, std::forward(args)...)); -} - -inline void log(eFormatType type, std::string str) { - log(type == ERR ? stderr : stdout, type, str); -} - -template -void log(eFormatType type, std::format_string fmt, Args &&...args) { - log(type == ERR ? stderr : stdout, type, fmt, std::forward(args)...); -} diff --git a/src/main.cpp b/src/main.cpp index 0ee7b3a..c4a6ce4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,5 @@ -#include "chatServer.hpp" -#include "log.hpp" +#include "core/chatServer.hpp" +#include "debug/log.hpp" #include #include #include diff --git a/src/misc/FileDescriptor.cpp b/src/misc/FileDescriptor.cpp new file mode 100644 index 0000000..92505f5 --- /dev/null +++ b/src/misc/FileDescriptor.cpp @@ -0,0 +1,86 @@ +#include "FileDescriptor.hpp" +#include +#include +#include +#include +#include + +CFileDescriptor::CFileDescriptor(int const fd) + : m_fd(fd) {} + +CFileDescriptor::CFileDescriptor(CFileDescriptor &&other) noexcept + : m_fd(std::exchange(other.m_fd, -1)) {} + +CFileDescriptor &CFileDescriptor::operator=(CFileDescriptor &&other) noexcept { + if (this == &other) // Shit will go haywire if there is duplicate ownership + abort(); + + reset(); + m_fd = std::exchange(other.m_fd, -1); + return *this; +} + +CFileDescriptor::~CFileDescriptor() { + reset(); +} + +bool CFileDescriptor::isValid() const { + return m_fd != -1; +} + +int CFileDescriptor::get() const { + return m_fd; +} + +int CFileDescriptor::getFlags() const { + return fcntl(m_fd, F_GETFD); +} + +bool CFileDescriptor::setFlags(int flags) { + return fcntl(m_fd, F_SETFD, flags) != -1; +} + +int CFileDescriptor::take() { + return std::exchange(m_fd, -1); +} + +void CFileDescriptor::reset() { + if (m_fd != -1) { + close(m_fd); + m_fd = -1; + } +} + +CFileDescriptor CFileDescriptor::duplicate(int flags) const { + if (m_fd == -1) + return {}; + + return CFileDescriptor{fcntl(m_fd, flags, 0)}; +} + +bool CFileDescriptor::isClosed() const { + return isClosed(m_fd); +} + +bool CFileDescriptor::isReadable() const { + return isReadable(m_fd); +} + +bool CFileDescriptor::isClosed(int fd) { + pollfd pfd = { + .fd = fd, + .events = POLLIN, + .revents = 0, + }; + + if (poll(&pfd, 1, 0) < 0) + return true; + + return pfd.revents & (POLLHUP | POLLERR); +} + +bool CFileDescriptor::isReadable(int fd) { + pollfd pfd = {.fd = fd, .events = POLLIN, .revents = 0}; + + return poll(&pfd, 1, 0) > 0 && (pfd.revents & POLLIN); +} diff --git a/src/misc/FileDescriptor.hpp b/src/misc/FileDescriptor.hpp new file mode 100644 index 0000000..75aeb73 --- /dev/null +++ b/src/misc/FileDescriptor.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +class CFileDescriptor { + public: + CFileDescriptor() = default; + explicit CFileDescriptor(int const fd); + CFileDescriptor(CFileDescriptor &&) noexcept; + CFileDescriptor &operator=(CFileDescriptor &&) noexcept; + ~CFileDescriptor(); + + CFileDescriptor(const CFileDescriptor &) = delete; + CFileDescriptor &operator=(const CFileDescriptor &) = delete; + + bool operator==(const CFileDescriptor &rhs) const { + return m_fd == rhs.m_fd; + } + + bool isValid() const; + int get() const; + int getFlags() const; + bool setFlags(int flags); + int take(); + void reset(); + CFileDescriptor duplicate(int flags = F_DUPFD_CLOEXEC) const; + + bool isReadable() const; + bool isClosed() const; + + static bool isReadable(int fd); + static bool isClosed(int fd); + + private: + int m_fd = -1; +}; diff --git a/src/renderManager.cpp b/src/renderManager.cpp deleted file mode 100644 index 18b0664..0000000 --- a/src/renderManager.cpp +++ /dev/null @@ -1,38 +0,0 @@ -#include "renderManager.hpp" -#include "log.hpp" - -CRenderManager::CRenderManager() - : screen(ftxui::ScreenInteractive::TerminalOutput()) { - if (std::atexit([]() { - if (g_pRenderManager) - g_pRenderManager->exitLoop(); - })) - log(ERR, "Failed to register RenderManager atexit handler, will fallback to quick_exit after hang"); - log(LOG, "RenderManager: initialized"); -}; - -CRenderManager::~CRenderManager() { - exitLoop(); - log(SYS, "RenderManager: bye"); -} - -void CRenderManager::exitLoop() { - if (renderer) { - try { - screen.ExitLoopClosure()(); - screen.Exit(); - } catch (const std::exception &e) { - log(WARN, "Exception in exitLoop: {}", e.what()); - } catch (...) { - log(WARN, "Unknown exception in exitLoop"); - } - } -} - -void CRenderManager::enterLoop() { - screen.Loop(renderer); -} - -void CRenderManager::setRenderer(ftxui::Component component) { - renderer = std::move(component); -} diff --git a/src/renderManager.hpp b/src/renderManager.hpp deleted file mode 100644 index 7a14cc0..0000000 --- a/src/renderManager.hpp +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include -#include - -class CRenderManager { - public: - CRenderManager(); - ~CRenderManager(); - - void enterLoop(); - - void setRenderer(ftxui::Component component); - - private: - void exitLoop(); - - ftxui::ScreenInteractive screen; - ftxui::Component renderer; - - friend class CInputManager; -}; -inline std::unique_ptr g_pRenderManager; diff --git a/src/scroller.cpp b/src/scroller.cpp deleted file mode 100644 index e841401..0000000 --- a/src/scroller.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#include "scroller.hpp" - -#include "log.hpp" -#include // for max, min -#include // for Component, ComponentBase -#include // for Event, Event::ArrowDown, Event::ArrowUp, Event::End, Event::Home, Event::PageDown, Event::PageUp -#include // for shared_ptr, allocator, __shared_ptr_access -#include // for move - -#include "ftxui/component/component.hpp" // for Make -#include "ftxui/component/mouse.hpp" // for Mouse, Mouse::WheelDown, Mouse::WheelUp -#include "ftxui/dom/deprecated.hpp" // for text -#include "ftxui/dom/elements.hpp" // for operator|, Element, size, vbox, EQUAL, HEIGHT, dbox, reflect, focus, inverted, nothing, select, vscroll_indicator, yflex, yframe -#include "ftxui/dom/node.hpp" // for Node -#include "ftxui/dom/requirement.hpp" // for Requirement -#include "ftxui/screen/box.hpp" // for Box - -namespace ftxui { // NOLINT - -class CScrollerBase : public ComponentBase { - public: - CScrollerBase(Component child) { Add(child); } - - private: - Element OnRender() final { // NOLINT - auto focused = Focused() ? focus : ftxui::select; - auto style = Focused() ? inverted : nothing; - - Element background = ComponentBase::Render(); - background->ComputeRequirement(); - size_ = background->requirement().min_y; - return dbox({ - std::move(background), - vbox({ - text(L"") | size(HEIGHT, EQUAL, selected_), - text(L"") | style | focused, - }), - }) | - vscroll_indicator | yframe | yflex | reflect(box_); - } - - bool OnEvent(Event event) final { // NOLINT - if (event.is_mouse() && box_.Contain(event.mouse().x, event.mouse().y)) - TakeFocus(); - - int selected_old = selected_; - if (event == Event::ArrowUp || event == Event::Character('k') || - (event.is_mouse() && event.mouse().button == Mouse::WheelUp)) - selected_--; - if ((event == Event::ArrowDown || event == Event::Character('j') || - (event.is_mouse() && event.mouse().button == Mouse::WheelDown))) - selected_++; - if (event == Event::PageDown) - selected_ += box_.y_max - box_.y_min; - if (event == Event::PageUp) - selected_ -= box_.y_max - box_.y_min; - if (event == Event::Home) - selected_ = 0; - if (event == Event::End) - selected_ = size_; - - if (event == Event::Custom && sticky_select) - selected_ = size_; - - selected_ = std::max(0, std::min(size_, selected_)); - sticky_select = selected_ >= size_ - 15; - return selected_old != selected_; - } - - bool Focusable() const final { return true; } // NOLINT - - bool sticky_select = true; - int selected_ = 0; - int size_ = 0; - Box box_; -}; - -Component Scroller(Component child) { // NOLINT - return Make(std::move(child)); -} -} // namespace ftxui - -// Copyright 2021 Arthur Sonzogni. All rights reserved. -// Use of this source code is governed by the MIT license that can be found in -// the LICENSE file. diff --git a/src/scroller.hpp b/src/scroller.hpp deleted file mode 100644 index b8b39b1..0000000 --- a/src/scroller.hpp +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef SCROLLER_H -#define SCROLLER_H - -#include - -#include "ftxui/component/component_base.hpp" // for Component - -namespace ftxui { // NOLINT -Component Scroller(Component child); // NOLINT -} -#endif /* end of include guard: SCROLLER_H */ - -// Copyright 2021 Arthur Sonzogni. All rights reserved. -// Use of this source code is governed by the MIT license that can be found in -// the LICENSE file. diff --git a/src/server.cpp b/src/server.cpp deleted file mode 100644 index db92481..0000000 --- a/src/server.cpp +++ /dev/null @@ -1,39 +0,0 @@ -#include "server.hpp" -#include "log.hpp" -#include -#include -#include - -CServer::CServer(uint16_t port) - : m_sockfd(std::make_shared(socket(AF_INET, SOCK_STREAM, 0))) { - if (!m_sockfd->isValid()) - throw std::runtime_error("Failed to create socket"); - - constexpr int opt = 1; - constexpr int size = sizeof(opt); - if (setsockopt(m_sockfd->get(), SOL_SOCKET, SO_REUSEADDR, &opt, size) < 0) - log(stderr, WARN, "setsockopt(SO_REUSEADDR) failed"); - - if (setsockopt(m_sockfd->get(), SOL_SOCKET, SO_KEEPALIVE, &opt, size) < 0) - log(stderr, WARN, "setsockopt(SO_KEEPALIVE) failed"); - - memset(&m_addr, 0, sizeof(m_addr)); - m_addr.sin_family = AF_INET; - m_addr.sin_port = htons(port); - m_addr.sin_addr.s_addr = INADDR_ANY; - bind(m_sockfd->get(), reinterpret_cast(&m_addr), sizeof(m_addr)) || - listen(m_sockfd->get(), 5) - ? throw std::runtime_error("Failed to bind or listen") - : log(LOG, "Server started on port: {}", port); - - log(LOG, "Server: initialized"); -}; - -CServer::~CServer() { - m_sockfd.reset(); - log(SYS, "Server: bye"); -} - -std::shared_ptr CServer::getSocket() const { - return m_sockfd; -} diff --git a/src/server.hpp b/src/server.hpp deleted file mode 100644 index ce375de..0000000 --- a/src/server.hpp +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "FileDescriptor.hpp" -#include -#include - -class CServer { - public: - CServer(uint16_t port); - ~CServer(); - std::shared_ptr getSocket() const; - - private: - std::shared_ptr m_sockfd; - sockaddr_in m_addr; - - mutable std::mutex m_mutex; -}; -inline std::unique_ptr g_pServer; diff --git a/src/session.cpp b/src/session.cpp deleted file mode 100644 index 126d547..0000000 --- a/src/session.cpp +++ /dev/null @@ -1,273 +0,0 @@ -#include "session.hpp" -#include "chatManager.hpp" -#include "commandHandler.hpp" -#include "format.hpp" -#include "log.hpp" -#include "server.hpp" -#include "sessionManager.hpp" -#include -#include -#include -#include -#include -#include -#include - -CSession::CSession() { - m_sockfd = std::make_shared(accept(g_pServer->getSocket()->get(), reinterpret_cast(&m_addr), &m_addrLen)); // if this is in the init list, it will run before m_addrLen, so it won't work :/ - - if (!m_sockfd->isValid()) - throw std::runtime_error("session: Failed to create socket"); - - m_ip.resize(INET_ADDRSTRLEN); - inet_ntop(AF_INET, &m_addr.sin_addr, &m_ip[0], INET_ADDRSTRLEN); - m_ip.resize(strlen(m_ip.c_str())); - m_port = ntohs(m_addr.sin_port); - m_isAdmin = std::ranges::any_of(m_adminIps, [this](const char *ip) { return m_ip == ip; }); - - log(LOG, "Client {} connected on port {}", m_ip, m_port); - - log(TRACE, "session: initialized"); -} - -CSession::~CSession() { -#ifdef DEBUG - onDisconnect(); -#endif - m_sockfd.reset(); - - log(LOG, "Client {}@{} disconnected", m_name, m_ip); - - log(TRACE, "session({}@{}): bye", m_name, m_ip); -} - -#ifdef DEBUG -void CSession::onConnect() { - log(TRACE, "Client {}@{} connected", m_name, m_ip); -} - -void CSession::onDisconnect() { - log(TRACE, "Client {}@{} disconnected", m_name, m_ip); -} -#endif - -void CSession::onErrno(eEventType eventType) { - char errbuf[256]; - const char *err = strerror_r(errno, errbuf, sizeof(errbuf)); - - switch (eventType) { - case READ: - log(WARN, "Error while reading: {}", err); - break; - case WRITE: - log(WARN, "Error while writing: {}", err); - break; - } -} - -#ifdef DEBUG -void CSession::onRecv(const SRecvData &data) { - log(TRACE, "Received: {}", data.data); -} - -void CSession::onSend(const std::string &msg) { - log(LOG, "Sent: {}", msg); -} -#endif - -void CSession::recvLoop() { - while (true) { - auto recvData = read(NFormatter::fmt(NONEWLINE, "{}: ", m_name)); - if (!recvData->good) - break; - - recvData->sanitize(); - if (recvData->isEmpty()) { - log(TRACE, "Empty message from {}", m_name); - continue; - } - - const auto isCommand = g_pCommandHandler->isCommand(recvData->data); - if (isCommand) - g_pCommandHandler->handleCommand(recvData->data, self.lock()); - - g_pChatManager->newMessage({.msg = recvData->data, .username = m_name, .sender = self, .admin = isCommand}); - } -} - -bool CSession::SRecvData::isEmpty() const { - return data.empty() || std::ranges::all_of(data, [](char c) { return std::isspace(c); }); -} - -void CSession::SRecvData::sanitize() { - if (!data.empty() && data.back() == '\n') - data.pop_back(); - - data.erase(std::remove(data.begin(), data.end(), asciiEscape), data.end()); // don't accept ASCII code, might mess up terminal // NOLINT - - if (const auto start = data.find_first_not_of(" \t\r\n"); start != std::string::npos) { - const auto end = data.find_last_not_of(" \t\r\n"); - data = data.substr(start, end - start + 1); - } else - data.clear(); -} - -std::unique_ptr CSession::read() { - auto recvData = std::make_unique(); - - recvData->data.resize(recvData->size); - ssize_t size = recv(m_sockfd->get(), &recvData->data[0], recvData->size, 0); - - if (size < 0) { - recvData->good = false; - onErrno(READ); - } else if (size == 0) - recvData->good = false; - else { - recvData->data.resize(size); - recvData->good = true; - } - -#ifdef DEBUG - onRecv(*recvData); -#endif - m_szReading.reset(); - return recvData; -} - -std::unique_ptr CSession::read(const std::string &msg, bool bypassDeaf) { - if (bypassDeaf && m_bDeaf) - m_bDeaf = false; - write(NONEWLINE, "{}", msg); - if (bypassDeaf && !m_bDeaf) - m_bDeaf = true; - m_szReading = msg; - return read(); -} - -void CSession::run() { -#ifdef DEBUG - onConnect(); -#endif - if (registerSession()) { - for (const auto &chat : g_pChatManager->getChat()) - writeChat(chat); - recvLoop(); - } else - log(LOG, "Client {} exited without even registering", m_ip); - - g_pSessionManager->kick(self); -} - -bool CSession::isMuted() const { - return m_bMuted; -} - -void CSession::setMuted(bool mute) { - m_bMuted = mute; -} - -bool CSession::isDeaf() const { - return m_bDeaf; -} - -void CSession::setDeaf(bool deaf) { - m_bDeaf = deaf; -} - -bool CSession::registerSession() { - auto recvData = read("Name: ", true); - if (!recvData->good) - return false; - - recvData->sanitize(); - if (recvData->isEmpty()) { - m_bDeaf = false; - write("Name cannot be empty"); - m_bDeaf = true; - return registerSession(); - } - - if (g_pSessionManager->nameExists(recvData->data)) { - m_bDeaf = false; - write("Name already exists"); - m_bDeaf = true; - return registerSession(); - } - - this->m_name = recvData->data; - - log(LOG, "Client {} registered as: {}", m_ip, m_name); - - g_pSessionManager->broadcast(NFormatter::fmt(NONEWLINE, "{} has joined the chat", m_name), self); - - m_bDeaf = false; - return true; -} - -bool CSession::isValid() { - if (!m_sockfd->isValid() || m_name.empty()) - return false; - - int err = 0; - socklen_t size = sizeof(err); - return getsockopt(m_sockfd->get(), SOL_SOCKET, SO_ERROR, &err, &size) >= 0 && err == 0; -} - -bool CSession::write(const std::string &msg) { - return write("{}", msg); -} - -bool CSession::writeChat(const CChatManager::SMessage &msg) { - const auto sender = msg.sender ? msg.sender->lock() : nullptr; - if (sender ? sender != self.lock() : msg.username != m_name) { - if (msg.admin && !m_isAdmin) - return false; - - return write(g_pChatManager->fmtBroadcastMessage(msg)); - } - return false; -} - -const std::string &CSession::getName() const { - return m_name; -} - -const std::string &CSession::getIp() const { - return m_ip; -} - -bool CSession::isAdmin() const { - return m_isAdmin; -} - -template -bool CSession::write(std::format_string fmt, Args &&...args) { - if (m_bDeaf) - return false; - return write(NONE, fmt, std::forward(args)...); -} - -template -bool CSession::write(eFormatType type, std::format_string fmt, Args &&...args) { - if (m_bDeaf) - return false; - - std::string msg = NFormatter::fmt(type, fmt, std::forward(args)...); - if (m_szReading) { - msg.insert(0, "\r"); - msg.append(*m_szReading); - } - - if (send(m_sockfd->get(), msg.c_str(), msg.size(), 0) < 0) { - onErrno(WRITE); - return false; - } -#ifdef DEBUG - onSend(msg); -#endif - return true; -} - -template bool CSession::write(std::format_string, const std::string &); -template bool CSession::write(eFormatType, std::format_string, const std::string &); diff --git a/src/session.hpp b/src/session.hpp deleted file mode 100644 index 2ca274e..0000000 --- a/src/session.hpp +++ /dev/null @@ -1,88 +0,0 @@ -#pragma once - -#include "FileDescriptor.hpp" -#include "chatManager.hpp" -#include "format.hpp" -#include -#include -#include -#include -#include -#include -#include - -class CSession { - public: - CSession(); - ~CSession(); - - struct SRecvData { - std::string data; - const size_t size = 1024; - bool good = false; - - bool isEmpty() const; - void sanitize(); - - static constexpr char asciiEscape = 0x1B; //  - }; - - const std::string &getName() const; - const std::string &getIp() const; - bool isAdmin() const; - - std::unique_ptr read(); - std::unique_ptr read(const std::string &msg, bool bypassDeaf = false); - bool write(const std::string &msg); - template - bool write(std::format_string fmt, Args &&...args); - template - bool write(eFormatType type, std::format_string fmt, Args &&...args); - bool writeChat(const CChatManager::SMessage &msg); - - bool isMuted() const; - void setMuted(bool mute); - bool isDeaf() const; - void setDeaf(bool deaf); - - void run(); - - bool isValid(); - - private: - constexpr static char m_adminIps[1][INET_ADDRSTRLEN] = {"127.0.0.1"}; - - enum eEventType : std::uint8_t { - READ, - WRITE - }; - - std::weak_ptr self; - std::shared_ptr m_sockfd; - sockaddr_in m_addr; - socklen_t m_addrLen = sizeof(m_addr); - std::string m_name; - std::string m_ip; - int m_port; - bool m_isAdmin = false; - - std::optional m_szReading = std::nullopt; - bool m_bMuted = false; - bool m_bDeaf = true; // init as true as we don't want anything during registerSession - - void recvLoop(); - bool registerSession(); - -#ifdef DEBUG - void onConnect(); - void onDisconnect(); -#endif - void onErrno(eEventType); -#ifdef DEBUG - void onRecv(const SRecvData &data); - void onSend(const std::string &msg); -#endif - - friend class CSessionManager; - friend void registerCommands(); -}; diff --git a/src/sessionManager.cpp b/src/sessionManager.cpp deleted file mode 100644 index 7ed1a8e..0000000 --- a/src/sessionManager.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include "sessionManager.hpp" -#include "IOManager.hpp" -#include "chatServer.hpp" -#include "format.hpp" -#include "log.hpp" -#include "session.hpp" -#include -#include -#include -#include - -void CSessionManager::shutdownSessions() { - if (m_vSessions.empty()) - return; - - for (auto &[thread, session] : m_vSessions) { - if (session) { - session->setDeaf(false); - session->write("Server shutting down"); - if (!session->getName().empty()) - g_pSessionManager->broadcast(NFormatter::fmt(NONEWLINE, "{} has left the chat", session->getName()), std::weak_ptr(session)); - } - } - - for (auto &[thread, session] : m_vSessions) { - if (thread.joinable()) - thread.detach(); - session.reset(); - } - - m_vSessions.clear(); -} - -CSessionManager::CSessionManager() { - if (std::atexit([]() { - if (g_pSessionManager) - g_pSessionManager->shutdownSessions(); - })) - log(ERR, "Failed to register SessionManager atexit handler, will fallback to quick_exit after hang"); - - log(LOG, "SessionManager: initialized"); -}; - -CSessionManager::~CSessionManager() { - for (auto &[thread, session] : m_vSessions) { - if (thread.joinable()) - thread.detach(); - kick(std::weak_ptr(session)); - } - - log(SYS, "SessionManager: bye"); -} - -void CSessionManager::newSession() { - std::shared_ptr session = std::make_shared(); - auto &instance = m_vSessions.emplace_back(std::jthread([session]() { session->run(); }), session); - instance.second->self = std::weak_ptr(instance.second); -} - -void CSessionManager::run() { - while (true) - newSession(); -} - -void CSessionManager::broadcast(const std::string &msg, std::optional> self) const { - for (const auto &[thread, session] : m_vSessions) - if (!self || (self && session != self->lock())) - session->write(msg); - - g_pIOManager->addCustom({"", msg}); -} - -void CSessionManager::broadcast(const CChatManager::SMessage &msg) const { - for (const auto &[thread, session] : m_vSessions) { - if (!session) - continue; - - session->writeChat(msg); - } - - g_pIOManager->addMessage(msg); -} - -bool CSessionManager::nameExists(const std::string &name) { - auto it = std::ranges::find_if(m_vSessions, [&name](const auto &s) { return s.second->getName() == name; }); - - if (it != m_vSessions.end()) { - if (!it->second->isValid()) { - kick(std::weak_ptr(it->second), true, "Connection lost"); - return false; - } - return true; - } - return false; -} - -std::shared_ptr CSessionManager::getByName(const std::string &name) const { - auto it = std::ranges::find_if(m_vSessions, [&name](const auto &s) { return s.second->getName() == name; }); - return it != m_vSessions.end() ? it->second : nullptr; -} - -std::shared_ptr CSessionManager::getByIp(const std::string &ip) const { - auto it = std::ranges::find_if(m_vSessions, [&ip](const auto &s) { log(LOG, s.second->getIp()); return s.second->getIp() == ip; }); - return it != m_vSessions.end() ? it->second : nullptr; -} - -std::vector> CSessionManager::getSessions() const { - return m_vSessions | std::views::transform([](const auto &s) { return s.second; }) | std::ranges::to(); -} - -void CSessionManager::kick(std::weak_ptr session_weak, const bool kill, const std::string &reason) { - auto session = session_weak.lock(); - if (!session) - return; - - for (auto it = m_vSessions.begin(); it != m_vSessions.end(); ++it) { - if (it->second == session) { - if (session) - // this only exist when the session is registered - // not sure why the second is null, but m_name definately is since it's setted during register - // but we don't need to notify people if the session didn't even "join"/register anyways - g_pSessionManager->broadcast(NFormatter::fmt(NONEWLINE, "{} has left the chat", session->getName()), session_weak); - - if (!reason.empty()) { - session->setDeaf(false); - session->write(reason); - } - const auto native_handle = it->first.native_handle(); - if (it->first.joinable()) - it->first.detach(); // .detach the thread since it's removing itself - it->second.reset(); - m_vSessions.erase(it); - if (kill && native_handle != 0) - // cancel(terminal/kill) the thread if it doesn't exit on its own - // pthread_cancel cuz it's the safest https://stackoverflow.com/a/3438576 - // need a condition because it might crash if the thread is already dead - pthread_cancel(native_handle); - return; - } - } -} diff --git a/src/sessionManager.hpp b/src/sessionManager.hpp deleted file mode 100644 index 179d7d8..0000000 --- a/src/sessionManager.hpp +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include "chatManager.hpp" -#include "format.hpp" -#include "session.hpp" -#include -#include -#include - -class CSessionManager { - public: - CSessionManager(); - ~CSessionManager(); - - void newSession(); - - void run(); - void broadcast(const std::string &msg, std::optional> self = std::nullopt) const; // second param only specified when we want to exclude the sender - void kick(std::weak_ptr session, const bool kill = false, const std::string &reason = ""); - void addSession(const CSession &session); - - bool nameExists(const std::string &name); - void shutdownSessions(); - - std::shared_ptr getByName(const std::string &name) const; - std::shared_ptr getByIp(const std::string &ip) const; - std::vector> getSessions() const; - - // Safer weak_ptr versions - recommended for most use cases to avoid accidental lifetime extension - std::weak_ptr getByNameWeak(const std::string &name) const; - std::weak_ptr getByIpWeak(const std::string &ip) const; - - private: - void broadcast(const CChatManager::SMessage &msg) const; - - std::vector>> m_vSessions; - - friend class CChatManager; -}; -inline std::shared_ptr g_pSessionManager; -- cgit v1.2.3