diff --git a/Basalaev.Daniil/laba1/.gitignore b/Basalaev.Daniil/laba1/.gitignore new file mode 100644 index 0000000..1af23dd --- /dev/null +++ b/Basalaev.Daniil/laba1/.gitignore @@ -0,0 +1,2 @@ +/build/ +.vscode diff --git a/Basalaev.Daniil/laba1/Demon/CMakeLists.txt b/Basalaev.Daniil/laba1/Demon/CMakeLists.txt new file mode 100644 index 0000000..7ac6ce1 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.5) + +project(Demon) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror") +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +add_subdirectory(src) +add_subdirectory(test) + +add_executable(Demon main.cpp) +target_link_libraries(Demon PRIVATE DemonLib) diff --git a/Basalaev.Daniil/laba1/Demon/config.txt b/Basalaev.Daniil/laba1/Demon/config.txt new file mode 100644 index 0000000..9b3a208 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/config.txt @@ -0,0 +1,3 @@ +Demon/src +Demon/test +30 diff --git a/Basalaev.Daniil/laba1/Demon/main.cpp b/Basalaev.Daniil/laba1/Demon/main.cpp new file mode 100644 index 0000000..2678b84 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/main.cpp @@ -0,0 +1,14 @@ +#include "Demon.hpp" +#include + +int main(int argc, char* argv[]) +{ + if (argc < 2) + { + std::cout << "Failed. No path to config was recieved" << std::endl; + return 1; + } + const char* configPath = argv[1]; + Demon::getInstance().start(configPath); + return 0; +} diff --git a/Basalaev.Daniil/laba1/Demon/src/CMakeLists.txt b/Basalaev.Daniil/laba1/Demon/src/CMakeLists.txt new file mode 100644 index 0000000..aab4217 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/CMakeLists.txt @@ -0,0 +1,5 @@ +set(SRC_LIST Demon.cpp Logger.cpp Reader.cpp) + +add_library(DemonLib STATIC ${SRC_LIST}) + +target_include_directories(DemonLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/Basalaev.Daniil/laba1/Demon/src/Demon.cpp b/Basalaev.Daniil/laba1/Demon/src/Demon.cpp new file mode 100644 index 0000000..f114fb0 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/Demon.cpp @@ -0,0 +1,159 @@ +#include "Demon.hpp" +#include "Logger.hpp" +#include "Reader.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::string const HIST_FILE{"/hist.log"}; + +Demon& Demon::getInstance() +{ + static Demon instance; + return instance; +} + +Demon::Demon() : m_reader(Reader::getInstance()), m_logger(Logger::getInstance()) {} + +void Demon::start(const char* configPath) +{ + m_logger.openLog("Demon"); + + if (isAlreadyRunning()) + { + m_logger.logInfo("Demon is already running. Re-start"); + } + + if (!m_reader.readConfig(configPath)) + { + m_logger.logError("Failed to open config. Destroy"); + unlink(PID_FILE); + m_logger.closeLog(); + exit(EXIT_FAILURE); + } + + demonize(); + handleSignals(); + + m_logger.logInfo("Demon start."); + + run(); +} + +void Demon::demonize() +{ + pid_t pid = fork(); + if (pid < 0) exit(EXIT_FAILURE); + if (pid > 0) exit(EXIT_SUCCESS); + + if (setsid() < 0) exit(EXIT_FAILURE); + + signal(SIGHUP, SIG_IGN); + pid = fork(); + if (pid < 0) exit(EXIT_FAILURE); + if (pid > 0) exit(EXIT_SUCCESS); + + umask(0); + chdir("/"); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (std::ofstream pidFile(PID_FILE); pidFile) + { + pidFile << getpid() << std::endl; + } + else + { + m_logger.logError("Failed to create PID file."); + exit(EXIT_FAILURE); + } +} + +bool Demon::isAlreadyRunning() const +{ + if (std::ifstream pidFile(PID_FILE); pidFile) + { + pid_t pid; + pidFile >> pid; + if (kill(pid, 0) == 0) + { + kill(pid, SIGTERM); + return true; + } + } + return false; +} + +void Demon::handleSignals() +{ + signal(SIGHUP, sighupHandler); + signal(SIGTERM, sigtermHandler); +} + +void Demon::sighupHandler(int) +{ + Logger::getInstance().logInfo("SIGHUP received, re-reading config."); + if (!Reader::getInstance().readConfig()) + { + Logger::getInstance().logError("Failed to open config. Destroy"); + unlink(PID_FILE); + Logger::getInstance().closeLog(); + exit(EXIT_FAILURE); + } +} + +void Demon::sigtermHandler(int) +{ + Logger::getInstance().logInfo("SIGTERM received, terminating."); + unlink(PID_FILE); + Logger::getInstance().closeLog(); + exit(0); +} + +void Demon::run() +{ + while (true) + { + monitor(m_reader.getDir1(), m_reader.getDir2() + HIST_FILE); + sleep(m_reader.getInterval()); + } +} + +void Demon::monitor(std::string const& dirPath, std::string const& logFile) +{ + DIR* dir = opendir(dirPath.c_str()); + if (!dir) + { + m_logger.logError("Failed to open directory: " + dirPath); + return; + } + + std::ofstream log(logFile, std::ios_base::app); + if (!log) + { + m_logger.logError("Failed to open log file: " + logFile); + closedir(dir); + return; + } + + time_t now = time(nullptr); + log << "Directory snapshot at: " << ctime(&now) << std::endl; + + while (auto* entry = readdir(dir)) + { + log << entry->d_name << std::endl; + } + + log << std::endl; + m_logger.logInfo("Folder " + dirPath + " successfully scaned to " + logFile); + closedir(dir); +} diff --git a/Basalaev.Daniil/laba1/Demon/src/Demon.hpp b/Basalaev.Daniil/laba1/Demon/src/Demon.hpp new file mode 100644 index 0000000..526b848 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/Demon.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include + +inline const char* PID_FILE = "/var/run/Demon.pid"; + +class Reader; +class Logger; + +class Demon +{ +public: + static Demon& getInstance(); + void start(const char* configPath); + void monitor(std::string const& dirPath, std::string const& logFile); + bool isAlreadyRunning() const; + +private: + Demon(); + Demon(Demon const&) = delete; + Demon& operator=(Demon const&) = delete; + + void demonize(); + void handleSignals(); + + void run(); + + static void sighupHandler(int signum); + static void sigtermHandler(int signum); + + Reader& m_reader; + Logger& m_logger; +}; diff --git a/Basalaev.Daniil/laba1/Demon/src/Logger.cpp b/Basalaev.Daniil/laba1/Demon/src/Logger.cpp new file mode 100644 index 0000000..d68b18c --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/Logger.cpp @@ -0,0 +1,30 @@ +#include "Logger.hpp" +#include + +Logger& Logger::getInstance() +{ + static Logger instance; + return instance; +} + +Logger::Logger() {} + +void Logger::openLog(std::string const& identifier) const +{ + openlog(identifier.c_str(), LOG_PID, LOG_DAEMON); +} + +void Logger::logInfo(std::string const& message) const +{ + syslog(LOG_INFO, "%s", message.c_str()); +} + +void Logger::logError(std::string const& message) const +{ + syslog(LOG_ERR, "%s", message.c_str()); +} + +void Logger::closeLog() const +{ + closelog(); +} diff --git a/Basalaev.Daniil/laba1/Demon/src/Logger.hpp b/Basalaev.Daniil/laba1/Demon/src/Logger.hpp new file mode 100644 index 0000000..e6f73de --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/Logger.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +class Logger +{ +public: + static Logger& getInstance(); + + void openLog(std::string const& identifier) const; + void logInfo(std::string const& message) const; + void logError(std::string const& message) const; + void closeLog() const; + +private: + Logger(); + Logger(Logger const&) = delete; + Logger& operator=(Logger const&) = delete; +}; diff --git a/Basalaev.Daniil/laba1/Demon/src/Reader.cpp b/Basalaev.Daniil/laba1/Demon/src/Reader.cpp new file mode 100644 index 0000000..a4de764 --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/Reader.cpp @@ -0,0 +1,61 @@ +#include "Reader.hpp" +#include +#include + +std::string getAbsPath(std::string const& path) +{ + char absPath[4096]; + if (realpath(path.c_str(), absPath) == nullptr) { return ""; } + return absPath; +} + +Reader& Reader::getInstance() +{ + static Reader instance; + return instance; +} + +bool Reader::readConfig(std::string const& configPath) +{ + // set config path + if (m_configPath.empty()) + { + m_configPath = getAbsPath(configPath); + if (m_configPath.empty()) { return false; } + } + + // open config + std::ifstream configFile(m_configPath); + if (!configFile) { return false; } + + static auto setDir = [](std::string& dir) + { + static auto currentDir = std::filesystem::current_path(); + + if (auto pathDir1 = std::filesystem::path(dir); pathDir1.is_absolute() && std::filesystem::is_directory(pathDir1)) + { + dir = pathDir1.c_str(); + } + else if (auto fullPathDir1 = currentDir / pathDir1; std::filesystem::is_directory(fullPathDir1)) + { + dir = fullPathDir1.c_str(); + } + else + { + return false; + } + return true; + }; + + // set dirs + std::getline(configFile, m_dir1); + if (!setDir(m_dir1)) { return false; } + + std::getline(configFile, m_dir2); + if (!setDir(m_dir2)) { return false; } + + configFile >> m_interval; + if (m_interval <= 0) { return false; } + + return true; +} diff --git a/Basalaev.Daniil/laba1/Demon/src/Reader.hpp b/Basalaev.Daniil/laba1/Demon/src/Reader.hpp new file mode 100644 index 0000000..b6a8eed --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/src/Reader.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include + +inline std::string const CONFIG_PATH{"Demon/config.txt"}; + +class Reader +{ +public: + static Reader& getInstance(); + bool readConfig(std::string const& configPath = CONFIG_PATH); + + std::string const& getDir1() const noexcept { return m_dir1; } + std::string const& getDir2() const noexcept { return m_dir2; } + int getInterval() const noexcept { return m_interval; } + +private: + Reader() = default; + Reader(const Reader&) = delete; + Reader& operator=(const Reader&) = delete; + + std::string m_configPath; + std::string m_dir1; + std::string m_dir2; + int m_interval; +}; diff --git a/Basalaev.Daniil/laba1/Demon/test/CMakeLists.txt b/Basalaev.Daniil/laba1/Demon/test/CMakeLists.txt new file mode 100644 index 0000000..a9e98cf --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/test/CMakeLists.txt @@ -0,0 +1,18 @@ +include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.6.0 +) + +FetchContent_MakeAvailable(Catch2) + +set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src) + +set(SRC_LIST_TESTS ut.cpp) + +add_executable(test ${SRC_LIST_TESTS}) + +target_include_directories(test PRIVATE ${SOURCE_DIR}) +target_link_libraries(test PRIVATE Catch2::Catch2WithMain DemonLib) diff --git a/Basalaev.Daniil/laba1/Demon/test/ut.cpp b/Basalaev.Daniil/laba1/Demon/test/ut.cpp new file mode 100644 index 0000000..02d900b --- /dev/null +++ b/Basalaev.Daniil/laba1/Demon/test/ut.cpp @@ -0,0 +1,68 @@ +#include "catch2/catch_all.hpp" +#include "Reader.hpp" +#include "Demon.hpp" +#include +#include +#include +#include + +TEST_CASE("Reader read config") +{ + Reader& reader = Reader::getInstance(); + + SECTION("Rainy") + { + auto result = reader.readConfig("LALALALA"); + REQUIRE(!result); + } + SECTION("Sanity") + { + auto result = reader.readConfig(); + REQUIRE(result); + REQUIRE(reader.getDir1() == std::filesystem::current_path() / "Demon/src"); + REQUIRE(reader.getDir2() == std::filesystem::current_path() / "Demon/test"); + REQUIRE(reader.getInterval() == 30); + } +} + +TEST_CASE("Demon monitor writes logs correctly") +{ + std::string testDir = "/tmp/testDir"; + mkdir(testDir.c_str(), 0777); + + std::ofstream(testDir + "/file1.txt").close(); + std::ofstream(testDir + "/file2.txt").close(); + + static std::string const TMP_LOG_FILE{"/tmp/test_log.log"}; + + Demon& demon = Demon::getInstance(); + demon.monitor(testDir, TMP_LOG_FILE); + + std::ifstream log(TMP_LOG_FILE); + REQUIRE(log.is_open()); + + std::string line; + bool foundFile1 = false; + bool foundFile2 = false; + + while (std::getline(log, line)) + { + if (line.find("file1.txt") != std::string::npos) + { + foundFile1 = true; + } + if (line.find("file2.txt") != std::string::npos) + { + foundFile2 = true; + } + } + + REQUIRE(foundFile1); + REQUIRE(foundFile2); + + // Remove tmp files + unlink((testDir + "/file1.txt").c_str()); + unlink((testDir + "/file2.txt").c_str()); + unlink(TMP_LOG_FILE.c_str()); + rmdir(TMP_LOG_FILE.c_str()); +} diff --git a/Basalaev.Daniil/laba1/README.md b/Basalaev.Daniil/laba1/README.md new file mode 100644 index 0000000..94084a4 --- /dev/null +++ b/Basalaev.Daniil/laba1/README.md @@ -0,0 +1,77 @@ +# Проект: Demon + +## Описание + +Это проект демон-процесса, написанного на C++, который с помощью `fork()` создаёт фоновой процесс. Демон периодически мониторит содержимое одной или двух папок, указанных в конфигурационном файле, и записывает их содержимое в лог-файл `hist.log`. Интервал между действиями также задаётся в конфигурационном файле. + +### Функциональные возможности: +- Демон запоминает абсолютный путь к конфигурационному файлу. +- Поддерживает работу с сигналами: + - По сигналу `SIGHUP` перечитывается конфигурационный файл. + - По сигналу `SIGTERM` демон завершает работу и регистрирует это в системном журнале. +- Демон защищён от повторного запуска с использованием PID-файла. +- Логирование ошибок и информации происходит с помощью системного журнала (syslog). + +## Требования + +- **CMake** (для сборки проекта) +- **GCC** или любой другой компилятор, поддерживающий C++ +- **Catch2** (для юнит-тестов) + +Убедитесь, что у вас установлены все необходимые библиотеки и утилиты для сборки и работы проекта. + +## Сборка + +1. **Клонируйте репозиторий:** + ```bash + git clone https://github.com/11AgReS1SoR11/OS.git +2. **Запустите скрипт сборки:** + ```bash + bash build.sh + +## Конфигурация + +Для настройки демона необходимо создать конфигурационный файл, в котором будут указаны две директории и интервал между действиями (в секундах). + +Пример конфигурационного файла находится в проекте (Demon/config.txt): +```txt +/home/daniil/Desktop/test1 +/home/daniil/Desktop/test2 +30 +``` + +test1 — папка, содержимое которой будет отслеживаться. +test2 — папка, в которой будет создан файл hist.log для записи истории содержимого папки test1. +30 — интервал между действиями (в секундах). +Конфигурационный файл можно разместить в любом месте. При первом запуске демон запоминает его абсолютный путь для дальнейшего использования. + +## Запуск демона + +1. **Запустите демона с использованием скрипта:** + ```bash + bash runDemon +2. **Убедитесь, что демон работает:** + ```bash + bash isDemonRunning + +## Управление демоном + +1. **Остановите демона с помощью сигнала SIGTERM:** + ```bash + bash killDemon +2. **Перечитывание конфигурационного файла:** + ```bash + bash reconfigDemon +3. **Перечитывание конфигурационного файла:** + ```bash + bash demonLogs + +## Тестирование + +Скрипт для запуска юнит-тестов: +```bash +bash runTest +``` + +## Автор +Студент: Басалаев Даниил Александрович 5030102/10201 diff --git a/Basalaev.Daniil/laba1/build.sh b/Basalaev.Daniil/laba1/build.sh new file mode 100644 index 0000000..0cdf49a --- /dev/null +++ b/Basalaev.Daniil/laba1/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +mkdir -p build +cd build +cmake ../Demon +cmake --build . diff --git a/Basalaev.Daniil/laba1/demonLogs b/Basalaev.Daniil/laba1/demonLogs new file mode 100644 index 0000000..fc9a207 --- /dev/null +++ b/Basalaev.Daniil/laba1/demonLogs @@ -0,0 +1,4 @@ +#!/bin/bash +DEMON_NAME="Demon" +echo "Просмотр логов демона $DEMON_NAME:" +grep $DEMON_NAME /var/log/syslog | tail -n 100 diff --git a/Basalaev.Daniil/laba1/isDemonRunning b/Basalaev.Daniil/laba1/isDemonRunning new file mode 100644 index 0000000..a3f8def --- /dev/null +++ b/Basalaev.Daniil/laba1/isDemonRunning @@ -0,0 +1,12 @@ +#!/bin/bash +PID_FILE="/var/run/Demon.pid" +if [ -f "$PID_FILE" ]; then + PID=$(cat "$PID_FILE") + if ps -p $PID > /dev/null 2>&1; then + echo "Daemon is running (PID: $PID)" + else + echo "Daemon is not running, but PID file exists." + fi +else + echo "Daemon is not running (no PID file)." +fi diff --git a/Basalaev.Daniil/laba1/killDemon b/Basalaev.Daniil/laba1/killDemon new file mode 100644 index 0000000..a5928d1 --- /dev/null +++ b/Basalaev.Daniil/laba1/killDemon @@ -0,0 +1,2 @@ +#!/bin/bash +sudo kill -SIGTERM $(cat /var/run/Demon.pid) diff --git a/Basalaev.Daniil/laba1/reconfigDemon b/Basalaev.Daniil/laba1/reconfigDemon new file mode 100644 index 0000000..04782a3 --- /dev/null +++ b/Basalaev.Daniil/laba1/reconfigDemon @@ -0,0 +1,2 @@ +#!/bin/bash +sudo kill -SIGHUP $(cat /var/run/Demon.pid) diff --git a/Basalaev.Daniil/laba1/runDemon b/Basalaev.Daniil/laba1/runDemon new file mode 100644 index 0000000..e3ece40 --- /dev/null +++ b/Basalaev.Daniil/laba1/runDemon @@ -0,0 +1,2 @@ +#!/bin/bash +sudo ./build/Demon ./Demon/config.txt diff --git a/Basalaev.Daniil/laba1/runTest b/Basalaev.Daniil/laba1/runTest new file mode 100644 index 0000000..38d4bd4 --- /dev/null +++ b/Basalaev.Daniil/laba1/runTest @@ -0,0 +1,2 @@ +#!/bin/bash +./build/test/test diff --git a/Basalaev.Daniil/laba2/.gitignore b/Basalaev.Daniil/laba2/.gitignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/Basalaev.Daniil/laba2/.gitignore @@ -0,0 +1 @@ +build diff --git a/Basalaev.Daniil/laba2/Books.json b/Basalaev.Daniil/laba2/Books.json new file mode 100644 index 0000000..0753db3 --- /dev/null +++ b/Basalaev.Daniil/laba2/Books.json @@ -0,0 +1,8 @@ +{ + "books": [ + {"name": "Book 1", "amount": 1}, + {"name": "Book 2", "amount": 2}, + {"name": "Book 3", "amount": 3}, + {"name": "Book 4", "amount": 0} + ] +} diff --git a/Basalaev.Daniil/laba2/Library/CMakeLists.txt b/Basalaev.Daniil/laba2/Library/CMakeLists.txt new file mode 100644 index 0000000..55048e3 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.5) + +project(Library) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror") +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED True) + +find_package(Qt5 COMPONENTS Widgets REQUIRED) + +set(CMAKE_AUTOMOC ON) + +add_subdirectory(src) +add_subdirectory(test) + +add_executable(host_fifo host_fifo.cpp) +target_link_libraries(host_fifo LibraryLib) + +add_executable(host_sock host_sock.cpp) +target_link_libraries(host_sock LibraryLib) + +add_executable(host_pipe host_pipe.cpp) +target_link_libraries(host_pipe LibraryLib) diff --git a/Basalaev.Daniil/laba2/Library/host_fifo.cpp b/Basalaev.Daniil/laba2/Library/host_fifo.cpp new file mode 100644 index 0000000..49f632f --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/host_fifo.cpp @@ -0,0 +1,81 @@ +#include "src/Host/Host.hpp" +#include "src/Client/Client.hpp" +#include "Conn/conn_fifo.hpp" +#include "Common/Logger.hpp" +#include "Common/Reader.hpp" + +#include + +int main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 3) + { + LOG_ERROR("APP", "Usage: ./host_fifo [optional: path_to_file]"); + return EXIT_FAILURE; + } + + int numClients = std::stoi(argv[1]); + + if (numClients <= 0) + { + LOG_ERROR("APP", "Number of clients must be greater than zero"); + return EXIT_FAILURE; + } + + std::string filePath = "Books.json"; + if (argc == 3) + { + filePath = argv[2]; + } + + auto books = Reader::parse(filePath); + if (!books) + { + LOG_ERROR("APP", "Failed to parse JSON file with books"); + return EXIT_FAILURE; + } + + SemaphoreLocal semaphore(numClients + 1); + + std::vector> hostConnections; + for (int i = 0; i < numClients; ++i) + { + auto hostFifoConn = ConnFifo::crateHostFifo("/tmp/my_fifo_" + std::to_string(i)); + if (!hostFifoConn) + { + LOG_ERROR(HOST_LOG, "Failed to initialize fifo by host"); + return EXIT_FAILURE; + } + + hostConnections.push_back(std::move(hostFifoConn)); + } + + std::vector clientsId; + for (int i = 0; i < numClients; ++i) + { + if (pid_t pid = fork(); pid == -1) // Error + { + LOG_ERROR("APP", "Failed to fork"); + return EXIT_FAILURE; + } + else if (pid == 0) // client + { + auto clientFifoConn = ConnFifo::crateClientFifo("/tmp/my_fifo_" + std::to_string(i)); + if (!clientFifoConn) + { + LOG_ERROR(CLIENT_LOG, "Failed to initialize client socket"); + return EXIT_FAILURE; + } + + Client client(getpid(), semaphore, *clientFifoConn, *books); + return client.start(); + } + else // host + { + clientsId.push_back(pid); + } + } + + Host host(semaphore, clientsId, std::move(hostConnections), *books); + return host.start(); +} diff --git a/Basalaev.Daniil/laba2/Library/host_pipe.cpp b/Basalaev.Daniil/laba2/Library/host_pipe.cpp new file mode 100644 index 0000000..d9b25bd --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/host_pipe.cpp @@ -0,0 +1,76 @@ +#include "src/Host/Host.hpp" +#include "src/Client/Client.hpp" +#include "Conn/conn_pipe.hpp" +#include "Common/Logger.hpp" +#include "Common/Reader.hpp" + +#include + +int main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 3) + { + LOG_ERROR("APP", "Usage: ./host_pipe "); + return EXIT_FAILURE; + } + + int numClients = std::stoi(argv[1]); + + if (numClients <= 0) + { + LOG_ERROR("APP", "Number of clients must be greater than zero"); + return EXIT_FAILURE; + } + + std::string filePath = "Books.json"; + if (argc == 3) + { + filePath = argv[2]; + } + + auto books = Reader::parse("Books.json"); + if (!books) + { + LOG_ERROR("APP", "Failed to parse JSON file with books"); + return EXIT_FAILURE; + } + + SemaphoreLocal semaphore(numClients + 1); + + std::vector> hostConnections; + std::vector> clientsConnections; + for (int i = 0; i < numClients; ++i) + { + auto [hostPipeConn, clientPipeConn] = ConnPipe::createPipeConns(); + if (!hostPipeConn && !clientPipeConn) + { + LOG_ERROR(HOST_LOG, "Failed to initialize pipe by host"); + return EXIT_FAILURE; + } + + hostConnections.push_back(std::move(hostPipeConn)); + clientsConnections.push_back(std::move(clientPipeConn)); + } + + std::vector clientsId; + for (int i = 0; i < numClients; ++i) + { + if (pid_t pid = fork(); pid == -1) // Error + { + LOG_ERROR("APP", "Failed to fork"); + return EXIT_FAILURE; + } + else if (pid == 0) // client + { + Client client(getpid(), semaphore, *clientsConnections[i], *books); + return client.start(); + } + else // host + { + clientsId.push_back(pid); + } + } + + Host host(semaphore, clientsId, std::move(hostConnections), *books); + return host.start(); +} diff --git a/Basalaev.Daniil/laba2/Library/host_sock.cpp b/Basalaev.Daniil/laba2/Library/host_sock.cpp new file mode 100644 index 0000000..663322d --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/host_sock.cpp @@ -0,0 +1,90 @@ +#include "src/Host/Host.hpp" +#include "src/Client/Client.hpp" +#include "Conn/conn_sock.hpp" +#include "Common/Logger.hpp" +#include "Common/Reader.hpp" + +#include + +int main(int argc, char* argv[]) +{ + if (argc < 2 || argc > 3) + { + LOG_ERROR("APP", "Usage: ./host_sock "); + return EXIT_FAILURE; + } + + alias::port_t port = std::stoi(argv[1]); + int numClients = std::stoi(argv[2]); + + if (numClients <= 0) + { + LOG_ERROR("APP", "Number of clients must be greater than zero"); + return EXIT_FAILURE; + } + + std::string filePath = "Books.json"; + if (argc == 3) + { + filePath = argv[2]; + } + + auto books = Reader::parse("Books.json"); + if (!books) + { + LOG_ERROR("APP", "Failed to parse JSON file with books"); + return EXIT_FAILURE; + } + + SemaphoreLocal semaphore(numClients + 1); + + auto hostSocketConn = ConnSock::craeteHostSocket(port); + if (!hostSocketConn) + { + LOG_ERROR(HOST_LOG, "Failed to initialize host socket"); + return EXIT_FAILURE; + } + + std::vector> hostConnections; + std::vector clientsId; + + for (int i = 0; i < numClients; ++i) + { + if (pid_t pid = fork(); pid == -1) // Error + { + LOG_ERROR("APP", "Failed to fork"); + return EXIT_FAILURE; + } + else if (pid == 0) // client + { + auto clientSocketConn = ConnSock::craeteClientSocket(port); + if (!clientSocketConn) + { + LOG_ERROR(CLIENT_LOG, "Failed to initialize client socket"); + return EXIT_FAILURE; + } + + Client client(getpid(), semaphore, *clientSocketConn, *books); + return client.start(); + } + else // host + { + clientsId.push_back(pid); + } + } + + for (int i = 0; i < numClients; ++i) + { + auto hostSocketConnAccepted = hostSocketConn->accept(); + if (!hostSocketConnAccepted) + { + LOG_ERROR(HOST_LOG, "Failed to accept connection"); + return EXIT_FAILURE; + } + + hostConnections.push_back(std::move(hostSocketConnAccepted)); + } + + Host host(semaphore, clientsId, std::move(hostConnections), *books); + return host.start(); +} diff --git a/Basalaev.Daniil/laba2/Library/src/CMakeLists.txt b/Basalaev.Daniil/laba2/Library/src/CMakeLists.txt new file mode 100644 index 0000000..da9f9d6 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/CMakeLists.txt @@ -0,0 +1,20 @@ + +add_library(LibraryLib + Client/Client.cpp + Client/ClientWindow.cpp + Common/Http.cpp + Common/LibraryWindowImpl.cpp + Common/Logger.cpp + Common/Reader.cpp + Common/SemaphoreLocal.cpp + Common/Utils.cpp + Conn/conn_fifo.cpp + Conn/conn_sock.cpp + Conn/conn_pipe.cpp + Host/Host.cpp + Host/HostWindow.cpp +) + +target_link_libraries(LibraryLib Qt5::Widgets) + +target_include_directories(LibraryLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) diff --git a/Basalaev.Daniil/laba2/Library/src/Client/Client.cpp b/Basalaev.Daniil/laba2/Library/src/Client/Client.cpp new file mode 100644 index 0000000..f1a36d3 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Client/Client.cpp @@ -0,0 +1,130 @@ +#include "Client.hpp" +#include "ClientWindow.hpp" +#include "Common/Logger.hpp" + +#include +#include +#include + +Client::Client(alias::id_t id, SemaphoreLocal& semaphore, connImpl& connection, alias::book_container_t const& books, QObject* parent) + : QObject(parent) + , m_id(id) + , m_semaphore(semaphore) + , m_connection(connection) + , m_books{books} +{} + +Client::~Client() = default; + +int Client::start() +{ + LOG_INFO(CLIENT_LOG, "[ID=" + std::to_string(getId()) + "] successfully start"); + + std::thread listener(&Client::listen, this); // start listen responses from host + + int argc = 0; + QApplication app(argc, nullptr); + m_window = std::unique_ptr(new ClientWindow(getId(), m_books)); + + QObject::connect(m_window.get(), &ClientWindow::bookSelected, this, &Client::handleBookSelected); + QObject::connect(m_window.get(), &ClientWindow::bookReturned, this, &Client::handleBookReturned); + + m_window->show(); + int res = app.exec(); + + m_isRunning = false; + m_connection.close(); + if (listener.joinable()) + { + listener.join(); + } + + return res; +} + +void Client::processHostMsg() +{ + char buffer[alias::MAX_MSG_SIZE] = {0}; + if (!m_connection.read(buffer)) return; + + // Check for notifications + if (auto notify = http::notification::parse(std::string(buffer))) + { + LOG_INFO(CLIENT_LOG, "[ID=" + std::to_string(getId()) + "] successfully read msg from host: " + notify->toString()); + m_books = std::move(notify->books); + m_window->updateBooks(m_books); + return; + } + + if (auto rsp = http::response::parse(std::string(buffer))) + { + LOG_INFO(CLIENT_LOG, "[ID=" + std::to_string(getId()) + "] successfully read msg from host: " + rsp->toString()); + + if (rsp->id != getId()) + { + LOG_ERROR(CLIENT_LOG, "[ID=" + std::to_string(getId()) + "] vs id = " + std::to_string(rsp->id) + " from host's msg"); + return; + } + + std::string const bookName = m_window->getCurrentBook(); + if (m_lastOpeartion == http::OperationType_e::POST) + { + rsp->status == http::OperationStatus_e::OK ? m_window->onSuccessTakeBook(bookName, getId()) : m_window->onFailedTakeBook(bookName, getId()); + } + else + { + rsp->status == http::OperationStatus_e::OK ? m_window->onSuccessReturnBook(bookName, getId()) : m_window->onFailedReturnBook(bookName, getId()); + } + } + else + { + LOG_ERROR(CLIENT_LOG, "[ID=" + std::to_string(getId()) + "] read, but failed to parse msg from host"); + } +} + +void Client::listen() +{ + while (m_isRunning) + { + m_semaphore.wait(); + + processHostMsg(); + + m_semaphore.post(); + sleep(0.1); + } +} + +void Client::handleBookSelected(std::string const& bookName, alias::id_t clientId) +{ + std::string const reqStr = http::request{.type = http::OperationType_e::POST, .id = clientId, .bookName = bookName}.toString(); + m_semaphore.wait(); + if (m_connection.write(reqStr.c_str(), reqStr.size())) + { + LOG_INFO(CLIENT_LOG, "[ID=" + std::to_string(clientId) + "] write to host: " + reqStr); + m_lastOpeartion = http::OperationType_e::POST; + } + else + { + LOG_ERROR(CLIENT_LOG, "[ID=" + std::to_string(clientId) + "] failed to write to host: " + reqStr); + m_window->onFailedTakeBook(bookName, clientId); + } + m_semaphore.post(); +} + +void Client::handleBookReturned(std::string const& bookName, alias::id_t clientId) +{ + std::string const reqStr = http::request{.type = http::OperationType_e::PUT, .id = clientId, .bookName = bookName}.toString(); + m_semaphore.wait(); + if (m_connection.write(reqStr.c_str(), reqStr.size())) + { + LOG_INFO(CLIENT_LOG, "[ID=" + std::to_string(clientId) + "] write to host: " + reqStr); + m_lastOpeartion = http::OperationType_e::PUT; + } + else + { + LOG_ERROR(CLIENT_LOG, "[ID=" + std::to_string(clientId) + "] failed to write to host: " + reqStr); + m_window->onFailedReturnBook(bookName, clientId); + } + m_semaphore.post(); +} diff --git a/Basalaev.Daniil/laba2/Library/src/Client/Client.hpp b/Basalaev.Daniil/laba2/Library/src/Client/Client.hpp new file mode 100644 index 0000000..91aec3a --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Client/Client.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "ClientWindow.hpp" +#include "Common/Alias.hpp" +#include "Common/Book.hpp" +#include "Common/Http.hpp" +#include "Common/SemaphoreLocal.hpp" +#include "Conn/conn_impl.hpp" + +#include + +class Client : public QObject +{ + Q_OBJECT + +public: + Client(alias::id_t, SemaphoreLocal&, connImpl&, alias::book_container_t const&, QObject* parent = nullptr); + ~Client(); + + int start(); + void listen(); + + alias::id_t getId() const noexcept { return m_id; } + +private slots: + void handleBookSelected(std::string const& bookName, alias::id_t clientId); + void handleBookReturned(std::string const& bookName, alias::id_t clientId); + +private: + void processHostMsg(); + + std::unique_ptr m_window{nullptr}; + std::atomic m_isRunning{true}; + http::OperationType_e m_lastOpeartion{}; + alias::id_t m_id; + SemaphoreLocal& m_semaphore; + connImpl& m_connection; + alias::book_container_t m_books; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Client/ClientWindow.cpp b/Basalaev.Daniil/laba2/Library/src/Client/ClientWindow.cpp new file mode 100644 index 0000000..30be7e4 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Client/ClientWindow.cpp @@ -0,0 +1,135 @@ +#include "ClientWindow.hpp" +#include "Common/Logger.hpp" + +#include +#include +#include + +ClientWindow::ClientWindow(alias::id_t id, alias::book_container_t const& books, QWidget* parent) + : LibraryWindowImpl(id, books, parent) +{ + m_stackedWidget = new QStackedWidget(this); + + createBookView(books); + createReadingView(); + + setCentralWidget(m_stackedWidget); + setWindowTitle(QString("Client Window [ID=%1]").arg(id)); + resize(400, 300); + + m_stackedWidget->setCurrentIndex(0); + m_stackedWidget->setStyleSheet( + "QStackedWidget {" + " background-color: #f8f9fa;" + " border: 2px solid #87ceeb;" + " border-radius: 10px;" + " padding: 10px;" + "}" + ); +} + +ClientWindow::~ClientWindow() = default; + +void ClientWindow::createBookView(alias::book_container_t const& books) +{ + QWidget* bookView = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(bookView); + + layout->addWidget(m_bookList); + + m_selectButton = new QPushButton("Select Book", this); + m_selectButton->setStyleSheet( + "QPushButton {" + " background-color: #4caf50;" + " color: white;" + " font-size: 14px;" + " font-weight: bold;" + " border-radius: 5px;" + " padding: 8px;" + "}" + "QPushButton:disabled {" + " background-color: #cccccc;" + " color: #666666;" + "}" + "QPushButton:hover {" + " background-color: #45a049;" + "}" + ); + m_selectButton->setEnabled(false); + layout->addWidget(m_selectButton); + + connect(m_bookList, &QListWidget::itemSelectionChanged, [this]() { + m_selectButton->setEnabled(m_bookList->currentItem() != nullptr); + }); + connect(m_selectButton, &QPushButton::clicked, this, &ClientWindow::selectBook); + + m_stackedWidget->addWidget(bookView); +} + +void ClientWindow::createReadingView() +{ + QWidget* readingView = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(readingView); + + m_readingLabel = new QLabel("Reading book: ", this); + QFont readingFont = m_readingLabel->font(); + readingFont.setPointSize(16); + readingFont.setBold(true); + m_readingLabel->setFont(readingFont); + m_readingLabel->setStyleSheet( + "QLabel {" + " color: #333333;" + "}" + ); + layout->addWidget(m_readingLabel); + + m_stopReadingButton = new QPushButton("Stop Reading", this); + m_stopReadingButton->setStyleSheet( + "QPushButton {" + " background-color: #ff6b6b;" + " color: white;" + " font-size: 14px;" + " font-weight: bold;" + " border-radius: 5px;" + " padding: 8px;" + "}" + "QPushButton:hover {" + " background-color: #ff4c4c;" + "}" + ); + layout->addWidget(m_stopReadingButton); + + connect(m_stopReadingButton, &QPushButton::clicked, this, &ClientWindow::stopReading); + + m_stackedWidget->addWidget(readingView); +} + +void ClientWindow::selectBook() +{ + if (m_bookList->currentItem()) + { + std::string const bookName = getCurrentBook(); + m_readingLabel->setText("Reading book: " + QString::fromStdString(bookName)); + + emit bookSelected(bookName, m_id); + } +} + +void ClientWindow::stopReading() +{ + QString bookName = m_readingLabel->text().split(": ").last(); + emit bookReturned(bookName.toStdString(), m_id); + +} + +void ClientWindow::onSuccessTakeBook(std::string const& bookName, alias::id_t clientId) +{ + m_stackedWidget->setCurrentIndex(1); + LibraryWindowImpl::onSuccessTakeBook(bookName, clientId); +} + +void ClientWindow::onSuccessReturnBook(std::string const& bookName, alias::id_t clientId) +{ + m_stackedWidget->setCurrentIndex(0); + LibraryWindowImpl::onSuccessReturnBook(bookName, clientId); +} diff --git a/Basalaev.Daniil/laba2/Library/src/Client/ClientWindow.hpp b/Basalaev.Daniil/laba2/Library/src/Client/ClientWindow.hpp new file mode 100644 index 0000000..4743ad1 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Client/ClientWindow.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "Common/Book.hpp" +#include "Common/LibraryWindowImpl.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class ClientWindow : public LibraryWindowImpl +{ + Q_OBJECT + +public: + ClientWindow(alias::id_t, alias::book_container_t const&, QWidget* parent = nullptr); + ~ClientWindow() override; + + void onSuccessTakeBook(std::string const& bookName, alias::id_t clientId) override; + void onSuccessReturnBook(std::string const& bookName, alias::id_t clientId) override; + +signals: + void bookSelected(std::string const& bookName, alias::id_t); + void bookReturned(std::string const& bookName, alias::id_t); + +private slots: + void selectBook(); + void stopReading(); + +private: + void createBookView(alias::book_container_t const&); + void createReadingView(); + + QStackedWidget* m_stackedWidget; + QPushButton* m_selectButton; + QLabel* m_readingLabel; + QPushButton* m_stopReadingButton; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Alias.hpp b/Basalaev.Daniil/laba2/Library/src/Common/Alias.hpp new file mode 100644 index 0000000..b345cf9 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Alias.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "Book.hpp" + +#include +#include + +namespace alias +{ + +using id_t = unsigned int; +using port_t = int; +using desriptor_t = int; +using address_t = sockaddr_in; +using book_container_t = std::vector; +using clients_id_container_t = std::vector; + +inline constexpr id_t HOST_ID = 0; +inline constexpr auto MAX_MSG_SIZE = 1024; + +} // namespace alias diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Book.hpp b/Basalaev.Daniil/laba2/Library/src/Common/Book.hpp new file mode 100644 index 0000000..4f26fd3 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Book.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +struct Book +{ + std::string name; + int amount; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Http.cpp b/Basalaev.Daniil/laba2/Library/src/Common/Http.cpp new file mode 100644 index 0000000..050b249 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Http.cpp @@ -0,0 +1,124 @@ +#include "Http.hpp" + +#include +#include +#include +#include + +namespace http +{ + +std::optional request::parse(std::string const& reqMsg) +{ + request req; + // TODO: use regex + // parse foramt + if (reqMsg.substr(0, 7) != "http://") { return {}; } + + // parse operation + uint idxOfStartId = 11; + if (reqMsg.substr(7, 4) == "POST") + { + idxOfStartId = 12; + req.type = OperationType_e::POST; + } + else if (reqMsg.substr(7, 3) == "PUT") + { + req.type = OperationType_e::PUT; + } + else { return {}; } + + // parse id + auto idxOfLastSlash = reqMsg.rfind('/'); + if (idxOfLastSlash == std::string::npos) { return {}; } + req.id = std::stoi(reqMsg.substr(idxOfStartId, idxOfLastSlash - idxOfStartId)); + + // parse book name + req.bookName = reqMsg.substr(idxOfLastSlash+1); + + return {std::move(req)}; +} + +std::string request::toString() const +{ + std::string opType = type == OperationType_e::POST ? "POST" : "PUT"; + std::string idStr = std::to_string(id); + return std::string("http://" + opType + "/" + idStr + "/" + bookName); +} + +std::optional response::parse(std::string const& rspMsg) +{ + response rsp; + // TODO: use regex + // parse foramt + if (rspMsg.substr(0, 12) != "http://head/") { return {}; } + + // parse id + auto idxOfLastSlash = rspMsg.rfind('/'); + if (idxOfLastSlash == std::string::npos || idxOfLastSlash <= 12) { return {}; } + rsp.id = std::stoi(rspMsg.substr(12, idxOfLastSlash - 12)); // TODO: catch ex + + // parse book name + if (auto op = rspMsg.substr(idxOfLastSlash+1); op == "OK") + { + rsp.status = OperationStatus_e::OK; + } + else if (op == "FAIL") + { + rsp.status = OperationStatus_e::FAIL; + } + else { return {}; } + + return {std::move(rsp)}; +} + +std::string response::toString() const +{ + std::string opStatus = status == OperationStatus_e::OK ? "OK" : "FAIL"; + std::string idStr = std::to_string(id); + return std::string("http://head/" + idStr + "/" + opStatus); +} + + +std::optional notification::parse(std::string const& notificationMsg) +{ + notification notify; + + // parse foramt + if (notificationMsg.substr(0, 20) != "http://notification/") { return {}; } + + QJsonDocument jsonDocument = QJsonDocument::fromJson(QString::fromStdString(notificationMsg.substr(20)).toUtf8()); + if (!jsonDocument.isArray()) { return {}; } + + // parse books + QJsonArray jsonArray = jsonDocument.array(); + for (auto const& jsonValue : jsonArray) + { + if (!jsonValue.isObject()) { return {}; } + + QJsonObject jsonBook = jsonValue.toObject(); + if (!jsonBook.contains("name") || !jsonBook.contains("amount")) { return {}; } + + notify.books.emplace_back(Book{.name = jsonBook["name"].toString().toStdString(), .amount = jsonBook["amount"].toInt()}); + } + + return {std::move(notify)}; +} + +std::string notification::toString() const +{ + QJsonArray jsonArray; + + for (auto const& book : books) + { + QJsonObject jsonBook; + jsonBook["name"] = QString::fromStdString(book.name); + jsonBook["amount"] = book.amount; + jsonArray.append(jsonBook); + } + + QJsonDocument jsonDocument(jsonArray); + return "http://notification/" + jsonDocument.toJson(QJsonDocument::Compact).toStdString(); +} + +} // namespace http diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Http.hpp b/Basalaev.Daniil/laba2/Library/src/Common/Http.hpp new file mode 100644 index 0000000..65957a2 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Http.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "Alias.hpp" + +#include +#include + +namespace http +{ + +enum class OperationType_e : bool +{ + POST, // Take book + PUT // Return book +}; + +enum class OperationStatus_e : bool +{ + OK, + FAIL +}; + +struct operation +{ + OperationType_e type; + OperationStatus_e status; +}; + +//! format request: http://{operation_type}/{id}/{bookName} +struct request +{ + static std::optional parse(std::string const& reqMsg); + std::string toString() const; + + OperationType_e type; + alias::id_t id; + std::string bookName; +}; + +//! format response: http://head/{id}/{status} +struct response +{ + static std::optional parse(std::string const& rspMsg); + std::string toString() const; + + alias::id_t id; + OperationStatus_e status; +}; + +//! format notification: http://notification/{books} +struct notification +{ + static std::optional parse(std::string const& notificationMsg); + std::string toString() const; + + alias::book_container_t books; +}; + +} // namespace http diff --git a/Basalaev.Daniil/laba2/Library/src/Common/LibraryWindowImpl.cpp b/Basalaev.Daniil/laba2/Library/src/Common/LibraryWindowImpl.cpp new file mode 100644 index 0000000..7a166db --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/LibraryWindowImpl.cpp @@ -0,0 +1,169 @@ +#include "LibraryWindowImpl.hpp" + +#include +#include + +LibraryWindowImpl::LibraryWindowImpl(alias::id_t id, alias::book_container_t const& books, QWidget* parent) + : QMainWindow(parent) + , m_id(id) +{ + m_bookList = new QListWidget(this); + m_bookList->setFixedWidth(250); + m_bookList->setStyleSheet( + "QListWidget {" + " background-color: #f4f4f4;" + " border: 1px solid #c0c0c0;" + " border-radius: 5px;" + " padding: 5px;" + " font-size: 14px;" + " color: #333333;" + "}" + "QListWidget::item {" + " padding: 5px;" + " border-bottom: 1px solid #d0d0d0;" + "}" + "QListWidget::item:selected {" + " background-color: #87cefa;" + " color: #ffffff;" + "}" + ); + + updateBooks(books); + createHistoryView(); + setWindowFlags(Qt::CustomizeWindowHint); +} + +std::string LibraryWindowImpl::getCurrentBook() const +{ + return m_bookList->currentItem() ? m_bookList->currentItem()->text().split(": ").first().toStdString() : ""; +} + +void LibraryWindowImpl::updateBooks(alias::book_container_t const& books) +{ + std::string const currentBook = getCurrentBook(); + + m_bookList->clear(); + for (auto const& book : books) + { + m_bookList->addItem(QString::fromStdString(book.name) + ": " + QString::number(book.amount)); + } + + for (int i = 0; i < m_bookList->count(); ++i) + { + auto* item = m_bookList->item(i); + std::string itemBookName = item->text().split(": ").first().toStdString(); + if (itemBookName == currentBook) + { + m_bookList->setCurrentItem(item); + break; + } + } +} + +void LibraryWindowImpl::addHistoryEntry(utils::HistoryBookInfo const& booksInfo) +{ + auto* listItem = new QListWidgetItem(booksInfo.toQString()); + + if (booksInfo.op.status == http::OperationStatus_e::OK) + { + listItem->setForeground(QColor(Qt::green)); + } + else + { + listItem->setForeground(QColor(Qt::red)); + } + + m_historyList->addItem(listItem); +} + +void LibraryWindowImpl::createHistoryView() +{ + m_historyList = new QListWidget(this); + m_historyList->setFixedWidth(400); + m_historyList->setStyleSheet( + "QListWidget {" + " background-color: #fff8e1;" + " border: 1px solid #c0c0c0;" + " border-radius: 5px;" + " padding: 5px;" + " font-size: 14px;" + " color: #333333;" + "}" + "QListWidget::item {" + " padding: 5px;" + " border-bottom: 1px solid #d0d0d0;" + "}" + "QListWidget::item:selected {" + " background-color: #ffd54f;" + " color: #ffffff;" + "}" + ); + + auto* historyWidget = new QDockWidget("History", this); + historyWidget->setWidget(m_historyList); + addDockWidget(Qt::LeftDockWidgetArea, historyWidget); +} + +void LibraryWindowImpl::onSuccessTakeBook(std::string const& bookName, alias::id_t clientId) +{ + utils::HistoryBookInfo bookInfo + { + .timeStamp = QDateTime::currentDateTime(), + .clientId = clientId, + .name = bookName, + .op = {.type = http::OperationType_e::POST, .status = http::OperationStatus_e::OK} + }; + + addHistoryEntry(bookInfo); +} + +void LibraryWindowImpl::onFailedTakeBook(std::string const& bookName, alias::id_t clientId) +{ + utils::HistoryBookInfo bookInfo + { + .timeStamp = QDateTime::currentDateTime(), + .clientId = clientId, + .name = bookName, + .op = {.type = http::OperationType_e::POST, .status = http::OperationStatus_e::FAIL} + }; + + addHistoryEntry(bookInfo); +} + +void LibraryWindowImpl::onSuccessReturnBook(std::string const& bookName, alias::id_t clientId) +{ + utils::HistoryBookInfo bookInfo + { + .timeStamp = QDateTime::currentDateTime(), + .clientId = clientId, + .name = bookName, + .op = {.type = http::OperationType_e::PUT, .status = http::OperationStatus_e::OK} + }; + + addHistoryEntry(bookInfo); +} + +void LibraryWindowImpl::onFailedReturnBook(std::string const& bookName, alias::id_t clientId) +{ + utils::HistoryBookInfo bookInfo + { + .timeStamp = QDateTime::currentDateTime(), + .clientId = clientId, + .name = bookName, + .op = {.type = http::OperationType_e::PUT, .status = http::OperationStatus_e::FAIL} + }; + + addHistoryEntry(bookInfo); +} + +void LibraryWindowImpl::closeEvent(QCloseEvent* event) +{ + if (m_letClose) + { + QMainWindow::closeEvent(event); + } + else + { + event->ignore(); + } +} diff --git a/Basalaev.Daniil/laba2/Library/src/Common/LibraryWindowImpl.hpp b/Basalaev.Daniil/laba2/Library/src/Common/LibraryWindowImpl.hpp new file mode 100644 index 0000000..41d74a7 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/LibraryWindowImpl.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "Alias.hpp" +#include "Utils.hpp" + +#include +#include +#include +#include + +class LibraryWindowImpl : public QMainWindow +{ + Q_OBJECT + +public: + LibraryWindowImpl(alias::id_t, alias::book_container_t const&, QWidget* parent = nullptr); + virtual ~LibraryWindowImpl() = default; + + std::string getCurrentBook() const; + + void updateBooks(alias::book_container_t const&); + void addHistoryEntry(utils::HistoryBookInfo const&); + + virtual void onSuccessTakeBook(std::string const& bookName, alias::id_t clientId); + virtual void onFailedTakeBook(std::string const& bookName, alias::id_t clientId); + virtual void onSuccessReturnBook(std::string const& bookName, alias::id_t clientId); + virtual void onFailedReturnBook(std::string const& bookName, alias::id_t clientId); + +protected: + void createHistoryView(); + void closeEvent(QCloseEvent*); + + QListWidget* m_historyList; + QListWidget* m_bookList; + alias::id_t m_id; + bool m_letClose{false}; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Logger.cpp b/Basalaev.Daniil/laba2/Library/src/Common/Logger.cpp new file mode 100644 index 0000000..1d6c835 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Logger.cpp @@ -0,0 +1,20 @@ +#include "Logger.hpp" +#include + +Logger& Logger::getInstance() +{ + static Logger instance; + return instance; +} + +Logger::Logger() {} + +void Logger::logInfo(std::string const& entity, std::string const& message) const +{ + std::cout << "INFO [" << entity << "]: " << message << std::endl; +} + +void Logger::logError(std::string const& entity, std::string const& message) const +{ + std::cout << "ERROR [" << entity << "]: " << message << std::endl; +} diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Logger.hpp b/Basalaev.Daniil/laba2/Library/src/Common/Logger.hpp new file mode 100644 index 0000000..649e8a0 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Logger.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +inline constexpr auto HOST_LOG = "HOST"; +inline constexpr auto CLIENT_LOG = "CLIENT"; + +#define LOG_INFO(entity, message) Logger::getInstance().logInfo(entity, message); +#define LOG_ERROR(entity, message) Logger::getInstance().logError(entity, message); + +class Logger +{ +public: + static Logger& getInstance(); + + void logInfo(std::string const& entity, std::string const& message) const; + void logError(std::string const& entity, std::string const& message) const; + +private: + Logger(); + Logger(Logger const&) = delete; + Logger& operator=(Logger const&) = delete; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Reader.cpp b/Basalaev.Daniil/laba2/Library/src/Common/Reader.cpp new file mode 100644 index 0000000..c538b58 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Reader.cpp @@ -0,0 +1,48 @@ +#include "Reader.hpp" + +#include +#include +#include +#include +#include + +std::optional Reader::parse(std::string const& filePath) +{ + QFile file(QString::fromStdString(filePath)); + + if (!file.open(QIODevice::ReadOnly)) { return {}; } + + QByteArray fileData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(fileData, &parseError); + + if (parseError.error != QJsonParseError::NoError) { return {}; } + + if (!jsonDoc.isObject()) { return {}; } + + QJsonObject rootObj = jsonDoc.object(); + + if (!rootObj.contains("books") || !rootObj["books"].isArray()) { return {}; } + + QJsonArray booksArray = rootObj["books"].toArray(); + alias::book_container_t books; + + for (const QJsonValue& bookValue : booksArray) + { + if (!bookValue.isObject()) { return {}; } + + QJsonObject bookObj = bookValue.toObject(); + + if (!bookObj.contains("name") || !bookObj["name"].isString() || + !bookObj.contains("amount") || !bookObj["amount"].isDouble()) + { + return {}; + } + + books.emplace_back(Book{.name = bookObj["name"].toString().toStdString(), .amount = static_cast(bookObj["amount"].toDouble())}); + } + + return books; +} diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Reader.hpp b/Basalaev.Daniil/laba2/Library/src/Common/Reader.hpp new file mode 100644 index 0000000..213cda7 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Reader.hpp @@ -0,0 +1,11 @@ +#include "Alias.hpp" + +#include + +class Reader +{ +public: + +static std::optional parse(std::string const& filePath); + +}; \ No newline at end of file diff --git a/Basalaev.Daniil/laba2/Library/src/Common/SemaphoreLocal.cpp b/Basalaev.Daniil/laba2/Library/src/Common/SemaphoreLocal.cpp new file mode 100644 index 0000000..bf2d2fd --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/SemaphoreLocal.cpp @@ -0,0 +1,35 @@ +#include "SemaphoreLocal.hpp" +#include "Logger.hpp" + +SemaphoreLocal::SemaphoreLocal(uint amountConnections) +{ + // If PSHARED then share it with other processes + if (sem_init(&semaphore, /*PSHARED*/1, amountConnections) == -1) + { + LOG_ERROR("SemaphoreLocal", "initialization failed"); + } +} + +void SemaphoreLocal::wait() +{ + if (sem_wait(&semaphore) == -1) + { + LOG_ERROR("SemaphoreLocal", "wait failed"); + } +} + +void SemaphoreLocal::post() +{ + if (sem_post(&semaphore) == -1) + { + LOG_ERROR("SemaphoreLocal", "post failed"); + } +} + +SemaphoreLocal::~SemaphoreLocal() +{ + if (sem_destroy(&semaphore) == -1) + { + LOG_ERROR("SemaphoreLocal", "destroy failed"); + } +} diff --git a/Basalaev.Daniil/laba2/Library/src/Common/SemaphoreLocal.hpp b/Basalaev.Daniil/laba2/Library/src/Common/SemaphoreLocal.hpp new file mode 100644 index 0000000..6b9593e --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/SemaphoreLocal.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include + +class SemaphoreLocal +{ +public: + SemaphoreLocal(uint amountConnections); + void wait(); + void post(); + ~SemaphoreLocal(); + +private: + sem_t semaphore; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Utils.cpp b/Basalaev.Daniil/laba2/Library/src/Common/Utils.cpp new file mode 100644 index 0000000..99e8aed --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Utils.cpp @@ -0,0 +1,27 @@ +#include "Utils.hpp" + +namespace utils +{ + +QString HistoryBookInfo::toQString() const +{ + QString timeStr = timeStamp.toString("yyyy-MM-dd HH:mm:ss"); + QString clientIdStr = QString::number(clientId); + QString opType = op.type == http::OperationType_e::POST ? "POST" : "PUT"; + QString status = op.status == http::OperationStatus_e::OK ? "OK" : "FAIL"; + return QString("[%1][ID=%2]: %3/%4 [%5]").arg(timeStr, clientIdStr, opType, QString::fromStdString(name), status); +} + +QString ClientInfo::toQString() const +{ + QString clientIdStr = QString::number(clientId); + if (!readingBook.empty()) + { + return QString("[Client][ID=%1] reading %2").arg(clientIdStr, QString::fromStdString(readingBook)); + } + + QString secondsToKillStr = QString::number(secondsToKill); + return QString("[Client][ID=%1] no read, time to unlink: %2 s").arg(clientIdStr, secondsToKillStr); +} + +} // namespace utils diff --git a/Basalaev.Daniil/laba2/Library/src/Common/Utils.hpp b/Basalaev.Daniil/laba2/Library/src/Common/Utils.hpp new file mode 100644 index 0000000..7028a66 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Common/Utils.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include "Alias.hpp" +#include "Http.hpp" + +#include +#include +#include +#include +#include + +namespace utils +{ +struct HistoryBookInfo +{ + QString toQString() const; + + QDateTime timeStamp; + alias::id_t clientId; + std::string name; + http::operation op; +}; + +struct ClientInfo +{ + QString toQString() const; + + alias::id_t clientId; + std::string readingBook; + int secondsToKill = 5; +}; + +struct ClientInfoWithTimer +{ + ClientInfo info; + std::unique_ptr timer; +}; + +} // namespace utils diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_fifo.cpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_fifo.cpp new file mode 100644 index 0000000..8515b89 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_fifo.cpp @@ -0,0 +1,128 @@ +#include "conn_fifo.hpp" +#include "Common/Logger.hpp" + +#include +#include +#include +#include + +std::unique_ptr ConnFifo::crateHostFifo(std::string const& fifoPath) +{ + ConnFifo* fifo = new ConnFifo(); + fifo->m_isHost = true; + fifo->m_fifoPath = fifoPath; + + if (mkfifo(fifo->m_fifoPath.c_str(), 0666) == -1) + { + if (errno != EEXIST) + { + LOG_ERROR("ConnFifo", "Host failed to create FIFO"); + return nullptr; + } + } + + fifo->m_readFileDisriptor = open(fifo->m_fifoPath.c_str(), O_RDONLY | O_NONBLOCK); + if (fifo->m_readFileDisriptor == -1) + { + LOG_ERROR("ConnFifo", "Host failed to open FIFO for reading"); + return nullptr; + } + + fifo->m_writeFileDisriptor = open(fifo->m_fifoPath.c_str(), O_WRONLY); + if (fifo->m_writeFileDisriptor == -1) + { + LOG_ERROR("ConnFifo", "Host failed to open FIFO for writing"); + fifo->close(); + return nullptr; + } + + return std::unique_ptr(fifo); +} + +std::unique_ptr ConnFifo::crateClientFifo(std::string const& fifoPath) +{ + ConnFifo* fifo = new ConnFifo(); + fifo->m_isHost = false; + fifo->m_fifoPath = fifoPath; + + fifo->m_readFileDisriptor = open(fifo->m_fifoPath.c_str(), O_RDONLY | O_NONBLOCK); + if (fifo->m_readFileDisriptor == -1) + { + LOG_ERROR("ConnFifo", "Client failed to open FIFO for reading"); + return nullptr; + } + + fifo->m_writeFileDisriptor = open(fifo->m_fifoPath.c_str(), O_WRONLY); + if (fifo->m_writeFileDisriptor == -1) + { + LOG_ERROR("ConnFifo", "Client failed to open FIFO for writing"); + fifo->close(); + return nullptr; + } + + return std::unique_ptr(fifo); +} + +bool ConnFifo::isValid() const +{ + return !(m_writeFileDisriptor == -1 || m_readFileDisriptor == -1); +} + +ConnFifo::~ConnFifo() +{ + close(); +} + +void ConnFifo::close() +{ + if (m_readFileDisriptor != -1) + { + ::close(m_readFileDisriptor); + m_readFileDisriptor = -1; + } + + if (m_writeFileDisriptor != -1) + { + ::close(m_writeFileDisriptor); + m_writeFileDisriptor = -1; + } + + if (!m_fifoPath.empty() && m_isHost) + { + unlink(m_fifoPath.c_str()); + } +} + +bool ConnFifo::read(void* buf, size_t maxSize) +{ + if (!isValid()) { return false; } + + char tmpBuf[alias::MAX_MSG_SIZE]; + ssize_t bytesRead = ::read(m_readFileDisriptor, tmpBuf, maxSize); + if (bytesRead == -1) { return false; } + + if ((tmpBuf[0] == '0' && m_isHost) || (tmpBuf[0] == '1' && !m_isHost)) + { + std::memcpy(buf, &tmpBuf[1], --bytesRead); + } + else + { + write(&tmpBuf[1], bytesRead - 1); + return false; + } + + return bytesRead > 0; +} + +bool ConnFifo::write(const void* buf, size_t count) +{ + if (!isValid()) { return false; } + + char tmpBuf[alias::MAX_MSG_SIZE]; + tmpBuf[0] = m_isHost ? '1' : '0'; + std::memcpy(&tmpBuf[1], buf, count); + ssize_t bytesWritten = ::write(m_writeFileDisriptor, tmpBuf, count + 1); + if (bytesWritten == -1) { return false; } + + return static_cast(bytesWritten) == count + 1; +} diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_fifo.hpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_fifo.hpp new file mode 100644 index 0000000..cce6a7d --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_fifo.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "Common/Alias.hpp" +#include "Conn/conn_impl.hpp" + +#include +#include + +class ConnFifo : public connImpl +{ +public: + ~ConnFifo() override; + + static std::unique_ptr crateHostFifo(std::string const& fifoPath); + static std::unique_ptr crateClientFifo(std::string const& fifoPath); + + bool read(void* buf, size_t maxSize = alias::MAX_MSG_SIZE) override; + bool write(const void* buf, size_t count) override; + + void close() override; + + bool isValid() const override; + +private: + + std::string m_fifoPath; + alias::desriptor_t m_readFileDisriptor = -1; + alias::desriptor_t m_writeFileDisriptor = -1; + bool m_isHost; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_impl.hpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_impl.hpp new file mode 100644 index 0000000..bdc41da --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_impl.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Common/Alias.hpp" + +class connImpl +{ +public: + virtual ~connImpl() {}; + + virtual bool read(void* buf, size_t maxSize = alias::MAX_MSG_SIZE) = 0; + virtual bool write(const void* buf, size_t count) = 0; + + virtual void close() = 0; + + virtual bool isValid() const = 0; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_pipe.cpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_pipe.cpp new file mode 100644 index 0000000..5b26af3 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_pipe.cpp @@ -0,0 +1,73 @@ +#include "conn_pipe.hpp" +#include "Common/Logger.hpp" + +#include +#include + +ConnPipe::~ConnPipe() +{ + close(); +} + +void ConnPipe::close() +{ + if (m_readFileDescriptor != -1) + { + ::close(m_readFileDescriptor); + m_readFileDescriptor = -1; + } + + if (m_writeFileDescriptor != -1) + { + ::close(m_writeFileDescriptor); + m_writeFileDescriptor = -1; + } +} + +std::pair, std::unique_ptr> ConnPipe::createPipeConns() +{ + // [0] - read, [1] - write + std::array hostToClient; + std::array clientToHost; + + if (pipe(hostToClient.data()) == -1 || pipe(clientToHost.data()) == -1) + { + LOG_ERROR("PIPE", "Failed to create pipes"); + return std::make_pair, std::unique_ptr>(nullptr, nullptr); + } + + ConnPipe* pipeHost = new ConnPipe(); + pipeHost->m_readFileDescriptor = clientToHost[0]; + pipeHost->m_writeFileDescriptor = hostToClient[1]; + + ConnPipe* pipeClient = new ConnPipe(); + pipeClient->m_readFileDescriptor = hostToClient[0]; + pipeClient->m_writeFileDescriptor = clientToHost[1]; + + return std::make_pair, std::unique_ptr>(std::unique_ptr(pipeHost), std::unique_ptr(pipeClient)); +} + +bool ConnPipe::isValid() const +{ + return !(m_writeFileDescriptor == -1 || m_readFileDescriptor == -1); +} + +bool ConnPipe::read(void* buf, size_t maxSize) +{ + if (!isValid()) { return false; } + + ssize_t bytesRead = ::read(m_readFileDescriptor, buf, maxSize); + if (bytesRead == -1) { return false; } + + return bytesRead > 0; +} + +bool ConnPipe::write(const void* buf, size_t count) +{ + if (!isValid()) { return false; } + + ssize_t bytesWritten = ::write(m_writeFileDescriptor, buf, count); + if (bytesWritten == -1) { return false; } + + return bytesWritten == static_cast(count); +} diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_pipe.hpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_pipe.hpp new file mode 100644 index 0000000..8cc0cff --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_pipe.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "Common/Alias.hpp" +#include "Conn/conn_impl.hpp" + +#include +#include +#include + +class ConnPipe : public connImpl +{ +public: + ~ConnPipe() override; + + static std::pair, std::unique_ptr> createPipeConns(); + + bool read(void* buf, size_t maxSize = alias::MAX_MSG_SIZE) override; + bool write(const void* buf, size_t count) override; + + void close() override; + + bool isValid() const override; + +private: + alias::desriptor_t m_readFileDescriptor = -1; + alias::desriptor_t m_writeFileDescriptor = -1; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_sock.cpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_sock.cpp new file mode 100644 index 0000000..22030ba --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_sock.cpp @@ -0,0 +1,129 @@ +#include "conn_sock.hpp" +#include "Common/Logger.hpp" + +#include +#include +#include +#include +#include +#include + +std::unique_ptr ConnSock::craeteHostSocket(alias::port_t hostPort) +{ + ConnSock* socket = new ConnSock(); + socket->m_socketFileDesriptor = ::socket(AF_INET, SOCK_STREAM, 0); + + if (!socket->isValid()) + { + LOG_ERROR("ConnSock", "Host failed to create socket"); + return nullptr; + } + + socket->m_address.sin_family = AF_INET; + socket->m_address.sin_port = htons(hostPort); + socket->m_address.sin_addr.s_addr = INADDR_ANY; + + if (bind(socket->m_socketFileDesriptor, (struct sockaddr*)&socket->m_address, sizeof(socket->m_address)) == -1) + { + LOG_ERROR("ConnSock", "Host failed to bind socket"); + socket->close(); + socket->m_socketFileDesriptor = -1; + return nullptr; + } + + if (listen(socket->m_socketFileDesriptor, 1) == -1) { + LOG_ERROR("ConnSock", "Host failed to listen socket"); + socket->close(); + socket->m_socketFileDesriptor = -1; + return nullptr; + } + + return std::unique_ptr(socket); +} + +std::unique_ptr ConnSock::craeteClientSocket(alias::port_t hostPort) +{ + ConnSock* socket = new ConnSock(); + socket->m_socketFileDesriptor = ::socket(AF_INET, SOCK_STREAM, 0); + + if (!socket->isValid()) + { + LOG_ERROR("ConnSock", "Client failed to create socket"); + return nullptr; + } + + socket->m_address.sin_family = AF_INET; + socket->m_address.sin_port = htons(hostPort); + + if (inet_pton(AF_INET, "127.0.0.1", &socket->m_address.sin_addr) <= 0) + { + LOG_ERROR("ConnSock", "Client recieve invalid host IP address"); + socket->close(); + socket->m_socketFileDesriptor = -1; + return nullptr; + } + + if (connect(socket->m_socketFileDesriptor, (struct sockaddr*)&socket->m_address, sizeof(socket->m_address)) == -1) + { + LOG_ERROR("ConnSock", "Client failed to connect to host"); + socket->close(); + socket->m_socketFileDesriptor = -1; + return nullptr; + } + + return std::unique_ptr(socket); +} + +std::unique_ptr ConnSock::accept() +{ + alias::address_t clientAddr; + socklen_t clientLen = sizeof(clientAddr); + alias::desriptor_t clientFd = ::accept(m_socketFileDesriptor, (struct sockaddr*)&clientAddr, &clientLen); + if (clientFd == -1) + { + LOG_ERROR("ConnSock", "Host failed to accept connection"); + return nullptr; + } + + ConnSock* socket = new ConnSock(); + socket->m_socketFileDesriptor = clientFd; + socket->m_address = clientAddr; + return std::unique_ptr(socket); +} + +bool ConnSock::read(void* buf, size_t maxSize) +{ + if (!isValid()) { return false; } + + if (recv(m_socketFileDesriptor, buf, maxSize, 0) <= 0) { return false; } + + return true; +} + +bool ConnSock::write(const void* buf, size_t count) +{ + if (!isValid()) { return false; } + + if (send(m_socketFileDesriptor, buf, count, 0) <= 0) { return false; } + + return true; +} + +ConnSock::~ConnSock() +{ + close(); +} + +void ConnSock::close() +{ + if (isValid()) + { + ::close(m_socketFileDesriptor); + m_socketFileDesriptor = -1; + } +} + +bool ConnSock::isValid() const +{ + return m_socketFileDesriptor != -1; +} diff --git a/Basalaev.Daniil/laba2/Library/src/Conn/conn_sock.hpp b/Basalaev.Daniil/laba2/Library/src/Conn/conn_sock.hpp new file mode 100644 index 0000000..6b09174 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Conn/conn_sock.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "Common/Alias.hpp" +#include "Conn/conn_impl.hpp" + +#include + +class ConnSock : public connImpl +{ +public: + ~ConnSock() override; + + static std::unique_ptr craeteHostSocket(alias::port_t hostPort); + static std::unique_ptr craeteClientSocket(alias::port_t hostPort); + + std::unique_ptr accept(); + + bool read(void* buf, size_t maxSize = alias::MAX_MSG_SIZE) override; + bool write(const void* buf, size_t count) override; + + void close() override; + + bool isValid() const override; + +private: + alias::desriptor_t m_socketFileDesriptor = -1; + alias::address_t m_address; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Host/Host.cpp b/Basalaev.Daniil/laba2/Library/src/Host/Host.cpp new file mode 100644 index 0000000..a6db8d8 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Host/Host.cpp @@ -0,0 +1,323 @@ +#include "Host.hpp" +#include "HostWindow.hpp" +#include "Common/Http.hpp" +#include "Common/Logger.hpp" + +#include +#include +#include + +namespace details +{ + +auto findBook(alias::book_container_t& books, std::string const& bookName) +{ + for (auto bookIt = books.begin(); bookIt != books.end(); ++bookIt) + { + if (bookIt->name == bookName) + { + return bookIt; + } + } + + return books.end(); +} + +} // namespace details + +Host::Host(SemaphoreLocal& semaphore, std::vector const& clientId, std::vector> connections, alias::book_container_t const& books, QObject* parent) + : QObject(parent) + , m_semaphore(semaphore) + , m_connections(std::move(connections)) + , m_books{books} +{ + for (auto const id : clientId) + { + m_clients.emplace_back(utils::ClientInfoWithTimer{ + .info = {.clientId = id}, + .timer = nullptr + }); + } + + connect(this, &Host::resetTimer, this, &Host::resetClientTimer); + connect(this, &Host::stopTimer, this, &Host::stopClientTimer); +} + +Host::~Host() +{ + stop(); +} + +void Host::stop() +{ + m_isRunning = false; + + for (auto& connection : m_connections) + { + connection->close(); + } + m_connections.clear(); + + for (auto const& client : m_clients) + { + kill(client.info.clientId, SIGKILL); + } + m_clients.clear(); + + kill(getpid(), SIGKILL); // TODO: close threads + + for (auto& thread : m_listenerThreads) + { + if (thread.joinable()) + { + thread.join(); + } + } + m_listenerThreads.clear(); +} + +int Host::start() +{ + LOG_INFO(HOST_LOG, "successfully start"); + + // start listen messages from clients + for (auto& connection : m_connections) + { + m_listenerThreads.emplace_back(&Host::listen, this, std::ref(*connection)); + } + + int argc = 0; + QApplication app(argc, nullptr); + m_window = std::unique_ptr(new HostWindow("HOST WINDOW", m_books)); + m_window->show(); + m_window->updateClientsInfo(m_clients); + setClientTimers(); + int res = app.exec(); + + stop(); + + return res; +} + +void Host::setClientTimers() +{ + for (auto& clientInfo : m_clients) + { + clientInfo.timer = std::unique_ptr{new QTimer()}; + clientInfo.timer->setInterval(1000); + clientInfo.timer->setSingleShot(false); + // update client information every 1 second + connect(clientInfo.timer.get(), &QTimer::timeout, this, [this, &clientInfo]() { + clientInfo.info.secondsToKill--; + if (clientInfo.info.secondsToKill == 0) + { + removeClient(clientInfo.info.clientId); + m_window->notifyClientTerminated(clientInfo.info.clientId); + } + m_window->updateClientsInfo(m_clients); + }); + + clientInfo.timer->start(); + } +} + +void Host::listen(connImpl& connection) +{ + while (m_isRunning) + { + m_semaphore.wait(); + + char buffer[alias::MAX_MSG_SIZE] = {0}; + if (connection.read(buffer)) + { + if (auto req = http::request::parse(std::string(buffer))) + { + LOG_INFO(HOST_LOG, "successfully read msg from client[ID=" + std::to_string(req->id) + "]: " + req->toString()); + + if (req->type == http::OperationType_e::POST) + { + handleBookSelected(req->bookName, req->id, connection); + } + else + { + emit resetTimer(req->id); + handleBookReturned(req->bookName, req->id, connection); + } + } + else + { + LOG_ERROR(HOST_LOG, "read, but failed to parse msg from client"); + } + } + + m_semaphore.post(); + sleep(0.1); + } +} + +void Host::updateClientInfo(utils::ClientInfo&& clientInfo) +{ + auto it = std::find_if(m_clients.begin(), m_clients.end(), [id = clientInfo.clientId](auto const& client){ return client.info.clientId == id; }); + if (it != m_clients.end()) + { + it->info = clientInfo; + } + else + { + LOG_ERROR(HOST_LOG, "Try update client info, but client[ID=" + std::to_string(clientInfo.clientId) + "] doesn't exist"); + } +} + +void Host::resetClientTimer(alias::id_t clientId) +{ + auto it = std::find_if(m_clients.begin(), m_clients.end(), [clientId](auto const& client) { return client.info.clientId == clientId; }); + if (it != m_clients.end()) + { + it->info.secondsToKill = 5; + it->timer->start(); + } + else + { + LOG_ERROR(HOST_LOG, "Try restart timer, but client[ID=" + std::to_string(clientId) + "] doesn't exist"); + } +} + +void Host::stopClientTimer(alias::id_t clientId) +{ + auto it = std::find_if(m_clients.begin(), m_clients.end(), [clientId](auto const& client) { return client.info.clientId == clientId; }); + if (it != m_clients.end()) + { + it->timer->stop(); + it->info.secondsToKill = 5; + } + else + { + LOG_ERROR(HOST_LOG, "Try stop timer, but client[ID=" + std::to_string(clientId) + "] doesn't exist"); + } +} + +void Host::removeClient(alias::id_t clientId) +{ + LOG_INFO(HOST_LOG, "Removing client[ID=" + std::to_string(clientId) + "]"); + + auto it = std::find_if(m_clients.begin(), m_clients.end(), [clientId](auto const& client) { return client.info.clientId == clientId; }); + if (it != m_clients.end()) + { + it->timer->stop(); + it->info.secondsToKill = 0; + kill(clientId, SIGKILL); + } + else + { + LOG_ERROR(HOST_LOG, "Try remove client, but client[ID=" + std::to_string(clientId) + "] doesn't exist"); + } +} + +void Host::notifyClientsUpdateBookStatus() +{ + http::notification const notification{.books = m_books}; + std::string const notificationStr = notification.toString(); + for (auto& connection : m_connections) + { + // w/o wait semaphore, because of wait yet + if (connection->write(notificationStr.c_str(), notificationStr.size())) + { + LOG_INFO(HOST_LOG, "write to client: " + notificationStr); + } + else + { + LOG_ERROR(HOST_LOG, "failed to write to client: " + notificationStr); + } + } +} + +void Host::handleBookSelected(std::string const& bookName, alias::id_t clientId, connImpl& connection) +{ + http::response rsp{.id = clientId}; + + if (auto book = details::findBook(m_books, bookName); book != m_books.end()) + { + if (book->amount > 0) + { + --book->amount; + rsp.status = http::OperationStatus_e::OK; + } + else + { + rsp.status = http::OperationStatus_e::FAIL; + } + } + else + { + rsp.status = http::OperationStatus_e::FAIL; + } + + std::string const rspStr = rsp.toString(); + if (connection.write(rspStr.c_str(), rspStr.size())) + { + LOG_INFO(HOST_LOG, "write to client: " + rspStr); + + if (rsp.status == http::OperationStatus_e::OK) + { + m_window->onSuccessTakeBook(bookName, clientId); + m_window->updateBooks(m_books); + updateClientInfo({.clientId = clientId, .readingBook = bookName, .secondsToKill = 5}); + emit stopTimer(clientId); + notifyClientsUpdateBookStatus(); + } + else + { + updateClientInfo({.clientId = clientId, .readingBook = "", .secondsToKill = 5}); + emit resetTimer(clientId); + m_window->onFailedTakeBook(bookName, clientId); + } + + m_window->updateClientsInfo(m_clients); + } + else + { + LOG_ERROR(HOST_LOG, "failed to write to client: " + rspStr); + + emit resetTimer(clientId); + m_window->onFailedTakeBook(bookName, clientId); + } +} + +void Host::handleBookReturned(std::string const& bookName, alias::id_t clientId, connImpl& connection) +{ + http::response rsp{.id = clientId}; + + if (auto book = details::findBook(m_books, bookName); book != m_books.end()) + { + ++book->amount; + rsp.status = http::OperationStatus_e::OK; + } + else + { + rsp.status = http::OperationStatus_e::FAIL; + } + + std::string const rspStr = rsp.toString(); + if (connection.write(rspStr.c_str(), rspStr.size())) + { + LOG_INFO(HOST_LOG, "write to client: " + rspStr); + + if (rsp.status == http::OperationStatus_e::OK) + { + m_window->onSuccessReturnBook(bookName, clientId); + m_window->updateBooks(m_books); + updateClientInfo({.clientId = clientId, .readingBook = "", .secondsToKill = 5}); + m_window->updateClientsInfo(m_clients); + notifyClientsUpdateBookStatus(); + } + else + { + m_window->onFailedReturnBook(bookName, clientId); + } + } + else + { + LOG_ERROR(HOST_LOG, "failed to write to client: " + rspStr); + m_window->onFailedReturnBook(bookName, clientId); + } +} diff --git a/Basalaev.Daniil/laba2/Library/src/Host/Host.hpp b/Basalaev.Daniil/laba2/Library/src/Host/Host.hpp new file mode 100644 index 0000000..d270ffd --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Host/Host.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include "Common/Alias.hpp" +#include "Common/Book.hpp" +#include "Common/SemaphoreLocal.hpp" +#include "Conn/conn_impl.hpp" +#include "HostWindow.hpp" + +#include +#include + +class Host : public QObject +{ + Q_OBJECT + +public: + Host(SemaphoreLocal&, std::vector const&, std::vector>, alias::book_container_t const&, QObject* parent = nullptr); + ~Host(); + + int start(); + void listen(connImpl&); + void stop(); + +signals: + void resetTimer(int); + void stopTimer(int); + +private slots: + void handleBookSelected(std::string const& bookName, alias::id_t clientId, connImpl&); + void handleBookReturned(std::string const& bookName, alias::id_t clientId, connImpl&); + +private: + void updateClientInfo(utils::ClientInfo&&); + void setClientTimers(); + void resetClientTimer(alias::id_t); + void stopClientTimer(alias::id_t); + void removeClient(alias::id_t); + void notifyClientsUpdateBookStatus(); + + SemaphoreLocal& m_semaphore; + std::vector> m_connections; + std::unique_ptr m_window{nullptr}; + std::atomic m_isRunning{true}; + alias::book_container_t m_books; + std::vector m_clients; + std::vector m_listenerThreads; +}; diff --git a/Basalaev.Daniil/laba2/Library/src/Host/HostWindow.cpp b/Basalaev.Daniil/laba2/Library/src/Host/HostWindow.cpp new file mode 100644 index 0000000..c4188d0 --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Host/HostWindow.cpp @@ -0,0 +1,131 @@ +#include "HostWindow.hpp" +#include "Common/Logger.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +HostWindow::HostWindow(std::string const& hostTitle, alias::book_container_t const& books, QWidget* parent) + : LibraryWindowImpl(alias::HOST_ID, books, parent) +{ + QWidget* centralWidget = new QWidget(this); + QVBoxLayout* layout = new QVBoxLayout(centralWidget); + + m_hostTitle = new QLabel(QString::fromStdString(hostTitle), this); + QFont titleFont = m_hostTitle->font(); + titleFont.setPointSize(16); + titleFont.setBold(true); + m_hostTitle->setFont(titleFont); + m_hostTitle->setAlignment(Qt::AlignCenter); + layout->addWidget(m_hostTitle); + + layout->addWidget(m_bookList); + + m_hostKillButton = new QPushButton("Terminate Host", this); + m_hostKillButton->setStyleSheet( + "QPushButton {" + " background-color: #ff6b6b;" + " color: white;" + " font-size: 14px;" + " font-weight: bold;" + " border-radius: 8px;" + " padding: 10px;" + " border: 1px solid #ff4c4c;" + "}" + "QPushButton:hover {" + " background-color: #ff4c4c;" + "}" + "QPushButton:pressed {" + " background-color: #ff3b3b;" + "}" + ); + layout->addWidget(m_hostKillButton); + + m_clientList = new QListWidget(this); + m_clientList->setMinimumHeight(200); + m_clientList->setMinimumWidth(400); + m_clientList->setStyleSheet( + "QListWidget {" + " background-color: #f4f4f4;" + " border: 1px solid #c0c0c0;" + " border-radius: 5px;" + " padding: 5px;" + " font-size: 14px;" + " color: #333333;" + "}" + "QListWidget::item {" + " padding: 5px;" + " border-bottom: 1px solid #d0d0d0;" + "}" + "QListWidget::item:selected {" + " background-color: #87cefa;" + " color: #ffffff;" + "}" + ); + QDockWidget* clientDock = new QDockWidget("Clients", this); + clientDock->setWidget(m_clientList); + addDockWidget(Qt::RightDockWidgetArea, clientDock); + + setCentralWidget(centralWidget); + setWindowTitle("Host Window"); + resize(800, 600); + + QPalette palette = centralWidget->palette(); + palette.setColor(QPalette::Window, QColor(240, 240, 240)); + centralWidget->setAutoFillBackground(true); + centralWidget->setPalette(palette); + centralWidget->setStyleSheet( + "QWidget {" + " border: 2px solid #87cefa;" + " border-radius: 10px;" + " background-color: #ffffff;" + "}" + ); + + connect(m_hostKillButton, &QPushButton::clicked, this, &HostWindow::terminateHost); +} + +HostWindow::~HostWindow() = default; + +void HostWindow::updateClientsInfo(std::vector const& clientsInfo) +{ + m_clientList->clear(); + for (auto const& clientInfo : clientsInfo) + { + QString const text = (clientInfo.info.secondsToKill == 0) ? QString("[Client][ID=%1] disconnected").arg(QString::number(clientInfo.info.clientId)) : clientInfo.info.toQString(); + + QListWidgetItem* clientItem = new QListWidgetItem(text, m_clientList); + + if (clientInfo.info.readingBook.empty()) + { + clientItem->setForeground(QColor(Qt::red)); + } + else + { + clientItem->setForeground(QColor(Qt::green)); + } + + m_clientList->addItem(clientItem); + } +} + +void HostWindow::notifyClientTerminated(alias::id_t id) +{ + std::string const msg = "Client[ID=" + std::to_string(id) + "] terminated."; + QMessageBox::information(this, "Terminate Client", QString::fromStdString(msg)); + LOG_INFO(HOST_LOG, msg); +} + +void HostWindow::terminateHost() +{ + QMessageBox::information(this, "Terminate Host", "Host terminated."); + LOG_INFO(HOST_LOG, "Terminate Host"); + m_letClose = true; + close(); + QCoreApplication::quit(); +} diff --git a/Basalaev.Daniil/laba2/Library/src/Host/HostWindow.hpp b/Basalaev.Daniil/laba2/Library/src/Host/HostWindow.hpp new file mode 100644 index 0000000..d90faeb --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/src/Host/HostWindow.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "Common/Book.hpp" +#include "Common/LibraryWindowImpl.hpp" +#include "Common/Utils.hpp" + +#include +#include +#include +#include + +class HostWindow : public LibraryWindowImpl +{ + Q_OBJECT + +public: + HostWindow(std::string const& hostTitle, alias::book_container_t const& books, QWidget* parent = nullptr); + ~HostWindow() override; + + void updateClientsInfo(std::vector const&); + void notifyClientTerminated(alias::id_t); + +signals: + void resetSignalTimer(); + void stopSignalTimer(); + +private slots: + void terminateHost(); + +private: + QListWidget* m_clientList; + QLabel* m_hostTitle; + QPushButton* m_hostKillButton; +}; diff --git a/Basalaev.Daniil/laba2/Library/test/CMakeLists.txt b/Basalaev.Daniil/laba2/Library/test/CMakeLists.txt new file mode 100644 index 0000000..3a7c82d --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/test/CMakeLists.txt @@ -0,0 +1,18 @@ +include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.6.0 +) + +FetchContent_MakeAvailable(Catch2) + +set(SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src) + +set(SRC_LIST_TESTS ut.cpp) + +add_executable(test ${SRC_LIST_TESTS}) + +target_include_directories(test PRIVATE ${SOURCE_DIR}) +target_link_libraries(test PRIVATE Catch2::Catch2WithMain LibraryLib) diff --git a/Basalaev.Daniil/laba2/Library/test/ut.cpp b/Basalaev.Daniil/laba2/Library/test/ut.cpp new file mode 100644 index 0000000..cde863f --- /dev/null +++ b/Basalaev.Daniil/laba2/Library/test/ut.cpp @@ -0,0 +1,340 @@ +#include "catch2/catch_all.hpp" +#include "Common/Alias.hpp" +#include "Common/Http.hpp" +#include "Common/Reader.hpp" +#include "Conn/conn_sock.hpp" +#include "Conn/conn_fifo.hpp" +#include "Conn/conn_pipe.hpp" + +#include +#include +#include +#include +#include + +static constexpr alias::id_t ID = 1; +static constexpr auto MAX_SIZE = 1024; + +TEST_CASE("HTTP request") +{ + static std::string const requestMsg = "http://POST/1/Book 1"; + SECTION("parse request [sunny]") + { + // parse from string + auto req = http::request::parse(requestMsg); + REQUIRE(req); + CHECK(req->type == http::OperationType_e::POST); + CHECK(req->id == ID); + CHECK(req->bookName == "Book 1"); + CHECK(req->toString() == requestMsg); + + // crate + http::request req2{ + .type = http::OperationType_e::POST, + .id = ID, + .bookName = "Book 1" + }; + CHECK(req2.toString() == requestMsg); + } + SECTION("parse request [rainy][invalid protocol prefix]") + { + REQUIRE_FALSE(http::request::parse("https://POST/1/Book 1")); + REQUIRE_FALSE(http::request::parse("HTTP://POST/1/Book 1")); + REQUIRE_FALSE(http::request::parse("__1111http://POST/1/Book 1")); + } + SECTION("parse request [rainy][unsupported operation]") + { + REQUIRE_FALSE(http::request::parse("http://GET/1/Book 1")); + REQUIRE_FALSE(http::request::parse("http://PLEASE/1/Book 1")); + } + SECTION("parse request [rainy][request without id]") + { + REQUIRE_FALSE(http::request::parse("http://GET/Book 1")); + } + SECTION("parse request [rainy][request without bookName]") + { + REQUIRE_FALSE(http::request::parse("http://GET/1")); + } +} + +TEST_CASE("HTTP response") +{ + static std::string const responseMsg = "http://head/0/OK"; + SECTION("parse response [sunny]") + { + // parse from string + auto rsp = http::response::parse(responseMsg); + REQUIRE(rsp); + CHECK(rsp->status == http::OperationStatus_e::OK); + CHECK(rsp->id == alias::HOST_ID); + + // crate + http::response rsp2{ + .id = alias::HOST_ID, + .status = http::OperationStatus_e::OK + }; + CHECK(rsp2.toString() == responseMsg); + } + SECTION("parse response [rainy][invalid protocol prefix]") + { + REQUIRE_FALSE(http::response::parse("https://head/0/OK")); + REQUIRE_FALSE(http::response::parse("HTTP://head/0/OK")); + REQUIRE_FALSE(http::response::parse("__1111http://head/0/OK")); + REQUIRE_FALSE(http::response::parse("http://POST/0/OK")); + REQUIRE_FALSE(http::response::parse("http://POST/1/Book 1")); + } + SECTION("parse response [rainy][unsupported status]") + { + REQUIRE_FALSE(http::response::parse("http://head/0/NO_CONTENT")); + REQUIRE_FALSE(http::response::parse("http://PLEASE/1/404")); + } + SECTION("parse response [rainy][response without id]") + { + REQUIRE_FALSE(http::response::parse("http://head/OK")); + } + SECTION("parse response [rainy][response without stauts]") + { + REQUIRE_FALSE(http::response::parse("http://head/1")); + } +} + +TEST_CASE("HTTP notification") +{ + // TODO + //! format notification: http://notification/{books} + static std::string const responseMsg = "http://notification/[{\"amount\":5,\"name\":\"Book 1\"},{\"amount\":3,\"name\":\"Book 2\"},{\"amount\":0,\"name\":\"Book 3\"}]"; + + Book book1 = {.name = "Book 1", .amount = 5}; + Book book2 = {.name = "Book 2", .amount = 3}; + Book book3 = {.name = "Book 3", .amount = 0}; + SECTION("parse notification [sunny]") + { + // parse from string + auto notify = http::notification::parse(responseMsg); + REQUIRE(notify); + REQUIRE(notify->books.size() == 3); + CHECK(notify->books[0].name == book1.name); + CHECK(notify->books[0].amount == book1.amount); + CHECK(notify->books[1].name == book2.name); + CHECK(notify->books[1].amount == book2.amount); + CHECK(notify->books[2].name == book3.name); + CHECK(notify->books[2].amount == book3.amount); + + // crate + http::notification notify2{.books = {book1, book2, book3} }; + CHECK(notify2.toString() == responseMsg); + } + SECTION("parse notification [rainy][invalid protocol prefix]") + { + REQUIRE_FALSE(http::notification::parse("https://notification/[{\"amount\":5,\"name\":\"Book 1\"}]")); + REQUIRE_FALSE(http::notification::parse("HTTP://notification/[{\"amount\":5,\"name\":\"Book 1\"}]")); + REQUIRE_FALSE(http::notification::parse("__1111https://notification/[{\"amount\":5,\"name\":\"Book 1\"}]")); + REQUIRE_FALSE(http::notification::parse("https://notificationLA/[{\"amount\":5,\"name\":\"Book 1\"}]\"")); + REQUIRE_FALSE(http::notification::parse("https://notification/POST/[{\"amount\":5,\"name\":\"Book 1\"}]")); + } + SECTION("parse notification [rainy][notification some book without amount]") + { + REQUIRE_FALSE(http::notification::parse("http://notification/[{\"name\":\"Book 1\"}]")); + } + SECTION("parse notification [rainy][notification some book without name]") + { + REQUIRE_FALSE(http::notification::parse("http://notification/[{\"amount\":5}]")); + } + SECTION("parse notification [rainy][notification without array of books]") + { + REQUIRE_FALSE(http::notification::parse("http://notification/")); + REQUIRE_FALSE(http::notification::parse("http://notification/\"amount\":5")); + } +} + +TEST_CASE("Reader::parse") +{ + SECTION("parse [sunny]") + { + auto const testFilePath = "test_books.json"; + QFile file(testFilePath); + REQUIRE(file.open(QIODevice::WriteOnly)); + + QJsonArray booksArray = { + QJsonObject{{"name", "Book 1"}, {"amount", 1}}, + QJsonObject{{"name", "Book 2"}, {"amount", 2}}, + QJsonObject{{"name", "Book 3"}, {"amount", 3}} + }; + + QJsonObject rootObj; + rootObj["books"] = booksArray; + + QJsonDocument doc(rootObj); + file.write(doc.toJson()); + file.close(); + + std::optional result = Reader::parse(testFilePath); + REQUIRE(result.has_value()); + REQUIRE(result->size() == 3); + + CHECK((*result)[0].name == "Book 1"); + CHECK((*result)[0].amount == 1); + CHECK((*result)[1].name == "Book 2"); + CHECK((*result)[1].amount == 2); + CHECK((*result)[2].name == "Book 3"); + CHECK((*result)[2].amount == 3); + + QFile::remove(testFilePath); + } + SECTION("parse [rainy][Invalid File Path]") + { + std::optional result = Reader::parse("invalid_path.json"); + REQUIRE_FALSE(result.has_value()); + } + SECTION("parse [rainy][Invalid JSON Format]") + { + auto const testFilePath = "invalid_json.json"; + QFile file(testFilePath); + REQUIRE(file.open(QIODevice::WriteOnly)); + + file.write("{ invalid json }"); + file.close(); + + std::optional result = Reader::parse(testFilePath); + REQUIRE_FALSE(result.has_value()); + + QFile::remove(testFilePath); + } + SECTION("parse [rainy][Missing Books Array]") + { + auto const testFilePath = "missing_books.json"; + QFile file(testFilePath); + REQUIRE(file.open(QIODevice::WriteOnly)); + + QJsonObject rootObj; // JSON без массива "books" + QJsonDocument doc(rootObj); + file.write(doc.toJson()); + file.close(); + + std::optional result = Reader::parse(testFilePath); + REQUIRE_FALSE(result.has_value()); + + QFile::remove(testFilePath); + } + SECTION("parse [rainy][Invalid Book Entry]") + { + auto const testFilePath = "invalid_book_entry.json"; + QFile file(testFilePath); + REQUIRE(file.open(QIODevice::WriteOnly)); + + QJsonArray booksArray = { + QJsonObject{{"name", "Book 1"}, {"amount", 1}}, + QJsonObject{{"invalid_field", "Book 2"}, {"amount", 2}}, + QJsonObject{{"name", "Book 3"}, {"amount", 3}} + }; + + QJsonObject rootObj; + rootObj["books"] = booksArray; + + QJsonDocument doc(rootObj); + file.write(doc.toJson()); + file.close(); + + std::optional result = Reader::parse(testFilePath); + REQUIRE_FALSE(result.has_value()); + + QFile::remove(testFilePath); + } +} + +TEST_CASE("Socket connection") +{ + // OS need time, that clear socket connection, so that + // run this test with interav several seconds + static alias::port_t PORT = 10101; + SECTION("Sanity connection and communication") + { + auto hostSocketConn = ConnSock::craeteHostSocket(PORT); + REQUIRE(hostSocketConn); + + auto clientSocketConn = ConnSock::craeteClientSocket(PORT); + REQUIRE(clientSocketConn); + + auto hostSocketConnAccepted = hostSocketConn->accept(); + REQUIRE(hostSocketConnAccepted); + + std::string const msg = "LALALA"; + REQUIRE(clientSocketConn->write(msg.c_str(), msg.size())); + + char buffer[MAX_SIZE] = {0}; + REQUIRE(hostSocketConnAccepted->read(buffer, MAX_SIZE)); + CHECK(std::string(buffer) == msg); + + REQUIRE(hostSocketConnAccepted->write(msg.c_str(), msg.size())); + + buffer[MAX_SIZE] = {0}; + REQUIRE(clientSocketConn->read(buffer, MAX_SIZE)); + CHECK(std::string(buffer) == msg); + } + SECTION("Failed to setup connection, because of different host's PORT") + { + PORT++; // For new connection new port + auto hostSocketConn = ConnSock::craeteHostSocket(PORT); + REQUIRE(hostSocketConn); + + auto clientSocketConn = ConnSock::craeteClientSocket(PORT + 1); + REQUIRE_FALSE(clientSocketConn); + } +} + +TEST_CASE("Fifo connection") +{ + static constexpr auto FIFO_PATH = "/tmp/my_fifo"; + SECTION("Sanity connection and communication") + { + auto hostFifoConn = ConnFifo::crateHostFifo(FIFO_PATH); + REQUIRE(hostFifoConn); + + auto clientFifoConn = ConnFifo::crateClientFifo(FIFO_PATH); + REQUIRE(clientFifoConn); + + std::string const msg = "LALALA"; + REQUIRE(clientFifoConn->write(msg.c_str(), msg.size())); + + char buffer[MAX_SIZE] = {0}; + REQUIRE(hostFifoConn->read(buffer, MAX_SIZE)); + CHECK(std::string(buffer) == msg); + + REQUIRE(hostFifoConn->write(msg.c_str(), msg.size())); + + buffer[MAX_SIZE] = {0}; + REQUIRE(clientFifoConn->read(buffer, MAX_SIZE)); + CHECK(std::string(buffer) == msg); + } + SECTION("Failed to setup connection, because of different fifo's path") + { + auto hostFifoConn = ConnFifo::crateHostFifo(FIFO_PATH); + REQUIRE(hostFifoConn); + + auto clientFifoConn = ConnFifo::crateClientFifo(FIFO_PATH + std::string("Lalala")); + REQUIRE_FALSE(clientFifoConn); + } +} + +TEST_CASE("Pipe connection") +{ + SECTION("Sanity connection and communication") + { + auto [hostPipeConn, clientPipeConn] = ConnPipe::createPipeConns(); + REQUIRE(hostPipeConn); + REQUIRE(clientPipeConn); + + std::string const msg = "LALALA"; + REQUIRE(clientPipeConn->write(msg.c_str(), msg.size())); + + char buffer[MAX_SIZE] = {0}; + REQUIRE(hostPipeConn->read(buffer, MAX_SIZE)); + CHECK(std::string(buffer) == msg); + + REQUIRE(hostPipeConn->write(msg.c_str(), msg.size())); + + buffer[MAX_SIZE] = {0}; + REQUIRE(clientPipeConn->read(buffer, MAX_SIZE)); + CHECK(std::string(buffer) == msg); + } +} diff --git a/Basalaev.Daniil/laba2/README.md b/Basalaev.Daniil/laba2/README.md new file mode 100644 index 0000000..f50bfc1 --- /dev/null +++ b/Basalaev.Daniil/laba2/README.md @@ -0,0 +1,74 @@ +# Проект: Demon + +## Описание + +Библиотека. Хост - библиотека, клиенты - читатели. Хост по запросу выдает книги (при наличии их в библиотеке), либо говорит что +книга занята/отсутсвует и получает их обратно. Клиенты запрашивают интересующие их книги, читают их какое-то время и возвращают +в библиотеку (скорость чтения случайна и линейна во времени). Статус всех книг библиотеки (название, сколько доступно свободных +копий, кем и когда взята) отражаются в графическом интерфейсе. В интерфейсе клиентов их история запросов (когда и какие книги +они брали и возвращали). + +Вариант 28. Типы соединений: 5, 6, 7 + +5. Программные каналы (pipe). TYPE_CODE - pipe. + +6. Именованные каналы (mkfifo). TYPE_CODE - fifo. Работа с именованным каналом должна производиться в каждом процессе уже после +создания дочернего (так, будто это несвязанные процессы). После окончания работы родительский процесс должен удалять файл +именованного канала. + +7. Сокеты (socket). TYPE_CODE – sock + + +## Функциональные возможности + +1. Хост создаёт столько клиентов, какое кол-во было передано в командной строке +2. ID каждого клиента совпадает с pid его процесса +3. Если клиент не берёт книгу более 5 секунд - его отключат +4. Пока клиент читает - его не отключают +5. Клиентов и хоста нельзя отключить на "крестик" +6. Книги считываются из файла Books.json (но можно третим параметром указать путь к другому файлу) + + +## Требования + +- **CMake** (для сборки проекта) +- **GCC** или любой другой компилятор, поддерживающий C++ +- **Catch2** (для юнит-тестов) + +Убедитесь, что у вас установлены все необходимые библиотеки и утилиты для сборки и работы проекта. + +## Сборка + +1. **Клонируйте репозиторий:** + ```bash + git clone https://github.com/BaevDaniil/Operating-Systems-labs-2024.git +2. **Спустится на директрию с проектом:** + ```bash + cd Basalaev.Daniil/laba2 +3. **Запустите скрипт сборки:** + ```bash + bash build.sh + + +## Запуск + +1. **Запустите хоста с клиентами с socket соединением** + ```bash + bash runSock.sh +2. **Запустите хоста с клиентами с fifo соединением** + ```bash + bash runFifo.sh +3. **Запустите хоста с клиентами с pipe соединением** + ```bash + bash runPipe.sh + + +## Тестирование + +Скрипт для запуска юнит-тестов: +```bash +bash runTest +``` + +## Автор +Студент: Басалаев Даниил Александрович 5030102/10201 diff --git a/Basalaev.Daniil/laba2/build.sh b/Basalaev.Daniil/laba2/build.sh new file mode 100644 index 0000000..c90b1dd --- /dev/null +++ b/Basalaev.Daniil/laba2/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash +mkdir -p build +cd build +cmake ../Library +make diff --git a/Basalaev.Daniil/laba2/runFifo.sh b/Basalaev.Daniil/laba2/runFifo.sh new file mode 100644 index 0000000..79e2b0a --- /dev/null +++ b/Basalaev.Daniil/laba2/runFifo.sh @@ -0,0 +1,2 @@ +#!/bin/bash +./build/host_fifo 2 diff --git a/Basalaev.Daniil/laba2/runPipe.sh b/Basalaev.Daniil/laba2/runPipe.sh new file mode 100644 index 0000000..3eb8b84 --- /dev/null +++ b/Basalaev.Daniil/laba2/runPipe.sh @@ -0,0 +1,2 @@ +#!/bin/bash +./build/host_pipe 2 diff --git a/Basalaev.Daniil/laba2/runSock.sh b/Basalaev.Daniil/laba2/runSock.sh new file mode 100644 index 0000000..f18c91c --- /dev/null +++ b/Basalaev.Daniil/laba2/runSock.sh @@ -0,0 +1,2 @@ +#!/bin/bash +./build/host_sock 10135 2 diff --git a/Basalaev.Daniil/laba2/runTest.sh b/Basalaev.Daniil/laba2/runTest.sh new file mode 100644 index 0000000..38d4bd4 --- /dev/null +++ b/Basalaev.Daniil/laba2/runTest.sh @@ -0,0 +1,2 @@ +#!/bin/bash +./build/test/test