From 0c2b8a421c8d60018802c4237a0e3f3489e4b458 Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Wed, 19 Oct 2022 00:52:47 +0300 Subject: [PATCH 01/32] primary layout --- .gitignore | 2 + CMakeLists.txt | 26 ++ client/CMakeLists.txt | 9 + client/src/client.cpp | 4 + client/tests/CMakeLists.txt | 5 + client/tests/test.cpp | 6 + cmake/HunterGate.cmake | 528 ++++++++++++++++++++++++++++++++++++ server/CMakeLists.txt | 9 + server/src/server.cpp | 7 + server/tests/CMakeLists.txt | 0 server/tests/test.cpp | 0 11 files changed, 596 insertions(+) create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 client/CMakeLists.txt create mode 100644 client/src/client.cpp create mode 100644 client/tests/CMakeLists.txt create mode 100644 client/tests/test.cpp create mode 100644 cmake/HunterGate.cmake create mode 100644 server/CMakeLists.txt create mode 100644 server/src/server.cpp create mode 100644 server/tests/CMakeLists.txt create mode 100644 server/tests/test.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..305ac1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +cmake-build-debug/ +.idea/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..3b37ace --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.18) + +include("cmake/HunterGate.cmake") +HunterGate( + URL "https://github.com/cpp-pm/hunter/archive/v0.23.314.tar.gz" + SHA1 "95c47c92f68edb091b5d6d18924baabe02a6962a" +) + +project(watch_up_project) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(BUILD_TESTS "Build tests" OFF) +option(BUILD_DOCS "Build documentation" OFF) +option(BUILD_COVERAGE "Build code coverage" OFF) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic -Werror -Wall -Wextra") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") + +hunter_add_package(GTest) +find_package(GTest CONFIG REQUIRED) + +add_subdirectory(server) +add_subdirectory(client) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt new file mode 100644 index 0000000..9b63c2d --- /dev/null +++ b/client/CMakeLists.txt @@ -0,0 +1,9 @@ +project(client) + +file(GLOB SOURCES "src/*.cpp") + +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +add_subdirectory(tests) diff --git a/client/src/client.cpp b/client/src/client.cpp new file mode 100644 index 0000000..7d23c0b --- /dev/null +++ b/client/src/client.cpp @@ -0,0 +1,4 @@ + +int main(){ + return 0; +} \ No newline at end of file diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt new file mode 100644 index 0000000..efeabc4 --- /dev/null +++ b/client/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +project(test_client) + +add_executable(${PROJECT_NAME} test.cpp) + +target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest_main) diff --git a/client/tests/test.cpp b/client/tests/test.cpp new file mode 100644 index 0000000..dec391d --- /dev/null +++ b/client/tests/test.cpp @@ -0,0 +1,6 @@ +#include "gtest/gtest.h" + +int main(){ + + return 0; +} \ No newline at end of file diff --git a/cmake/HunterGate.cmake b/cmake/HunterGate.cmake new file mode 100644 index 0000000..04886ef --- /dev/null +++ b/cmake/HunterGate.cmake @@ -0,0 +1,528 @@ +# Copyright (c) 2013-2019, Ruslan Baratov +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# This is a gate file to Hunter package manager. +# Include this file using `include` command and add package you need, example: +# +# cmake_minimum_required(VERSION 3.2) +# +# include("cmake/HunterGate.cmake") +# HunterGate( +# URL "https://github.com/path/to/hunter/archive.tar.gz" +# SHA1 "798501e983f14b28b10cda16afa4de69eee1da1d" +# ) +# +# project(MyProject) +# +# hunter_add_package(Foo) +# hunter_add_package(Boo COMPONENTS Bar Baz) +# +# Projects: +# * https://github.com/hunter-packages/gate/ +# * https://github.com/ruslo/hunter + +option(HUNTER_ENABLED "Enable Hunter package manager support" ON) + +if(HUNTER_ENABLED) + if(CMAKE_VERSION VERSION_LESS "3.2") + message( + FATAL_ERROR + "At least CMake version 3.2 required for Hunter dependency management." + " Update CMake or set HUNTER_ENABLED to OFF." + ) + endif() +endif() + +include(CMakeParseArguments) # cmake_parse_arguments + +option(HUNTER_STATUS_PRINT "Print working status" ON) +option(HUNTER_STATUS_DEBUG "Print a lot info" OFF) +option(HUNTER_TLS_VERIFY "Enable/disable TLS certificate checking on downloads" ON) + +set(HUNTER_ERROR_PAGE "https://docs.hunter.sh/en/latest/reference/errors") + +function(hunter_gate_status_print) + if(HUNTER_STATUS_PRINT OR HUNTER_STATUS_DEBUG) + foreach(print_message ${ARGV}) + message(STATUS "[hunter] ${print_message}") + endforeach() + endif() +endfunction() + +function(hunter_gate_status_debug) + if(HUNTER_STATUS_DEBUG) + foreach(print_message ${ARGV}) + string(TIMESTAMP timestamp) + message(STATUS "[hunter *** DEBUG *** ${timestamp}] ${print_message}") + endforeach() + endif() +endfunction() + +function(hunter_gate_error_page error_page) + message("------------------------------ ERROR ------------------------------") + message(" ${HUNTER_ERROR_PAGE}/${error_page}.html") + message("-------------------------------------------------------------------") + message("") + message(FATAL_ERROR "") +endfunction() + +function(hunter_gate_internal_error) + message("") + foreach(print_message ${ARGV}) + message("[hunter ** INTERNAL **] ${print_message}") + endforeach() + message("[hunter ** INTERNAL **] [Directory:${CMAKE_CURRENT_LIST_DIR}]") + message("") + hunter_gate_error_page("error.internal") +endfunction() + +function(hunter_gate_fatal_error) + cmake_parse_arguments(hunter "" "ERROR_PAGE" "" "${ARGV}") + if("${hunter_ERROR_PAGE}" STREQUAL "") + hunter_gate_internal_error("Expected ERROR_PAGE") + endif() + message("") + foreach(x ${hunter_UNPARSED_ARGUMENTS}) + message("[hunter ** FATAL ERROR **] ${x}") + endforeach() + message("[hunter ** FATAL ERROR **] [Directory:${CMAKE_CURRENT_LIST_DIR}]") + message("") + hunter_gate_error_page("${hunter_ERROR_PAGE}") +endfunction() + +function(hunter_gate_user_error) + hunter_gate_fatal_error(${ARGV} ERROR_PAGE "error.incorrect.input.data") +endfunction() + +function(hunter_gate_self root version sha1 result) + string(COMPARE EQUAL "${root}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("root is empty") + endif() + + string(COMPARE EQUAL "${version}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("version is empty") + endif() + + string(COMPARE EQUAL "${sha1}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("sha1 is empty") + endif() + + string(SUBSTRING "${sha1}" 0 7 archive_id) + + set( + hunter_self + "${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked" + ) + + set("${result}" "${hunter_self}" PARENT_SCOPE) +endfunction() + +# Set HUNTER_GATE_ROOT cmake variable to suitable value. +function(hunter_gate_detect_root) + # Check CMake variable + string(COMPARE NOTEQUAL "${HUNTER_ROOT}" "" not_empty) + if(not_empty) + set(HUNTER_GATE_ROOT "${HUNTER_ROOT}" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT detected by cmake variable") + return() + endif() + + # Check environment variable + string(COMPARE NOTEQUAL "$ENV{HUNTER_ROOT}" "" not_empty) + if(not_empty) + set(HUNTER_GATE_ROOT "$ENV{HUNTER_ROOT}" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT detected by environment variable") + return() + endif() + + # Check HOME environment variable + string(COMPARE NOTEQUAL "$ENV{HOME}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{HOME}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug("HUNTER_ROOT set using HOME environment variable") + return() + endif() + + # Check SYSTEMDRIVE and USERPROFILE environment variable (windows only) + if(WIN32) + string(COMPARE NOTEQUAL "$ENV{SYSTEMDRIVE}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{SYSTEMDRIVE}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug( + "HUNTER_ROOT set using SYSTEMDRIVE environment variable" + ) + return() + endif() + + string(COMPARE NOTEQUAL "$ENV{USERPROFILE}" "" result) + if(result) + set(HUNTER_GATE_ROOT "$ENV{USERPROFILE}/.hunter" PARENT_SCOPE) + hunter_gate_status_debug( + "HUNTER_ROOT set using USERPROFILE environment variable" + ) + return() + endif() + endif() + + hunter_gate_fatal_error( + "Can't detect HUNTER_ROOT" + ERROR_PAGE "error.detect.hunter.root" + ) +endfunction() + +function(hunter_gate_download dir) + string( + COMPARE + NOTEQUAL + "$ENV{HUNTER_DISABLE_AUTOINSTALL}" + "" + disable_autoinstall + ) + if(disable_autoinstall AND NOT HUNTER_RUN_INSTALL) + hunter_gate_fatal_error( + "Hunter not found in '${dir}'" + "Set HUNTER_RUN_INSTALL=ON to auto-install it from '${HUNTER_GATE_URL}'" + "Settings:" + " HUNTER_ROOT: ${HUNTER_GATE_ROOT}" + " HUNTER_SHA1: ${HUNTER_GATE_SHA1}" + ERROR_PAGE "error.run.install" + ) + endif() + string(COMPARE EQUAL "${dir}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("Empty 'dir' argument") + endif() + + string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("HUNTER_GATE_SHA1 empty") + endif() + + string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" is_bad) + if(is_bad) + hunter_gate_internal_error("HUNTER_GATE_URL empty") + endif() + + set(done_location "${dir}/DONE") + set(sha1_location "${dir}/SHA1") + + set(build_dir "${dir}/Build") + set(cmakelists "${dir}/CMakeLists.txt") + + hunter_gate_status_debug("Locking directory: ${dir}") + file(LOCK "${dir}" DIRECTORY GUARD FUNCTION) + hunter_gate_status_debug("Lock done") + + if(EXISTS "${done_location}") + # while waiting for lock other instance can do all the job + hunter_gate_status_debug("File '${done_location}' found, skip install") + return() + endif() + + file(REMOVE_RECURSE "${build_dir}") + file(REMOVE_RECURSE "${cmakelists}") + + file(MAKE_DIRECTORY "${build_dir}") # check directory permissions + + # Disabling languages speeds up a little bit, reduces noise in the output + # and avoids path too long windows error + file( + WRITE + "${cmakelists}" + "cmake_minimum_required(VERSION 3.2)\n" + "project(HunterDownload LANGUAGES NONE)\n" + "include(ExternalProject)\n" + "ExternalProject_Add(\n" + " Hunter\n" + " URL\n" + " \"${HUNTER_GATE_URL}\"\n" + " URL_HASH\n" + " SHA1=${HUNTER_GATE_SHA1}\n" + " DOWNLOAD_DIR\n" + " \"${dir}\"\n" + " TLS_VERIFY\n" + " ${HUNTER_TLS_VERIFY}\n" + " SOURCE_DIR\n" + " \"${dir}/Unpacked\"\n" + " CONFIGURE_COMMAND\n" + " \"\"\n" + " BUILD_COMMAND\n" + " \"\"\n" + " INSTALL_COMMAND\n" + " \"\"\n" + ")\n" + ) + + if(HUNTER_STATUS_DEBUG) + set(logging_params "") + else() + set(logging_params OUTPUT_QUIET) + endif() + + hunter_gate_status_debug("Run generate") + + # Need to add toolchain file too. + # Otherwise on Visual Studio + MDD this will fail with error: + # "Could not find an appropriate version of the Windows 10 SDK installed on this machine" + if(EXISTS "${CMAKE_TOOLCHAIN_FILE}") + get_filename_component(absolute_CMAKE_TOOLCHAIN_FILE "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE) + set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=${absolute_CMAKE_TOOLCHAIN_FILE}") + else() + # 'toolchain_arg' can't be empty + set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=") + endif() + + string(COMPARE EQUAL "${CMAKE_MAKE_PROGRAM}" "" no_make) + if(no_make) + set(make_arg "") + else() + # Test case: remove Ninja from PATH but set it via CMAKE_MAKE_PROGRAM + set(make_arg "-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}") + endif() + + execute_process( + COMMAND + "${CMAKE_COMMAND}" + "-H${dir}" + "-B${build_dir}" + "-G${CMAKE_GENERATOR}" + "${toolchain_arg}" + ${make_arg} + WORKING_DIRECTORY "${dir}" + RESULT_VARIABLE download_result + ${logging_params} + ) + + if(NOT download_result EQUAL 0) + hunter_gate_internal_error( + "Configure project failed." + "To reproduce the error run: ${CMAKE_COMMAND} -H${dir} -B${build_dir} -G${CMAKE_GENERATOR} ${toolchain_arg} ${make_arg}" + "In directory ${dir}" + ) + endif() + + hunter_gate_status_print( + "Initializing Hunter workspace (${HUNTER_GATE_SHA1})" + " ${HUNTER_GATE_URL}" + " -> ${dir}" + ) + execute_process( + COMMAND "${CMAKE_COMMAND}" --build "${build_dir}" + WORKING_DIRECTORY "${dir}" + RESULT_VARIABLE download_result + ${logging_params} + ) + + if(NOT download_result EQUAL 0) + hunter_gate_internal_error("Build project failed") + endif() + + file(REMOVE_RECURSE "${build_dir}") + file(REMOVE_RECURSE "${cmakelists}") + + file(WRITE "${sha1_location}" "${HUNTER_GATE_SHA1}") + file(WRITE "${done_location}" "DONE") + + hunter_gate_status_debug("Finished") +endfunction() + +# Must be a macro so master file 'cmake/Hunter' can +# apply all variables easily just by 'include' command +# (otherwise PARENT_SCOPE magic needed) +macro(HunterGate) + if(HUNTER_GATE_DONE) + # variable HUNTER_GATE_DONE set explicitly for external project + # (see `hunter_download`) + set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) + endif() + + # First HunterGate command will init Hunter, others will be ignored + get_property(_hunter_gate_done GLOBAL PROPERTY HUNTER_GATE_DONE SET) + + if(NOT HUNTER_ENABLED) + # Empty function to avoid error "unknown function" + function(hunter_add_package) + endfunction() + + set( + _hunter_gate_disabled_mode_dir + "${CMAKE_CURRENT_LIST_DIR}/cmake/Hunter/disabled-mode" + ) + if(EXISTS "${_hunter_gate_disabled_mode_dir}") + hunter_gate_status_debug( + "Adding \"disabled-mode\" modules: ${_hunter_gate_disabled_mode_dir}" + ) + list(APPEND CMAKE_PREFIX_PATH "${_hunter_gate_disabled_mode_dir}") + endif() + elseif(_hunter_gate_done) + hunter_gate_status_debug("Secondary HunterGate (use old settings)") + hunter_gate_self( + "${HUNTER_CACHED_ROOT}" + "${HUNTER_VERSION}" + "${HUNTER_SHA1}" + _hunter_self + ) + include("${_hunter_self}/cmake/Hunter") + else() + set(HUNTER_GATE_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}") + + string(COMPARE NOTEQUAL "${PROJECT_NAME}" "" _have_project_name) + if(_have_project_name) + hunter_gate_fatal_error( + "Please set HunterGate *before* 'project' command. " + "Detected project: ${PROJECT_NAME}" + ERROR_PAGE "error.huntergate.before.project" + ) + endif() + + cmake_parse_arguments( + HUNTER_GATE "LOCAL" "URL;SHA1;GLOBAL;FILEPATH" "" ${ARGV} + ) + + string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" _empty_sha1) + string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" _empty_url) + string( + COMPARE + NOTEQUAL + "${HUNTER_GATE_UNPARSED_ARGUMENTS}" + "" + _have_unparsed + ) + string(COMPARE NOTEQUAL "${HUNTER_GATE_GLOBAL}" "" _have_global) + string(COMPARE NOTEQUAL "${HUNTER_GATE_FILEPATH}" "" _have_filepath) + + if(_have_unparsed) + hunter_gate_user_error( + "HunterGate unparsed arguments: ${HUNTER_GATE_UNPARSED_ARGUMENTS}" + ) + endif() + if(_empty_sha1) + hunter_gate_user_error("SHA1 suboption of HunterGate is mandatory") + endif() + if(_empty_url) + hunter_gate_user_error("URL suboption of HunterGate is mandatory") + endif() + if(_have_global) + if(HUNTER_GATE_LOCAL) + hunter_gate_user_error("Unexpected LOCAL (already has GLOBAL)") + endif() + if(_have_filepath) + hunter_gate_user_error("Unexpected FILEPATH (already has GLOBAL)") + endif() + endif() + if(HUNTER_GATE_LOCAL) + if(_have_global) + hunter_gate_user_error("Unexpected GLOBAL (already has LOCAL)") + endif() + if(_have_filepath) + hunter_gate_user_error("Unexpected FILEPATH (already has LOCAL)") + endif() + endif() + if(_have_filepath) + if(_have_global) + hunter_gate_user_error("Unexpected GLOBAL (already has FILEPATH)") + endif() + if(HUNTER_GATE_LOCAL) + hunter_gate_user_error("Unexpected LOCAL (already has FILEPATH)") + endif() + endif() + + hunter_gate_detect_root() # set HUNTER_GATE_ROOT + + # Beautify path, fix probable problems with windows path slashes + get_filename_component( + HUNTER_GATE_ROOT "${HUNTER_GATE_ROOT}" ABSOLUTE + ) + hunter_gate_status_debug("HUNTER_ROOT: ${HUNTER_GATE_ROOT}") + if(NOT HUNTER_ALLOW_SPACES_IN_PATH) + string(FIND "${HUNTER_GATE_ROOT}" " " _contain_spaces) + if(NOT _contain_spaces EQUAL -1) + hunter_gate_fatal_error( + "HUNTER_ROOT (${HUNTER_GATE_ROOT}) contains spaces." + "Set HUNTER_ALLOW_SPACES_IN_PATH=ON to skip this error" + "(Use at your own risk!)" + ERROR_PAGE "error.spaces.in.hunter.root" + ) + endif() + endif() + + string( + REGEX + MATCH + "[0-9]+\\.[0-9]+\\.[0-9]+[-_a-z0-9]*" + HUNTER_GATE_VERSION + "${HUNTER_GATE_URL}" + ) + string(COMPARE EQUAL "${HUNTER_GATE_VERSION}" "" _is_empty) + if(_is_empty) + set(HUNTER_GATE_VERSION "unknown") + endif() + + hunter_gate_self( + "${HUNTER_GATE_ROOT}" + "${HUNTER_GATE_VERSION}" + "${HUNTER_GATE_SHA1}" + _hunter_self + ) + + set(_master_location "${_hunter_self}/cmake/Hunter") + get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE) + set(_done_location "${_archive_id_location}/DONE") + set(_sha1_location "${_archive_id_location}/SHA1") + + # Check Hunter already downloaded by HunterGate + if(NOT EXISTS "${_done_location}") + hunter_gate_download("${_archive_id_location}") + endif() + + if(NOT EXISTS "${_done_location}") + hunter_gate_internal_error("hunter_gate_download failed") + endif() + + if(NOT EXISTS "${_sha1_location}") + hunter_gate_internal_error("${_sha1_location} not found") + endif() + file(READ "${_sha1_location}" _sha1_value) + string(COMPARE EQUAL "${_sha1_value}" "${HUNTER_GATE_SHA1}" _is_equal) + if(NOT _is_equal) + hunter_gate_internal_error( + "Short SHA1 collision:" + " ${_sha1_value} (from ${_sha1_location})" + " ${HUNTER_GATE_SHA1} (HunterGate)" + ) + endif() + if(NOT EXISTS "${_master_location}") + hunter_gate_user_error( + "Master file not found:" + " ${_master_location}" + "try to update Hunter/HunterGate" + ) + endif() + include("${_master_location}") + set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES) + endif() +endmacro() \ No newline at end of file diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt new file mode 100644 index 0000000..e1166d8 --- /dev/null +++ b/server/CMakeLists.txt @@ -0,0 +1,9 @@ +project(server) + +file(GLOB SOURCES "src/*.cpp") + +add_executable(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +add_subdirectory(tests) \ No newline at end of file diff --git a/server/src/server.cpp b/server/src/server.cpp new file mode 100644 index 0000000..fb8fb19 --- /dev/null +++ b/server/src/server.cpp @@ -0,0 +1,7 @@ + + + +int main(){ + + return 0; +} \ No newline at end of file diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/test.cpp b/server/tests/test.cpp new file mode 100644 index 0000000..e69de29 From 6fd178e9cd6e055a3ca2fca6c6ce397029eb0588 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 19 Oct 2022 01:52:05 +0300 Subject: [PATCH 02/32] Added unit test support. --- client/CMakeLists.txt | 10 +++++++++- client/include/lib.hpp | 8 ++++++++ client/src/client.cpp | 5 +++++ client/src/lib.cpp | 7 +++++++ client/tests/CMakeLists.txt | 7 +++++-- client/tests/test.cpp | 9 ++++++--- server/CMakeLists.txt | 12 ++++++++++-- server/include/lib.hpp | 8 ++++++++ server/src/lib.cpp | 7 +++++++ server/src/server.cpp | 5 ++++- server/tests/CMakeLists.txt | 7 +++++++ server/tests/test.cpp | 9 +++++++++ 12 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 client/include/lib.hpp create mode 100644 client/src/lib.cpp create mode 100644 server/include/lib.hpp create mode 100644 server/src/lib.cpp diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 9b63c2d..a1a99f4 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -2,8 +2,16 @@ project(client) file(GLOB SOURCES "src/*.cpp") -add_executable(${PROJECT_NAME} ${SOURCES}) +list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/client.cpp") + +add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) + +target_include_directories(${PROJECT_NAME}_lib PUBLIC include) + +add_executable(${PROJECT_NAME} ./src/client.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) + add_subdirectory(tests) diff --git a/client/include/lib.hpp b/client/include/lib.hpp new file mode 100644 index 0000000..169fb60 --- /dev/null +++ b/client/include/lib.hpp @@ -0,0 +1,8 @@ +#ifndef WATCH_UP_PROJECT_LIB_H +#define WATCH_UP_PROJECT_LIB_H + +#include + +std::string foo(); + +#endif //WATCH_UP_PROJECT_LIB_H diff --git a/client/src/client.cpp b/client/src/client.cpp index 7d23c0b..6a12f41 100644 --- a/client/src/client.cpp +++ b/client/src/client.cpp @@ -1,4 +1,9 @@ +#include + +#include "lib.hpp" int main(){ + std::cout << "Client exe" << "\n"; + std::cout << foo() << "\n"; return 0; } \ No newline at end of file diff --git a/client/src/lib.cpp b/client/src/lib.cpp new file mode 100644 index 0000000..7f2dc78 --- /dev/null +++ b/client/src/lib.cpp @@ -0,0 +1,7 @@ +#include + +#include "lib.hpp" + +std::string foo() { + return "Foo lib"; +} \ No newline at end of file diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index efeabc4..4cc3996 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -1,5 +1,8 @@ project(test_client) -add_executable(${PROJECT_NAME} test.cpp) +file(GLOB TEST_SOURCES "*.cpp") + +add_executable(${PROJECT_NAME} ${TEST_SOURCES}) + +target_link_libraries(${PROJECT_NAME} client_lib GTest::gtest_main) -target_link_libraries(${PROJECT_NAME} PRIVATE GTest::gtest_main) diff --git a/client/tests/test.cpp b/client/tests/test.cpp index dec391d..54c4d27 100644 --- a/client/tests/test.cpp +++ b/client/tests/test.cpp @@ -1,6 +1,9 @@ -#include "gtest/gtest.h" +#include +#include -int main(){ +#include "lib.hpp" - return 0; +TEST(Lib_test, test_foo) { + std::string a = "Foo lib"; + EXPECT_EQ(a, foo()); } \ No newline at end of file diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index e1166d8..9d99743 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -2,8 +2,16 @@ project(server) file(GLOB SOURCES "src/*.cpp") -add_executable(${PROJECT_NAME} ${SOURCES}) +list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/server.cpp") + +add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) + +target_include_directories(${PROJECT_NAME}_lib PUBLIC include) + +add_executable(${PROJECT_NAME} ./src/server.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) -add_subdirectory(tests) \ No newline at end of file +target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) + +add_subdirectory(tests) diff --git a/server/include/lib.hpp b/server/include/lib.hpp new file mode 100644 index 0000000..6e12646 --- /dev/null +++ b/server/include/lib.hpp @@ -0,0 +1,8 @@ +#ifndef WATCH_UP_PROJECT_LIB_HPP +#define WATCH_UP_PROJECT_LIB_HPP + +#include + +std::string foo(); + +#endif //WATCH_UP_PROJECT_LIB_HPP diff --git a/server/src/lib.cpp b/server/src/lib.cpp new file mode 100644 index 0000000..a18be76 --- /dev/null +++ b/server/src/lib.cpp @@ -0,0 +1,7 @@ +#include + +#include "lib.hpp" + +std::string foo() { + return "foo"; +} diff --git a/server/src/server.cpp b/server/src/server.cpp index fb8fb19..e231862 100644 --- a/server/src/server.cpp +++ b/server/src/server.cpp @@ -1,7 +1,10 @@ +#include - +#include "lib.hpp" int main(){ + std::cout << "Server exe" << "\n"; + std::cout << foo() << "\n"; return 0; } \ No newline at end of file diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt index e69de29..c14fa75 100644 --- a/server/tests/CMakeLists.txt +++ b/server/tests/CMakeLists.txt @@ -0,0 +1,7 @@ +project(test_server) + +file(GLOB TEST_SOURCES "*.cpp") + +add_executable(${PROJECT_NAME} ${TEST_SOURCES}) + +target_link_libraries(${PROJECT_NAME} server_lib GTest::gtest_main) diff --git a/server/tests/test.cpp b/server/tests/test.cpp index e69de29..22b0d75 100644 --- a/server/tests/test.cpp +++ b/server/tests/test.cpp @@ -0,0 +1,9 @@ +#include +#include + +#include "lib.hpp" + +TEST(Lib_test, test_foo) { + std::string a = "foo"; + EXPECT_EQ(a, foo()); +} From fbf17ba509de726fd500970d417da56a6836cacf Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Thu, 20 Oct 2022 04:24:26 +0300 Subject: [PATCH 03/32] Added coverage scripts and test env. --- .gitignore | 3 ++- .gitmodules | 3 +++ CMakeLists.txt | 18 +++++++++++++++++- Dockerfile | 9 +++++++++ client/CMakeLists.txt | 9 ++++++++- client/tests/CMakeLists.txt | 5 +++++ scripts/build_image.sh | 3 +++ scripts/coverage.sh | 13 +++++++++++++ server/CMakeLists.txt | 10 +++++++++- server/tests/CMakeLists.txt | 6 ++++++ 10 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 .gitmodules create mode 100644 Dockerfile create mode 100644 scripts/build_image.sh create mode 100644 scripts/coverage.sh diff --git a/.gitignore b/.gitignore index 305ac1d..8cc3478 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ cmake-build-debug/ -.idea/ \ No newline at end of file +.idea/ +build/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..ed19dca --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "externals/CMake-codecov"] + path = externals/CMake-codecov + url = git@github.com:ivan12093/CMake-codecov.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 3b37ace..d6fdcfc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,7 +11,7 @@ project(watch_up_project) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -option(BUILD_TESTS "Build tests" OFF) +option(BUILD_TESTS "Build tests" ON) option(BUILD_DOCS "Build documentation" OFF) option(BUILD_COVERAGE "Build code coverage" OFF) @@ -22,5 +22,21 @@ SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") hunter_add_package(GTest) find_package(GTest CONFIG REQUIRED) +if (BUILD_TESTS) + enable_testing() +endif() + +if (BUILD_COVERAGE) + set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage build." FORCE) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/CMake-codecov/cmake") + find_package(codecov) + string(APPEND CMAKE_CXX_FLAGS " --coverage") +endif() + add_subdirectory(server) add_subdirectory(client) + +if (BUILD_COVERAGE) + coverage_evaluate() +endif() + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fbf16e6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM ubuntu:20.04 +ENV DEBIAN_FRONTEND noninteractive +WORKDIR cash_map +COPY . . +RUN apt -y update && \ + apt -y install gcovr python3-pip && \ + pip3 install cpplint && \ + pip3 install --upgrade cmake && \ + apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index a1a99f4..76bb7e8 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -14,4 +14,11 @@ target_include_directories(${PROJECT_NAME} PUBLIC include) target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) -add_subdirectory(tests) +if (BUILD_TESTS) + add_subdirectory(tests) +endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 4cc3996..0305c54 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -6,3 +6,8 @@ add_executable(${PROJECT_NAME} ${TEST_SOURCES}) target_link_libraries(${PROJECT_NAME} client_lib GTest::gtest_main) +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) +endif() diff --git a/scripts/build_image.sh b/scripts/build_image.sh new file mode 100644 index 0000000..986884e --- /dev/null +++ b/scripts/build_image.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +cd ../ +docker build -t cash_map . diff --git a/scripts/coverage.sh b/scripts/coverage.sh new file mode 100644 index 0000000..94b9b2d --- /dev/null +++ b/scripts/coverage.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -e +export GTEST_COLOR=1 +export CTEST_OUTPUT_ON_FAILURE=true + +cd ../ + +cmake -H. -Bbuild -DBUILD_COVERAGE=ON +cmake --build build +cmake --build build --target test --verbose +cmake --build build --target gcov +gcovr -r . diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 9d99743..d8e2681 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -14,4 +14,12 @@ target_include_directories(${PROJECT_NAME} PUBLIC include) target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) -add_subdirectory(tests) +if (BUILD_TESTS) + add_subdirectory(tests) +endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() + diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt index c14fa75..8a12b55 100644 --- a/server/tests/CMakeLists.txt +++ b/server/tests/CMakeLists.txt @@ -5,3 +5,9 @@ file(GLOB TEST_SOURCES "*.cpp") add_executable(${PROJECT_NAME} ${TEST_SOURCES}) target_link_libraries(${PROJECT_NAME} server_lib GTest::gtest_main) + +add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) +endif() From 988a8ccb8e95e727dd77c37e1483ec14ed180082 Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Thu, 20 Oct 2022 14:34:10 +0300 Subject: [PATCH 04/32] deleted submodule. --- .gitmodules | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index ed19dca..8b13789 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1 @@ -[submodule "externals/CMake-codecov"] - path = externals/CMake-codecov - url = git@github.com:ivan12093/CMake-codecov.git + From d1d3e6928a7926a4cd3260df51f31252fd851f94 Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Thu, 20 Oct 2022 14:35:17 +0300 Subject: [PATCH 05/32] added submodule. --- .gitmodules | 4 +++- externals/CMake-codecov | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 160000 externals/CMake-codecov diff --git a/.gitmodules b/.gitmodules index 8b13789..f047566 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1 +1,3 @@ - +[submodule "externals/CMake-codecov"] + path = externals/CMake-codecov + url = https://github.com/ivan12093/CMake-codecov.git diff --git a/externals/CMake-codecov b/externals/CMake-codecov new file mode 160000 index 0000000..1974181 --- /dev/null +++ b/externals/CMake-codecov @@ -0,0 +1 @@ +Subproject commit 1974181d8515441329e3a03ccf47a99518402491 From b6307934c7685799f4e029758c63ace3eb7a72b2 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Sat, 22 Oct 2022 15:57:34 +0300 Subject: [PATCH 06/32] add ci and linters --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ .gitignore | 1 + CMakeLists.txt | 1 + CPPLINT.cfg | 8 ++++++++ Dockerfile | 5 +++-- Makefile | 15 +++++++++++++++ client/include/lib.hpp | 5 +---- client/src/client.cpp | 5 +++-- client/src/lib.cpp | 2 +- client/tests/CMakeLists.txt | 3 ++- scripts/build.sh | 10 ++++++++++ scripts/run_linters.sh | 28 ++++++++++++++++++++++++++++ scripts/run_tests.sh | 7 +++++++ server/include/lib.hpp | 4 +--- server/tests/CMakeLists.txt | 3 ++- 15 files changed, 109 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CPPLINT.cfg create mode 100644 Makefile create mode 100644 scripts/build.sh create mode 100644 scripts/run_linters.sh create mode 100644 scripts/run_tests.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..58facb0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +on: push + +jobs: + check: + runs-on: ubuntu-latest + container: ilyaaid/gcc_cmake_gtest_linters + steps: + - uses: actions/checkout@v2 + - run: make check + + build: + runs-on: ubuntu-latest + container: ilyaaid/gcc_cmake_gtest_linters + needs: [check] + steps: + - uses: actions/checkout@v2 + - run: make build + + test: + runs-on: ubuntu-latest + container: ilyaaid/gcc_cmake_gtest_linters + needs: [build] + steps: + - uses: actions/checkout@v2 + working-directory: ${{github.workspace}}/build + - run: make test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8cc3478..b75634d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ cmake-build-debug/ +cmake-build-wsl/ .idea/ build/ \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index d6fdcfc..f85429d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ hunter_add_package(GTest) find_package(GTest CONFIG REQUIRED) if (BUILD_TESTS) + include(GoogleTest) enable_testing() endif() diff --git a/CPPLINT.cfg b/CPPLINT.cfg new file mode 100644 index 0000000..5fa421e --- /dev/null +++ b/CPPLINT.cfg @@ -0,0 +1,8 @@ +headers=h,hpp +linelength=110 +filter=-whitespace/tab +filter=-runtime/int +filter=-legal/copyright +filter=-build/include_subdir +filter=-build/include +filter=-readability/casting \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index fbf16e6..762e5da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,8 @@ ENV DEBIAN_FRONTEND noninteractive WORKDIR cash_map COPY . . RUN apt -y update && \ - apt -y install gcovr python3-pip && \ + apt -y install python3 cppcheck clang-tidy make git gcovr python3-pip && \ pip3 install cpplint && \ pip3 install --upgrade cmake && \ - apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + apt clean && \ + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f67906d --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: all build rebuild check test testextra memtest memtestextra clean + +all: clean check build test + +clean: + rm -rf build + +check: + cd scripts && ./run_linters.sh + +build: + cd scripts && ./build.sh + +test: + cd scripts && ./run_tests.sh diff --git a/client/include/lib.hpp b/client/include/lib.hpp index 169fb60..e3fc9b0 100644 --- a/client/include/lib.hpp +++ b/client/include/lib.hpp @@ -1,8 +1,5 @@ -#ifndef WATCH_UP_PROJECT_LIB_H -#define WATCH_UP_PROJECT_LIB_H +#pragma once #include std::string foo(); - -#endif //WATCH_UP_PROJECT_LIB_H diff --git a/client/src/client.cpp b/client/src/client.cpp index 6a12f41..7b0b505 100644 --- a/client/src/client.cpp +++ b/client/src/client.cpp @@ -2,8 +2,9 @@ #include "lib.hpp" -int main(){ +int main() { std::cout << "Client exe" << "\n"; std::cout << foo() << "\n"; + for (int i = 1; i < 2; ++i) {} return 0; -} \ No newline at end of file +} diff --git a/client/src/lib.cpp b/client/src/lib.cpp index 7f2dc78..c4b52a4 100644 --- a/client/src/lib.cpp +++ b/client/src/lib.cpp @@ -4,4 +4,4 @@ std::string foo() { return "Foo lib"; -} \ No newline at end of file +} diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 0305c54..4161068 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -6,7 +6,8 @@ add_executable(${PROJECT_NAME} ${TEST_SOURCES}) target_link_libraries(${PROJECT_NAME} client_lib GTest::gtest_main) -add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +#add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +gtest_discover_tests(${PROJECT_NAME}) if (BUILD_COVERAGE) add_coverage(${PROJECT_NAME}) diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..6bce93e --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +set -e + +cd ../ + +mkdir -p build +cd build +cmake .. +cmake --build . diff --git a/scripts/run_linters.sh b/scripts/run_linters.sh new file mode 100644 index 0000000..19fae24 --- /dev/null +++ b/scripts/run_linters.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -o pipefail + +cd ../ + +echo -e "\n==============cppcheck server================" +cppcheck server --enable=all --inconclusive --error-exitcode=1 \ +-I server/include -I server/tests/include --suppress=missingIncludeSystem + +echo -e "\n==============cppcheck client================" +cppcheck client --enable=all --inconclusive --error-exitcode=1 \ +-I client/include -I client/tests/include --suppress=missingIncludeSystem + + + +echo -e "\n==============clang-tidy server================" +clang-tidy server/src/* server/include/* -warnings-as-errors=* -extra-arg=-std=c++17 -- -Iserver/include + +echo -e "\n==============clang-tidy client================" +clang-tidy client/src/* client/include/* -warnings-as-errors=* -extra-arg=-std=c++17 -- -Iclient/include + + +echo -e "\n==============cpplint server====================" +cpplint --extensions=c server/include/* server/src/* + +echo -e "\n==============cpplint client====================" +cpplint --extensions=cpp client/include/* client/src/* \ No newline at end of file diff --git a/scripts/run_tests.sh b/scripts/run_tests.sh new file mode 100644 index 0000000..0d9f7e5 --- /dev/null +++ b/scripts/run_tests.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e + +cd ../build +ctest -V -R + diff --git a/server/include/lib.hpp b/server/include/lib.hpp index 6e12646..b16f542 100644 --- a/server/include/lib.hpp +++ b/server/include/lib.hpp @@ -1,8 +1,6 @@ -#ifndef WATCH_UP_PROJECT_LIB_HPP -#define WATCH_UP_PROJECT_LIB_HPP +#pragma once #include std::string foo(); -#endif //WATCH_UP_PROJECT_LIB_HPP diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt index 8a12b55..051e2f3 100644 --- a/server/tests/CMakeLists.txt +++ b/server/tests/CMakeLists.txt @@ -6,7 +6,8 @@ add_executable(${PROJECT_NAME} ${TEST_SOURCES}) target_link_libraries(${PROJECT_NAME} server_lib GTest::gtest_main) -add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +#add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) +gtest_discover_tests(${PROJECT_NAME}) if (BUILD_COVERAGE) add_coverage(${PROJECT_NAME}) From 73ee0872272b280f1cb8820771804b25b5cbdc7d Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Sat, 22 Oct 2022 16:00:44 +0300 Subject: [PATCH 07/32] ci correction --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58facb0..b699e63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,5 +22,5 @@ jobs: needs: [build] steps: - uses: actions/checkout@v2 - working-directory: ${{github.workspace}}/build + - working-directory: ${{github.workspace}}/build - run: make test \ No newline at end of file From 9ef17ff01c3fa74a8bf5cab5e560d8bf5503bba6 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Sat, 22 Oct 2022 16:11:37 +0300 Subject: [PATCH 08/32] ci correction 2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b699e63..26a4b6d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,4 +23,4 @@ jobs: steps: - uses: actions/checkout@v2 - working-directory: ${{github.workspace}}/build - - run: make test \ No newline at end of file + run: make test \ No newline at end of file From d4b9ad038bd39893e3daf1902db13595c25c9064 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Sat, 22 Oct 2022 16:25:58 +0300 Subject: [PATCH 09/32] allowed execution sh --- .github/workflows/ci.yml | 6 +++--- Makefile | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26a4b6d..f11fe11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest container: ilyaaid/gcc_cmake_gtest_linters steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: make check build: @@ -13,7 +13,7 @@ jobs: container: ilyaaid/gcc_cmake_gtest_linters needs: [check] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - run: make build test: @@ -21,6 +21,6 @@ jobs: container: ilyaaid/gcc_cmake_gtest_linters needs: [build] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - working-directory: ${{github.workspace}}/build run: make test \ No newline at end of file diff --git a/Makefile b/Makefile index f67906d..4f01742 100644 --- a/Makefile +++ b/Makefile @@ -6,10 +6,10 @@ clean: rm -rf build check: - cd scripts && ./run_linters.sh + cd scripts && chmod +x run_linters.sh && ./run_linters.sh build: - cd scripts && ./build.sh + cd scripts && chmod +x build.sh && ./build.sh test: - cd scripts && ./run_tests.sh + cd scripts && chmod +x run_tests.sh && ./run_tests.sh From 709857a62d845e1a239c14942b5237461c7b15dc Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Sat, 22 Oct 2022 16:33:04 +0300 Subject: [PATCH 10/32] ci correction 3 --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f11fe11..a4ce665 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,5 +22,4 @@ jobs: needs: [build] steps: - uses: actions/checkout@v3 - - working-directory: ${{github.workspace}}/build - run: make test \ No newline at end of file + - run: make test \ No newline at end of file From d58e2a5f9cacc38ba5eade24a2785d6aa2eb3fa1 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Sat, 22 Oct 2022 16:40:38 +0300 Subject: [PATCH 11/32] fix makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 4f01742..32305f1 100644 --- a/Makefile +++ b/Makefile @@ -11,5 +11,5 @@ check: build: cd scripts && chmod +x build.sh && ./build.sh -test: +test: build cd scripts && chmod +x run_tests.sh && ./run_tests.sh From a5b3294bb24faeb12b3273715e336c8c6a82500d Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Tue, 25 Oct 2022 12:34:03 +0300 Subject: [PATCH 12/32] add gtest to dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 762e5da..c65fd93 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ENV DEBIAN_FRONTEND noninteractive WORKDIR cash_map COPY . . RUN apt -y update && \ - apt -y install python3 cppcheck clang-tidy make git gcovr python3-pip && \ + apt -y install python3 cppcheck clang-tidy make git gcovr python3-pip libgtest-dev && \ pip3 install cpplint && \ pip3 install --upgrade cmake && \ apt clean && \ From 1ec19740c6102cbcab243874abfc84d5ed6244c8 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Tue, 25 Oct 2022 14:04:01 +0300 Subject: [PATCH 13/32] change container in ci --- .github/workflows/ci.yml | 6 +++--- CMakeLists.txt | 2 +- Dockerfile | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4ce665..4c3d544 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,14 +3,14 @@ on: push jobs: check: runs-on: ubuntu-latest - container: ilyaaid/gcc_cmake_gtest_linters + container: ilyaaid/watch_up steps: - uses: actions/checkout@v3 - run: make check build: runs-on: ubuntu-latest - container: ilyaaid/gcc_cmake_gtest_linters + container: ilyaaid/watch_up needs: [check] steps: - uses: actions/checkout@v3 @@ -18,7 +18,7 @@ jobs: test: runs-on: ubuntu-latest - container: ilyaaid/gcc_cmake_gtest_linters + container: ilyaaid/watch_up needs: [build] steps: - uses: actions/checkout@v3 diff --git a/CMakeLists.txt b/CMakeLists.txt index f85429d..b711a7d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,7 @@ SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic -Werror -Wall -Wextra") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") -hunter_add_package(GTest) +#hunter_add_package(GTest) find_package(GTest CONFIG REQUIRED) if (BUILD_TESTS) diff --git a/Dockerfile b/Dockerfile index c65fd93..4842813 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:20.04 +FROM ubuntu ENV DEBIAN_FRONTEND noninteractive WORKDIR cash_map COPY . . From 534de8279b253cc7aa256b37a9012186b2aaad96 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Tue, 25 Oct 2022 18:40:18 +0300 Subject: [PATCH 14/32] add .dockerignore --- .dockerignore | 6 ++++++ .gitignore | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1912c72 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +cmake-build-debug/ +cmake-build-wsl/ +.idea/ +build/ +Dockerfile +.git diff --git a/.gitignore b/.gitignore index b75634d..efd0e33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ cmake-build-debug/ cmake-build-wsl/ .idea/ -build/ \ No newline at end of file +build/ From e98944f7dc6db16d205f92fc93d28cbcf9c97b87 Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Tue, 25 Oct 2022 23:37:46 +0300 Subject: [PATCH 15/32] update dockerfile --- .gitignore | 2 ++ Dockerfile | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index efd0e33..92b9f01 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ cmake-build-debug/ +cmake-build-docker/ cmake-build-wsl/ +cmake-build-mingw/ .idea/ build/ diff --git a/Dockerfile b/Dockerfile index 4842813..42f6ba1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,8 +3,9 @@ ENV DEBIAN_FRONTEND noninteractive WORKDIR cash_map COPY . . RUN apt -y update && \ - apt -y install python3 cppcheck clang-tidy make git gcovr python3-pip libgtest-dev && \ + apt -y install python3 cppcheck clang-tidy make git \ + gcovr python3-pip libgtest-dev build-essential gcc g++ gdb clang cmake && \ pip3 install cpplint && \ pip3 install --upgrade cmake && \ apt clean && \ - rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \ No newline at end of file From 6bb67c2e4c343783903ae3793d46b548b7ee51d1 Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Fri, 28 Oct 2022 03:24:53 +0300 Subject: [PATCH 16/32] Removed submodule and add coverage workflow. --- .github/workflows/ci.yml | 15 ++++++++++++++- .gitmodules | 3 --- CMakeLists.txt | 11 ++++------- CPPLINT.cfg | 2 +- Makefile | 7 +++++-- client/CMakeLists.txt | 5 ----- client/tests/CMakeLists.txt | 3 --- externals/CMake-codecov | 1 - scripts/coverage.sh | 3 +-- server/CMakeLists.txt | 6 ------ server/src/server.cpp | 2 +- server/tests/CMakeLists.txt | 4 ---- 12 files changed, 26 insertions(+), 36 deletions(-) delete mode 100644 .gitmodules delete mode 160000 externals/CMake-codecov diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c3d544..a1dc2ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,4 +22,17 @@ jobs: needs: [build] steps: - uses: actions/checkout@v3 - - run: make test \ No newline at end of file + - run: make test + + coverage: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + needs: [test] + steps: + - uses: actions/checkout@v3 { echo -n ' '; cat file; } + - run: make coverage + - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt + - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt + - run: git add *README.txt + - run: git commit --allow-empty --author="github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + - run: git push diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f047566..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "externals/CMake-codecov"] - path = externals/CMake-codecov - url = https://github.com/ivan12093/CMake-codecov.git diff --git a/CMakeLists.txt b/CMakeLists.txt index b711a7d..06743ec 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,7 +19,10 @@ SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic -Werror -Wall -Wextra") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") -#hunter_add_package(GTest) +find_package(GTest CONFIG) +if (NOT GTest_FOUND) + hunter_add_package(GTest) +endif() find_package(GTest CONFIG REQUIRED) if (BUILD_TESTS) @@ -30,14 +33,8 @@ endif() if (BUILD_COVERAGE) set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage build." FORCE) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/CMake-codecov/cmake") - find_package(codecov) string(APPEND CMAKE_CXX_FLAGS " --coverage") endif() add_subdirectory(server) add_subdirectory(client) - -if (BUILD_COVERAGE) - coverage_evaluate() -endif() - diff --git a/CPPLINT.cfg b/CPPLINT.cfg index 5fa421e..eb8ba28 100644 --- a/CPPLINT.cfg +++ b/CPPLINT.cfg @@ -5,4 +5,4 @@ filter=-runtime/int filter=-legal/copyright filter=-build/include_subdir filter=-build/include -filter=-readability/casting \ No newline at end of file +filter=-readability/casting diff --git a/Makefile b/Makefile index 32305f1..8478bd4 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ -.PHONY: all build rebuild check test testextra memtest memtestextra clean +.PHONY: all build rebuild check test testextra memtest memtestextra clean coverage -all: clean check build test +all: clean check build test coverage clean: rm -rf build @@ -13,3 +13,6 @@ build: test: build cd scripts && chmod +x run_tests.sh && ./run_tests.sh + +coverage: + cd scripts && chmod +x coverage.sh && ./coverage.sh diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 76bb7e8..0a7441b 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -17,8 +17,3 @@ target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) if (BUILD_TESTS) add_subdirectory(tests) endif() - -if (BUILD_COVERAGE) - add_coverage(${PROJECT_NAME}) - add_coverage(${PROJECT_NAME}_lib) -endif() diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 4161068..652be47 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -9,6 +9,3 @@ target_link_libraries(${PROJECT_NAME} client_lib GTest::gtest_main) #add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) gtest_discover_tests(${PROJECT_NAME}) -if (BUILD_COVERAGE) - add_coverage(${PROJECT_NAME}) -endif() diff --git a/externals/CMake-codecov b/externals/CMake-codecov deleted file mode 160000 index 1974181..0000000 --- a/externals/CMake-codecov +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 1974181d8515441329e3a03ccf47a99518402491 diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 94b9b2d..6babc2d 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -9,5 +9,4 @@ cd ../ cmake -H. -Bbuild -DBUILD_COVERAGE=ON cmake --build build cmake --build build --target test --verbose -cmake --build build --target gcov -gcovr -r . +gcovr -r . | sed '/tests/d' diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index d8e2681..0febcaf 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -17,9 +17,3 @@ target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) if (BUILD_TESTS) add_subdirectory(tests) endif() - -if (BUILD_COVERAGE) - add_coverage(${PROJECT_NAME}) - add_coverage(${PROJECT_NAME}_lib) -endif() - diff --git a/server/src/server.cpp b/server/src/server.cpp index e231862..2a85902 100644 --- a/server/src/server.cpp +++ b/server/src/server.cpp @@ -7,4 +7,4 @@ int main(){ std::cout << "Server exe" << "\n"; std::cout << foo() << "\n"; return 0; -} \ No newline at end of file +} diff --git a/server/tests/CMakeLists.txt b/server/tests/CMakeLists.txt index 051e2f3..d31a4ed 100644 --- a/server/tests/CMakeLists.txt +++ b/server/tests/CMakeLists.txt @@ -8,7 +8,3 @@ target_link_libraries(${PROJECT_NAME} server_lib GTest::gtest_main) #add_test(NAME ${PROJECT_NAME} COMMAND ${PROJECT_NAME}) gtest_discover_tests(${PROJECT_NAME}) - -if (BUILD_COVERAGE) - add_coverage(${PROJECT_NAME}) -endif() From c92b28afcb2cc5cb7d4639d27b5bd358f94a02af Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Fri, 28 Oct 2022 03:28:39 +0300 Subject: [PATCH 17/32] Resolved issue in workflow. --- .github/workflows/ci.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a1dc2ad..bb5194c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ jobs: build: runs-on: ubuntu-latest container: ilyaaid/watch_up - needs: [check] steps: - uses: actions/checkout@v3 - run: make build @@ -19,7 +18,6 @@ jobs: test: runs-on: ubuntu-latest container: ilyaaid/watch_up - needs: [build] steps: - uses: actions/checkout@v3 - run: make test @@ -27,12 +25,16 @@ jobs: coverage: runs-on: ubuntu-latest container: ilyaaid/watch_up - needs: [test] steps: - - uses: actions/checkout@v3 { echo -n ' '; cat file; } + - uses: actions/checkout@v3 - run: make coverage - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt - - run: git add *README.txt - - run: git commit --allow-empty --author="github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" + - run: git config --global --add safe.directory /__w/test/test + - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + - run: git config --global user.name "github-actions[bot]" + - run: git add client/README.txt server/README.txt + - run: git commit -m "auto commit coverage report" - run: git push + - run: ls -la + - run: pwd From b6a7f2590789559a6d0b5a66e302191986f769b5 Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Fri, 28 Oct 2022 03:45:28 +0300 Subject: [PATCH 18/32] Resolved issue in workflow. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb5194c..7426ae9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,7 +30,7 @@ jobs: - run: make coverage - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt - - run: git config --global --add safe.directory /__w/test/test + - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - run: git config --global user.name "github-actions[bot]" - run: git add client/README.txt server/README.txt From 22aa92e1b943d66f81bc498f0aa9bc89db89c75a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Oct 2022 00:46:21 +0000 Subject: [PATCH 19/32] auto commit coverage report --- client/README.txt | 13 +++++++++++++ server/README.txt | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 client/README.txt create mode 100644 server/README.txt diff --git a/client/README.txt b/client/README.txt new file mode 100644 index 0000000..af5c657 --- /dev/null +++ b/client/README.txt @@ -0,0 +1,13 @@ +------------------------------------------------------------------------------ + GCC Code Coverage Report +Directory: . +------------------------------------------------------------------------------ +File Lines Exec Cover Missing +------------------------------------------------------------------------------ +client/src/client.cpp 5 0 0% 5-9 +client/src/lib.cpp 2 2 100% +------------------------------------------------------------------------------ +TOTAL 21 12 57% +------------------------------------------------------------------------------ +lines: 57.1% (12 out of 21) +branches: 23.8% (10 out of 42) diff --git a/server/README.txt b/server/README.txt new file mode 100644 index 0000000..c705e9c --- /dev/null +++ b/server/README.txt @@ -0,0 +1,13 @@ +------------------------------------------------------------------------------ + GCC Code Coverage Report +Directory: . +------------------------------------------------------------------------------ +File Lines Exec Cover Missing +------------------------------------------------------------------------------ +server/src/lib.cpp 2 2 100% +server/src/server.cpp 4 0 0% 5,7-9 +------------------------------------------------------------------------------ +TOTAL 21 12 57% +------------------------------------------------------------------------------ +lines: 57.1% (12 out of 21) +branches: 23.8% (10 out of 42) From 763939742a9bbd7b97fba233feebaa14ee547cd0 Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Fri, 28 Oct 2022 15:38:55 +0300 Subject: [PATCH 20/32] added artifact. --- .github/workflows/ci.yml | 86 +++++++++++++++++++++------------------- .gitmodules | 3 ++ CMakeLists.txt | 5 +++ client/CMakeLists.txt | 5 +++ externals/CMake-codecov | 1 + scripts/coverage.sh | 1 + server/CMakeLists.txt | 5 +++ 7 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 .gitmodules create mode 160000 externals/CMake-codecov diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7426ae9..cd7477a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,40 +1,46 @@ -on: push - -jobs: - check: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make check - - build: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make build - - test: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make test - - coverage: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make coverage - - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt - - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt - - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map - - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - run: git config --global user.name "github-actions[bot]" - - run: git add client/README.txt server/README.txt - - run: git commit -m "auto commit coverage report" - - run: git push - - run: ls -la - - run: pwd +on: push + +jobs: + check: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make check + + build: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make build + + test: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make test + + coverage: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: apt -y update + - run: apt -y install lcov + - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map + - run: git submodule update --init + - run: make coverage + - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt + - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt + - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + - run: git config --global user.name "github-actions[bot]" + - run: git add client/README.txt server/README.txt + - run: git commit -m "auto commit coverage report" + - run: git push + - uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: build/lcov/html + retention-days: 5 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f047566 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "externals/CMake-codecov"] + path = externals/CMake-codecov + url = https://github.com/ivan12093/CMake-codecov.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 06743ec..305a23c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,8 +33,13 @@ endif() if (BUILD_COVERAGE) set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage build." FORCE) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/CMake-codecov/cmake") + find_package(codecov) string(APPEND CMAKE_CXX_FLAGS " --coverage") endif() add_subdirectory(server) add_subdirectory(client) + +if (BUILD_COVERAGE) + coverage_evaluate() +endif() diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0a7441b..76bb7e8 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -17,3 +17,8 @@ target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) if (BUILD_TESTS) add_subdirectory(tests) endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() diff --git a/externals/CMake-codecov b/externals/CMake-codecov new file mode 160000 index 0000000..1974181 --- /dev/null +++ b/externals/CMake-codecov @@ -0,0 +1 @@ +Subproject commit 1974181d8515441329e3a03ccf47a99518402491 diff --git a/scripts/coverage.sh b/scripts/coverage.sh index 6babc2d..ce7673c 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -9,4 +9,5 @@ cd ../ cmake -H. -Bbuild -DBUILD_COVERAGE=ON cmake --build build cmake --build build --target test --verbose +cmake --build build --target lcov gcovr -r . | sed '/tests/d' diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 0febcaf..916c2ee 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -17,3 +17,8 @@ target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) if (BUILD_TESTS) add_subdirectory(tests) endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() From a47165911a1a22cc44a7b6d41fce84b14113cd9b Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Fri, 28 Oct 2022 16:10:54 +0300 Subject: [PATCH 21/32] fixed workflow. --- .github/workflows/ci.yml | 91 ++++++++++++++++++++-------------------- .gitmodules | 6 +-- CMakeLists.txt | 90 +++++++++++++++++++-------------------- client/CMakeLists.txt | 48 ++++++++++----------- scripts/coverage.sh | 26 ++++++------ server/CMakeLists.txt | 48 ++++++++++----------- 6 files changed, 154 insertions(+), 155 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd7477a..2409220 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,46 +1,45 @@ -on: push - -jobs: - check: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make check - - build: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make build - - test: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: make test - - coverage: - runs-on: ubuntu-latest - container: ilyaaid/watch_up - steps: - - uses: actions/checkout@v3 - - run: apt -y update - - run: apt -y install lcov - - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map - - run: git submodule update --init - - run: make coverage - - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt - - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt - - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" - - run: git config --global user.name "github-actions[bot]" - - run: git add client/README.txt server/README.txt - - run: git commit -m "auto commit coverage report" - - run: git push - - uses: actions/upload-artifact@v3 - with: - name: coverage-report - path: build/lcov/html - retention-days: 5 +on: push + +jobs: + check: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make check + + build: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make build + + test: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: make test + + coverage: + runs-on: ubuntu-latest + container: ilyaaid/watch_up + steps: + - uses: actions/checkout@v3 + - run: apt -y update + - run: apt -y install lcov + - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map + - run: git submodule update --init + - run: make coverage + - run: gcovr -r . -s | sed '/tests/d' | sed '/server/d' > client/README.txt + - run: gcovr -r . -s | sed '/tests/d' | sed '/client/d' > server/README.txt + - run: git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + - run: git config --global user.name "github-actions[bot]" + - run: git add client/README.txt server/README.txt + - run: git commit -m "auto commit coverage report" && git push || echo "coverage not changed." + - uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: build/lcov/html + retention-days: 5 diff --git a/.gitmodules b/.gitmodules index f047566..5fcaa96 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "externals/CMake-codecov"] - path = externals/CMake-codecov - url = https://github.com/ivan12093/CMake-codecov.git +[submodule "externals/CMake-codecov"] + path = externals/CMake-codecov + url = https://github.com/ivan12093/CMake-codecov.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 305a23c..8a50b04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,45 +1,45 @@ -cmake_minimum_required(VERSION 3.18) - -include("cmake/HunterGate.cmake") -HunterGate( - URL "https://github.com/cpp-pm/hunter/archive/v0.23.314.tar.gz" - SHA1 "95c47c92f68edb091b5d6d18924baabe02a6962a" -) - -project(watch_up_project) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -option(BUILD_TESTS "Build tests" ON) -option(BUILD_DOCS "Build documentation" OFF) -option(BUILD_COVERAGE "Build code coverage" OFF) - -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic -Werror -Wall -Wextra") -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") -SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") - -find_package(GTest CONFIG) -if (NOT GTest_FOUND) - hunter_add_package(GTest) -endif() -find_package(GTest CONFIG REQUIRED) - -if (BUILD_TESTS) - include(GoogleTest) - enable_testing() -endif() - -if (BUILD_COVERAGE) - set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage build." FORCE) - list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/CMake-codecov/cmake") - find_package(codecov) - string(APPEND CMAKE_CXX_FLAGS " --coverage") -endif() - -add_subdirectory(server) -add_subdirectory(client) - -if (BUILD_COVERAGE) - coverage_evaluate() -endif() +cmake_minimum_required(VERSION 3.18) + +include("cmake/HunterGate.cmake") +HunterGate( + URL "https://github.com/cpp-pm/hunter/archive/v0.23.314.tar.gz" + SHA1 "95c47c92f68edb091b5d6d18924baabe02a6962a" +) + +project(watch_up_project) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +option(BUILD_TESTS "Build tests" ON) +option(BUILD_DOCS "Build documentation" OFF) +option(BUILD_COVERAGE "Build code coverage" OFF) + +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wpedantic -Werror -Wall -Wextra") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wnon-virtual-dtor") + +find_package(GTest CONFIG) +if (NOT GTest_FOUND) + hunter_add_package(GTest) +endif() +find_package(GTest CONFIG REQUIRED) + +if (BUILD_TESTS) + include(GoogleTest) + enable_testing() +endif() + +if (BUILD_COVERAGE) + set(ENABLE_COVERAGE ON CACHE BOOL "Enable coverage build." FORCE) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/CMake-codecov/cmake") + find_package(codecov) + string(APPEND CMAKE_CXX_FLAGS " --coverage") +endif() + +add_subdirectory(server) +add_subdirectory(client) + +if (BUILD_COVERAGE) + coverage_evaluate() +endif() diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 76bb7e8..3ec0098 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,24 +1,24 @@ -project(client) - -file(GLOB SOURCES "src/*.cpp") - -list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/client.cpp") - -add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) - -target_include_directories(${PROJECT_NAME}_lib PUBLIC include) - -add_executable(${PROJECT_NAME} ./src/client.cpp) - -target_include_directories(${PROJECT_NAME} PUBLIC include) - -target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) - -if (BUILD_TESTS) - add_subdirectory(tests) -endif() - -if (BUILD_COVERAGE) - add_coverage(${PROJECT_NAME}) - add_coverage(${PROJECT_NAME}_lib) -endif() +project(client) + +file(GLOB SOURCES "src/*.cpp") + +list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/client.cpp") + +add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) + +target_include_directories(${PROJECT_NAME}_lib PUBLIC include) + +add_executable(${PROJECT_NAME} ./src/client.cpp) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) + +if (BUILD_TESTS) + add_subdirectory(tests) +endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() diff --git a/scripts/coverage.sh b/scripts/coverage.sh index ce7673c..ece68c2 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -1,13 +1,13 @@ -#!/usr/bin/env bash - -set -e -export GTEST_COLOR=1 -export CTEST_OUTPUT_ON_FAILURE=true - -cd ../ - -cmake -H. -Bbuild -DBUILD_COVERAGE=ON -cmake --build build -cmake --build build --target test --verbose -cmake --build build --target lcov -gcovr -r . | sed '/tests/d' +#!/usr/bin/env bash + +set -e +export GTEST_COLOR=1 +export CTEST_OUTPUT_ON_FAILURE=true + +cd ../ + +cmake -H. -Bbuild -DBUILD_COVERAGE=ON +cmake --build build +cmake --build build --target test --verbose +cmake --build build --target lcov +gcovr -r . | sed '/tests/d' diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 916c2ee..5d30db4 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,24 +1,24 @@ -project(server) - -file(GLOB SOURCES "src/*.cpp") - -list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/server.cpp") - -add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) - -target_include_directories(${PROJECT_NAME}_lib PUBLIC include) - -add_executable(${PROJECT_NAME} ./src/server.cpp) - -target_include_directories(${PROJECT_NAME} PUBLIC include) - -target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) - -if (BUILD_TESTS) - add_subdirectory(tests) -endif() - -if (BUILD_COVERAGE) - add_coverage(${PROJECT_NAME}) - add_coverage(${PROJECT_NAME}_lib) -endif() +project(server) + +file(GLOB SOURCES "src/*.cpp") + +list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/server.cpp") + +add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) + +target_include_directories(${PROJECT_NAME}_lib PUBLIC include) + +add_executable(${PROJECT_NAME} ./src/server.cpp) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) + +if (BUILD_TESTS) + add_subdirectory(tests) +endif() + +if (BUILD_COVERAGE) + add_coverage(${PROJECT_NAME}) + add_coverage(${PROJECT_NAME}_lib) +endif() From b437f342cd6c282d6a4b7eea20909e8584a0547a Mon Sep 17 00:00:00 2001 From: Ivan Bulgakov Date: Fri, 28 Oct 2022 16:14:39 +0300 Subject: [PATCH 22/32] fixed workflow. --- scripts/coverage.sh | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/scripts/coverage.sh b/scripts/coverage.sh index ece68c2..ce7673c 100644 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -1,13 +1,13 @@ -#!/usr/bin/env bash - -set -e -export GTEST_COLOR=1 -export CTEST_OUTPUT_ON_FAILURE=true - -cd ../ - -cmake -H. -Bbuild -DBUILD_COVERAGE=ON -cmake --build build -cmake --build build --target test --verbose -cmake --build build --target lcov -gcovr -r . | sed '/tests/d' +#!/usr/bin/env bash + +set -e +export GTEST_COLOR=1 +export CTEST_OUTPUT_ON_FAILURE=true + +cd ../ + +cmake -H. -Bbuild -DBUILD_COVERAGE=ON +cmake --build build +cmake --build build --target test --verbose +cmake --build build --target lcov +gcovr -r . | sed '/tests/d' From 80fabeea6225ecf056bcefc18a0686d69e2ac5cb Mon Sep 17 00:00:00 2001 From: ilyaaid Date: Fri, 28 Oct 2022 23:00:44 +0300 Subject: [PATCH 23/32] add lcov to project image --- .github/workflows/ci.yml | 2 -- Dockerfile | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2409220..245ceb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,8 +27,6 @@ jobs: container: ilyaaid/watch_up steps: - uses: actions/checkout@v3 - - run: apt -y update - - run: apt -y install lcov - run: git config --global --add safe.directory /__w/2022_2_cash_map/2022_2_cash_map - run: git submodule update --init - run: make coverage diff --git a/Dockerfile b/Dockerfile index 42f6ba1..aea7085 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ ENV DEBIAN_FRONTEND noninteractive WORKDIR cash_map COPY . . RUN apt -y update && \ - apt -y install python3 cppcheck clang-tidy make git \ + apt -y install python3 cppcheck clang-tidy make git lcov \ gcovr python3-pip libgtest-dev build-essential gcc g++ gdb clang cmake && \ pip3 install cpplint && \ pip3 install --upgrade cmake && \ From c683b4c7270da5e8cc633a1e870673bc28922f17 Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Mon, 21 Nov 2022 17:45:03 +0300 Subject: [PATCH 24/32] server code frame & uml --- server/CMakeLists.txt | 4 +-- server/include/chat.h | 14 +++++++++ server/include/lib.hpp | 6 ---- server/include/listener.h | 26 ++++++++++++++++ server/include/room.h | 37 +++++++++++++++++++++++ server/include/server.h | 22 ++++++++++++++ server/include/shared_state.h | 17 +++++++++++ server/include/viewer.h | 55 ++++++++++++++++++++++++++++++++++ server/include/ws_session.hpp | 28 +++++++++++++++++ server/main.cpp | 10 +++++++ server/src/lib.cpp | 7 ----- server/src/server.cpp | 12 ++++---- server/tests/test.cpp | 4 +-- server/uml/uml_server | 1 + server/uml/uml_server.jpg | Bin 0 -> 196663 bytes 15 files changed, 218 insertions(+), 25 deletions(-) create mode 100644 server/include/chat.h delete mode 100644 server/include/lib.hpp create mode 100644 server/include/listener.h create mode 100644 server/include/room.h create mode 100644 server/include/server.h create mode 100644 server/include/shared_state.h create mode 100644 server/include/viewer.h create mode 100644 server/include/ws_session.hpp create mode 100644 server/main.cpp delete mode 100644 server/src/lib.cpp create mode 100644 server/uml/uml_server create mode 100644 server/uml/uml_server.jpg diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 5d30db4..848fd25 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -2,13 +2,11 @@ project(server) file(GLOB SOURCES "src/*.cpp") -list(REMOVE_ITEM SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/src/server.cpp") - add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) target_include_directories(${PROJECT_NAME}_lib PUBLIC include) -add_executable(${PROJECT_NAME} ./src/server.cpp) +add_executable(${PROJECT_NAME} main.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) diff --git a/server/include/chat.h b/server/include/chat.h new file mode 100644 index 0000000..783025d --- /dev/null +++ b/server/include/chat.h @@ -0,0 +1,14 @@ +#ifndef WATCH_UP_PROJECT_CHAT_H +#define WATCH_UP_PROJECT_CHAT_H + +class IChat{ +public: +}; + +class Chat : public IChat{ +public: + +}; + + +#endif //WATCH_UP_PROJECT_CHAT_H diff --git a/server/include/lib.hpp b/server/include/lib.hpp deleted file mode 100644 index b16f542..0000000 --- a/server/include/lib.hpp +++ /dev/null @@ -1,6 +0,0 @@ -#pragma once - -#include - -std::string foo(); - diff --git a/server/include/listener.h b/server/include/listener.h new file mode 100644 index 0000000..a1e59e1 --- /dev/null +++ b/server/include/listener.h @@ -0,0 +1,26 @@ +#ifndef WATCH_UP_PROJECT_LISTENER_H +#define WATCH_UP_PROJECT_LISTENER_H + +#include + +#include + +#include "shared_state.h" + +class Listener: public std::enable_shared_from_this{ +public: + Listener(boost::asio::io_context& ioc, boost::asio::ip::tcp::endpoint endpoint); + ~Listener(); + + void async_accept(); + void on_accept(); + +private: + Shared_state state_; + boost::asio::io_context& ioc_; + boost::asio::ip::tcp::acceptor acceptor_; + boost::asio::ip::tcp::socket socket_; + +}; + +#endif //WATCH_UP_PROJECT_LISTENER_H diff --git a/server/include/room.h b/server/include/room.h new file mode 100644 index 0000000..6c363c1 --- /dev/null +++ b/server/include/room.h @@ -0,0 +1,37 @@ +#ifndef WATCH_UP_PROJECT_ROOM_H +#define WATCH_UP_PROJECT_ROOM_H + +#include +#include + +#include "viewer.h" + +typedef std::weak_ptr w_viewer_ptr; +typedef std::shared_ptr viewer_ptr; + +class IRoom { +public: + virtual ~IRoom() = 0; + virtual void join (viewer_ptr)= 0; + virtual void leave(viewer_ptr)= 0; + virtual void check_timings() = 0; + virtual void pause() = 0; + virtual void play() = 0; + virtual void synchronize() = 0; + +}; + + +class Room: public IRoom{ +public: + void join (viewer_ptr) override; + void leave(viewer_ptr) override; + void check_timings() override; + void pause() override; + void play() override; + void synchronize() override; +private: + std::vector members_; //TODO choose container +}; + +#endif //WATCH_UP_PROJECT_ROOM_H diff --git a/server/include/server.h b/server/include/server.h new file mode 100644 index 0000000..d33111b --- /dev/null +++ b/server/include/server.h @@ -0,0 +1,22 @@ +#ifndef WATCH_UP_PROJECT_SERVER_HPP +#define WATCH_UP_PROJECT_SERVER_HPP + +#include + +struct Options{ + std::string ip; + unsigned short port; +}; + +class Server{ +public: + Server(); + Server(const Options& opts); + void run(); + void stop(); + +private: + bool started; + Options opts_; +}; +#endif //WATCH_UP_PROJECT_SERVER_HPP diff --git a/server/include/shared_state.h b/server/include/shared_state.h new file mode 100644 index 0000000..458a9d3 --- /dev/null +++ b/server/include/shared_state.h @@ -0,0 +1,17 @@ +#ifndef WATCH_UP_PROJECT_SHARED_STATE_H +#define WATCH_UP_PROJECT_SHARED_STATE_H + +#include +#include + +#include "room.h" + + +typedef std::shared_ptr room_ptr; + +class Shared_state{ +public: + std::vector rooms_; +}; + +#endif //WATCH_UP_PROJECT_SHARED_STATE_H diff --git a/server/include/viewer.h b/server/include/viewer.h new file mode 100644 index 0000000..475071a --- /dev/null +++ b/server/include/viewer.h @@ -0,0 +1,55 @@ +#ifndef WATCH_UP_PROJECT_VIEWER_H +#define WATCH_UP_PROJECT_VIEWER_H + +#include +#include + +#include "room.h" +#include "chat.h" + + +typedef std::shared_ptr room_ptr; +typedef std::shared_ptr chat_ptr; + +struct Access_options{ + bool can_pause; + bool can_rewind; +}; + + +class IViewer { +public: + virtual ~IViewer(); + virtual void pause() =0; + virtual void play() =0; + virtual void rewind(int offset) =0; + virtual std::string get_nickname() =0; + virtual void set_nickname(std::string) =0; + virtual void send_chat_msg(std::string msg) =0; + virtual void set_access_opts(Access_options opts)=0; +}; + +class Viewer: public IViewer{ +public: + void pause() override; + void play() override; + void rewind(int offset) override; + std::string get_nickname() override; + void set_nickname(std::string) override; + void send_chat_msg(std::string msg) override; + void set_access_opts(Access_options opts)override; +protected: + room_ptr room_; + chat_ptr chat_; + +}; + +class Host: public Viewer{ +public: + void synchronize(); + void change_access_opts(); +}; + +#endif //WATCH_UP_PROJECT_VIEWER_H + + diff --git a/server/include/ws_session.hpp b/server/include/ws_session.hpp new file mode 100644 index 0000000..4a3eec7 --- /dev/null +++ b/server/include/ws_session.hpp @@ -0,0 +1,28 @@ +#ifndef WATCH_UP_PROJECT_WS_SESSION_HPP +#define WATCH_UP_PROJECT_WS_SESSION_HPP + +#include + +#include + +namespace beast = boost::beast; +namespace websocket = boost::beast::websocket; + +class WS_Session : std::enable_shared_from_this { +public: + void run(); + void on_run(); + void on_accept(); + void on_read(); + void handle_request(); + +private: + websocket::stream ws_; + beast::flat_buffer buffer_; + +}; + + +#endif //WATCH_UP_PROJECT_WS_SESSION_HPP + + diff --git a/server/main.cpp b/server/main.cpp new file mode 100644 index 0000000..a0e016e --- /dev/null +++ b/server/main.cpp @@ -0,0 +1,10 @@ +#include + +#include "server.h" + +int main(){ + + Server server; + server.run(); + return 0; +} diff --git a/server/src/lib.cpp b/server/src/lib.cpp deleted file mode 100644 index a18be76..0000000 --- a/server/src/lib.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include - -#include "lib.hpp" - -std::string foo() { - return "foo"; -} diff --git a/server/src/server.cpp b/server/src/server.cpp index 2a85902..254644f 100644 --- a/server/src/server.cpp +++ b/server/src/server.cpp @@ -1,10 +1,10 @@ -#include +#include "server.h" -#include "lib.hpp" +#include -int main(){ +Server::Server(): started(false), opts_({"127.0.0.1", 8080}) +{} - std::cout << "Server exe" << "\n"; - std::cout << foo() << "\n"; - return 0; +void Server::run() { + std::cout << "Server run" << std::endl; } diff --git a/server/tests/test.cpp b/server/tests/test.cpp index 22b0d75..c68e28c 100644 --- a/server/tests/test.cpp +++ b/server/tests/test.cpp @@ -1,9 +1,7 @@ #include #include -#include "lib.hpp" TEST(Lib_test, test_foo) { - std::string a = "foo"; - EXPECT_EQ(a, foo()); + EXPECT_EQ(1,1); } diff --git a/server/uml/uml_server b/server/uml/uml_server new file mode 100644 index 0000000..3d59a37 --- /dev/null +++ b/server/uml/uml_server @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/server/uml/uml_server.jpg b/server/uml/uml_server.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0780221233cd254e25fe570e93783234f1620c51 GIT binary patch literal 196663 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 Date: Mon, 21 Nov 2022 14:46:23 +0000 Subject: [PATCH 25/32] auto commit coverage report --- client/README.txt | 6 +++--- server/README.txt | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/README.txt b/client/README.txt index af5c657..166cc18 100644 --- a/client/README.txt +++ b/client/README.txt @@ -7,7 +7,7 @@ File Lines Exec Cover Missing client/src/client.cpp 5 0 0% 5-9 client/src/lib.cpp 2 2 100% ------------------------------------------------------------------------------ -TOTAL 21 12 57% +TOTAL 21 9 42% ------------------------------------------------------------------------------ -lines: 57.1% (12 out of 21) -branches: 23.8% (10 out of 42) +lines: 42.9% (9 out of 21) +branches: 18.4% (7 out of 38) diff --git a/server/README.txt b/server/README.txt index c705e9c..aea75ef 100644 --- a/server/README.txt +++ b/server/README.txt @@ -4,10 +4,10 @@ Directory: . ------------------------------------------------------------------------------ File Lines Exec Cover Missing ------------------------------------------------------------------------------ -server/src/lib.cpp 2 2 100% -server/src/server.cpp 4 0 0% 5,7-9 +server/main.cpp 4 0 0% 5,7-9 +server/src/server.cpp 3 0 0% 5,8-9 ------------------------------------------------------------------------------ -TOTAL 21 12 57% +TOTAL 21 9 42% ------------------------------------------------------------------------------ -lines: 57.1% (12 out of 21) -branches: 23.8% (10 out of 42) +lines: 42.9% (9 out of 21) +branches: 18.4% (7 out of 38) From 9b0663ceae1800d09954deeb822991b42013085d Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Tue, 6 Dec 2022 13:52:10 +0300 Subject: [PATCH 26/32] rooms w/o sync impl --- server/CMakeLists.txt | 10 +++- server/include/listener.h | 12 +++-- server/include/room.h | 56 ++++++++++++++----- server/include/room_creator.hpp | 20 +++++++ server/include/serializer.h | 33 ++++++++++++ server/include/shared_state.h | 23 ++++++-- server/include/types.hpp | 17 ++++++ server/include/viewer.h | 82 ++++++++++++++++++++-------- server/include/viewer_creator.hpp | 24 +++++++++ server/include/viewer_manager.hpp | 73 +++++++++++++++++++++++++ server/include/ws_session.hpp | 44 ++++++++++----- server/src/listener.cpp | 68 +++++++++++++++++++++++ server/src/room.cpp | 73 +++++++++++++++++++++++++ server/src/server.cpp | 20 +++++++ server/src/shared_state.cpp | 28 ++++++++++ server/src/viewer.cpp | 89 +++++++++++++++++++++++++++++++ server/src/ws_session.cpp | 42 +++++++++++++++ 17 files changed, 656 insertions(+), 58 deletions(-) create mode 100644 server/include/room_creator.hpp create mode 100644 server/include/serializer.h create mode 100644 server/include/types.hpp create mode 100644 server/include/viewer_creator.hpp create mode 100644 server/include/viewer_manager.hpp create mode 100644 server/src/listener.cpp create mode 100644 server/src/room.cpp create mode 100644 server/src/shared_state.cpp create mode 100644 server/src/viewer.cpp create mode 100644 server/src/ws_session.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 848fd25..41755bc 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,6 +1,9 @@ project(server) -file(GLOB SOURCES "src/*.cpp") +file(GLOB SOURCES "src/*") + +find_package(Boost REQUIRED) +find_package(Threads REQUIRED) add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) @@ -10,7 +13,10 @@ add_executable(${PROJECT_NAME} main.cpp) target_include_directories(${PROJECT_NAME} PUBLIC include) -target_link_libraries(${PROJECT_NAME} ${PROJECT_NAME}_lib) +target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_lib) +target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) +target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) + if (BUILD_TESTS) add_subdirectory(tests) diff --git a/server/include/listener.h b/server/include/listener.h index a1e59e1..a85f68f 100644 --- a/server/include/listener.h +++ b/server/include/listener.h @@ -7,16 +7,18 @@ #include "shared_state.h" -class Listener: public std::enable_shared_from_this{ +typedef std::shared_ptr state_ptr; + +class listener: public std::enable_shared_from_this{ public: - Listener(boost::asio::io_context& ioc, boost::asio::ip::tcp::endpoint endpoint); - ~Listener(); + listener(boost::asio::io_context& ioc, boost::asio::ip::tcp::endpoint&& endpoint); + ~listener(); void async_accept(); - void on_accept(); + void on_accept(boost::system::error_code& ec); private: - Shared_state state_; + state_ptr state_; boost::asio::io_context& ioc_; boost::asio::ip::tcp::acceptor acceptor_; boost::asio::ip::tcp::socket socket_; diff --git a/server/include/room.h b/server/include/room.h index 6c363c1..1ad34ce 100644 --- a/server/include/room.h +++ b/server/include/room.h @@ -4,34 +4,64 @@ #include #include -#include "viewer.h" +#include +#include +#include +#include +#include + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; +using uuid = boost::uuids::uuid; + + +class IViewer; +class Host; +class shared_state; typedef std::weak_ptr w_viewer_ptr; typedef std::shared_ptr viewer_ptr; +typedef std::weak_ptr w_host_ptr; +typedef std::shared_ptr state_ptr; + class IRoom { public: - virtual ~IRoom() = 0; - virtual void join (viewer_ptr)= 0; - virtual void leave(viewer_ptr)= 0; - virtual void check_timings() = 0; - virtual void pause() = 0; - virtual void play() = 0; - virtual void synchronize() = 0; - + virtual ~IRoom() = default; + virtual void join (w_viewer_ptr) = 0; + virtual void leave(uuid id) = 0; + virtual void check_timings() = 0; + virtual void pause() = 0; + virtual void play() = 0; + virtual void synchronize() = 0; + virtual const uuid get_id() const = 0; }; -class Room: public IRoom{ +class Room: public IRoom, public std::enable_shared_from_this{ public: - void join (viewer_ptr) override; - void leave(viewer_ptr) override; + Room(w_host_ptr host, uuid id, state_ptr state); + ~Room() override; + void start(); + void join (w_viewer_ptr) override; + void leave(uuid id) override; void check_timings() override; void pause() override; void play() override; void synchronize() override; + + const uuid get_id() const override; + private: - std::vector members_; //TODO choose container + std::unordered_map> participants_; + uuid id_; + w_host_ptr host_; + state_ptr state_; + boost::asio::deadline_timer check_timer_; }; #endif //WATCH_UP_PROJECT_ROOM_H diff --git a/server/include/room_creator.hpp b/server/include/room_creator.hpp new file mode 100644 index 0000000..997e41f --- /dev/null +++ b/server/include/room_creator.hpp @@ -0,0 +1,20 @@ +#ifndef WATCH_UP_PROJECT_ROOM_CREATOR_HPP +#define WATCH_UP_PROJECT_ROOM_CREATOR_HPP + +#include +#include + +#include "room.h" +#include "shared_state.h" + +typedef boost::uuids::uuid uuid; +class Host; + +class room_creator{ +public: + static std::shared_ptr create_room(const std::weak_ptr& host, state_ptr state){ + return std::make_shared(host, boost::uuids::random_generator()(), std::move(state)); + } +}; + +#endif //WATCH_UP_PROJECT_ROOM_CREATOR_HPP diff --git a/server/include/serializer.h b/server/include/serializer.h new file mode 100644 index 0000000..38cc2a2 --- /dev/null +++ b/server/include/serializer.h @@ -0,0 +1,33 @@ +#ifndef WATCH_UP_PROJECT_SERIALIZER_H +#define WATCH_UP_PROJECT_SERIALIZER_H + +#include + +#include "nlohmann/json.hpp" +#include +#include +#include + +#include "types.hpp" + +using uuid = boost::uuids::uuid; + +class serializer { +public: + static nlohmann::json parse(const std::string& json_str){ + return nlohmann::json::parse(json_str); + } + + static type get_type(const std::string& json_str){ + auto json = parse(json_str); + return json["type"].get(); + } + + static uuid get_room_id(const std::string& json_str){ + auto json = parse(json_str); + return boost::lexical_cast + (std::string(json["room_id"])); + } +}; + +#endif //WATCH_UP_PROJECT_SERIALIZER_H diff --git a/server/include/shared_state.h b/server/include/shared_state.h index 458a9d3..b088b55 100644 --- a/server/include/shared_state.h +++ b/server/include/shared_state.h @@ -2,16 +2,31 @@ #define WATCH_UP_PROJECT_SHARED_STATE_H #include -#include +#include -#include "room.h" +#include +#include +#include +#include "room.h" typedef std::shared_ptr room_ptr; +typedef std::weak_ptr w_room_ptr; +typedef std::shared_ptr viewer_ptr; +typedef boost::uuids::uuid uuid; -class Shared_state{ +class shared_state: public std::enable_shared_from_this{ public: - std::vector rooms_; + explicit shared_state(net::io_context &ioc); + void add_room(const uuid &room_id, w_room_ptr&& room); + void remove_room(uuid room_id); + w_room_ptr get_room(uuid room_id); + net::io_context &get_io_context() const; +private: + std::unordered_map> rooms_; + net::io_context &ioc_; }; + + #endif //WATCH_UP_PROJECT_SHARED_STATE_H diff --git a/server/include/types.hpp b/server/include/types.hpp new file mode 100644 index 0000000..4587fa2 --- /dev/null +++ b/server/include/types.hpp @@ -0,0 +1,17 @@ +#ifndef WATCH_UP_PROJECT_TYPES_HPP +#define WATCH_UP_PROJECT_TYPES_HPP + +enum type { + invalid = -1, + create = 0, + join, +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(type, { + {invalid, nullptr}, + {create, "create"}, + {join, "join"} +}) + + +#endif //WATCH_UP_PROJECT_TYPES_HPP diff --git a/server/include/viewer.h b/server/include/viewer.h index 475071a..ea9adf7 100644 --- a/server/include/viewer.h +++ b/server/include/viewer.h @@ -1,51 +1,91 @@ #ifndef WATCH_UP_PROJECT_VIEWER_H #define WATCH_UP_PROJECT_VIEWER_H +#include #include #include +#include +#include +#include + + #include "room.h" -#include "chat.h" +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; +using uuid = boost::uuids::uuid; + typedef std::shared_ptr room_ptr; -typedef std::shared_ptr chat_ptr; +typedef websocket::stream ws_stream; +typedef std::unique_ptr stream_ptr; + -struct Access_options{ +struct access_options{ bool can_pause; bool can_rewind; }; -class IViewer { +class IViewer{ public: - virtual ~IViewer(); - virtual void pause() =0; - virtual void play() =0; - virtual void rewind(int offset) =0; - virtual std::string get_nickname() =0; - virtual void set_nickname(std::string) =0; - virtual void send_chat_msg(std::string msg) =0; - virtual void set_access_opts(Access_options opts)=0; + virtual ~IViewer() = default; + virtual void start() = 0; + virtual void pause() = 0; + virtual void play() = 0; + virtual void rewind(int offset) = 0; + virtual void send_chat_msg(std::string msg) = 0; + + virtual void do_read() = 0; + + virtual uuid get_id() const = 0; + virtual std::string get_nickname() const = 0; + + virtual void set_nickname(std::string) = 0; + virtual void set_room(const room_ptr& room) = 0; + virtual void set_access_opts(access_options opts)= 0; + }; -class Viewer: public IViewer{ +class Viewer: public IViewer, public std::enable_shared_from_this { public: - void pause() override; - void play() override; - void rewind(int offset) override; - std::string get_nickname() override; - void set_nickname(std::string) override; - void send_chat_msg(std::string msg) override; - void set_access_opts(Access_options opts)override; + Viewer(stream_ptr && ws, uuid&& id, room_ptr&& room = nullptr); + ~Viewer() override; + + void start() override; + void pause() override; + void play() override; + void rewind(int ) override; + void send_chat_msg(std::string ) override; + + void do_read() override; + + uuid get_id() const override; + std::string get_nickname() const override; + + void set_nickname(std::string) override; + void set_room(const room_ptr& room) override; + void set_access_opts(access_options )override; + protected: + uuid id_; room_ptr room_; - chat_ptr chat_; + std::string nickname_; + access_options a_opts_{}; + + stream_ptr ws_; + beast::flat_buffer buffer_; + // TODO response queue }; class Host: public Viewer{ public: + Host(stream_ptr&& stream, uuid&& id); void synchronize(); void change_access_opts(); }; diff --git a/server/include/viewer_creator.hpp b/server/include/viewer_creator.hpp new file mode 100644 index 0000000..d1e6bc6 --- /dev/null +++ b/server/include/viewer_creator.hpp @@ -0,0 +1,24 @@ +#ifndef WATCH_UP_PROJECT_VIEWER_CREATOR_HPP +#define WATCH_UP_PROJECT_VIEWER_CREATOR_HPP + +#include +#include + +#include "viewer.h" + +typedef boost::uuids::uuid uuid; + +class viewer_creator { +public: + static std::shared_ptr create_viewer(stream_ptr && ws, room_ptr&& room = nullptr){ + return std::make_shared + (std::move(ws), boost::uuids::random_generator()(), std::move(room)); + } + + static std::shared_ptr create_host(stream_ptr && ws){ + return std::make_shared(std::move(ws), boost::uuids::random_generator()()); + } +}; + + +#endif //WATCH_UP_PROJECT_VIEWER_CREATOR_HPP diff --git a/server/include/viewer_manager.hpp b/server/include/viewer_manager.hpp new file mode 100644 index 0000000..76635bc --- /dev/null +++ b/server/include/viewer_manager.hpp @@ -0,0 +1,73 @@ +#ifndef WATCH_UP_PROJECT_VIEWER_MANAGER_HPP +#define WATCH_UP_PROJECT_VIEWER_MANAGER_HPP + +#include +#include +#include + +#include "types.hpp" +#include "serializer.h" +#include "viewer_creator.hpp" +#include "room_creator.hpp" + +namespace beast = boost::beast; +namespace websocket = beast::websocket; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; + +class viewer_manager { +public: + viewer_manager() = delete; + viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state): + ws_(std::move(ws)), + state_(std::move(state)), + ws_req_(std::move(ws_req)) + {} + + void do_close(){ + //TODO log close reason + ws_->close(""); + } + + void define_type(){ + type_ = serializer::get_type(ws_req_); + } + + void handle_request(){ + define_type(); + // TODO wrap whole block in try-catch + if(type_ == create){ + auto host = viewer_creator::create_host(std::move(ws_)); + + auto room = room_creator::create_room(std::weak_ptr(host), state_); + state_->add_room(room->get_id(), std::weak_ptr(room)); + room->start(); + host->set_room(room); + host->start(); + + } else if(type_ == join){ + auto room_id = serializer::get_room_id(ws_req_); + auto room = state_->get_room(room_id); + if(!room.lock()){ + // TODO send not found + return; + } + viewer_creator::create_viewer(std::move(ws_), room.lock())->start(); + } else if(type_ == invalid){ + // TODO send bad request + return; + } else { + // TODO send method not allowed + } + } +private: + stream_ptr ws_; + state_ptr state_; + + type type_ = invalid; + std::string ws_req_; + +}; + +#endif //WATCH_UP_PROJECT_VIEWER_MANAGER_HPP diff --git a/server/include/ws_session.hpp b/server/include/ws_session.hpp index 4a3eec7..96d17d4 100644 --- a/server/include/ws_session.hpp +++ b/server/include/ws_session.hpp @@ -1,25 +1,43 @@ #ifndef WATCH_UP_PROJECT_WS_SESSION_HPP #define WATCH_UP_PROJECT_WS_SESSION_HPP - +#include #include #include +#include + +#include "shared_state.h" +#include "serializer.h" +#include "viewer.h" +#include "room.h" + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; -namespace beast = boost::beast; -namespace websocket = boost::beast::websocket; +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; +typedef std::shared_ptr state_ptr; -class WS_Session : std::enable_shared_from_this { + +class ws_session : public std::enable_shared_from_this { public: - void run(); - void on_run(); - void on_accept(); - void on_read(); - void handle_request(); - -private: - websocket::stream ws_; - beast::flat_buffer buffer_; + // TODO rm virtual + ws_session(tcp::socket &&socket, state_ptr state); + virtual ~ws_session() = default; + virtual void run(); + virtual void do_read(); + virtual void do_close(); + virtual void handle_request(); + +protected: + stream_ptr ws_; + beast::flat_buffer buffer_; + state_ptr state_; }; diff --git a/server/src/listener.cpp b/server/src/listener.cpp new file mode 100644 index 0000000..176d56c --- /dev/null +++ b/server/src/listener.cpp @@ -0,0 +1,68 @@ +#include "listener.h" + +#include + +#include + +#include "ws_session.hpp" + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + + +listener::listener(net::io_context &ioc, tcp::endpoint&& endpoint): + state_(std::make_shared(ioc)), ioc_(ioc), acceptor_(ioc), socket_(ioc) +{ + error_code ec; + acceptor_.open(endpoint.protocol(), ec); + if(ec){ + std::cout << "[acceptor open error] " << ec.message() << std::endl; + } + acceptor_.set_option(net::socket_base::reuse_address(true), ec); + if (ec){ + std::cout << "[acceptor set_option error] " << ec.message() << std::endl; + } + acceptor_.bind(endpoint, ec); + if(ec){ + std::cout << "[acceptor bind error] " << ec.message() << std::endl; + } + acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec); + if(ec){ + std::cout << "[acceptor listen error] " << ec.message() << std::endl; + } + //TODO remove + std::cout << endpoint.address().to_string() << std::endl; +} + + +void listener::async_accept() { + acceptor_.async_accept(socket_, [self{shared_from_this()}](error_code ec){ + //TODO check for ec + //TODO log + if(ec){ + std::cout <<"[listener async_accept error] " << ec.message() << std::endl; + } + + std::cout << "[connection accepted]" << std::endl; + self->on_accept(ec); + }); +} + + +void listener::on_accept(error_code& ec) { + // TODO check ec + if(ec){ + std::cout << "[on accept error] "<< ec.message() << std::endl; + } + + std::make_shared(std::move(socket_), state_)->run(); + async_accept(); +} + +listener::~listener() { + acceptor_.close(); +} + diff --git a/server/src/room.cpp b/server/src/room.cpp new file mode 100644 index 0000000..1a3e91e --- /dev/null +++ b/server/src/room.cpp @@ -0,0 +1,73 @@ +#include "room.h" + +#include +#include + +#include + +#include "viewer.h" +#include "shared_state.h" + +Room::Room(w_host_ptr host, uuid id, state_ptr state): + id_(id), + host_(std::move(host)), + state_(std::move(state)), + check_timer_(state_->get_io_context(), boost::posix_time::seconds(5)) +{ + std::cout << "[room constructor]\n"; +} + +void Room::start() { + std::cout << "[room start]\n"; + check_timer_.async_wait([self{shared_from_this()}](const error_code& ec){ + if(ec){ + std::cout << "[room start error] " << ec.message() << std::endl; + } + self->check_timings(); + }); +} + +void Room::join(w_viewer_ptr viewer) { + participants_.insert(std::make_pair(viewer.lock()->get_id(), viewer)); +} + +void Room::leave(uuid id) { + participants_.erase(id); +} + +void Room::check_timings() { + std::cout << "[check timings] id: "<< boost::uuids::to_string(id_)<< std::endl; + check_timer_.expires_at(check_timer_.expires_at() + boost::posix_time::seconds(5)); + std::cout << "[participants count] : " << participants_.size() << std::endl; + if(participants_.empty()){ + return; + } + check_timer_.async_wait([self{shared_from_this()}](const error_code& ec){ + if(ec){ + std::cout << "[check timings error] " << ec.message() << std::endl; + } + self->check_timings(); + }); +} + +void Room::pause() { + +} + +void Room::play() { + +} + +void Room::synchronize() { + +} + +const uuid Room::get_id() const { + return id_; +} + +Room::~Room() { + state_->remove_room(id_); + std::cout << "[room destructor]\n"; +} + diff --git a/server/src/server.cpp b/server/src/server.cpp index 254644f..dcf71f2 100644 --- a/server/src/server.cpp +++ b/server/src/server.cpp @@ -1,10 +1,30 @@ #include "server.h" #include +#include + +#include + +#include "listener.h" + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; Server::Server(): started(false), opts_({"127.0.0.1", 8080}) {} void Server::run() { std::cout << "Server run" << std::endl; + net::io_context ioc; + + tcp::endpoint endpoint{{net::ip::make_address(opts_.ip)}, opts_.port}; + + + std::make_shared(ioc, std::move(endpoint))->async_accept(); + + ioc.run(); + started = true; } diff --git a/server/src/shared_state.cpp b/server/src/shared_state.cpp new file mode 100644 index 0000000..f8ad6af --- /dev/null +++ b/server/src/shared_state.cpp @@ -0,0 +1,28 @@ +#include "shared_state.h" +#include "viewer.h" + + +shared_state::shared_state(net::io_context &ioc): + ioc_(ioc) +{} + +void shared_state::add_room(const uuid &room_id, w_room_ptr&& room){ + rooms_.insert(std::make_pair(room_id, std::forward(room))); +} + +void shared_state::remove_room(uuid room_id) { + rooms_.erase(room_id); + std::cout << "[rooms count] : " << rooms_.size() << std::endl; +} + +net::io_context &shared_state::get_io_context() const { + return ioc_; +} + +w_room_ptr shared_state::get_room(uuid room_id) { + auto it = rooms_.find(room_id); + if(it == rooms_.end()){ + return {}; + } + return it->second; +} diff --git a/server/src/viewer.cpp b/server/src/viewer.cpp new file mode 100644 index 0000000..763aa92 --- /dev/null +++ b/server/src/viewer.cpp @@ -0,0 +1,89 @@ +#include "viewer.h" + +#include + + +Viewer::Viewer(stream_ptr &&ws, uuid&& id, room_ptr&& room): + id_(std::forward(id)), + room_(std::move(room)), + ws_(std::move(ws)) +{} + +Viewer::~Viewer(){ + std::cout << "[viewer destructor]\n"; + room_->leave(id_); +} + + +void Viewer::start() { + room_->join(shared_from_this()->weak_from_this()); + do_read(); +} + +void Viewer::pause() { + +} + +void Viewer::play() { + +} + +void Viewer::rewind(int) { + +} + + +void Viewer::send_chat_msg(std::string) { + +} + + +void Viewer::do_read() { + + ws_->async_read(buffer_, + [self{shared_from_this()}](error_code ec, std::size_t){ + if(ec){ + std::cout << "[viewer async read] " << ec.message() << std::endl; + return; + } + auto in = beast::buffers_to_string(self->buffer_.cdata()); + std::cout << "[viewer handle] " << in << std::endl; + self->do_read(); + }); + +} + + +uuid Viewer::get_id() const { + return id_; +} + + +std::string Viewer::get_nickname() const { + return std::string(); +} + +void Viewer::set_nickname(std::string) { + +} + +void Viewer::set_room(const room_ptr &room) { + room_ = room; +} + + +void Viewer::set_access_opts(access_options) { + +} + + +Host::Host(stream_ptr &&stream, uuid&& id): Viewer(std::move(stream), std::forward(id)) +{} + +void Host::synchronize() { + +} + +void Host::change_access_opts() { + +} diff --git a/server/src/ws_session.cpp b/server/src/ws_session.cpp new file mode 100644 index 0000000..df586cf --- /dev/null +++ b/server/src/ws_session.cpp @@ -0,0 +1,42 @@ +#include "ws_session.hpp" + +#include "viewer_manager.hpp" + +ws_session::ws_session(tcp::socket &&socket, state_ptr state): + ws_(std::make_unique(std::move(socket))), + state_(std::move(state)) +{} + +void ws_session::run(){ + std::cout << "[ws run]" << std::endl; + ws_->async_accept([self{shared_from_this()}](error_code ec){ + if(ec){ + std::cout << "[ws accepted] " << ec.message() << std::endl; + } + self->do_read(); + }); +} + +void ws_session::do_read() { + ws_->async_read(buffer_, + [self{shared_from_this()}](error_code ec, std::size_t){ + if(ec){ + std::cout << "[ws async read error]" << ec.message() << std::endl; + return; + } + auto in = beast::buffers_to_string(self->buffer_.cdata()); + self->handle_request(); + }); +} + + +void ws_session::do_close(){ + ws_->close(""); +} + +void ws_session::handle_request(){ + std::cout << "[handle request]" << std::endl; + + std::make_unique + (beast::buffers_to_string(buffer_.data()), std::move(ws_), state_)->handle_request(); +} From 89c0f1fc2734ceb577aab412135d08c79c93ea88 Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Tue, 6 Dec 2022 14:49:31 +0300 Subject: [PATCH 27/32] updated project structure --- server/CMakeLists.txt | 17 +++++++---------- server/api/CMakeLists.txt | 4 ++++ server/api/chat/CMakeLists.txt | 7 +++++++ server/{ => api/chat}/include/chat.h | 0 server/api/room/CMakeLists.txt | 10 ++++++++++ server/{ => api/room}/include/room.h | 0 server/{ => api/room}/include/room_creator.hpp | 0 server/{ => api/room}/src/room.cpp | 0 server/api/viewer/CMakeLists.txt | 12 ++++++++++++ server/{ => api/viewer}/include/types.hpp | 2 ++ server/{ => api/viewer}/include/viewer.h | 2 -- .../{ => api/viewer}/include/viewer_creator.hpp | 0 .../{ => api/viewer}/include/viewer_manager.hpp | 0 server/{ => api/viewer}/src/viewer.cpp | 0 server/core/CMakeLists.txt | 11 +++++++++++ server/{ => core}/include/listener.h | 0 server/{ => core}/include/server.h | 0 server/{ => core}/include/shared_state.h | 10 ++++++++-- server/{ => core}/include/ws_session.hpp | 4 ---- server/{ => core}/src/listener.cpp | 0 server/{ => core}/src/server.cpp | 0 server/{ => core}/src/shared_state.cpp | 2 +- server/{ => core}/src/ws_session.cpp | 0 server/lib/CMakeLists.txt | 1 + server/lib/serializer/CMakeLists.txt | 5 +++++ server/{include => lib/serializer}/serializer.h | 0 server/main.cpp | 1 - 27 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 server/api/CMakeLists.txt create mode 100644 server/api/chat/CMakeLists.txt rename server/{ => api/chat}/include/chat.h (100%) create mode 100644 server/api/room/CMakeLists.txt rename server/{ => api/room}/include/room.h (100%) rename server/{ => api/room}/include/room_creator.hpp (100%) rename server/{ => api/room}/src/room.cpp (100%) create mode 100644 server/api/viewer/CMakeLists.txt rename server/{ => api/viewer}/include/types.hpp (90%) rename server/{ => api/viewer}/include/viewer.h (99%) rename server/{ => api/viewer}/include/viewer_creator.hpp (100%) rename server/{ => api/viewer}/include/viewer_manager.hpp (100%) rename server/{ => api/viewer}/src/viewer.cpp (100%) create mode 100644 server/core/CMakeLists.txt rename server/{ => core}/include/listener.h (100%) rename server/{ => core}/include/server.h (100%) rename server/{ => core}/include/shared_state.h (68%) rename server/{ => core}/include/ws_session.hpp (94%) rename server/{ => core}/src/listener.cpp (100%) rename server/{ => core}/src/server.cpp (100%) rename server/{ => core}/src/shared_state.cpp (96%) rename server/{ => core}/src/ws_session.cpp (100%) create mode 100644 server/lib/CMakeLists.txt create mode 100644 server/lib/serializer/CMakeLists.txt rename server/{include => lib/serializer}/serializer.h (100%) diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 41755bc..e421575 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,28 +1,25 @@ project(server) -file(GLOB SOURCES "src/*") - find_package(Boost REQUIRED) find_package(Threads REQUIRED) -add_library(${PROJECT_NAME}_lib STATIC ${SOURCES}) - -target_include_directories(${PROJECT_NAME}_lib PUBLIC include) - add_executable(${PROJECT_NAME} main.cpp) -target_include_directories(${PROJECT_NAME} PUBLIC include) - -target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_lib) +target_link_libraries(${PROJECT_NAME} PUBLIC server_core) target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) +add_subdirectory(api) +add_subdirectory(lib) +add_subdirectory(core) + + if (BUILD_TESTS) add_subdirectory(tests) endif() if (BUILD_COVERAGE) add_coverage(${PROJECT_NAME}) - add_coverage(${PROJECT_NAME}_lib) + #add_coverage(${PROJECT_NAME}_lib) endif() diff --git a/server/api/CMakeLists.txt b/server/api/CMakeLists.txt new file mode 100644 index 0000000..e96c5c7 --- /dev/null +++ b/server/api/CMakeLists.txt @@ -0,0 +1,4 @@ + +add_subdirectory(viewer) +add_subdirectory(room) +add_subdirectory(chat) diff --git a/server/api/chat/CMakeLists.txt b/server/api/chat/CMakeLists.txt new file mode 100644 index 0000000..60a5617 --- /dev/null +++ b/server/api/chat/CMakeLists.txt @@ -0,0 +1,7 @@ +project(chat) + +file(GLOB SOURCES "src/*") + +add_library(${PROJECT_NAME} INTERFACE) + +target_include_directories(${PROJECT_NAME} INTERFACE include) diff --git a/server/include/chat.h b/server/api/chat/include/chat.h similarity index 100% rename from server/include/chat.h rename to server/api/chat/include/chat.h diff --git a/server/api/room/CMakeLists.txt b/server/api/room/CMakeLists.txt new file mode 100644 index 0000000..e73d5d5 --- /dev/null +++ b/server/api/room/CMakeLists.txt @@ -0,0 +1,10 @@ +project(room) + +file(GLOB SOURCES "src/*") + +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} PUBLIC viewer server_core) + diff --git a/server/include/room.h b/server/api/room/include/room.h similarity index 100% rename from server/include/room.h rename to server/api/room/include/room.h diff --git a/server/include/room_creator.hpp b/server/api/room/include/room_creator.hpp similarity index 100% rename from server/include/room_creator.hpp rename to server/api/room/include/room_creator.hpp diff --git a/server/src/room.cpp b/server/api/room/src/room.cpp similarity index 100% rename from server/src/room.cpp rename to server/api/room/src/room.cpp diff --git a/server/api/viewer/CMakeLists.txt b/server/api/viewer/CMakeLists.txt new file mode 100644 index 0000000..c36e238 --- /dev/null +++ b/server/api/viewer/CMakeLists.txt @@ -0,0 +1,12 @@ +project(viewer) + +file(GLOB SOURCES "src/*") +find_package(nlohmann_json 3.10.5 REQUIRED) + +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} PUBLIC room serializer) +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) + diff --git a/server/include/types.hpp b/server/api/viewer/include/types.hpp similarity index 90% rename from server/include/types.hpp rename to server/api/viewer/include/types.hpp index 4587fa2..77a01a7 100644 --- a/server/include/types.hpp +++ b/server/api/viewer/include/types.hpp @@ -1,6 +1,8 @@ #ifndef WATCH_UP_PROJECT_TYPES_HPP #define WATCH_UP_PROJECT_TYPES_HPP +#include "nlohmann/json.hpp" + enum type { invalid = -1, create = 0, diff --git a/server/include/viewer.h b/server/api/viewer/include/viewer.h similarity index 99% rename from server/include/viewer.h rename to server/api/viewer/include/viewer.h index ea9adf7..1624db2 100644 --- a/server/include/viewer.h +++ b/server/api/viewer/include/viewer.h @@ -9,10 +9,8 @@ #include #include - #include "room.h" - namespace beast = boost::beast; // from namespace websocket = beast::websocket; // from namespace net = boost::asio; // from diff --git a/server/include/viewer_creator.hpp b/server/api/viewer/include/viewer_creator.hpp similarity index 100% rename from server/include/viewer_creator.hpp rename to server/api/viewer/include/viewer_creator.hpp diff --git a/server/include/viewer_manager.hpp b/server/api/viewer/include/viewer_manager.hpp similarity index 100% rename from server/include/viewer_manager.hpp rename to server/api/viewer/include/viewer_manager.hpp diff --git a/server/src/viewer.cpp b/server/api/viewer/src/viewer.cpp similarity index 100% rename from server/src/viewer.cpp rename to server/api/viewer/src/viewer.cpp diff --git a/server/core/CMakeLists.txt b/server/core/CMakeLists.txt new file mode 100644 index 0000000..a2737f6 --- /dev/null +++ b/server/core/CMakeLists.txt @@ -0,0 +1,11 @@ +project(server_core) + +file(GLOB SOURCES "src/*") + +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) + +target_link_libraries(${PROJECT_NAME} PUBLIC viewer) +target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) +target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) diff --git a/server/include/listener.h b/server/core/include/listener.h similarity index 100% rename from server/include/listener.h rename to server/core/include/listener.h diff --git a/server/include/server.h b/server/core/include/server.h similarity index 100% rename from server/include/server.h rename to server/core/include/server.h diff --git a/server/include/shared_state.h b/server/core/include/shared_state.h similarity index 68% rename from server/include/shared_state.h rename to server/core/include/shared_state.h index b088b55..2c95a6e 100644 --- a/server/include/shared_state.h +++ b/server/core/include/shared_state.h @@ -4,15 +4,21 @@ #include #include +#include #include #include #include -#include "room.h" +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +class IRoom; typedef std::shared_ptr room_ptr; typedef std::weak_ptr w_room_ptr; -typedef std::shared_ptr viewer_ptr; typedef boost::uuids::uuid uuid; class shared_state: public std::enable_shared_from_this{ diff --git a/server/include/ws_session.hpp b/server/core/include/ws_session.hpp similarity index 94% rename from server/include/ws_session.hpp rename to server/core/include/ws_session.hpp index 96d17d4..be80a21 100644 --- a/server/include/ws_session.hpp +++ b/server/core/include/ws_session.hpp @@ -7,10 +7,6 @@ #include #include "shared_state.h" -#include "serializer.h" -#include "viewer.h" -#include "room.h" - namespace beast = boost::beast; // from namespace websocket = beast::websocket; // from diff --git a/server/src/listener.cpp b/server/core/src/listener.cpp similarity index 100% rename from server/src/listener.cpp rename to server/core/src/listener.cpp diff --git a/server/src/server.cpp b/server/core/src/server.cpp similarity index 100% rename from server/src/server.cpp rename to server/core/src/server.cpp diff --git a/server/src/shared_state.cpp b/server/core/src/shared_state.cpp similarity index 96% rename from server/src/shared_state.cpp rename to server/core/src/shared_state.cpp index f8ad6af..5ce5ad2 100644 --- a/server/src/shared_state.cpp +++ b/server/core/src/shared_state.cpp @@ -1,6 +1,6 @@ #include "shared_state.h" -#include "viewer.h" +#include shared_state::shared_state(net::io_context &ioc): ioc_(ioc) diff --git a/server/src/ws_session.cpp b/server/core/src/ws_session.cpp similarity index 100% rename from server/src/ws_session.cpp rename to server/core/src/ws_session.cpp diff --git a/server/lib/CMakeLists.txt b/server/lib/CMakeLists.txt new file mode 100644 index 0000000..c5ddbaa --- /dev/null +++ b/server/lib/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(serializer) diff --git a/server/lib/serializer/CMakeLists.txt b/server/lib/serializer/CMakeLists.txt new file mode 100644 index 0000000..eeafeed --- /dev/null +++ b/server/lib/serializer/CMakeLists.txt @@ -0,0 +1,5 @@ +project(serializer) + +add_library(${PROJECT_NAME} INTERFACE) + +target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} ) diff --git a/server/include/serializer.h b/server/lib/serializer/serializer.h similarity index 100% rename from server/include/serializer.h rename to server/lib/serializer/serializer.h diff --git a/server/main.cpp b/server/main.cpp index a0e016e..4b09b97 100644 --- a/server/main.cpp +++ b/server/main.cpp @@ -3,7 +3,6 @@ #include "server.h" int main(){ - Server server; server.run(); return 0; From a6ea2bdd3917d67226b73f0043f2973663c377a6 Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Fri, 9 Dec 2022 02:15:31 +0300 Subject: [PATCH 28/32] handlers impl start --- server/api/response/response_creator.hpp | 5 + server/api/room/include/room.h | 16 ++- server/api/room/include/room_creator.hpp | 2 +- server/api/room/src/room.cpp | 70 +++++++++--- server/api/viewer/include/handler.h | 111 +++++++++++++++++++ server/api/viewer/include/types.hpp | 17 +++ server/api/viewer/include/viewer.h | 70 ++++++------ server/api/viewer/include/viewer_manager.hpp | 59 +++++----- server/api/viewer/src/viewer.cpp | 47 +++++--- server/lib/serializer/serializer.h | 26 +++-- 10 files changed, 324 insertions(+), 99 deletions(-) create mode 100644 server/api/response/response_creator.hpp create mode 100644 server/api/viewer/include/handler.h diff --git a/server/api/response/response_creator.hpp b/server/api/response/response_creator.hpp new file mode 100644 index 0000000..9c26fa8 --- /dev/null +++ b/server/api/response/response_creator.hpp @@ -0,0 +1,5 @@ +#ifndef WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP +#define WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP + + +#endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/api/room/include/room.h b/server/api/room/include/room.h index 1ad34ce..d0792f5 100644 --- a/server/api/room/include/room.h +++ b/server/api/room/include/room.h @@ -20,12 +20,10 @@ using uuid = boost::uuids::uuid; class IViewer; -class Host; class shared_state; typedef std::weak_ptr w_viewer_ptr; typedef std::shared_ptr viewer_ptr; -typedef std::weak_ptr w_host_ptr; typedef std::shared_ptr state_ptr; @@ -38,6 +36,11 @@ class IRoom { virtual void pause() = 0; virtual void play() = 0; virtual void synchronize() = 0; + virtual void set_resource(const std::string&) = 0; + virtual void set_nickname(const uuid& viewer, std::string&& nick) = 0; + virtual void send_chat_msg(uuid viewer, const std::string& msg) = 0; + + virtual w_viewer_ptr get_viewer(const uuid& ) = 0; virtual const uuid get_id() const = 0; }; @@ -53,15 +56,22 @@ class Room: public IRoom, public std::enable_shared_from_this{ void pause() override; void play() override; void synchronize() override; + void send_chat_msg(uuid viewer, const std::string& msg) override; + void set_resource(const std::string&) override; + + void start_timer(); + void stop_timer(); const uuid get_id() const override; + w_viewer_ptr get_viewer(const uuid& ) override; private: - std::unordered_map> participants_; + bool playing; uuid id_; w_host_ptr host_; state_ptr state_; boost::asio::deadline_timer check_timer_; + std::unordered_map> participants_; }; #endif //WATCH_UP_PROJECT_ROOM_H diff --git a/server/api/room/include/room_creator.hpp b/server/api/room/include/room_creator.hpp index 997e41f..3c1aca6 100644 --- a/server/api/room/include/room_creator.hpp +++ b/server/api/room/include/room_creator.hpp @@ -13,7 +13,7 @@ class Host; class room_creator{ public: static std::shared_ptr create_room(const std::weak_ptr& host, state_ptr state){ - return std::make_shared(host, boost::uuids::random_generator()(), std::move(state)); + return std::make_shared(host.lock()->get_id(), boost::uuids::random_generator()(), std::move(state)); } }; diff --git a/server/api/room/src/room.cpp b/server/api/room/src/room.cpp index 1a3e91e..afe0a4e 100644 --- a/server/api/room/src/room.cpp +++ b/server/api/room/src/room.cpp @@ -19,12 +19,7 @@ Room::Room(w_host_ptr host, uuid id, state_ptr state): void Room::start() { std::cout << "[room start]\n"; - check_timer_.async_wait([self{shared_from_this()}](const error_code& ec){ - if(ec){ - std::cout << "[room start error] " << ec.message() << std::endl; - } - self->check_timings(); - }); + } void Room::join(w_viewer_ptr viewer) { @@ -37,31 +32,51 @@ void Room::leave(uuid id) { void Room::check_timings() { std::cout << "[check timings] id: "<< boost::uuids::to_string(id_)<< std::endl; - check_timer_.expires_at(check_timer_.expires_at() + boost::posix_time::seconds(5)); - std::cout << "[participants count] : " << participants_.size() << std::endl; if(participants_.empty()){ return; } - check_timer_.async_wait([self{shared_from_this()}](const error_code& ec){ - if(ec){ - std::cout << "[check timings error] " << ec.message() << std::endl; - } - self->check_timings(); - }); + if(!playing){ + stop_timer(); + return; + } + std::cout << "[check timings]\n"; + start_timer(); } void Room::pause() { - + if(!playing){ + return; + } + playing = false; + for(const auto& p : participants_){ + p.second.lock()->pause(); + } } void Room::play() { - + if(playing){ + return; + } + playing = true; + for(const auto& p : participants_){ + p.second.lock()->play(); + } + start_timer(); } void Room::synchronize() { + for(const auto& p : participants_){ + p.second.lock()->set_timestamp(); + } +} +void Room::send_chat_msg(uuid /*viewer*/, const std::string &/*msg*/) { + for(const auto& p : participants_){ + p.second.lock()->send_chat_msg(""); + } } + const uuid Room::get_id() const { return id_; } @@ -71,3 +86,26 @@ Room::~Room() { std::cout << "[room destructor]\n"; } +void Room::start_timer() { + check_timer_.expires_from_now(boost::posix_time::seconds(5)); + check_timer_.async_wait([self{shared_from_this()}](const error_code& ec){ + if(ec){ + std::cout << "[check timings error] " << ec.message() << std::endl; + } + self->check_timings(); + }); +} + +void Room::stop_timer() { + check_timer_.cancel(); +} + +void Room::set_resource(const std::string &) { + +} + +w_viewer_ptr Room::get_viewer(const uuid& v_id) { + return participants_[v_id]; +} + + diff --git a/server/api/viewer/include/handler.h b/server/api/viewer/include/handler.h new file mode 100644 index 0000000..d03b056 --- /dev/null +++ b/server/api/viewer/include/handler.h @@ -0,0 +1,111 @@ +#ifndef WATCH_UP_PROJECT_HANDLER_H +#define WATCH_UP_PROJECT_HANDLER_H + +#include +#include + + +#include "room.h" +#include "types.hpp" +#include "serializer.h" + +typedef std::shared_ptr room_ptr; +typedef std::shared_ptr viewer_ptr; + +class handler { +public: + handler(std::string&& msg, const viewer_ptr& viewer, const room_ptr& room); + void define_type(){ + auto it = types.find(req_["type"]); + type_ = it->second; + } + void handle_request(){ + try{ + req_=serializer::deserialize(str_req_); + define_type(); + switch (type_) { + case leave: + + // TODO send ok + break; + case pong: + // TODO implement + break; + case play: + if(!viewer->get_a_opts().can_pause){ + // TODO send forbidden + return; + } + room_->play(); + // TODO send ok + break; + case pause_: + if(!viewer->get_a_opts().can_pause){ + // TODO send forbidden + return; + } + room_->pause(); + // TODO send ok + break; + case s_time: + if(!viewer->get_a_opts().can_rewind){ + // TODO send forbidden + return; + } + // TODO change signature + room_->synchronize(); + // TODO send ok + break; + case s_nick: + if(viewer->get_nickname() == req_["nick"]){ + // TODO send ok + return; + } + room_->set_nickname(viewer->get_id(), std::move(req_["nick"])); + break; + case sync_: + if(!viewer->get_a_opts().is_host){ + // TODO send forbidden + return; + } + room_->synchronize(); + break; + case s_src: + if(!viewer->get_a_opts().is_host){ + // TODO send forbidden + return; + } + room_->set_resource(req_["src"]); + //TODO send ok + break; + case s_access: { + if (!viewer->get_a_opts().is_host) { + // TODO send forbidden + return; + } + auto opts = serializer::deserialize_access_opts(req_["opts"]); + access_options to_set {opts["pause"], opts["rewind"], opts["host"]}; + room_->get_viewer(boost::lexical_cast(req_["viewer"])).lock()->set_access_opts(to_set); + + break; + } + default: + + break; + } + } + catch (...){ + + } + + + } +private: + type type_ = invalid; + viewer_ptr viewer; + room_ptr room_; + std::string str_req_; + std::unordered_map req_; +}; + +#endif //WATCH_UP_PROJECT_HANDLER_H diff --git a/server/api/viewer/include/types.hpp b/server/api/viewer/include/types.hpp index 77a01a7..951dedd 100644 --- a/server/api/viewer/include/types.hpp +++ b/server/api/viewer/include/types.hpp @@ -7,6 +7,23 @@ enum type { invalid = -1, create = 0, join, + leave, + ping, + pong, + play, + pause_, + s_time, + s_nick, + sync_, + s_src, + s_access, + +}; + +static std::unordered_map const types = { + {"invalid", type::invalid}, + {"create", type::create }, + {"join", type::join } }; NLOHMANN_JSON_SERIALIZE_ENUM(type, { diff --git a/server/api/viewer/include/viewer.h b/server/api/viewer/include/viewer.h index 1624db2..aa9412b 100644 --- a/server/api/viewer/include/viewer.h +++ b/server/api/viewer/include/viewer.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -22,54 +23,66 @@ typedef std::shared_ptr room_ptr; typedef websocket::stream ws_stream; typedef std::unique_ptr stream_ptr; +class handler; struct access_options{ bool can_pause; bool can_rewind; + bool is_host; }; class IViewer{ public: - virtual ~IViewer() = default; - virtual void start() = 0; - virtual void pause() = 0; - virtual void play() = 0; - virtual void rewind(int offset) = 0; - virtual void send_chat_msg(std::string msg) = 0; + virtual ~IViewer() = default; + virtual void start() = 0; + virtual void pause() = 0; + virtual void play() = 0; + virtual void rewind(int offset) = 0; + virtual void set_timestamp() = 0; + virtual void send_chat_msg(std::string msg) = 0; - virtual void do_read() = 0; + virtual void do_read() = 0; + virtual void do_close() = 0; + virtual void send_response(const std::string&) = 0; - virtual uuid get_id() const = 0; - virtual std::string get_nickname() const = 0; + virtual uuid get_id() const = 0; + virtual std::string get_nickname() const = 0; + virtual access_options get_a_opts() const = 0; + + virtual void set_nickname(std::string) = 0; + virtual void set_room(const room_ptr& room) = 0; + virtual void set_access_opts(const access_options& opts)= 0; - virtual void set_nickname(std::string) = 0; - virtual void set_room(const room_ptr& room) = 0; - virtual void set_access_opts(access_options opts)= 0; }; class Viewer: public IViewer, public std::enable_shared_from_this { public: Viewer(stream_ptr && ws, uuid&& id, room_ptr&& room = nullptr); - ~Viewer() override; + ~Viewer() override; - void start() override; - void pause() override; - void play() override; - void rewind(int ) override; - void send_chat_msg(std::string ) override; + void start() override; + void pause() override; + void play() override; + void rewind(int ) override; + void set_timestamp() override; + void send_chat_msg(std::string ) override; - void do_read() override; + void do_read() override; + void do_close() override; + void send_response(const std::string&) override; - uuid get_id() const override; - std::string get_nickname() const override; + uuid get_id() const override; + std::string get_nickname() const override; + access_options get_a_opts() const override; - void set_nickname(std::string) override; - void set_room(const room_ptr& room) override; - void set_access_opts(access_options )override; + void set_nickname(std::string) override; + void set_room(const room_ptr& room) override; + void set_access_opts(const access_options& )override; protected: + bool is_host; uuid id_; room_ptr room_; std::string nickname_; @@ -78,14 +91,7 @@ class Viewer: public IViewer, public std::enable_shared_from_this { stream_ptr ws_; beast::flat_buffer buffer_; - // TODO response queue -}; - -class Host: public Viewer{ -public: - Host(stream_ptr&& stream, uuid&& id); - void synchronize(); - void change_access_opts(); + std::queue response_q; }; #endif //WATCH_UP_PROJECT_VIEWER_H diff --git a/server/api/viewer/include/viewer_manager.hpp b/server/api/viewer/include/viewer_manager.hpp index 76635bc..09f4d8e 100644 --- a/server/api/viewer/include/viewer_manager.hpp +++ b/server/api/viewer/include/viewer_manager.hpp @@ -2,8 +2,10 @@ #define WATCH_UP_PROJECT_VIEWER_MANAGER_HPP #include -#include #include +#include + +#include #include "types.hpp" #include "serializer.h" @@ -22,7 +24,7 @@ class viewer_manager { viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state): ws_(std::move(ws)), state_(std::move(state)), - ws_req_(std::move(ws_req)) + str_req_(std::move(ws_req)) {} void do_close(){ @@ -31,34 +33,39 @@ class viewer_manager { } void define_type(){ - type_ = serializer::get_type(ws_req_); + auto it = types.find(req_["type"]); + type_ = it->second; } void handle_request(){ - define_type(); - // TODO wrap whole block in try-catch - if(type_ == create){ - auto host = viewer_creator::create_host(std::move(ws_)); - - auto room = room_creator::create_room(std::weak_ptr(host), state_); - state_->add_room(room->get_id(), std::weak_ptr(room)); - room->start(); - host->set_room(room); - host->start(); + try { + req_ = serializer::deserialize(str_req_); + define_type(); + if (type_ == create) { + auto host = viewer_creator::create_host(std::move(ws_)); + auto room = room_creator::create_room(std::weak_ptr(host), state_); + state_->add_room(room->get_id(), std::weak_ptr(room)); + room->start(); + host->set_room(room); + host->start(); - } else if(type_ == join){ - auto room_id = serializer::get_room_id(ws_req_); - auto room = state_->get_room(room_id); - if(!room.lock()){ - // TODO send not found + } else if (type_ == join) { + auto room_id = boost::lexical_cast(req_["room_id"]); + auto room = state_->get_room(room_id); + if (!room.lock()) { + // TODO send not found + return; + } + viewer_creator::create_viewer(std::move(ws_), room.lock())->start(); + } else if (type_ == invalid) { + // TODO send bad request return; + } else { + // TODO send method not allowed } - viewer_creator::create_viewer(std::move(ws_), room.lock())->start(); - } else if(type_ == invalid){ - // TODO send bad request - return; - } else { - // TODO send method not allowed + } + catch (...){ + } } private: @@ -66,7 +73,9 @@ class viewer_manager { state_ptr state_; type type_ = invalid; - std::string ws_req_; + + std::string str_req_; + std::unordered_map req_; }; diff --git a/server/api/viewer/src/viewer.cpp b/server/api/viewer/src/viewer.cpp index 763aa92..564cc6e 100644 --- a/server/api/viewer/src/viewer.cpp +++ b/server/api/viewer/src/viewer.cpp @@ -2,6 +2,7 @@ #include +#include "handler.h" Viewer::Viewer(stream_ptr &&ws, uuid&& id, room_ptr&& room): id_(std::forward(id)), @@ -39,18 +40,18 @@ void Viewer::send_chat_msg(std::string) { void Viewer::do_read() { - ws_->async_read(buffer_, - [self{shared_from_this()}](error_code ec, std::size_t){ - if(ec){ - std::cout << "[viewer async read] " << ec.message() << std::endl; - return; - } - auto in = beast::buffers_to_string(self->buffer_.cdata()); - std::cout << "[viewer handle] " << in << std::endl; - self->do_read(); - }); - + [self{shared_from_this()}](error_code ec, std::size_t bytes_read){ + if(ec){ + std::cout << "[viewer async read] " << ec.message() << std::endl; + return; + } + auto in = beast::buffers_to_string(self->buffer_.cdata()); + self->buffer_.consume(bytes_read); + std::cout << "[viewer handle] " << in << std::endl; + self->do_read(); + std::make_unique(std::move(in), self->shared_from_this(), self->room_)->handle_request(); + }); } @@ -60,11 +61,11 @@ uuid Viewer::get_id() const { std::string Viewer::get_nickname() const { - return std::string(); + return nickname_; } -void Viewer::set_nickname(std::string) { - +void Viewer::set_nickname(std::string nickname) { + nickname_ = nickname; } void Viewer::set_room(const room_ptr &room) { @@ -72,7 +73,23 @@ void Viewer::set_room(const room_ptr &room) { } -void Viewer::set_access_opts(access_options) { +void Viewer::set_access_opts(const access_options& opts) { + a_opts_ = opts; +} + +void Viewer::set_timestamp() { + +} + +access_options Viewer::get_a_opts() const { + return a_opts_; +} + +void Viewer::do_close() { + ws_->close(""); +} + +void Viewer::send_response(const std::string &) { } diff --git a/server/lib/serializer/serializer.h b/server/lib/serializer/serializer.h index 38cc2a2..010aea7 100644 --- a/server/lib/serializer/serializer.h +++ b/server/lib/serializer/serializer.h @@ -2,6 +2,7 @@ #define WATCH_UP_PROJECT_SERIALIZER_H #include +#include #include "nlohmann/json.hpp" #include @@ -14,19 +15,30 @@ using uuid = boost::uuids::uuid; class serializer { public: + static nlohmann::json parse(const std::string& json_str){ return nlohmann::json::parse(json_str); } - static type get_type(const std::string& json_str){ - auto json = parse(json_str); - return json["type"].get(); + static std::unordered_map deserialize(const std::string& json_str){ + //TODO try-catch + nlohmann::json json = nlohmann::json::parse(json_str); + std::unordered_map data{}; + + for(const auto& item: json.items()){ + data[item.key()] = item.value(); + } + return data; } - static uuid get_room_id(const std::string& json_str){ - auto json = parse(json_str); - return boost::lexical_cast - (std::string(json["room_id"])); + static std::unordered_map deserialize_access_opts(const std::string& json_str){ + nlohmann::json json = nlohmann::json::parse(json_str); + std::unordered_map data{}; + + for(const auto& item: json.items()){ + data[item.key()] = boost::lexical_cast(item.value()); + } + return data; } }; From 26217dee513bdc194d548ceba74ac4715eb5e18c Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Sat, 10 Dec 2022 15:43:21 +0300 Subject: [PATCH 29/32] basic api done --- server/api/response/response_creator.hpp | 5 - server/api/room/include/room.h | 74 ++++++++------ server/api/room/include/room_creator.hpp | 4 +- server/api/room/src/room.cpp | 58 ++++++++--- server/api/viewer/include/handler.h | 79 +++++++++------ .../api/viewer/include/response_creator.hpp | 38 ++++++++ server/api/viewer/include/types.hpp | 33 +++++-- server/api/viewer/include/viewer.h | 27 ++---- server/api/viewer/include/viewer_creator.hpp | 5 +- server/api/viewer/include/viewer_manager.hpp | 2 +- server/api/viewer/src/viewer.cpp | 96 +++++++++---------- server/core/src/server.cpp | 2 +- server/lib/serializer/serializer.h | 37 +++++-- 13 files changed, 300 insertions(+), 160 deletions(-) delete mode 100644 server/api/response/response_creator.hpp create mode 100644 server/api/viewer/include/response_creator.hpp diff --git a/server/api/response/response_creator.hpp b/server/api/response/response_creator.hpp deleted file mode 100644 index 9c26fa8..0000000 --- a/server/api/response/response_creator.hpp +++ /dev/null @@ -1,5 +0,0 @@ -#ifndef WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP -#define WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP - - -#endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/api/room/include/room.h b/server/api/room/include/room.h index d0792f5..1ad3e06 100644 --- a/server/api/room/include/room.h +++ b/server/api/room/include/room.h @@ -29,49 +29,61 @@ typedef std::shared_ptr state_ptr; class IRoom { public: - virtual ~IRoom() = default; - virtual void join (w_viewer_ptr) = 0; - virtual void leave(uuid id) = 0; - virtual void check_timings() = 0; - virtual void pause() = 0; - virtual void play() = 0; - virtual void synchronize() = 0; - virtual void set_resource(const std::string&) = 0; - virtual void set_nickname(const uuid& viewer, std::string&& nick) = 0; - virtual void send_chat_msg(uuid viewer, const std::string& msg) = 0; - - virtual w_viewer_ptr get_viewer(const uuid& ) = 0; - virtual const uuid get_id() const = 0; + virtual ~IRoom() = default; + + virtual void start() = 0; + virtual void join (w_viewer_ptr) = 0; + virtual void leave(const uuid& id, const std::string& nick) = 0; + + virtual void check_timings() = 0; + virtual void pause() = 0; + virtual void play() = 0; + virtual void synchronize(const boost::posix_time::time_duration &) = 0; + + virtual void set_resource(const std::string&) = 0; + virtual void send_chat_msg(uuid viewer, const std::string& msg) = 0; + virtual void set_nickname(const uuid& viewer, std::string&& nick)= 0; + + virtual w_viewer_ptr get_viewer(const uuid& ) = 0; + virtual const uuid get_id() const = 0; + + + virtual void start_timer() = 0; + virtual void stop_timer() = 0; + }; class Room: public IRoom, public std::enable_shared_from_this{ public: - Room(w_host_ptr host, uuid id, state_ptr state); - ~Room() override; - void start(); - void join (w_viewer_ptr) override; - void leave(uuid id) override; - void check_timings() override; - void pause() override; - void play() override; - void synchronize() override; - void send_chat_msg(uuid viewer, const std::string& msg) override; - void set_resource(const std::string&) override; - - void start_timer(); - void stop_timer(); - - const uuid get_id() const override; - w_viewer_ptr get_viewer(const uuid& ) override; + Room(uuid&& host, uuid&& id, state_ptr state); + ~Room() override; + void start() override; + void join (w_viewer_ptr) override; + void leave(const uuid& id, const std::string& nick) override; + void check_timings() override; + void pause() override; + void play() override; + void synchronize(const boost::posix_time::time_duration&)override; + void send_chat_msg(uuid viewer, const std::string& msg) override; + void set_resource(const std::string&) override; + void set_nickname(const uuid& viewer, std::string&& nick)override; + + void start_timer() override; + void stop_timer() override; + + const uuid get_id() const override; + w_viewer_ptr get_viewer(const uuid& ) override; private: bool playing; uuid id_; - w_host_ptr host_; + uuid host_; state_ptr state_; boost::asio::deadline_timer check_timer_; std::unordered_map> participants_; + std::vector> viewers_; + std::string src_; }; #endif //WATCH_UP_PROJECT_ROOM_H diff --git a/server/api/room/include/room_creator.hpp b/server/api/room/include/room_creator.hpp index 3c1aca6..ecf8c2a 100644 --- a/server/api/room/include/room_creator.hpp +++ b/server/api/room/include/room_creator.hpp @@ -8,11 +8,11 @@ #include "shared_state.h" typedef boost::uuids::uuid uuid; -class Host; + class room_creator{ public: - static std::shared_ptr create_room(const std::weak_ptr& host, state_ptr state){ + static std::shared_ptr create_room(const std::weak_ptr& host, state_ptr state){ return std::make_shared(host.lock()->get_id(), boost::uuids::random_generator()(), std::move(state)); } }; diff --git a/server/api/room/src/room.cpp b/server/api/room/src/room.cpp index afe0a4e..dedce32 100644 --- a/server/api/room/src/room.cpp +++ b/server/api/room/src/room.cpp @@ -7,10 +7,13 @@ #include "viewer.h" #include "shared_state.h" +#include "serializer.h" +#include "response_creator.hpp" -Room::Room(w_host_ptr host, uuid id, state_ptr state): - id_(id), - host_(std::move(host)), +Room::Room(uuid&& host, uuid&& id, state_ptr state): + playing(false), + id_(std::forward(id)), + host_(std::forward(host)), state_(std::move(state)), check_timer_(state_->get_io_context(), boost::posix_time::seconds(5)) { @@ -19,15 +22,34 @@ Room::Room(w_host_ptr host, uuid id, state_ptr state): void Room::start() { std::cout << "[room start]\n"; - } void Room::join(w_viewer_ptr viewer) { + if(!viewers_.empty()) { + auto welcome_msg = serializer::serialize_viewers(type::join, viewers_); + viewer.lock()->send_message(welcome_msg); + + auto incomer_msg = serializer::serialize_viewers(type::join, {std::make_pair(viewer.lock()->get_id(), viewer.lock()->get_nickname())}); + for (const auto &p: participants_) { + p.second.lock()->send_message(incomer_msg); + } + } else { + auto res = response_creator::create_with_room(200, "created", id_); + viewer.lock()->send_message(serializer::serialize_response(create, res)); + } participants_.insert(std::make_pair(viewer.lock()->get_id(), viewer)); + viewers_.emplace_back(viewer.lock()->get_id(), viewer.lock()->get_nickname()); } -void Room::leave(uuid id) { +void Room::leave(const uuid& id, const std::string& nick) { + auto viewer_dto = std::make_pair(id, nick); + auto it = find(viewers_.begin(), viewers_.end(), viewer_dto); participants_.erase(id); + viewers_.erase(it); + auto leave_msg = serializer::serialize_viewers(type::leave, {viewer_dto}); + for(const auto& p : participants_){ + p.second.lock()->send_message(leave_msg); + } } void Room::check_timings() { @@ -40,6 +62,9 @@ void Room::check_timings() { return; } std::cout << "[check timings]\n"; + for(const auto& viewer : participants_){ + viewer.second.lock()->send_message("ping"); + } start_timer(); } @@ -49,7 +74,7 @@ void Room::pause() { } playing = false; for(const auto& p : participants_){ - p.second.lock()->pause(); + p.second.lock()->send_message("");; } } @@ -59,20 +84,22 @@ void Room::play() { } playing = true; for(const auto& p : participants_){ - p.second.lock()->play(); + p.second.lock()->send_message(""); } start_timer(); } -void Room::synchronize() { +void Room::synchronize(const boost::posix_time::time_duration & timing) { + auto msg = serializer::serialize_response(s_time, + response_creator::create_with_timing(timing)); for(const auto& p : participants_){ - p.second.lock()->set_timestamp(); + p.second.lock()->send_message(""); } } void Room::send_chat_msg(uuid /*viewer*/, const std::string &/*msg*/) { for(const auto& p : participants_){ - p.second.lock()->send_chat_msg(""); + p.second.lock()->send_message(""); } } @@ -100,12 +127,19 @@ void Room::stop_timer() { check_timer_.cancel(); } -void Room::set_resource(const std::string &) { - +void Room::set_resource(const std::string & src) { + src_ = src; } w_viewer_ptr Room::get_viewer(const uuid& v_id) { return participants_[v_id]; } +void Room::set_nickname(const uuid &viewer, std::string &&nick) { + auto msg = serializer::serialize_viewers(s_nick, {std::make_pair(viewer, std::move(nick))}); + for(const auto& v: participants_){ + v.second.lock()->send_message(msg); + } +} + diff --git a/server/api/viewer/include/handler.h b/server/api/viewer/include/handler.h index d03b056..db4e381 100644 --- a/server/api/viewer/include/handler.h +++ b/server/api/viewer/include/handler.h @@ -3,20 +3,26 @@ #include #include - +#include #include "room.h" #include "types.hpp" #include "serializer.h" +#include "response_creator.hpp" typedef std::shared_ptr room_ptr; typedef std::shared_ptr viewer_ptr; class handler { public: - handler(std::string&& msg, const viewer_ptr& viewer, const room_ptr& room); + handler(std::string&& msg, viewer_ptr viewer, room_ptr room): + viewer_(std::move(viewer)), + room_(std::move(room)), + str_req_(std::move(msg)) + {} void define_type(){ auto it = types.find(req_["type"]); + std::cout << req_["type"] << std::endl; type_ = it->second; } void handle_request(){ @@ -25,68 +31,84 @@ class handler { define_type(); switch (type_) { case leave: - + viewer_->do_close(); // TODO send ok break; case pong: // TODO implement break; - case play: - if(!viewer->get_a_opts().can_pause){ - // TODO send forbidden + case play: { + if (!viewer_->get_a_opts().can_pause) { + auto msg_ = response_creator::create_with_status(403, "operation forbidden"); + auto str_msg_ = serializer::serialize_response(play, msg_); + viewer_->send_message(str_msg_); return; } + auto msg = response_creator::create_with_status(200, "ok"); + auto str_msg = serializer::serialize_response(play, msg); room_->play(); - // TODO send ok + } break; case pause_: - if(!viewer->get_a_opts().can_pause){ - // TODO send forbidden + if(!viewer_->get_a_opts().can_pause){ + auto msg_ = response_creator::create_with_status(403, "operation forbidden"); + auto str_msg_ = serializer::serialize_response(pause_, msg_); + viewer_->send_message(str_msg_); return; } room_->pause(); // TODO send ok break; case s_time: - if(!viewer->get_a_opts().can_rewind){ - // TODO send forbidden + if(!viewer_->get_a_opts().can_rewind){ + auto msg_ = response_creator::create_with_status(403, "operation forbidden"); + auto str_msg_ = serializer::serialize_response(s_time, msg_); + viewer_->send_message(str_msg_); return; } - // TODO change signature - room_->synchronize(); + + room_->synchronize(boost::lexical_cast(req_["time"])); // TODO send ok break; case s_nick: - if(viewer->get_nickname() == req_["nick"]){ - // TODO send ok + if(viewer_->get_nickname() == req_["nick"]){ + auto msg = response_creator::create_with_status(200, "ok"); + auto str_msg = serializer::serialize_response(s_nick, msg); + viewer_->send_message(str_msg); return; } - room_->set_nickname(viewer->get_id(), std::move(req_["nick"])); + viewer_->set_nickname(req_["nick"]); + room_->set_nickname(viewer_->get_id(), std::move(req_["nick"])); break; case sync_: - if(!viewer->get_a_opts().is_host){ + if(!viewer_->get_a_opts().is_host){ // TODO send forbidden return; } - room_->synchronize(); + room_->synchronize(boost::lexical_cast(req_["time"])); break; case s_src: - if(!viewer->get_a_opts().is_host){ - // TODO send forbidden + if(!viewer_->get_a_opts().is_host){ + auto msg_ = response_creator::create_with_status(403, "operation forbidden"); + auto str_msg_ = serializer::serialize_response(s_src, msg_); + viewer_->send_message(str_msg_); return; } room_->set_resource(req_["src"]); //TODO send ok break; case s_access: { - if (!viewer->get_a_opts().is_host) { - // TODO send forbidden + if (!viewer_->get_a_opts().is_host) { + auto msg_ = response_creator::create_with_status(403, "operation forbidden"); + auto str_msg_ = serializer::serialize_response(s_access, msg_); + viewer_->send_message(str_msg_); return; } - auto opts = serializer::deserialize_access_opts(req_["opts"]); - access_options to_set {opts["pause"], opts["rewind"], opts["host"]}; - room_->get_viewer(boost::lexical_cast(req_["viewer"])).lock()->set_access_opts(to_set); + access_options to_set {boost::lexical_cast(req_["pause"]), + boost::lexical_cast(req_["rewind"]), + boost::lexical_cast(req_["host"])}; + room_->get_viewer(boost::lexical_cast(req_["viewer"])).lock()->set_access_opts(to_set); break; } default: @@ -94,15 +116,16 @@ class handler { break; } } - catch (...){ - + // TODO replace with custom exceptions + catch (nlohmann::json::exception& ec){ + std::cout << ec.what() << std::endl; } } private: type type_ = invalid; - viewer_ptr viewer; + viewer_ptr viewer_; room_ptr room_; std::string str_req_; std::unordered_map req_; diff --git a/server/api/viewer/include/response_creator.hpp b/server/api/viewer/include/response_creator.hpp new file mode 100644 index 0000000..826b7bf --- /dev/null +++ b/server/api/viewer/include/response_creator.hpp @@ -0,0 +1,38 @@ +#ifndef WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP +#define WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP + +#include +#include + +#include + +#include "serializer.h" +#include "types.hpp" + +class response_creator{ +public: + static std::unordered_map create_with_status(unsigned short code, const std::string& msg){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["message"] = msg; + return data; + } + static std::unordered_map create_with_room(unsigned short code, const std::string& msg, const uuid& room_id){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["message"] = msg; + data["room_id"] = boost::lexical_cast(room_id); + return data; + } + + static std::unordered_map create_with_timing + (const boost::posix_time::time_duration& time){ + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << time; + auto str_time = ss_time.str(); + data["time"] = str_time; + return data; + } +}; +#endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/api/viewer/include/types.hpp b/server/api/viewer/include/types.hpp index 951dedd..e3a0b3f 100644 --- a/server/api/viewer/include/types.hpp +++ b/server/api/viewer/include/types.hpp @@ -21,15 +21,36 @@ enum type { }; static std::unordered_map const types = { - {"invalid", type::invalid}, - {"create", type::create }, - {"join", type::join } + {"invalid", type::invalid }, + {"create", type::create }, + {"join", type::join }, + {"leave", type::leave }, + {"ping", type::ping }, + {"pong", type::pong }, + {"play", type::play }, + {"pause", type::pause_ }, + {"s_time", type::s_time }, + {"s_nick", type::s_nick }, + {"sync", type::sync_ }, + {"s_src", type::s_src }, + {"s_access",type::s_access}, + }; NLOHMANN_JSON_SERIALIZE_ENUM(type, { - {invalid, nullptr}, - {create, "create"}, - {join, "join"} + {invalid, nullptr }, + {create, "create" }, + {join, "join" }, + {leave, "leave" }, + {ping, "ping" }, + {pong, "pong" }, + {play, "play" }, + {pause_, "pause" }, + {s_time, "s_time" }, + {s_nick, "s_nick" }, + {sync_, "sync" }, + {s_src, "s_src" }, + {s_access, "s_access"} }) diff --git a/server/api/viewer/include/viewer.h b/server/api/viewer/include/viewer.h index aa9412b..8e03990 100644 --- a/server/api/viewer/include/viewer.h +++ b/server/api/viewer/include/viewer.h @@ -9,6 +9,7 @@ #include #include #include +#include #include "room.h" @@ -26,9 +27,9 @@ typedef std::unique_ptr stream_ptr; class handler; struct access_options{ - bool can_pause; - bool can_rewind; - bool is_host; + bool can_pause = false; + bool can_rewind = false; + bool is_host = false; }; @@ -36,15 +37,12 @@ class IViewer{ public: virtual ~IViewer() = default; virtual void start() = 0; - virtual void pause() = 0; - virtual void play() = 0; - virtual void rewind(int offset) = 0; - virtual void set_timestamp() = 0; - virtual void send_chat_msg(std::string msg) = 0; + virtual void do_read() = 0; virtual void do_close() = 0; - virtual void send_response(const std::string&) = 0; + virtual void send_message(const std::string&) = 0; + virtual void on_write(error_code ec) = 0; virtual uuid get_id() const = 0; virtual std::string get_nickname() const = 0; @@ -60,18 +58,15 @@ class IViewer{ class Viewer: public IViewer, public std::enable_shared_from_this { public: Viewer(stream_ptr && ws, uuid&& id, room_ptr&& room = nullptr); + Viewer(stream_ptr && ws, uuid&& id, access_options a_opts, room_ptr&& room = nullptr); ~Viewer() override; void start() override; - void pause() override; - void play() override; - void rewind(int ) override; - void set_timestamp() override; - void send_chat_msg(std::string ) override; void do_read() override; void do_close() override; - void send_response(const std::string&) override; + void send_message(const std::string&) override; + void on_write(error_code ec) override; uuid get_id() const override; std::string get_nickname() const override; @@ -82,7 +77,6 @@ class Viewer: public IViewer, public std::enable_shared_from_this { void set_access_opts(const access_options& )override; protected: - bool is_host; uuid id_; room_ptr room_; std::string nickname_; @@ -90,7 +84,6 @@ class Viewer: public IViewer, public std::enable_shared_from_this { stream_ptr ws_; beast::flat_buffer buffer_; - std::queue response_q; }; diff --git a/server/api/viewer/include/viewer_creator.hpp b/server/api/viewer/include/viewer_creator.hpp index d1e6bc6..63638c2 100644 --- a/server/api/viewer/include/viewer_creator.hpp +++ b/server/api/viewer/include/viewer_creator.hpp @@ -15,8 +15,9 @@ class viewer_creator { (std::move(ws), boost::uuids::random_generator()(), std::move(room)); } - static std::shared_ptr create_host(stream_ptr && ws){ - return std::make_shared(std::move(ws), boost::uuids::random_generator()()); + static std::shared_ptr create_host(stream_ptr && ws){ + access_options opts{true, true, true}; + return std::make_shared(std::move(ws), boost::uuids::random_generator()(), opts); } }; diff --git a/server/api/viewer/include/viewer_manager.hpp b/server/api/viewer/include/viewer_manager.hpp index 09f4d8e..791c003 100644 --- a/server/api/viewer/include/viewer_manager.hpp +++ b/server/api/viewer/include/viewer_manager.hpp @@ -45,9 +45,9 @@ class viewer_manager { auto host = viewer_creator::create_host(std::move(ws_)); auto room = room_creator::create_room(std::weak_ptr(host), state_); state_->add_room(room->get_id(), std::weak_ptr(room)); - room->start(); host->set_room(room); host->start(); + room->start(); } else if (type_ == join) { auto room_id = boost::lexical_cast(req_["room_id"]); diff --git a/server/api/viewer/src/viewer.cpp b/server/api/viewer/src/viewer.cpp index 564cc6e..f57a48d 100644 --- a/server/api/viewer/src/viewer.cpp +++ b/server/api/viewer/src/viewer.cpp @@ -10,44 +10,34 @@ Viewer::Viewer(stream_ptr &&ws, uuid&& id, room_ptr&& room): ws_(std::move(ws)) {} +Viewer::Viewer(stream_ptr &&ws, uuid &&id, access_options a_opts, room_ptr&& room): + id_(std::forward(id)), + room_(std::move(room)), + a_opts_(a_opts), + ws_(std::move(ws)) +{} + Viewer::~Viewer(){ std::cout << "[viewer destructor]\n"; - room_->leave(id_); + room_->leave(id_, nickname_); } void Viewer::start() { + room_->join(shared_from_this()->weak_from_this()); do_read(); } -void Viewer::pause() { - -} - -void Viewer::play() { - -} - -void Viewer::rewind(int) { - -} - - -void Viewer::send_chat_msg(std::string) { - -} - - void Viewer::do_read() { ws_->async_read(buffer_, - [self{shared_from_this()}](error_code ec, std::size_t bytes_read){ + [self{shared_from_this()}](error_code ec, std::size_t ){ if(ec){ std::cout << "[viewer async read] " << ec.message() << std::endl; return; } auto in = beast::buffers_to_string(self->buffer_.cdata()); - self->buffer_.consume(bytes_read); + self->buffer_.consume(self->buffer_.size()); std::cout << "[viewer handle] " << in << std::endl; self->do_read(); std::make_unique(std::move(in), self->shared_from_this(), self->room_)->handle_request(); @@ -55,52 +45,62 @@ void Viewer::do_read() { } -uuid Viewer::get_id() const { - return id_; +void Viewer::do_close() { + ws_->close(""); } +void Viewer::send_message(const std::string & msg) { -std::string Viewer::get_nickname() const { - return nickname_; -} + response_q.push(msg); -void Viewer::set_nickname(std::string nickname) { - nickname_ = nickname; -} + if(response_q.size() > 1) + return; -void Viewer::set_room(const room_ptr &room) { - room_ = room; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); } -void Viewer::set_access_opts(const access_options& opts) { - a_opts_ = opts; -} +void Viewer::on_write(error_code ec) { + if(ec){ + std::cout << "viewer write error: " << ec.message() << std::endl; + return; + } + response_q.pop(); -void Viewer::set_timestamp() { + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } } -access_options Viewer::get_a_opts() const { - return a_opts_; -} - -void Viewer::do_close() { - ws_->close(""); +uuid Viewer::get_id() const { + return id_; } -void Viewer::send_response(const std::string &) { +std::string Viewer::get_nickname() const { + return nickname_; } +access_options Viewer::get_a_opts() const { + return a_opts_; +} -Host::Host(stream_ptr &&stream, uuid&& id): Viewer(std::move(stream), std::forward(id)) -{} - -void Host::synchronize() { - +void Viewer::set_nickname(std::string nickname) { + nickname_ = nickname; } -void Host::change_access_opts() { +void Viewer::set_room(const room_ptr &room) { + room_ = room; +} +void Viewer::set_access_opts(const access_options& opts) { + a_opts_ = opts; + std::cout << "can pause " << a_opts_.can_pause << std::endl; } diff --git a/server/core/src/server.cpp b/server/core/src/server.cpp index dcf71f2..0aea5df 100644 --- a/server/core/src/server.cpp +++ b/server/core/src/server.cpp @@ -13,7 +13,7 @@ namespace websocket = beast::websocket; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; -Server::Server(): started(false), opts_({"127.0.0.1", 8080}) +Server::Server(): started(false), opts_({"172.20.10.2", 8080}) {} void Server::run() { diff --git a/server/lib/serializer/serializer.h b/server/lib/serializer/serializer.h index 010aea7..050285a 100644 --- a/server/lib/serializer/serializer.h +++ b/server/lib/serializer/serializer.h @@ -16,16 +16,13 @@ using uuid = boost::uuids::uuid; class serializer { public: - static nlohmann::json parse(const std::string& json_str){ - return nlohmann::json::parse(json_str); - } - static std::unordered_map deserialize(const std::string& json_str){ //TODO try-catch - nlohmann::json json = nlohmann::json::parse(json_str); + nlohmann::json json = nlohmann::json::parse(json_str); std::unordered_map data{}; for(const auto& item: json.items()){ + data[item.key()] = item.value(); } return data; @@ -36,10 +33,36 @@ class serializer { std::unordered_map data{}; for(const auto& item: json.items()){ - data[item.key()] = boost::lexical_cast(item.value()); + data[item.key()] = boost::lexical_cast(item.value()); } return data; } -}; + + static std::string serialize_viewers(type type_, const std::vector>& viewers){ + nlohmann::json data{}; + data["type"] = type_; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: viewers){ + auto v_obj = nlohmann::json{}; + v_obj["id"] = boost::lexical_cast(viewer.first); + v_obj["nick"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["viewers"] = viewers_arr; + return nlohmann::to_string(data); + } + + + static std::string serialize_response(type type_, const std::unordered_map& fields){ + nlohmann::json data{}; + data["type"] = type_; + for(const auto& [key, value] : fields){ + data[key] = value; + } + return nlohmann::to_string(data); + } + + +}; #endif //WATCH_UP_PROJECT_SERIALIZER_H From a04d3aa2a764000d5f1ffe0bdfc2be17d42ee185 Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Sun, 11 Dec 2022 21:54:18 +0300 Subject: [PATCH 30/32] simple timing check --- server/api/room/include/room.h | 17 ++++++-- server/api/room/src/room.cpp | 43 ++++++++++++++++--- server/api/viewer/include/handler.h | 4 +- .../api/viewer/include/response_creator.hpp | 9 ++++ server/core/src/server.cpp | 2 +- server/lib/serializer/serializer.h | 15 +++++++ 6 files changed, 78 insertions(+), 12 deletions(-) diff --git a/server/api/room/include/room.h b/server/api/room/include/room.h index 1ad3e06..fd3fc05 100644 --- a/server/api/room/include/room.h +++ b/server/api/room/include/room.h @@ -35,10 +35,10 @@ class IRoom { virtual void join (w_viewer_ptr) = 0; virtual void leave(const uuid& id, const std::string& nick) = 0; - virtual void check_timings() = 0; + virtual void ping() = 0; virtual void pause() = 0; virtual void play() = 0; - virtual void synchronize(const boost::posix_time::time_duration &) = 0; + virtual void synchronize(const boost::posix_time::time_duration &)= 0; virtual void set_resource(const std::string&) = 0; virtual void send_chat_msg(uuid viewer, const std::string& msg) = 0; @@ -50,6 +50,7 @@ class IRoom { virtual void start_timer() = 0; virtual void stop_timer() = 0; + virtual void on_pong(std::unordered_map &req) =0; }; @@ -61,7 +62,7 @@ class Room: public IRoom, public std::enable_shared_from_this{ void start() override; void join (w_viewer_ptr) override; void leave(const uuid& id, const std::string& nick) override; - void check_timings() override; + void ping() override; void pause() override; void play() override; void synchronize(const boost::posix_time::time_duration&)override; @@ -74,16 +75,26 @@ class Room: public IRoom, public std::enable_shared_from_this{ const uuid get_id() const override; w_viewer_ptr get_viewer(const uuid& ) override; + void on_pong(std::unordered_map &req) override; private: + struct ping_info{ + boost::posix_time::time_duration player_time; + boost::posix_time::time_duration latency; + boost::posix_time::ptime response; + uuid viewer_id; + }; bool playing; uuid id_; uuid host_; state_ptr state_; boost::asio::deadline_timer check_timer_; + // boost::asio::deadline_timer ping_timeout; std::unordered_map> participants_; std::vector> viewers_; + std::pair> ping_; std::string src_; + }; #endif //WATCH_UP_PROJECT_ROOM_H diff --git a/server/api/room/src/room.cpp b/server/api/room/src/room.cpp index dedce32..a361771 100644 --- a/server/api/room/src/room.cpp +++ b/server/api/room/src/room.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "viewer.h" #include "shared_state.h" @@ -41,7 +42,7 @@ void Room::join(w_viewer_ptr viewer) { viewers_.emplace_back(viewer.lock()->get_id(), viewer.lock()->get_nickname()); } -void Room::leave(const uuid& id, const std::string& nick) { +void Room::leave(const uuid& id, const std::string& nick){ auto viewer_dto = std::make_pair(id, nick); auto it = find(viewers_.begin(), viewers_.end(), viewer_dto); participants_.erase(id); @@ -52,20 +53,23 @@ void Room::leave(const uuid& id, const std::string& nick) { } } -void Room::check_timings() { +void Room::ping(){ std::cout << "[check timings] id: "<< boost::uuids::to_string(id_)<< std::endl; if(participants_.empty()){ return; } if(!playing){ stop_timer(); - return; } std::cout << "[check timings]\n"; + auto ping_time = boost::posix_time::microsec_clock::universal_time(); + auto res = response_creator::create_ping(ping_time); + auto msg = serializer::serialize_response(type::ping, res); + ping_.first = ping_time; for(const auto& viewer : participants_){ - viewer.second.lock()->send_message("ping"); + viewer.second.lock()->send_message(msg); } - start_timer(); + } void Room::pause() { @@ -119,7 +123,7 @@ void Room::start_timer() { if(ec){ std::cout << "[check timings error] " << ec.message() << std::endl; } - self->check_timings(); + self->ping(); }); } @@ -143,3 +147,30 @@ void Room::set_nickname(const uuid &viewer, std::string &&nick) { } +void Room::on_pong(std::unordered_map &req) { + ping_.second.emplace_back(ping_info{ + boost::posix_time::duration_from_string(req["cur_time"]), + boost::posix_time::microsec_clock::universal_time() - ping_.first, + boost::posix_time::microsec_clock::universal_time(), + boost::lexical_cast(req["v_id"]) + }); + if (ping_.second.size() == participants_.size()){ + std::vector> timings; + for(const auto& ping : ping_.second){ + auto timing = ping.player_time + ping.latency + + (ping_.second.back().response - ping.response); + + std::stringstream ss_time; + ss_time << timing; + timings.emplace_back + (boost::lexical_cast(ping.viewer_id), ss_time.str()); + } + auto msg = serializer::serialize_timings(type::pong, timings); + for(const auto& p: participants_){ + p.second.lock()->send_message(msg); + } + ping_.second.clear(); + start_timer(); + } +} + diff --git a/server/api/viewer/include/handler.h b/server/api/viewer/include/handler.h index db4e381..24ab5e1 100644 --- a/server/api/viewer/include/handler.h +++ b/server/api/viewer/include/handler.h @@ -32,10 +32,10 @@ class handler { switch (type_) { case leave: viewer_->do_close(); - // TODO send ok break; case pong: - // TODO implement + req_["v_id"] = boost::lexical_cast(viewer_->get_id()); + room_->on_pong(req_); break; case play: { if (!viewer_->get_a_opts().can_pause) { diff --git a/server/api/viewer/include/response_creator.hpp b/server/api/viewer/include/response_creator.hpp index 826b7bf..850a023 100644 --- a/server/api/viewer/include/response_creator.hpp +++ b/server/api/viewer/include/response_creator.hpp @@ -34,5 +34,14 @@ class response_creator{ data["time"] = str_time; return data; } + + static std::unordered_map create_ping(const boost::posix_time::ptime& server_time){ + ; + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << server_time; + data["time"] = ss_time.str(); + return data; + } }; #endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/core/src/server.cpp b/server/core/src/server.cpp index 0aea5df..dcf71f2 100644 --- a/server/core/src/server.cpp +++ b/server/core/src/server.cpp @@ -13,7 +13,7 @@ namespace websocket = beast::websocket; // from namespace net = boost::asio; // from using tcp = boost::asio::ip::tcp; -Server::Server(): started(false), opts_({"172.20.10.2", 8080}) +Server::Server(): started(false), opts_({"127.0.0.1", 8080}) {} void Server::run() { diff --git a/server/lib/serializer/serializer.h b/server/lib/serializer/serializer.h index 050285a..0cac170 100644 --- a/server/lib/serializer/serializer.h +++ b/server/lib/serializer/serializer.h @@ -63,6 +63,21 @@ class serializer { return nlohmann::to_string(data); } + static std::string serialize_timings(type type_, const std::vector>& timings){ + nlohmann::json data{}; + data["type"] = type_; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: timings){ + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["time"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["timings"] = viewers_arr; + return nlohmann::to_string(data); + } + + }; #endif //WATCH_UP_PROJECT_SERIALIZER_H From 6cbd29331f6fdf945372d21a783497abb67ce02a Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Mon, 12 Dec 2022 16:34:40 +0300 Subject: [PATCH 31/32] dz#6 --- server/api/room/include/room.h | 4 +- server/api/room/src/room.cpp | 20 +++- server/api/viewer/include/handler.h | 92 +++++++++---------- .../api/viewer/include/response_creator.hpp | 7 +- server/api/viewer/include/types.hpp | 15 +++ server/api/viewer/include/viewer.h | 4 +- server/api/viewer/include/viewer_creator.hpp | 8 +- server/api/viewer/include/viewer_manager.hpp | 29 ++++-- server/api/viewer/src/viewer.cpp | 21 ++--- server/core/src/ws_session.cpp | 2 +- server/lib/serializer/serializer.h | 7 +- 11 files changed, 127 insertions(+), 82 deletions(-) diff --git a/server/api/room/include/room.h b/server/api/room/include/room.h index fd3fc05..9a5ab6e 100644 --- a/server/api/room/include/room.h +++ b/server/api/room/include/room.h @@ -36,7 +36,7 @@ class IRoom { virtual void leave(const uuid& id, const std::string& nick) = 0; virtual void ping() = 0; - virtual void pause() = 0; + virtual void pause(const boost::posix_time::time_duration &) = 0; virtual void play() = 0; virtual void synchronize(const boost::posix_time::time_duration &)= 0; @@ -63,7 +63,7 @@ class Room: public IRoom, public std::enable_shared_from_this{ void join (w_viewer_ptr) override; void leave(const uuid& id, const std::string& nick) override; void ping() override; - void pause() override; + void pause(const boost::posix_time::time_duration &) override; void play() override; void synchronize(const boost::posix_time::time_duration&)override; void send_chat_msg(uuid viewer, const std::string& msg) override; diff --git a/server/api/room/src/room.cpp b/server/api/room/src/room.cpp index a361771..f960891 100644 --- a/server/api/room/src/room.cpp +++ b/server/api/room/src/room.cpp @@ -72,14 +72,16 @@ void Room::ping(){ } -void Room::pause() { +void Room::pause(const boost::posix_time::time_duration & time) { if(!playing){ return; } playing = false; + auto msg = serializer::serialize_response(pause_, {}); for(const auto& p : participants_){ - p.second.lock()->send_message("");; + p.second.lock()->send_message(msg);; } + synchronize(time); } void Room::play() { @@ -87,8 +89,9 @@ void Room::play() { return; } playing = true; + auto msg = serializer::serialize_response(type::play, {}); for(const auto& p : participants_){ - p.second.lock()->send_message(""); + p.second.lock()->send_message(msg); } start_timer(); } @@ -96,8 +99,9 @@ void Room::play() { void Room::synchronize(const boost::posix_time::time_duration & timing) { auto msg = serializer::serialize_response(s_time, response_creator::create_with_timing(timing)); + for(const auto& p : participants_){ - p.second.lock()->send_message(""); + p.second.lock()->send_message(msg); } } @@ -133,6 +137,14 @@ void Room::stop_timer() { void Room::set_resource(const std::string & src) { src_ = src; + // TODO refactor + std::unordered_map res{ + {"src", src} + }; + auto msg = serializer::serialize_response(s_src, res); + for(const auto& p : participants_){ + p.second.lock()->send_message(msg); + } } w_viewer_ptr Room::get_viewer(const uuid& v_id) { diff --git a/server/api/viewer/include/handler.h b/server/api/viewer/include/handler.h index 24ab5e1..14b8119 100644 --- a/server/api/viewer/include/handler.h +++ b/server/api/viewer/include/handler.h @@ -28,6 +28,7 @@ class handler { void handle_request(){ try{ req_=serializer::deserialize(str_req_); + std::cout <<"[viewer handle request]\n" << str_req_ << std::endl; define_type(); switch (type_) { case leave: @@ -39,92 +40,91 @@ class handler { break; case play: { if (!viewer_->get_a_opts().can_pause) { - auto msg_ = response_creator::create_with_status(403, "operation forbidden"); - auto str_msg_ = serializer::serialize_response(play, msg_); - viewer_->send_message(str_msg_); - return; + status_ = 403; + break; } - auto msg = response_creator::create_with_status(200, "ok"); - auto str_msg = serializer::serialize_response(play, msg); + status_ = 200; room_->play(); } break; - case pause_: - if(!viewer_->get_a_opts().can_pause){ - auto msg_ = response_creator::create_with_status(403, "operation forbidden"); - auto str_msg_ = serializer::serialize_response(pause_, msg_); - viewer_->send_message(str_msg_); - return; + case pause_: { + if (!viewer_->get_a_opts().can_pause) { + status_ = 403; + break; } - room_->pause(); - // TODO send ok + + room_->pause(boost::lexical_cast(req_["time"])); + status_ = 200; + } break; - case s_time: - if(!viewer_->get_a_opts().can_rewind){ - auto msg_ = response_creator::create_with_status(403, "operation forbidden"); - auto str_msg_ = serializer::serialize_response(s_time, msg_); - viewer_->send_message(str_msg_); - return; + case s_time: { + if (!viewer_->get_a_opts().can_rewind) { + status_ = 403; + break; } - + status_ = 200; room_->synchronize(boost::lexical_cast(req_["time"])); - // TODO send ok + } break; case s_nick: - if(viewer_->get_nickname() == req_["nick"]){ - auto msg = response_creator::create_with_status(200, "ok"); - auto str_msg = serializer::serialize_response(s_nick, msg); - viewer_->send_message(str_msg); - return; + if (viewer_->get_nickname() == req_["nick"]) { + status_ = 200; + break; } viewer_->set_nickname(req_["nick"]); room_->set_nickname(viewer_->get_id(), std::move(req_["nick"])); + status_ = 200; break; - case sync_: - if(!viewer_->get_a_opts().is_host){ - // TODO send forbidden - return; + case sync_: { + if (!viewer_->get_a_opts().is_host) { + status_ = 403; + break; } + status_ = 200; room_->synchronize(boost::lexical_cast(req_["time"])); + } break; - case s_src: - if(!viewer_->get_a_opts().is_host){ - auto msg_ = response_creator::create_with_status(403, "operation forbidden"); - auto str_msg_ = serializer::serialize_response(s_src, msg_); - viewer_->send_message(str_msg_); - return; + case s_src: { + if (!viewer_->get_a_opts().is_host) { + status_ = 403; + break; } + status_ = 200; room_->set_resource(req_["src"]); - //TODO send ok + } break; case s_access: { if (!viewer_->get_a_opts().is_host) { - auto msg_ = response_creator::create_with_status(403, "operation forbidden"); - auto str_msg_ = serializer::serialize_response(s_access, msg_); - viewer_->send_message(str_msg_); - return; + status_ = 403; + break; } - access_options to_set {boost::lexical_cast(req_["pause"]), boost::lexical_cast(req_["rewind"]), boost::lexical_cast(req_["host"])}; room_->get_viewer(boost::lexical_cast(req_["viewer"])).lock()->set_access_opts(to_set); + status_ = 200; break; } default: - + status_ = 405; break; } } // TODO replace with custom exceptions catch (nlohmann::json::exception& ec){ + status_ = 400; std::cout << ec.what() << std::endl; } - - + catch(...){ + std::cout << "ex\n"; + status_ = 500; + } + auto msg = response_creator::create_with_status(type_, status_); + viewer_->send_message(msg); } private: type type_ = invalid; + unsigned short status_; viewer_ptr viewer_; room_ptr room_; std::string str_req_; diff --git a/server/api/viewer/include/response_creator.hpp b/server/api/viewer/include/response_creator.hpp index 850a023..4a97bfb 100644 --- a/server/api/viewer/include/response_creator.hpp +++ b/server/api/viewer/include/response_creator.hpp @@ -11,11 +11,11 @@ class response_creator{ public: - static std::unordered_map create_with_status(unsigned short code, const std::string& msg){ + static std::string create_with_status(type type_, unsigned short code){ std::unordered_map data{}; data["code"] = std::to_string(code); - data["message"] = msg; - return data; + auto out = serializer::serialize_response(type_, data); + return out; } static std::unordered_map create_with_room(unsigned short code, const std::string& msg, const uuid& room_id){ std::unordered_map data{}; @@ -36,7 +36,6 @@ class response_creator{ } static std::unordered_map create_ping(const boost::posix_time::ptime& server_time){ - ; std::unordered_map data{}; std::stringstream ss_time; ss_time << server_time; diff --git a/server/api/viewer/include/types.hpp b/server/api/viewer/include/types.hpp index e3a0b3f..7db13a5 100644 --- a/server/api/viewer/include/types.hpp +++ b/server/api/viewer/include/types.hpp @@ -34,7 +34,22 @@ static std::unordered_map const types = { {"sync", type::sync_ }, {"s_src", type::s_src }, {"s_access",type::s_access}, +}; +static std::unordered_map const type_to_string = { + {type::invalid, "invalid" }, + {type::create, "create" }, + {type::join , "join" }, + {type::leave, "leave" }, + {type::ping, "ping" }, + {type::pong, "pong" }, + {type::play, "play" }, + {type::pause_, "pause" }, + {type::s_time, "s_time" }, + {type::s_nick, "s_nick" }, + {type::sync_ , "sync" }, + {type::s_src , "s_src" }, + {type::s_access, "s_access"}, }; NLOHMANN_JSON_SERIALIZE_ENUM(type, { diff --git a/server/api/viewer/include/viewer.h b/server/api/viewer/include/viewer.h index 8e03990..51448ff 100644 --- a/server/api/viewer/include/viewer.h +++ b/server/api/viewer/include/viewer.h @@ -57,8 +57,8 @@ class IViewer{ class Viewer: public IViewer, public std::enable_shared_from_this { public: - Viewer(stream_ptr && ws, uuid&& id, room_ptr&& room = nullptr); - Viewer(stream_ptr && ws, uuid&& id, access_options a_opts, room_ptr&& room = nullptr); + Viewer(stream_ptr && ws, uuid&& id, std::string nickname, room_ptr&& room = nullptr); + Viewer(stream_ptr && ws, uuid&& id, access_options a_opts, std::string nickname, room_ptr&& room = nullptr); ~Viewer() override; void start() override; diff --git a/server/api/viewer/include/viewer_creator.hpp b/server/api/viewer/include/viewer_creator.hpp index 63638c2..58e5413 100644 --- a/server/api/viewer/include/viewer_creator.hpp +++ b/server/api/viewer/include/viewer_creator.hpp @@ -10,14 +10,14 @@ typedef boost::uuids::uuid uuid; class viewer_creator { public: - static std::shared_ptr create_viewer(stream_ptr && ws, room_ptr&& room = nullptr){ + static std::shared_ptr create_viewer(stream_ptr && ws, std::string nickname, room_ptr&& room = nullptr){ return std::make_shared - (std::move(ws), boost::uuids::random_generator()(), std::move(room)); + (std::move(ws), boost::uuids::random_generator()(), std::move(nickname), std::move(room)); } - static std::shared_ptr create_host(stream_ptr && ws){ + static std::shared_ptr create_host(stream_ptr && ws, std::string nickname){ access_options opts{true, true, true}; - return std::make_shared(std::move(ws), boost::uuids::random_generator()(), opts); + return std::make_shared(std::move(ws), boost::uuids::random_generator()(), opts, std::move(nickname)); } }; diff --git a/server/api/viewer/include/viewer_manager.hpp b/server/api/viewer/include/viewer_manager.hpp index 791c003..ab850f9 100644 --- a/server/api/viewer/include/viewer_manager.hpp +++ b/server/api/viewer/include/viewer_manager.hpp @@ -11,6 +11,7 @@ #include "serializer.h" #include "viewer_creator.hpp" #include "room_creator.hpp" +#include "response_creator.hpp" namespace beast = boost::beast; namespace websocket = beast::websocket; @@ -34,16 +35,23 @@ class viewer_manager { void define_type(){ auto it = types.find(req_["type"]); + if(it == types.end()) + return; type_ = it->second; } void handle_request(){ try { req_ = serializer::deserialize(str_req_); + define_type(); + std::cout << str_req_ << std::endl; if (type_ == create) { - auto host = viewer_creator::create_host(std::move(ws_)); + //auto nickname = req_["nick"]; + auto host = viewer_creator::create_host(std::move(ws_), "blank"); auto room = room_creator::create_room(std::weak_ptr(host), state_); + + state_->add_room(room->get_id(), std::weak_ptr(room)); host->set_room(room); host->start(); @@ -51,21 +59,30 @@ class viewer_manager { } else if (type_ == join) { auto room_id = boost::lexical_cast(req_["room_id"]); + //auto nickname = req_["nick"]; auto room = state_->get_room(room_id); if (!room.lock()) { - // TODO send not found + //ws_->async_write("not found"); return; } - viewer_creator::create_viewer(std::move(ws_), room.lock())->start(); + viewer_creator::create_viewer(std::move(ws_), "blank", room.lock())->start(); } else if (type_ == invalid) { - // TODO send bad request + ws_->async_write(net::buffer("bad request"), + [](error_code , std::size_t){ + std::cout << "send\n"; + }); return; } else { // TODO send method not allowed + ws_->async_write(net::buffer("not allowed"), + [](error_code , std::size_t){ + std::cout << "send\n"; + }); } } - catch (...){ - + catch (nlohmann::json::exception& ec){ + std::cout << ec.what(); + return; } } private: diff --git a/server/api/viewer/src/viewer.cpp b/server/api/viewer/src/viewer.cpp index f57a48d..182f450 100644 --- a/server/api/viewer/src/viewer.cpp +++ b/server/api/viewer/src/viewer.cpp @@ -4,15 +4,17 @@ #include "handler.h" -Viewer::Viewer(stream_ptr &&ws, uuid&& id, room_ptr&& room): +Viewer::Viewer(stream_ptr &&ws, uuid&& id, std::string nickname, room_ptr&& room): id_(std::forward(id)), room_(std::move(room)), + nickname_(std::move(nickname)), ws_(std::move(ws)) {} -Viewer::Viewer(stream_ptr &&ws, uuid &&id, access_options a_opts, room_ptr&& room): +Viewer::Viewer(stream_ptr &&ws, uuid &&id, access_options a_opts, std::string nickname,room_ptr&& room): id_(std::forward(id)), room_(std::move(room)), + nickname_(std::move(nickname)), a_opts_(a_opts), ws_(std::move(ws)) {} @@ -24,21 +26,21 @@ Viewer::~Viewer(){ void Viewer::start() { - room_->join(shared_from_this()->weak_from_this()); do_read(); } void Viewer::do_read() { ws_->async_read(buffer_, + // expands the lifetime of viewer instance [self{shared_from_this()}](error_code ec, std::size_t ){ - if(ec){ - std::cout << "[viewer async read] " << ec.message() << std::endl; + if(ec && ec != boost::asio::error::eof){ + std::cout << "[viewer async read error] " << ec.message() << std::endl; return; } auto in = beast::buffers_to_string(self->buffer_.cdata()); self->buffer_.consume(self->buffer_.size()); - std::cout << "[viewer handle] " << in << std::endl; + // creates an event loop self->do_read(); std::make_unique(std::move(in), self->shared_from_this(), self->room_)->handle_request(); }); @@ -50,22 +52,18 @@ void Viewer::do_close() { } void Viewer::send_message(const std::string & msg) { - response_q.push(msg); - if(response_q.size() > 1) return; - ws_->async_write(net::buffer(msg), [self{shared_from_this()}](error_code ec, std::size_t){ self->on_write(ec); }); } - void Viewer::on_write(error_code ec) { if(ec){ - std::cout << "viewer write error: " << ec.message() << std::endl; + std::cout << "[viewer write error] " << ec.message() << std::endl; return; } response_q.pop(); @@ -76,7 +74,6 @@ void Viewer::on_write(error_code ec) { self->on_write(ec_); }); } - } uuid Viewer::get_id() const { diff --git a/server/core/src/ws_session.cpp b/server/core/src/ws_session.cpp index df586cf..4996bfc 100644 --- a/server/core/src/ws_session.cpp +++ b/server/core/src/ws_session.cpp @@ -20,7 +20,7 @@ void ws_session::run(){ void ws_session::do_read() { ws_->async_read(buffer_, [self{shared_from_this()}](error_code ec, std::size_t){ - if(ec){ + if(ec && ec != boost::asio::error::eof){ std::cout << "[ws async read error]" << ec.message() << std::endl; return; } diff --git a/server/lib/serializer/serializer.h b/server/lib/serializer/serializer.h index 0cac170..7ab9923 100644 --- a/server/lib/serializer/serializer.h +++ b/server/lib/serializer/serializer.h @@ -56,7 +56,12 @@ class serializer { static std::string serialize_response(type type_, const std::unordered_map& fields){ nlohmann::json data{}; - data["type"] = type_; + if(type_ == invalid){ + data["type"] = "invalid"; + } + else { + data["type"] = type_; + } for(const auto& [key, value] : fields){ data[key] = value; } From 70b3cc12b771a3d6c0238890e854678130122572 Mon Sep 17 00:00:00 2001 From: WhoIsYgim Date: Mon, 26 Dec 2022 04:09:51 +0300 Subject: [PATCH 32/32] ready to present --- server/CMakeLists.txt | 6 + server/DB_client/CMakeLists.txt | 7 + server/DB_client/include/DB.h | 25 +++ server/DB_client/src/DB.cpp | 32 +++ server/api/CMakeLists.txt | 6 +- server/api/chat/CMakeLists.txt | 7 - server/api/chat/include/chat.h | 14 -- server/api/isession/CMakeLists.txt | 0 server/api/isession/session.cpp | 26 +++ server/api/isession/session.h | 41 ++++ server/api/room/CMakeLists.txt | 3 +- server/api/room/include/room.h | 113 ++++++----- server/api/room/src/room.cpp | 186 ++++++++++++------ server/api/user/CMakeLists.txt | 6 + server/api/user/include/us_types.hpp | 26 +++ server/api/user/include/user.h | 35 ++++ server/api/user/include/user_creator.h | 12 ++ server/api/user/include/user_session.h | 51 +++++ server/api/user/src/user_session.cpp | 97 +++++++++ server/api/viewer/CMakeLists.txt | 5 +- server/api/viewer/include/handler.h | 79 +++++--- .../api/viewer/include/response_creator.hpp | 46 ----- server/api/viewer/include/types.hpp | 72 ------- server/api/viewer/include/viewer.h | 24 ++- server/api/viewer/include/viewer_creator.hpp | 17 +- server/api/viewer/include/viewer_manager.hpp | 76 ++----- server/api/viewer/include/vs_types.hpp | 71 +++++++ server/api/viewer/src/viewer.cpp | 48 +++-- server/api/viewer/src/viewer_manager.cpp | 96 +++++++++ server/api/ws_session/CMakeLists.txt | 6 + server/api/ws_session/include/ps_types.hpp | 26 +++ .../ws_session}/include/ws_session.hpp | 16 +- server/api/ws_session/src/ws_session.cpp | 124 ++++++++++++ server/core/CMakeLists.txt | 2 +- server/core/src/ws_session.cpp | 42 ---- server/lib/CMakeLists.txt | 1 + server/lib/response/CMakeLists.txt | 6 + .../lib/response/include/response_creator.hpp | 33 ++++ server/lib/response/src/response_creator.cpp | 68 +++++++ server/lib/serializer/CMakeLists.txt | 7 +- server/lib/serializer/include/serializer.h | 35 ++++ server/lib/serializer/serializer.h | 88 --------- server/lib/serializer/src/serializer.cpp | 84 ++++++++ server/uml/uml_server | 2 +- 44 files changed, 1263 insertions(+), 504 deletions(-) create mode 100644 server/DB_client/CMakeLists.txt create mode 100644 server/DB_client/include/DB.h create mode 100644 server/DB_client/src/DB.cpp delete mode 100644 server/api/chat/CMakeLists.txt delete mode 100644 server/api/chat/include/chat.h create mode 100644 server/api/isession/CMakeLists.txt create mode 100644 server/api/isession/session.cpp create mode 100644 server/api/isession/session.h create mode 100644 server/api/user/CMakeLists.txt create mode 100644 server/api/user/include/us_types.hpp create mode 100644 server/api/user/include/user.h create mode 100644 server/api/user/include/user_creator.h create mode 100644 server/api/user/include/user_session.h create mode 100644 server/api/user/src/user_session.cpp delete mode 100644 server/api/viewer/include/response_creator.hpp delete mode 100644 server/api/viewer/include/types.hpp create mode 100644 server/api/viewer/include/vs_types.hpp create mode 100644 server/api/viewer/src/viewer_manager.cpp create mode 100644 server/api/ws_session/CMakeLists.txt create mode 100644 server/api/ws_session/include/ps_types.hpp rename server/{core => api/ws_session}/include/ws_session.hpp (76%) create mode 100644 server/api/ws_session/src/ws_session.cpp delete mode 100644 server/core/src/ws_session.cpp create mode 100644 server/lib/response/CMakeLists.txt create mode 100644 server/lib/response/include/response_creator.hpp create mode 100644 server/lib/response/src/response_creator.cpp create mode 100644 server/lib/serializer/include/serializer.h delete mode 100644 server/lib/serializer/serializer.h create mode 100644 server/lib/serializer/src/serializer.cpp diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index e421575..99b2316 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,18 +1,24 @@ project(server) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Boost REQUIRED) find_package(Threads REQUIRED) +add_compile_definitions(_GLIBCXX_USE_CXX11_ABI=0) + add_executable(${PROJECT_NAME} main.cpp) target_link_libraries(${PROJECT_NAME} PUBLIC server_core) target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) +#target_link_libraries(${PROJECT_NAME} PUBLIC /usr/lib/x86_64-linux-gnu/libboost_date_time.a) add_subdirectory(api) add_subdirectory(lib) add_subdirectory(core) +add_subdirectory(DB_client) if (BUILD_TESTS) diff --git a/server/DB_client/CMakeLists.txt b/server/DB_client/CMakeLists.txt new file mode 100644 index 0000000..d4df130 --- /dev/null +++ b/server/DB_client/CMakeLists.txt @@ -0,0 +1,7 @@ +project(db_client) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) + +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC user_session) \ No newline at end of file diff --git a/server/DB_client/include/DB.h b/server/DB_client/include/DB.h new file mode 100644 index 0000000..d704838 --- /dev/null +++ b/server/DB_client/include/DB.h @@ -0,0 +1,25 @@ +#ifndef SERVER_DB_H +#define SERVER_DB_H + +#include +#include + +#include "user.h" + + +class DB { +public: + static DB& get_instance(); + std::unique_ptr get_user_by_id(const std::string& id, const std::string& pass); + bool add_user(const std::string& nickname, const std::string& login, const std::string& password); + + DB(const DB& ) = delete; + void operator=(const DB&) = delete; +private: + DB()= default; + + + std::unordered_map> users; +}; + +#endif //SERVER_DB_H diff --git a/server/DB_client/src/DB.cpp b/server/DB_client/src/DB.cpp new file mode 100644 index 0000000..d2249a6 --- /dev/null +++ b/server/DB_client/src/DB.cpp @@ -0,0 +1,32 @@ +#include "DB.h" + + +DB &DB::get_instance() { + static DB instance; + return instance; +} + +std::unique_ptr DB::get_user_by_id(const std::string& id, const std::string& pass) { + auto it = users.find(id); + if(it == users.end()){ + return {}; + } + if(it->second.second != pass){ + return {}; + } + return std::make_unique(it->second.first); +} + +bool DB::add_user(const std::string& nickname, const std::string& login, const std::string& password) { + if(login.empty()) + return false; + if(users.find(login) == users.end()){ + User new_user; + new_user.set_nick(nickname); + new_user.set_id(login); + new_user.set_pass(password); + users.insert({login, {new_user,password}}); + return true; + } + return false; +} diff --git a/server/api/CMakeLists.txt b/server/api/CMakeLists.txt index e96c5c7..0f904e1 100644 --- a/server/api/CMakeLists.txt +++ b/server/api/CMakeLists.txt @@ -1,4 +1,4 @@ - -add_subdirectory(viewer) add_subdirectory(room) -add_subdirectory(chat) +add_subdirectory(viewer) +add_subdirectory(ws_session) +add_subdirectory(user) diff --git a/server/api/chat/CMakeLists.txt b/server/api/chat/CMakeLists.txt deleted file mode 100644 index 60a5617..0000000 --- a/server/api/chat/CMakeLists.txt +++ /dev/null @@ -1,7 +0,0 @@ -project(chat) - -file(GLOB SOURCES "src/*") - -add_library(${PROJECT_NAME} INTERFACE) - -target_include_directories(${PROJECT_NAME} INTERFACE include) diff --git a/server/api/chat/include/chat.h b/server/api/chat/include/chat.h deleted file mode 100644 index 783025d..0000000 --- a/server/api/chat/include/chat.h +++ /dev/null @@ -1,14 +0,0 @@ -#ifndef WATCH_UP_PROJECT_CHAT_H -#define WATCH_UP_PROJECT_CHAT_H - -class IChat{ -public: -}; - -class Chat : public IChat{ -public: - -}; - - -#endif //WATCH_UP_PROJECT_CHAT_H diff --git a/server/api/isession/CMakeLists.txt b/server/api/isession/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/server/api/isession/session.cpp b/server/api/isession/session.cpp new file mode 100644 index 0000000..ece15fd --- /dev/null +++ b/server/api/isession/session.cpp @@ -0,0 +1,26 @@ +#include "session.h" + +session::session(tcp::socket &&socket) +{} +session::session(stream_ptr&& ws){} + + +void session::run(){ + +} +void session::do_read(){ + +} +void session::do_close(){ + +} +void session::handle_request(){ + +} +void session::send_msg(const std::string& msg){ + +} +void session::on_write(error_code ec){ + +} + diff --git a/server/api/isession/session.h b/server/api/isession/session.h new file mode 100644 index 0000000..2682bca --- /dev/null +++ b/server/api/isession/session.h @@ -0,0 +1,41 @@ +#ifndef SERVER_SESSION_H +#define SERVER_SESSION_H + +#include +#include + +#include +#include + + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; + +class session : public std::enable_shared_from_this{ +public: + session(tcp::socket &&socket); + session(stream_ptr&& ws); + + void run(); + void do_read(); + void do_close(); + void handle_request(); + void send_msg(const std::string& msg); + void on_write(error_code ec); +protected: + stream_ptr ws_; + beast::flat_buffer buffer_; + + std::queue response_q; +}; + + +#endif //SERVER_SESSION_H + + diff --git a/server/api/room/CMakeLists.txt b/server/api/room/CMakeLists.txt index e73d5d5..87e5d7f 100644 --- a/server/api/room/CMakeLists.txt +++ b/server/api/room/CMakeLists.txt @@ -2,9 +2,10 @@ project(room) file(GLOB SOURCES "src/*") + add_library(${PROJECT_NAME} ${SOURCES}) target_include_directories(${PROJECT_NAME} PUBLIC include) -target_link_libraries(${PROJECT_NAME} PUBLIC viewer server_core) +target_link_libraries(${PROJECT_NAME} PUBLIC viewer server_core serializer response) diff --git a/server/api/room/include/room.h b/server/api/room/include/room.h index 9a5ab6e..10e6ceb 100644 --- a/server/api/room/include/room.h +++ b/server/api/room/include/room.h @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -29,71 +30,93 @@ typedef std::shared_ptr state_ptr; class IRoom { public: - virtual ~IRoom() = default; + virtual ~IRoom() = default; - virtual void start() = 0; - virtual void join (w_viewer_ptr) = 0; - virtual void leave(const uuid& id, const std::string& nick) = 0; + virtual void start() =0; + virtual void join (w_viewer_ptr) =0; + virtual void leave(const std::string& id, const std::string& nick) =0; - virtual void ping() = 0; - virtual void pause(const boost::posix_time::time_duration &) = 0; - virtual void play() = 0; - virtual void synchronize(const boost::posix_time::time_duration &)= 0; + virtual void ping() =0; + virtual void pause(const boost::posix_time::time_duration &, const std::string&)=0; + virtual void play(const std::string& sender_id) =0; + virtual void synchronize(const boost::posix_time::time_duration &) =0; - virtual void set_resource(const std::string&) = 0; - virtual void send_chat_msg(uuid viewer, const std::string& msg) = 0; - virtual void set_nickname(const uuid& viewer, std::string&& nick)= 0; + virtual void set_resource(const std::string &, const std::string &viewer) =0; + virtual void send_chat_msg(const std::string &nick, const std::string& msg) =0; + virtual void set_nickname(const std::string& viewer, std::string&& nick) =0; + virtual void set_service(const std::string& service, const std::string& viewer) =0; - virtual w_viewer_ptr get_viewer(const uuid& ) = 0; - virtual const uuid get_id() const = 0; + virtual w_viewer_ptr get_viewer(const std::string& ) =0; + virtual const uuid get_id() const =0; + virtual state_ptr get_state() =0; + virtual void start_timer() =0; + virtual void stop_timer() =0; + virtual void on_pong(std::unordered_map &req) =0; + virtual bool is_playing() =0; - virtual void start_timer() = 0; - virtual void stop_timer() = 0; - virtual void on_pong(std::unordered_map &req) =0; - + virtual void service(const std::string&) =0; }; class Room: public IRoom, public std::enable_shared_from_this{ public: - Room(uuid&& host, uuid&& id, state_ptr state); - ~Room() override; - void start() override; - void join (w_viewer_ptr) override; - void leave(const uuid& id, const std::string& nick) override; - void ping() override; - void pause(const boost::posix_time::time_duration &) override; - void play() override; - void synchronize(const boost::posix_time::time_duration&)override; - void send_chat_msg(uuid viewer, const std::string& msg) override; - void set_resource(const std::string&) override; - void set_nickname(const uuid& viewer, std::string&& nick)override; - - void start_timer() override; - void stop_timer() override; - - const uuid get_id() const override; - w_viewer_ptr get_viewer(const uuid& ) override; - void on_pong(std::unordered_map &req) override; + Room(std::string&& host, uuid&& id, state_ptr state); + ~Room() override; + void start() override; + void join (w_viewer_ptr) override; + void leave(const std::string& id, const std::string& nick) override; + void ping() override; + void pause(const boost::posix_time::time_duration &, const std::string&)override; + void play(const std::string& sender_id) override; + void synchronize(const boost::posix_time::time_duration&) override; + void send_chat_msg(const std::string &nick, const std::string& msg) override; + void set_resource(const std::string &, const std::string &viewer) override; + void set_nickname(const std::string& viewer, std::string&& nick) override; + void set_service(const std::string& service, const std::string& viewer) override; + + void start_timer() override; + void stop_timer() override; + + const uuid get_id() const override; + w_viewer_ptr get_viewer(const std::string& ) override; + state_ptr get_state() override; + void on_pong(std::unordered_map &req) override; + + bool is_playing() override; + + void service(const std::string&) override; +private: + std::string calculate_timestamp(); private: struct ping_info{ boost::posix_time::time_duration player_time; boost::posix_time::time_duration latency; boost::posix_time::ptime response; - uuid viewer_id; + std::string viewer_id; }; - bool playing; - uuid id_; - uuid host_; state_ptr state_; - boost::asio::deadline_timer check_timer_; - // boost::asio::deadline_timer ping_timeout; - std::unordered_map> participants_; - std::vector> viewers_; - std::pair> ping_; + + bool playing = false; + + uuid id_; + std::string host_; std::string src_; + std::string service_; + + boost::posix_time::time_duration current_timestamp; + boost::posix_time::ptime last_update; + boost::posix_time::ptime last_server_time; + + std::unordered_map participants_; + std::vector> viewers_; + + boost::asio::deadline_timer check_timer_; + + //user id -> latency + std::unordered_map latency_; + }; diff --git a/server/api/room/src/room.cpp b/server/api/room/src/room.cpp index f960891..e299d50 100644 --- a/server/api/room/src/room.cpp +++ b/server/api/room/src/room.cpp @@ -11,93 +11,100 @@ #include "serializer.h" #include "response_creator.hpp" -Room::Room(uuid&& host, uuid&& id, state_ptr state): +Room::Room(std::string&& host, uuid&& id, state_ptr state): playing(false), id_(std::forward(id)), - host_(std::forward(host)), + host_(std::move(host)), state_(std::move(state)), check_timer_(state_->get_io_context(), boost::posix_time::seconds(5)) { + std::cout << "[room constructor]\n"; } void Room::start() { std::cout << "[room start]\n"; + start_timer(); } void Room::join(w_viewer_ptr viewer) { + std::cout << "[ROOM JOIN]\n"; + if(participants_.find(viewer.lock()->get_id()) != participants_.end()){ + participants_[viewer.lock()->get_id()].lock()->set_room(nullptr); + participants_[viewer.lock()->get_id()] = viewer; + auto welcome_msg = serializer::serialize_welcome_msg("join", viewers_, src_, calculate_timestamp(), + playing, id_, service_, viewer.lock()->get_id()); + viewer.lock()->send_message(welcome_msg); + return; + } if(!viewers_.empty()) { - auto welcome_msg = serializer::serialize_viewers(type::join, viewers_); + + auto welcome_msg = serializer::serialize_welcome_msg("join", viewers_, src_, calculate_timestamp(), playing, id_, service_); viewer.lock()->send_message(welcome_msg); - auto incomer_msg = serializer::serialize_viewers(type::join, {std::make_pair(viewer.lock()->get_id(), viewer.lock()->get_nickname())}); + auto incomer_msg = serializer::serialize_viewers("incomer", {std::make_pair(viewer.lock()->get_id(), viewer.lock()->get_nickname())}); for (const auto &p: participants_) { + std::cout << incomer_msg << std::endl; p.second.lock()->send_message(incomer_msg); } } else { auto res = response_creator::create_with_room(200, "created", id_); - viewer.lock()->send_message(serializer::serialize_response(create, res)); + viewer.lock()->send_message(serializer::serialize_response("create", res)); } + participants_.insert(std::make_pair(viewer.lock()->get_id(), viewer)); viewers_.emplace_back(viewer.lock()->get_id(), viewer.lock()->get_nickname()); + latency_.insert(std::make_pair(viewer.lock()->get_id(), boost::posix_time::time_duration(0,0,0,0))); } -void Room::leave(const uuid& id, const std::string& nick){ +void Room::leave(const std::string& id, const std::string& nick){ + std::cout << "[ROOM LEAVE]\n"; auto viewer_dto = std::make_pair(id, nick); auto it = find(viewers_.begin(), viewers_.end(), viewer_dto); - participants_.erase(id); viewers_.erase(it); - auto leave_msg = serializer::serialize_viewers(type::leave, {viewer_dto}); + participants_.erase(id); + latency_.erase(id); + auto leave_msg = serializer::serialize_viewers("leave", {viewer_dto}); for(const auto& p : participants_){ p.second.lock()->send_message(leave_msg); } -} - -void Room::ping(){ - std::cout << "[check timings] id: "<< boost::uuids::to_string(id_)<< std::endl; - if(participants_.empty()){ - return; - } - if(!playing){ + if (participants_.empty()){ stop_timer(); } - std::cout << "[check timings]\n"; - auto ping_time = boost::posix_time::microsec_clock::universal_time(); - auto res = response_creator::create_ping(ping_time); - auto msg = serializer::serialize_response(type::ping, res); - ping_.first = ping_time; - for(const auto& viewer : participants_){ - viewer.second.lock()->send_message(msg); - } - } -void Room::pause(const boost::posix_time::time_duration & time) { + + +void Room::pause(const boost::posix_time::time_duration & time, const std::string& sender_id) { if(!playing){ return; } playing = false; - auto msg = serializer::serialize_response(pause_, {}); + auto msg = serializer::serialize_response("pause", {{"time", boost::lexical_cast(time)}}); for(const auto& p : participants_){ - p.second.lock()->send_message(msg);; + if(p.first != sender_id) + p.second.lock()->send_message(msg); } - synchronize(time); } -void Room::play() { +void Room::play(const std::string& sender_id) { if(playing){ return; } playing = true; - auto msg = serializer::serialize_response(type::play, {}); + auto msg = serializer::serialize_response("play", {}); for(const auto& p : participants_){ - p.second.lock()->send_message(msg); + if (p.first != sender_id){ + p.second.lock()->send_message(msg); + + std::cout << p.second.lock()->get_nickname() << " " << msg << std::endl; + } } - start_timer(); + } void Room::synchronize(const boost::posix_time::time_duration & timing) { - auto msg = serializer::serialize_response(s_time, + auto msg = serializer::serialize_response("s_time", response_creator::create_with_timing(timing)); for(const auto& p : participants_){ @@ -105,9 +112,10 @@ void Room::synchronize(const boost::posix_time::time_duration & timing) { } } -void Room::send_chat_msg(uuid /*viewer*/, const std::string &/*msg*/) { +void Room::send_chat_msg(const std::string& nick, const std::string &msg) { + auto res = response_creator::create_chat_mag(nick, msg); for(const auto& p : participants_){ - p.second.lock()->send_message(""); + p.second.lock()->send_message(res); } } @@ -135,54 +143,108 @@ void Room::stop_timer() { check_timer_.cancel(); } -void Room::set_resource(const std::string & src) { +void Room::set_resource(const std::string &src, const std::string &viewer_id) { src_ = src; - // TODO refactor + playing = false; + current_timestamp = boost::posix_time::time_duration(0,0,0,0); + std::unordered_map res{ {"src", src} }; - auto msg = serializer::serialize_response(s_src, res); + auto msg = serializer::serialize_response("s_src", res); for(const auto& p : participants_){ - p.second.lock()->send_message(msg); + if(p.first != viewer_id){ + p.second.lock()->send_message(msg); + } } } -w_viewer_ptr Room::get_viewer(const uuid& v_id) { +w_viewer_ptr Room::get_viewer(const std::string& v_id) { return participants_[v_id]; } -void Room::set_nickname(const uuid &viewer, std::string &&nick) { - auto msg = serializer::serialize_viewers(s_nick, {std::make_pair(viewer, std::move(nick))}); +void Room::set_nickname(const std::string &viewer, std::string &&nick) { + auto msg = serializer::serialize_viewers("s_nick", {std::make_pair(viewer, std::move(nick))}); for(const auto& v: participants_){ v.second.lock()->send_message(msg); } } +void Room::ping(){ + std::cout << "[check timings] id: "<< boost::uuids::to_string(id_)<< std::endl; + if(participants_.empty()){ + return; + } + auto ping_time = boost::posix_time::microsec_clock::universal_time(); + auto msg = response_creator::create_ping(ping_time); + + for(const auto& viewer : participants_){ + viewer.second.lock()->send_message(msg); + } + last_server_time = ping_time; + start_timer(); +} + + void Room::on_pong(std::unordered_map &req) { - ping_.second.emplace_back(ping_info{ - boost::posix_time::duration_from_string(req["cur_time"]), - boost::posix_time::microsec_clock::universal_time() - ping_.first, + ping_info ping{ + boost::posix_time::duration_from_string(req["time"]), + // TODO server time from response + boost::posix_time::microsec_clock::universal_time() - last_server_time + /*boost::posix_time::time_from_string(req["server_time"])*/, boost::posix_time::microsec_clock::universal_time(), - boost::lexical_cast(req["v_id"]) - }); - if (ping_.second.size() == participants_.size()){ - std::vector> timings; - for(const auto& ping : ping_.second){ - auto timing = ping.player_time + ping.latency + - (ping_.second.back().response - ping.response); - - std::stringstream ss_time; - ss_time << timing; - timings.emplace_back - (boost::lexical_cast(ping.viewer_id), ss_time.str()); + req["v_id"] + }; + if(ping.player_time > current_timestamp){ + if(playing){ + current_timestamp = ping.player_time + ping.latency; + } else { + current_timestamp = ping.player_time; } - auto msg = serializer::serialize_timings(type::pong, timings); - for(const auto& p: participants_){ + last_update = boost::posix_time::microsec_clock::universal_time(); + } + + latency_[ping.viewer_id] = ping.latency; + for(const auto& v : participants_){ + boost::posix_time::time_duration pong_time = playing ? ping.player_time + latency_[v.first] : ping.player_time; + auto msg = response_creator::create_pong(pong_time, ping.viewer_id); + v.second.lock()->send_message(msg); + } +} + +state_ptr Room::get_state() { + return state_; +} + + +bool Room::is_playing(){ + return playing; +} + +std::string Room::calculate_timestamp() { + std::stringstream ss_time; + auto timestamp = current_timestamp + (boost::posix_time::microsec_clock::universal_time() - last_update); + ss_time << timestamp; + return ss_time.str(); +} + +void Room::set_service(const std::string &service, const std::string &viewer) { + playing = false; + service_ = service; + src_ = ""; + current_timestamp = boost::posix_time::time_duration(0,0,0,0); + std::unordered_map res{ + {"service", service} + }; + auto msg = serializer::serialize_response("s_service", res); + for(const auto& p : participants_){ + if(p.first != viewer){ p.second.lock()->send_message(msg); } - ping_.second.clear(); - start_timer(); } } +void Room::service(const std::string & service) { + service_ = service; +} diff --git a/server/api/user/CMakeLists.txt b/server/api/user/CMakeLists.txt new file mode 100644 index 0000000..a488634 --- /dev/null +++ b/server/api/user/CMakeLists.txt @@ -0,0 +1,6 @@ +project(user_session) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC serializer ws_session response) diff --git a/server/api/user/include/us_types.hpp b/server/api/user/include/us_types.hpp new file mode 100644 index 0000000..11caac2 --- /dev/null +++ b/server/api/user/include/us_types.hpp @@ -0,0 +1,26 @@ +#ifndef SERVER_US_TYPES_HPP +#define SERVER_US_TYPES_HPP + + +enum class us_type{ + invalid, + create, + join, + logout, +}; + +static std::unordered_map const us_types = { + {"invalid", us_type::invalid}, + {"create", us_type::create}, + {"join", us_type::join}, + {"logout", us_type::logout} +}; + + +static std::unordered_map const us_types_to_string ={ + {us_type::invalid, "invalid"}, + {us_type::create,"create"}, + {us_type::join,"join"}, + {us_type::logout,"logout"} +}; +#endif //SERVER_US_TYPES_HPP diff --git a/server/api/user/include/user.h b/server/api/user/include/user.h new file mode 100644 index 0000000..3a799ee --- /dev/null +++ b/server/api/user/include/user.h @@ -0,0 +1,35 @@ +#ifndef SERVER_USER_H +#define SERVER_USER_H + +#include + +class User { +public: + const std::string get_nick() const{ + return nickname_; + } + const std::string get_id() const{ + return uuid_; + } + + const std::string get_pass() const{ + return password_; + + } + void set_nick(const std::string& nick){ + nickname_ = nick; + } + void set_id(const std::string id){ + uuid_ = id; + } + + void set_pass(const std::string& pass){ + password_ = pass; + } +private: + std::string nickname_; + std::string uuid_; + std::string password_; +}; + +#endif //SERVER_USER_H diff --git a/server/api/user/include/user_creator.h b/server/api/user/include/user_creator.h new file mode 100644 index 0000000..5f393d0 --- /dev/null +++ b/server/api/user/include/user_creator.h @@ -0,0 +1,12 @@ +#ifndef SERVER_USER_CREATOR_H +#define SERVER_USER_CREATOR_H + +#include "user_session.h" + +class user_creator{ +public: + static std::shared_ptr create(stream_ptr&& ws, user_ptr&& user, const state_ptr& state){ + return std::make_shared(std::move(ws), std::move(user), state); + } +}; +#endif //SERVER_USER_CREATOR_H diff --git a/server/api/user/include/user_session.h b/server/api/user/include/user_session.h new file mode 100644 index 0000000..7381d5b --- /dev/null +++ b/server/api/user/include/user_session.h @@ -0,0 +1,51 @@ +#ifndef SERVER_USER_SESSION_H +#define SERVER_USER_SESSION_H + +#include +#include +#include + +#include +#include + +#include "user.h" +#include "us_types.hpp" + +namespace beast = boost::beast; // from +namespace websocket = beast::websocket; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from +using error_code = boost::system::error_code; + +class shared_state; + +typedef websocket::stream stream; +typedef std::unique_ptr stream_ptr; +typedef std::shared_ptr state_ptr; +typedef std::unique_ptr user_ptr; + + +class user_session : public std::enable_shared_from_this{ +public: + user_session(stream_ptr&& ws, user_ptr && user, state_ptr state); + ~user_session() = default; + + void run(); + void do_read(); + void do_close(); + void logout(); + void handle_request(); + void on_write(error_code ec); + void send_message(const std::string& msg); + +private: + user_ptr user_; + + stream_ptr ws_; + beast::flat_buffer buffer_; + state_ptr state_; + + std::queue response_q; +}; + +#endif //SERVER_USER_SESSION_H diff --git a/server/api/user/src/user_session.cpp b/server/api/user/src/user_session.cpp new file mode 100644 index 0000000..17e491a --- /dev/null +++ b/server/api/user/src/user_session.cpp @@ -0,0 +1,97 @@ +#include "user_session.h" +#include "ws_session.hpp" +#include "serializer.h" +#include "response_creator.hpp" +#include "viewer_manager.hpp" + +#include + +user_session::user_session(stream_ptr &&ws, user_ptr &&user, state_ptr state) : + ws_(std::move(ws)), + user_(std::move(user)), + state_(std::move(state)) {} + +void user_session::run() { + std::cout << "[user run]\n"; + auto msg = response_creator::create_login("login", 200, user_->get_id(), user_->get_nick(), user_->get_pass()); + send_message(msg); + do_read(); +} + +void user_session::do_read() { + ws_->async_read(buffer_, + [self{shared_from_this()}](error_code ec, std::size_t) { + if (ec && ec != boost::asio::error::eof) { + std::cout << "[us async read error]" << ec.message() << std::endl; + return; + } + self->handle_request(); + }); +} + +void user_session::handle_request() { + std::cout << "[user session handle request]\n"; + try{ + auto req = serializer::deserialize(beast::buffers_to_string(buffer_.data())); + std::cout << beast::buffers_to_string(buffer_.data()) << std::endl; + auto t = us_types.at(req["type"]); + switch(t){ + case us_type::create: + case us_type::join:{ + std::make_shared + (std::move(req), std::move(ws_), state_, std::move(user_))->handle_request(); + return; + } + case us_type::logout: + logout(); + return; + default: + { + auto msg = response_creator::create_with_status(req["type"], 405); + send_message(msg); + } + } + } + catch(...){ + auto msg = response_creator::create_with_status("invalid", 400); + send_message(msg); + + } + do_read(); +} + +void user_session::on_write(error_code ec) { + if(ec){ + std::cout << "[user write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} + +void user_session::do_close() { + ws_->close(""); +} + +void user_session::logout() { + auto session = std::make_shared(std::move(ws_), state_); + session->do_read(); + auto msg = response_creator::create_with_status("logout", 200); + session->send_msg(msg); +} + +void user_session::send_message(const std::string& msg){ + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} \ No newline at end of file diff --git a/server/api/viewer/CMakeLists.txt b/server/api/viewer/CMakeLists.txt index c36e238..320c881 100644 --- a/server/api/viewer/CMakeLists.txt +++ b/server/api/viewer/CMakeLists.txt @@ -1,12 +1,11 @@ project(viewer) file(GLOB SOURCES "src/*") -find_package(nlohmann_json 3.10.5 REQUIRED) + add_library(${PROJECT_NAME} ${SOURCES}) target_include_directories(${PROJECT_NAME} PUBLIC include) -target_link_libraries(${PROJECT_NAME} PUBLIC room serializer) -target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) +target_link_libraries(${PROJECT_NAME} PUBLIC room serializer ws_session user_session response) diff --git a/server/api/viewer/include/handler.h b/server/api/viewer/include/handler.h index 14b8119..8a1eb97 100644 --- a/server/api/viewer/include/handler.h +++ b/server/api/viewer/include/handler.h @@ -6,7 +6,7 @@ #include #include "room.h" -#include "types.hpp" +#include "vs_types.hpp" #include "serializer.h" #include "response_creator.hpp" @@ -23,6 +23,8 @@ class handler { void define_type(){ auto it = types.find(req_["type"]); std::cout << req_["type"] << std::endl; + if (it == types.end()) + return; type_ = it->second; } void handle_request(){ @@ -31,33 +33,41 @@ class handler { std::cout <<"[viewer handle request]\n" << str_req_ << std::endl; define_type(); switch (type_) { - case leave: + case vs_type::leave: viewer_->do_close(); - break; - case pong: + return; + case vs_type::pong: req_["v_id"] = boost::lexical_cast(viewer_->get_id()); room_->on_pong(req_); - break; - case play: { + viewer_->do_read(); + return; + case vs_type::play: { if (!viewer_->get_a_opts().can_pause) { status_ = 403; break; } + if (room_->is_playing()){ + status_ = 409; + break; + } status_ = 200; - room_->play(); + room_->play(viewer_->get_id()); } break; - case pause_: { + case vs_type::pause_: { if (!viewer_->get_a_opts().can_pause) { status_ = 403; break; } - - room_->pause(boost::lexical_cast(req_["time"])); + if (!room_->is_playing()){ + status_ = 409; + break; + } + room_->pause(boost::lexical_cast(req_["time"]), viewer_->get_id()); status_ = 200; } break; - case s_time: { + case vs_type::s_time: { if (!viewer_->get_a_opts().can_rewind) { status_ = 403; break; @@ -66,7 +76,7 @@ class handler { room_->synchronize(boost::lexical_cast(req_["time"])); } break; - case s_nick: + case vs_type::s_nick: if (viewer_->get_nickname() == req_["nick"]) { status_ = 200; break; @@ -75,25 +85,26 @@ class handler { room_->set_nickname(viewer_->get_id(), std::move(req_["nick"])); status_ = 200; break; - case sync_: { + case vs_type::sync_: { if (!viewer_->get_a_opts().is_host) { status_ = 403; break; } status_ = 200; - room_->synchronize(boost::lexical_cast(req_["time"])); + room_->synchronize(boost::posix_time::duration_from_string(req_["time"])); } break; - case s_src: { + case vs_type::s_src: { if (!viewer_->get_a_opts().is_host) { status_ = 403; break; } status_ = 200; - room_->set_resource(req_["src"]); + room_->set_resource(req_["src"], viewer_->get_id()); + } break; - case s_access: { + case vs_type::s_access: { if (!viewer_->get_a_opts().is_host) { status_ = 403; break; @@ -101,29 +112,51 @@ class handler { access_options to_set {boost::lexical_cast(req_["pause"]), boost::lexical_cast(req_["rewind"]), boost::lexical_cast(req_["host"])}; - room_->get_viewer(boost::lexical_cast(req_["viewer"])).lock()->set_access_opts(to_set); + room_->get_viewer(req_["viewer"]).lock()->set_access_opts(to_set); status_ = 200; break; } + case vs_type::chat: { + room_->send_chat_msg(viewer_->get_nickname(), req_["msg"]); + break; + } + case vs_type::s_service: { + if(!viewer_->get_a_opts().is_host){ + status_ = 403; + break; + } + status_=200; + room_->set_service(req_["service"], viewer_->get_id()); + break; + } default: status_ = 405; break; } } - // TODO replace with custom exceptions catch (nlohmann::json::exception& ec){ status_ = 400; std::cout << ec.what() << std::endl; } + catch(boost::bad_lexical_cast const& e){ + std::cout << "Error: " << e.what() << "\n"; + } catch(...){ std::cout << "ex\n"; - status_ = 500; + status_ = 400; + } + std::string msg; + if(type_ == vs_type::s_src){ + msg = response_creator::create_src_success(type_to_string.at(type_), status_, req_["src"]); + viewer_->send_message(msg); + } else if(type_ != vs_type::chat){ + msg = response_creator::create_with_status(type_to_string.at(type_), status_); + viewer_->send_message(msg); } - auto msg = response_creator::create_with_status(type_, status_); - viewer_->send_message(msg); + viewer_->do_read(); } private: - type type_ = invalid; + vs_type type_ = vs_type::invalid; unsigned short status_; viewer_ptr viewer_; room_ptr room_; diff --git a/server/api/viewer/include/response_creator.hpp b/server/api/viewer/include/response_creator.hpp deleted file mode 100644 index 4a97bfb..0000000 --- a/server/api/viewer/include/response_creator.hpp +++ /dev/null @@ -1,46 +0,0 @@ -#ifndef WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP -#define WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP - -#include -#include - -#include - -#include "serializer.h" -#include "types.hpp" - -class response_creator{ -public: - static std::string create_with_status(type type_, unsigned short code){ - std::unordered_map data{}; - data["code"] = std::to_string(code); - auto out = serializer::serialize_response(type_, data); - return out; - } - static std::unordered_map create_with_room(unsigned short code, const std::string& msg, const uuid& room_id){ - std::unordered_map data{}; - data["code"] = std::to_string(code); - data["message"] = msg; - data["room_id"] = boost::lexical_cast(room_id); - return data; - } - - static std::unordered_map create_with_timing - (const boost::posix_time::time_duration& time){ - std::unordered_map data{}; - std::stringstream ss_time; - ss_time << time; - auto str_time = ss_time.str(); - data["time"] = str_time; - return data; - } - - static std::unordered_map create_ping(const boost::posix_time::ptime& server_time){ - std::unordered_map data{}; - std::stringstream ss_time; - ss_time << server_time; - data["time"] = ss_time.str(); - return data; - } -}; -#endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/api/viewer/include/types.hpp b/server/api/viewer/include/types.hpp deleted file mode 100644 index 7db13a5..0000000 --- a/server/api/viewer/include/types.hpp +++ /dev/null @@ -1,72 +0,0 @@ -#ifndef WATCH_UP_PROJECT_TYPES_HPP -#define WATCH_UP_PROJECT_TYPES_HPP - -#include "nlohmann/json.hpp" - -enum type { - invalid = -1, - create = 0, - join, - leave, - ping, - pong, - play, - pause_, - s_time, - s_nick, - sync_, - s_src, - s_access, - -}; - -static std::unordered_map const types = { - {"invalid", type::invalid }, - {"create", type::create }, - {"join", type::join }, - {"leave", type::leave }, - {"ping", type::ping }, - {"pong", type::pong }, - {"play", type::play }, - {"pause", type::pause_ }, - {"s_time", type::s_time }, - {"s_nick", type::s_nick }, - {"sync", type::sync_ }, - {"s_src", type::s_src }, - {"s_access",type::s_access}, -}; - -static std::unordered_map const type_to_string = { - {type::invalid, "invalid" }, - {type::create, "create" }, - {type::join , "join" }, - {type::leave, "leave" }, - {type::ping, "ping" }, - {type::pong, "pong" }, - {type::play, "play" }, - {type::pause_, "pause" }, - {type::s_time, "s_time" }, - {type::s_nick, "s_nick" }, - {type::sync_ , "sync" }, - {type::s_src , "s_src" }, - {type::s_access, "s_access"}, -}; - -NLOHMANN_JSON_SERIALIZE_ENUM(type, { - {invalid, nullptr }, - {create, "create" }, - {join, "join" }, - {leave, "leave" }, - {ping, "ping" }, - {pong, "pong" }, - {play, "play" }, - {pause_, "pause" }, - {s_time, "s_time" }, - {s_nick, "s_nick" }, - {sync_, "sync" }, - {s_src, "s_src" }, - {s_access, "s_access"} -}) - - -#endif //WATCH_UP_PROJECT_TYPES_HPP diff --git a/server/api/viewer/include/viewer.h b/server/api/viewer/include/viewer.h index 51448ff..6451a1d 100644 --- a/server/api/viewer/include/viewer.h +++ b/server/api/viewer/include/viewer.h @@ -12,6 +12,7 @@ #include #include "room.h" +#include "user.h" namespace beast = boost::beast; // from namespace websocket = beast::websocket; // from @@ -24,12 +25,13 @@ typedef std::shared_ptr room_ptr; typedef websocket::stream ws_stream; typedef std::unique_ptr stream_ptr; +typedef std::unique_ptr user_ptr; class handler; struct access_options{ - bool can_pause = false; - bool can_rewind = false; - bool is_host = false; + bool can_pause = true; + bool can_rewind = true; + bool is_host = true; }; @@ -44,7 +46,7 @@ class IViewer{ virtual void send_message(const std::string&) = 0; virtual void on_write(error_code ec) = 0; - virtual uuid get_id() const = 0; + virtual std::string get_id() const = 0; virtual std::string get_nickname() const = 0; virtual access_options get_a_opts() const = 0; @@ -57,8 +59,10 @@ class IViewer{ class Viewer: public IViewer, public std::enable_shared_from_this { public: - Viewer(stream_ptr && ws, uuid&& id, std::string nickname, room_ptr&& room = nullptr); - Viewer(stream_ptr && ws, uuid&& id, access_options a_opts, std::string nickname, room_ptr&& room = nullptr); + Viewer(stream_ptr && ws, user_ptr&& user = nullptr, room_ptr&& room = nullptr); + Viewer(stream_ptr && ws, access_options a_opts, user_ptr&& user = nullptr, room_ptr&& room = nullptr); + Viewer(stream_ptr&& ws, std::string&& id, std::string&& nick, room_ptr&& room = nullptr); + ~Viewer() override; void start() override; @@ -68,7 +72,7 @@ class Viewer: public IViewer, public std::enable_shared_from_this { void send_message(const std::string&) override; void on_write(error_code ec) override; - uuid get_id() const override; + std::string get_id() const override; std::string get_nickname() const override; access_options get_a_opts() const override; @@ -77,10 +81,12 @@ class Viewer: public IViewer, public std::enable_shared_from_this { void set_access_opts(const access_options& )override; protected: - uuid id_; + std::string id_; + std::unique_ptr user_; room_ptr room_; + std::string nickname_; - access_options a_opts_{}; + access_options a_opts_; stream_ptr ws_; beast::flat_buffer buffer_; diff --git a/server/api/viewer/include/viewer_creator.hpp b/server/api/viewer/include/viewer_creator.hpp index 58e5413..1715c6d 100644 --- a/server/api/viewer/include/viewer_creator.hpp +++ b/server/api/viewer/include/viewer_creator.hpp @@ -5,19 +5,28 @@ #include #include "viewer.h" +#include "user.h" typedef boost::uuids::uuid uuid; class viewer_creator { public: - static std::shared_ptr create_viewer(stream_ptr && ws, std::string nickname, room_ptr&& room = nullptr){ + static std::shared_ptr create_anonymous(stream_ptr&& ws, room_ptr&& room = nullptr){ return std::make_shared - (std::move(ws), boost::uuids::random_generator()(), std::move(nickname), std::move(room)); + (std::move(ws), boost::lexical_cast(boost::uuids::random_generator()()), "anonymous", std::move(room)); } - static std::shared_ptr create_host(stream_ptr && ws, std::string nickname){ + static std::shared_ptr create(stream_ptr && ws, user_ptr&& user, room_ptr&& room = nullptr){ + return std::make_shared + (std::move(ws), std::move(user), std::move(room)); + } + + static std::shared_ptr create_host(stream_ptr && ws, user_ptr&& user){ + if (!user){ + return create_anonymous(std::move(ws)); + } access_options opts{true, true, true}; - return std::make_shared(std::move(ws), boost::uuids::random_generator()(), opts, std::move(nickname)); + return std::make_shared(std::move(ws), opts, std::move(user)); } }; diff --git a/server/api/viewer/include/viewer_manager.hpp b/server/api/viewer/include/viewer_manager.hpp index ab850f9..82e1f27 100644 --- a/server/api/viewer/include/viewer_manager.hpp +++ b/server/api/viewer/include/viewer_manager.hpp @@ -7,11 +7,13 @@ #include -#include "types.hpp" +#include "us_types.hpp" #include "serializer.h" #include "viewer_creator.hpp" #include "room_creator.hpp" #include "response_creator.hpp" +#include "ws_session.hpp" + namespace beast = boost::beast; namespace websocket = beast::websocket; @@ -19,77 +21,27 @@ namespace websocket = beast::websocket; typedef websocket::stream stream; typedef std::unique_ptr stream_ptr; -class viewer_manager { +class viewer_manager: public std::enable_shared_from_this { public: viewer_manager() = delete; - viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state): - ws_(std::move(ws)), - state_(std::move(state)), - str_req_(std::move(ws_req)) - {} - - void do_close(){ - //TODO log close reason - ws_->close(""); - } - - void define_type(){ - auto it = types.find(req_["type"]); - if(it == types.end()) - return; - type_ = it->second; - } - - void handle_request(){ - try { - req_ = serializer::deserialize(str_req_); + viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state, user_ptr&& user = nullptr); + viewer_manager(std::unordered_map&& req, stream_ptr&& ws, state_ptr state, user_ptr&& user = nullptr); - define_type(); - std::cout << str_req_ << std::endl; - if (type_ == create) { - //auto nickname = req_["nick"]; - auto host = viewer_creator::create_host(std::move(ws_), "blank"); - auto room = room_creator::create_room(std::weak_ptr(host), state_); + void do_close(); + void handle_request(); + void send_message(const std::string& msg); + void on_write(error_code ec); + void define_type(); - state_->add_room(room->get_id(), std::weak_ptr(room)); - host->set_room(room); - host->start(); - room->start(); - } else if (type_ == join) { - auto room_id = boost::lexical_cast(req_["room_id"]); - //auto nickname = req_["nick"]; - auto room = state_->get_room(room_id); - if (!room.lock()) { - //ws_->async_write("not found"); - return; - } - viewer_creator::create_viewer(std::move(ws_), "blank", room.lock())->start(); - } else if (type_ == invalid) { - ws_->async_write(net::buffer("bad request"), - [](error_code , std::size_t){ - std::cout << "send\n"; - }); - return; - } else { - // TODO send method not allowed - ws_->async_write(net::buffer("not allowed"), - [](error_code , std::size_t){ - std::cout << "send\n"; - }); - } - } - catch (nlohmann::json::exception& ec){ - std::cout << ec.what(); - return; - } - } private: stream_ptr ws_; state_ptr state_; + std::queue response_q; - type type_ = invalid; + user_ptr user_; + us_type type_ = us_type::invalid; std::string str_req_; std::unordered_map req_; diff --git a/server/api/viewer/include/vs_types.hpp b/server/api/viewer/include/vs_types.hpp new file mode 100644 index 0000000..49dda01 --- /dev/null +++ b/server/api/viewer/include/vs_types.hpp @@ -0,0 +1,71 @@ +#ifndef WATCH_UP_PROJECT_TYPES_HPP +#define WATCH_UP_PROJECT_TYPES_HPP + +#include "nlohmann/json.hpp" + +enum class vs_type { + invalid = -1, + leave, + ping, + pong, + play, + pause_, + s_time, + s_nick, + sync_, + s_src, + s_access, + chat, + s_service, +}; + +static std::unordered_map const types = { + {"invalid", vs_type::invalid }, + {"leave", vs_type::leave }, + {"ping", vs_type::ping }, + {"pong", vs_type::pong }, + {"play", vs_type::play }, + {"pause", vs_type::pause_ }, + {"s_time", vs_type::s_time }, + {"s_nick", vs_type::s_nick }, + {"sync", vs_type::sync_ }, + {"s_src", vs_type::s_src }, + {"s_access", vs_type::s_access}, + {"chat", vs_type::chat }, + {"s_service",vs_type::s_service} +}; + +static std::unordered_map const type_to_string = { + {vs_type::invalid, "invalid" }, + {vs_type::leave, "leave" }, + {vs_type::ping, "ping" }, + {vs_type::pong, "pong" }, + {vs_type::play, "play" }, + {vs_type::pause_, "pause" }, + {vs_type::s_time, "s_time" }, + {vs_type::s_nick, "s_nick" }, + {vs_type::sync_ , "sync" }, + {vs_type::s_src , "s_src" }, + {vs_type::s_access, "s_access"}, + {vs_type::chat, "chat"}, + {vs_type::s_service, "s_service"}, +}; + +NLOHMANN_JSON_SERIALIZE_ENUM(vs_type, { + {vs_type::invalid, nullptr }, + {vs_type::leave, "leave" }, + {vs_type::ping, "ping" }, + {vs_type::pong, "pong" }, + {vs_type::play, "play" }, + {vs_type::pause_, "pause" }, + {vs_type::s_time, "s_time" }, + {vs_type::s_nick, "s_nick" }, + {vs_type::sync_, "sync" }, + {vs_type::s_src, "s_src" }, + {vs_type::s_access, "s_access"}, + {vs_type::chat, "chat" }, + {vs_type::s_service, "s_service"}, +}) + + +#endif //WATCH_UP_PROJECT_TYPES_HPP diff --git a/server/api/viewer/src/viewer.cpp b/server/api/viewer/src/viewer.cpp index 182f450..677f07c 100644 --- a/server/api/viewer/src/viewer.cpp +++ b/server/api/viewer/src/viewer.cpp @@ -3,25 +3,42 @@ #include #include "handler.h" +#include "ws_session.hpp" +#include "user_session.h" -Viewer::Viewer(stream_ptr &&ws, uuid&& id, std::string nickname, room_ptr&& room): - id_(std::forward(id)), +Viewer::Viewer(stream_ptr &&ws, user_ptr&& user, room_ptr&& room): + user_(std::move(user)), room_(std::move(room)), - nickname_(std::move(nickname)), ws_(std::move(ws)) -{} +{ + id_ = user_->get_id(); + nickname_ = user_->get_nick(); +} -Viewer::Viewer(stream_ptr &&ws, uuid &&id, access_options a_opts, std::string nickname,room_ptr&& room): - id_(std::forward(id)), +Viewer::Viewer(stream_ptr &&ws, access_options a_opts, user_ptr&& user, room_ptr&& room): + user_(std::move(user)), room_(std::move(room)), - nickname_(std::move(nickname)), - a_opts_(a_opts), - ws_(std::move(ws)) + ws_(std::move(ws)), + + a_opts_(a_opts) +{ + id_ = user_->get_id(); + nickname_ = user_->get_nick(); +} + +Viewer::Viewer(stream_ptr &&ws, std::string &&id, std::string &&nick, room_ptr&& room) : + id_(id), + nickname_(std::move(nick)), + + user_(user_ptr(nullptr)), + room_(std::move(room)), + ws_(std::move(ws)) {} Viewer::~Viewer(){ std::cout << "[viewer destructor]\n"; - room_->leave(id_, nickname_); + if(room_) + room_->leave(id_, nickname_); } @@ -41,14 +58,17 @@ void Viewer::do_read() { auto in = beast::buffers_to_string(self->buffer_.cdata()); self->buffer_.consume(self->buffer_.size()); // creates an event loop - self->do_read(); - std::make_unique(std::move(in), self->shared_from_this(), self->room_)->handle_request(); + handler(std::move(in), self->shared_from_this(), self->room_).handle_request(); }); } void Viewer::do_close() { - ws_->close(""); + if(user_){ + std::make_shared(std::move(ws_), std::move(user_), room_->get_state())->do_read(); + return; + } + std::make_shared(std::move(ws_), room_->get_state())->do_read(); } void Viewer::send_message(const std::string & msg) { @@ -76,7 +96,7 @@ void Viewer::on_write(error_code ec) { } } -uuid Viewer::get_id() const { +std::string Viewer::get_id() const { return id_; } diff --git a/server/api/viewer/src/viewer_manager.cpp b/server/api/viewer/src/viewer_manager.cpp new file mode 100644 index 0000000..62e06d1 --- /dev/null +++ b/server/api/viewer/src/viewer_manager.cpp @@ -0,0 +1,96 @@ +#include "viewer_manager.hpp" +#include "us_types.hpp" + +viewer_manager::viewer_manager(std::string&& ws_req, stream_ptr&& ws, state_ptr state, user_ptr&& user): + ws_(std::move(ws)), + state_(std::move(state)), + str_req_(std::move(ws_req)), + user_(std::move(user)) +{} + +viewer_manager::viewer_manager(std::unordered_map&& req, stream_ptr&& ws, state_ptr state, user_ptr&& user): + ws_(std::move(ws)), + state_(std::move(state)), + req_(std::move(req)), + user_(std::move(user)) +{} + +void viewer_manager::do_close(){ + // pass to low level + std::make_shared(std::move(ws_), state_)->do_read(); +} + + +void viewer_manager::define_type(){ + auto it = us_types.find(req_["type"]); + if(it == us_types.end()) + return; + type_ = it->second; +} + +void viewer_manager::handle_request(){ + try { + if (req_.empty()){ + req_ = serializer::deserialize(str_req_); + } + define_type(); + if (type_ == us_type::create) { + auto host = viewer_creator::create_host(std::move(ws_), std::move(user_)); + auto room = room_creator::create_room(std::weak_ptr(host), state_); + room->service(req_["service"]); + state_->add_room(room->get_id(), std::weak_ptr(room)); + host->set_room(room); + + host->start(); + room->start(); + + } else if (type_ == us_type::join) { + auto room_id = boost::lexical_cast(req_["room_id"]); + auto room = state_->get_room(room_id); + if (!room.lock()) { + auto msg = response_creator::create_with_status("join", 404); + send_message(msg); + do_close(); + return; + } + if(!user_){ + viewer_creator::create_anonymous(std::move(ws_), room.lock())->start(); + return; + } + viewer_creator::create(std::move(ws_),std::move(user_), room.lock())->start(); + } else { + + } + } + catch (...){ + auto msg = response_creator::create_with_status(us_types_to_string.at(type_), 400); + send_message(msg); + do_close(); + return; + } +} + +void viewer_manager::send_message(const std::string &msg) { + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} + +void viewer_manager::on_write(error_code ec) { + if(ec){ + std::cout << "[viewer manager write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} diff --git a/server/api/ws_session/CMakeLists.txt b/server/api/ws_session/CMakeLists.txt new file mode 100644 index 0000000..998e18c --- /dev/null +++ b/server/api/ws_session/CMakeLists.txt @@ -0,0 +1,6 @@ +project(ws_session) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC room serializer db_client) diff --git a/server/api/ws_session/include/ps_types.hpp b/server/api/ws_session/include/ps_types.hpp new file mode 100644 index 0000000..9a1f8d9 --- /dev/null +++ b/server/api/ws_session/include/ps_types.hpp @@ -0,0 +1,26 @@ +#ifndef SERVER_PS_TYPES_HPP +#define SERVER_PS_TYPES_HPP + +#include +#include + +enum class ps_type{ + login, + reg, + join, + +}; + +static std::unordered_map const ps_types = { + {"login", ps_type::login}, + {"reg", ps_type::reg }, + {"join", ps_type::join } +}; + +static std::unordered_map const ps_type_to_string = { + {ps_type::login, "login"}, + {ps_type::reg, "reg" }, + {ps_type::join, "join" }, +}; + +#endif //SERVER_PS_TYPES_HPP diff --git a/server/core/include/ws_session.hpp b/server/api/ws_session/include/ws_session.hpp similarity index 76% rename from server/core/include/ws_session.hpp rename to server/api/ws_session/include/ws_session.hpp index be80a21..b2adc3e 100644 --- a/server/core/include/ws_session.hpp +++ b/server/api/ws_session/include/ws_session.hpp @@ -2,6 +2,7 @@ #define WATCH_UP_PROJECT_WS_SESSION_HPP #include #include +#include #include #include @@ -23,17 +24,22 @@ class ws_session : public std::enable_shared_from_this { public: // TODO rm virtual ws_session(tcp::socket &&socket, state_ptr state); - virtual ~ws_session() = default; + ws_session(stream_ptr&& ws, state_ptr state); + ~ws_session() = default; - virtual void run(); - virtual void do_read(); - virtual void do_close(); - virtual void handle_request(); + void run(); + void do_read(); + void do_close(); + void handle_request(); + void send_msg(const std::string& msg); + void on_write(error_code ec); protected: stream_ptr ws_; beast::flat_buffer buffer_; state_ptr state_; + + std::queue response_q; }; diff --git a/server/api/ws_session/src/ws_session.cpp b/server/api/ws_session/src/ws_session.cpp new file mode 100644 index 0000000..f992033 --- /dev/null +++ b/server/api/ws_session/src/ws_session.cpp @@ -0,0 +1,124 @@ +#include "ws_session.hpp" +#include "serializer.h" +#include "viewer_manager.hpp" +#include "user_session.h" +#include "user_creator.h" +#include "DB.h" + + +#include "ps_types.hpp" + +ws_session::ws_session(tcp::socket &&socket, state_ptr state): + ws_(std::make_unique(std::move(socket))), + state_(std::move(state)) +{} + +ws_session::ws_session(stream_ptr &&ws, state_ptr state): + ws_(std::move(ws)), + state_(std::move(state)) +{} + + +void ws_session::run(){ + std::cout << "[ws run]" << std::endl; + ws_->async_accept([self{shared_from_this()}](error_code ec){ + if(ec){ + std::cout << "[ws accepted] " << ec.message() << std::endl; + } + self->do_read(); + }); +} + +void ws_session::do_read() { + ws_->async_read(buffer_, + [self{shared_from_this()}](error_code ec, std::size_t){ + if(ec && ec != boost::asio::error::eof){ + std::cout << "[ws async read error]" << ec.message() << std::endl; + return; + } + self->handle_request(); + }); +} + + +void ws_session::do_close(){ + ws_->close(""); +} + +void ws_session::on_write(error_code ec) { + if(ec){ + std::cout << "[ws_session write error] " << ec.message() << std::endl; + return; + } + response_q.pop(); + + if(!response_q.empty()){ + ws_->async_write(net::buffer(response_q.front()), + [self{shared_from_this()}](error_code ec_, std::size_t){ + self->on_write(ec_); + }); + } +} + +void ws_session::handle_request(){ + std::cout << "[ws_session handle request]" << std::endl; + try { + auto req = serializer::deserialize(beast::buffers_to_string(buffer_.data())); + std::cout << beast::buffers_to_string(buffer_.data()) << std::endl; + buffer_.consume(buffer_.size()); + auto t = ps_types.at(req["type"]); + switch (t) { + case ps_type::login: + { + // TODO normal db client + auto user = DB::get_instance().get_user_by_id(req["login"], req["password"]); + if (!user){ + auto msg = response_creator::create_with_status("login", 403); + send_msg(msg); + break; + } + user_creator::create(std::move(ws_), std::move(user), state_)->run(); + return; + } + case ps_type::reg: + { + bool pass = DB::get_instance().add_user(req["nick"], req["login"], req["password"]); + if(!pass){ + auto msg = response_creator::create_with_status("reg", 409); + send_msg(msg); + break; + } + auto user = DB::get_instance().get_user_by_id(req["login"], req["password"]); + user_creator::create(std::move(ws_), std::move(user), state_)->run(); + return; + } + case ps_type::join: + std::make_shared + (std::move(req), std::move(ws_), state_)->handle_request(); + return; + default: + { + auto msg = response_creator::create_with_status("invalid", 400); + send_msg(msg); + return; + } + } + } + catch(...){ + auto msg = response_creator::create_with_status("invalid", 400); + send_msg(msg); + } + do_read(); + +} + +void ws_session::send_msg(const std::string &msg) { + response_q.push(msg); + if(response_q.size() > 1) + return; + ws_->async_write(net::buffer(msg), + [self{shared_from_this()}](error_code ec, std::size_t){ + self->on_write(ec); + }); +} + diff --git a/server/core/CMakeLists.txt b/server/core/CMakeLists.txt index a2737f6..3f4e8a4 100644 --- a/server/core/CMakeLists.txt +++ b/server/core/CMakeLists.txt @@ -6,6 +6,6 @@ add_library(${PROJECT_NAME} ${SOURCES}) target_include_directories(${PROJECT_NAME} PUBLIC include) -target_link_libraries(${PROJECT_NAME} PUBLIC viewer) +target_link_libraries(${PROJECT_NAME} PUBLIC ws_session) target_link_libraries(${PROJECT_NAME} PRIVATE ${Boost_LIBRARIES}) target_link_libraries(${PROJECT_NAME} PRIVATE Threads::Threads) diff --git a/server/core/src/ws_session.cpp b/server/core/src/ws_session.cpp deleted file mode 100644 index 4996bfc..0000000 --- a/server/core/src/ws_session.cpp +++ /dev/null @@ -1,42 +0,0 @@ -#include "ws_session.hpp" - -#include "viewer_manager.hpp" - -ws_session::ws_session(tcp::socket &&socket, state_ptr state): - ws_(std::make_unique(std::move(socket))), - state_(std::move(state)) -{} - -void ws_session::run(){ - std::cout << "[ws run]" << std::endl; - ws_->async_accept([self{shared_from_this()}](error_code ec){ - if(ec){ - std::cout << "[ws accepted] " << ec.message() << std::endl; - } - self->do_read(); - }); -} - -void ws_session::do_read() { - ws_->async_read(buffer_, - [self{shared_from_this()}](error_code ec, std::size_t){ - if(ec && ec != boost::asio::error::eof){ - std::cout << "[ws async read error]" << ec.message() << std::endl; - return; - } - auto in = beast::buffers_to_string(self->buffer_.cdata()); - self->handle_request(); - }); -} - - -void ws_session::do_close(){ - ws_->close(""); -} - -void ws_session::handle_request(){ - std::cout << "[handle request]" << std::endl; - - std::make_unique - (beast::buffers_to_string(buffer_.data()), std::move(ws_), state_)->handle_request(); -} diff --git a/server/lib/CMakeLists.txt b/server/lib/CMakeLists.txt index c5ddbaa..0571c1c 100644 --- a/server/lib/CMakeLists.txt +++ b/server/lib/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(serializer) +add_subdirectory(response) diff --git a/server/lib/response/CMakeLists.txt b/server/lib/response/CMakeLists.txt new file mode 100644 index 0000000..aa7cf6e --- /dev/null +++ b/server/lib/response/CMakeLists.txt @@ -0,0 +1,6 @@ +project(response) + +file(GLOB SOURCES "src/*") +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PUBLIC serializer) \ No newline at end of file diff --git a/server/lib/response/include/response_creator.hpp b/server/lib/response/include/response_creator.hpp new file mode 100644 index 0000000..29484b5 --- /dev/null +++ b/server/lib/response/include/response_creator.hpp @@ -0,0 +1,33 @@ +#ifndef WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP +#define WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP + +#include +#include + +#include + +#include "serializer.h" + +class response_creator { +public: + static std::string create_with_status(std::string type_, unsigned short code); + + static std::unordered_map + create_with_room(unsigned short code, const std::string &msg, const uuid &room_id); + + static std::unordered_map create_with_timing + (const boost::posix_time::time_duration &time); + + static std::string create_ping(const boost::posix_time::ptime &server_time); + + static std::string + create_login(std::string type_, unsigned short code, const std::string &login, const std::string &nick, + const std::string &pass); + + static std::string create_src_success(std::string type_, unsigned short code, const std::string &src); + + static std::string create_chat_mag(const std::string &nick, const std::string &msg); + + static std::string create_pong(const boost::posix_time::time_duration& timestamp, const std::string& viewer_id); +}; +#endif //WATCH_UP_PROJECT_RESPONSE_CREATOR_HPP diff --git a/server/lib/response/src/response_creator.cpp b/server/lib/response/src/response_creator.cpp new file mode 100644 index 0000000..31906fe --- /dev/null +++ b/server/lib/response/src/response_creator.cpp @@ -0,0 +1,68 @@ +#include "response_creator.hpp" + +std::string response_creator::create_with_status(std::string type_, unsigned short code){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + auto out = serializer::serialize_response(type_, data); + return out; +} +std::unordered_map response_creator::create_with_room(unsigned short code, const std::string& msg, const uuid& room_id){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["message"] = msg; + data["room_id"] = boost::lexical_cast(room_id); + return data; +} + +std::unordered_map response_creator::create_with_timing + (const boost::posix_time::time_duration& time){ + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << time; + auto str_time = ss_time.str(); + data["time"] = str_time; + return data; +} + +std::string response_creator::create_ping(const boost::posix_time::ptime& server_time){ + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << server_time; + data["server_time"] = ss_time.str(); + return serializer::serialize_response("ping", data); +} + +std::string response_creator::create_login(std::string type_, unsigned short code, const std::string& login, const std::string& nick, const std::string& pass){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["login"] = login; + data["nick"] = nick; + data["password"] = pass; + auto out = serializer::serialize_response(type_, data); + return out; +} + +std::string response_creator::create_src_success(std::string type_, unsigned short code,const std::string& src){ + std::unordered_map data{}; + data["code"] = std::to_string(code); + data["src"] = src; + auto out = serializer::serialize_response(type_, data); + return out; +} + +std::string response_creator::create_chat_mag(const std::string& nick, const std::string& msg){ + std::unordered_map data{}; + data["sender"] = nick; + data["msg"] = msg; + auto out = serializer::serialize_response("chat", data); + return out; +} + +std::string response_creator::create_pong(const boost::posix_time::time_duration ×tamp, const std::string &viewer_id) { + std::unordered_map data{}; + std::stringstream ss_time; + ss_time << timestamp; + data["time"] = ss_time.str(); + data["viewer"] = viewer_id; + return serializer::serialize_response("pong", data); +} diff --git a/server/lib/serializer/CMakeLists.txt b/server/lib/serializer/CMakeLists.txt index eeafeed..7a5850c 100644 --- a/server/lib/serializer/CMakeLists.txt +++ b/server/lib/serializer/CMakeLists.txt @@ -1,5 +1,8 @@ project(serializer) -add_library(${PROJECT_NAME} INTERFACE) +file(GLOB SOURCES "src/*") +find_package(nlohmann_json 3.10.5 REQUIRED) -target_include_directories(${PROJECT_NAME} INTERFACE ${CMAKE_CURRENT_SOURCE_DIR} ) +add_library(${PROJECT_NAME} ${SOURCES}) +target_include_directories(${PROJECT_NAME} PUBLIC include) +target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json) diff --git a/server/lib/serializer/include/serializer.h b/server/lib/serializer/include/serializer.h new file mode 100644 index 0000000..36b60ae --- /dev/null +++ b/server/lib/serializer/include/serializer.h @@ -0,0 +1,35 @@ +#ifndef WATCH_UP_PROJECT_SERIALIZER_H +#define WATCH_UP_PROJECT_SERIALIZER_H + +#include +#include +#include + +#include "nlohmann/json.hpp" +#include +#include +#include + +using uuid = boost::uuids::uuid; + +class serializer { +public: + + static std::unordered_map deserialize(const std::string& json_str); + + static std::unordered_map deserialize_access_opts(const std::string& json_str); + + + static std::string serialize_viewers(std::string type, const std::vector>& viewers); + + static std::string serialize_welcome_msg(std::string type, const std::vector>& viewers, + std::string src, std::string timing, bool state, const uuid& room_id, const std::string& service, + const std::string& self = ""); + + static std::string serialize_response(std::string type, const std::unordered_map& fields); + + static std::string serialize_timings(std::string type, const std::vector>& timings); + + + }; +#endif //WATCH_UP_PROJECT_SERIALIZER_H diff --git a/server/lib/serializer/serializer.h b/server/lib/serializer/serializer.h deleted file mode 100644 index 7ab9923..0000000 --- a/server/lib/serializer/serializer.h +++ /dev/null @@ -1,88 +0,0 @@ -#ifndef WATCH_UP_PROJECT_SERIALIZER_H -#define WATCH_UP_PROJECT_SERIALIZER_H - -#include -#include - -#include "nlohmann/json.hpp" -#include -#include -#include - -#include "types.hpp" - -using uuid = boost::uuids::uuid; - -class serializer { -public: - - static std::unordered_map deserialize(const std::string& json_str){ - //TODO try-catch - nlohmann::json json = nlohmann::json::parse(json_str); - std::unordered_map data{}; - - for(const auto& item: json.items()){ - - data[item.key()] = item.value(); - } - return data; - } - - static std::unordered_map deserialize_access_opts(const std::string& json_str){ - nlohmann::json json = nlohmann::json::parse(json_str); - std::unordered_map data{}; - - for(const auto& item: json.items()){ - data[item.key()] = boost::lexical_cast(item.value()); - } - return data; - } - - - static std::string serialize_viewers(type type_, const std::vector>& viewers){ - nlohmann::json data{}; - data["type"] = type_; - auto viewers_arr = nlohmann::json::array(); - for(const auto& viewer: viewers){ - auto v_obj = nlohmann::json{}; - v_obj["id"] = boost::lexical_cast(viewer.first); - v_obj["nick"] = viewer.second; - viewers_arr.push_back(v_obj); - } - data["viewers"] = viewers_arr; - return nlohmann::to_string(data); - } - - - static std::string serialize_response(type type_, const std::unordered_map& fields){ - nlohmann::json data{}; - if(type_ == invalid){ - data["type"] = "invalid"; - } - else { - data["type"] = type_; - } - for(const auto& [key, value] : fields){ - data[key] = value; - } - return nlohmann::to_string(data); - } - - static std::string serialize_timings(type type_, const std::vector>& timings){ - nlohmann::json data{}; - data["type"] = type_; - auto viewers_arr = nlohmann::json::array(); - for(const auto& viewer: timings){ - auto v_obj = nlohmann::json{}; - v_obj["id"] = viewer.first; - v_obj["time"] = viewer.second; - viewers_arr.push_back(v_obj); - } - data["timings"] = viewers_arr; - return nlohmann::to_string(data); - } - - - -}; -#endif //WATCH_UP_PROJECT_SERIALIZER_H diff --git a/server/lib/serializer/src/serializer.cpp b/server/lib/serializer/src/serializer.cpp new file mode 100644 index 0000000..b687cec --- /dev/null +++ b/server/lib/serializer/src/serializer.cpp @@ -0,0 +1,84 @@ +#include "serializer.h" + +std::unordered_map serializer::deserialize(const std::string &json_str) { + //TODO try-catch + nlohmann::json json = nlohmann::json::parse(json_str); + std::unordered_map data{}; + + for(const auto& item: json.items()){ + + data[item.key()] = item.value(); + } + return data; +} + +std::unordered_map serializer::deserialize_access_opts(const std::string &json_str) { + nlohmann::json json = nlohmann::json::parse(json_str); + std::unordered_map data{}; + + for(const auto& item: json.items()){ + data[item.key()] = boost::lexical_cast(item.value()); + } + return data; +} + +std::string serializer::serialize_viewers(std::string type, const std::vector> &viewers) { + nlohmann::json data{}; + data["type"] = type; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: viewers){ + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["nick"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["viewers"] = viewers_arr; + return nlohmann::to_string(data); +} + +std::string serializer::serialize_welcome_msg(std::string type, const std::vector> &viewers, + std::string src, std::string timing, bool state, const uuid &room_id, const std::string& service, + const std::string &self) { + nlohmann::json data{}; + data["type"] = type; + data["src"] = src; + data["time"] = timing; + data["code"] = "200"; + data["state"] = state ? "True" : "False"; + data["room_id"] = boost::lexical_cast(room_id); + data["service"] = service; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: viewers){ + if (viewer.first == self){ + continue; + } + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["nick"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["viewers"] = viewers_arr; + return nlohmann::to_string(data); +} + +std::string serializer::serialize_response(std::string type, const std::unordered_map& fields){ + nlohmann::json data{}; + data["type"] = type; + for(const auto& [key, value] : fields){ + data[key] = value; + } + return nlohmann::to_string(data); +} +std::string serializer::serialize_timings(std::string type, const std::vector>& timings){ + nlohmann::json data{}; + data["type"] = type; + auto viewers_arr = nlohmann::json::array(); + for(const auto& viewer: timings){ + auto v_obj = nlohmann::json{}; + v_obj["id"] = viewer.first; + v_obj["time"] = viewer.second; + viewers_arr.push_back(v_obj); + } + data["timings"] = viewers_arr; + return nlohmann::to_string(data); +} diff --git a/server/uml/uml_server b/server/uml/uml_server index 3d59a37..4b1882c 100644 --- a/server/uml/uml_server +++ b/server/uml/uml_server @@ -1 +1 @@ -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 \ No newline at end of file +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