diff --git a/CMakeLists.txt b/CMakeLists.txt index 586c3cb5..fe6a0366 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,7 @@ option(ICHOR_BUILD_BENCHMARKS "Build benchmarks" ON) option(ICHOR_BUILD_TESTING "Build tests" ON) option(ICHOR_ENABLE_INTERNAL_DEBUGGING "Add verbose logging of Ichor internals" OFF) option(ICHOR_ENABLE_INTERNAL_COROUTINE_DEBUGGING "Add verbose logging of Ichor coroutine internals" OFF) +option(ICHOR_ENABLE_INTERNAL_IO_DEBUGGING "Add verbose logging of Ichor I/O internals" OFF) option(ICHOR_BUILD_COVERAGE "Build ichor with coverage" OFF) option(ICHOR_USE_SPDLOG "Use spdlog as framework logging implementation" OFF) option(ICHOR_USE_ETCD "Add etcd services (may not work, not used much)" OFF) @@ -68,6 +69,7 @@ else() endif() option(ICHOR_USE_HIREDIS "Add hiredis dependency" OFF) option(ICHOR_MUSL "Use when building for musl instead of glibc" OFF) +option(ICHOR_AARCH64 "Use when building for aarch64. Turns off some x86-specific compiler flags." OFF) set(BUILD_TESTING OFF) #disable Catch 2 testing @@ -92,10 +94,11 @@ file(GLOB_RECURSE ICHOR_NETWORK_SOURCES ${ICHOR_TOP_DIR}/src/services/network/*. file(GLOB_RECURSE ICHOR_METRICS_SOURCES ${ICHOR_TOP_DIR}/src/services/metrics/*.cpp) file(GLOB_RECURSE ICHOR_TIMER_SOURCES ${ICHOR_TOP_DIR}/src/services/timer/*.cpp) file(GLOB_RECURSE ICHOR_HIREDIS_SOURCES ${ICHOR_TOP_DIR}/src/services/redis/*.cpp) +file(GLOB_RECURSE ICHOR_IO_SOURCES ${ICHOR_TOP_DIR}/src/services/io/*.cpp) file(GLOB SPDLOG_SOURCES ${ICHOR_EXTERNAL_DIR}/spdlog/src/*.cpp) -add_library(ichor ${FMT_SOURCES} ${ICHOR_FRAMEWORK_SOURCES} ${ICHOR_LOGGING_SOURCES} ${ICHOR_NETWORK_SOURCES} ${ICHOR_METRICS_SOURCES} ${ICHOR_TIMER_SOURCES} ${ICHOR_HIREDIS_SOURCES}) +add_library(ichor ${FMT_SOURCES} ${ICHOR_FRAMEWORK_SOURCES} ${ICHOR_LOGGING_SOURCES} ${ICHOR_NETWORK_SOURCES} ${ICHOR_METRICS_SOURCES} ${ICHOR_TIMER_SOURCES} ${ICHOR_HIREDIS_SOURCES} ${ICHOR_IO_SOURCES}) if(ICHOR_ENABLE_INTERNAL_DEBUGGING) target_compile_definitions(ichor PUBLIC ICHOR_ENABLE_INTERNAL_DEBUGGING) @@ -103,19 +106,14 @@ endif() if(ICHOR_ENABLE_INTERNAL_COROUTINE_DEBUGGING) target_compile_definitions(ichor PUBLIC ICHOR_ENABLE_INTERNAL_COROUTINE_DEBUGGING) endif() +if(ICHOR_ENABLE_INTERNAL_IO_DEBUGGING) + target_compile_definitions(ichor PUBLIC ICHOR_ENABLE_INTERNAL_IO_DEBUGGING) +endif() if(ICHOR_USE_SPDLOG) target_compile_definitions(ichor PUBLIC SPDLOG_COMPILED_LIB SPDLOG_NO_EXCEPTIONS SPDLOG_FMT_EXTERNAL SPDLOG_DISABLE_DEFAULT_LOGGER SPDLOG_NO_ATOMIC_LEVELS ICHOR_USE_SPDLOG SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_TRACE) endif() -if(ICHOR_USE_BOOST_BEAST) #json - target_compile_definitions(ichor PUBLIC ICHOR_USE_BOOST_JSON BOOST_JSON_STANDALONE) - find_package(Boost 1.75.0 REQUIRED COMPONENTS json context) - target_include_directories(ichor PUBLIC ${Boost_INCLUDE_DIR}) - target_link_directories(ichor PUBLIC ${Boost_LIBRARY_DIRS}) - target_link_libraries(ichor PUBLIC ${Boost_LIBRARIES}) -endif() - if(ICHOR_REMOVE_SOURCE_NAMES) target_compile_definitions(ichor PUBLIC ICHOR_REMOVE_SOURCE_NAMES_FROM_LOGGING) endif() @@ -145,7 +143,10 @@ if(ICHOR_USE_BOOST_BEAST) endif() if(ICHOR_MUSL) target_compile_definitions(ichor PUBLIC ICHOR_MUSL) - target_link_options(ichor PUBLIC -static-libgcc -static-libstdc++ -static) + target_link_options(ichor PUBLIC -static-libgcc -static-libstdc++) +endif() +if(ICHOR_AARCH64) + target_compile_definitions(ichor PUBLIC ICHOR_AARCH64) endif() if(NOT WIN32) @@ -249,7 +250,7 @@ endif() if(ICHOR_USE_THREAD_SANITIZER) target_compile_options(ichor PUBLIC -fsanitize=thread -fno-omit-frame-pointer) - target_link_options(ichor PUBLIC -fsanitize=thread) + target_link_options(ichor PUBLIC -fsanitize=thread -static-libtsan) # clang on OSX doesn't accept -no-pie for some reason if(NOT APPLE) @@ -278,7 +279,11 @@ if(WIN32 AND ICHOR_USE_HARDENING) target_compile_options(ichor PUBLIC /GS /guard:cf) target_compile_definitions(ichor PUBLIC ICHOR_USE_HARDENING) elseif(ICHOR_USE_HARDENING) - target_compile_options(ichor PUBLIC -fstack-protector-strong -fcf-protection) + target_compile_options(ichor PUBLIC -fstack-protector-strong) + + if(NOT ICHOR_AARCH64) + target_compile_options(ichor PUBLIC -fcf-protection) + endif() # stack clash protection not available on OSX if(NOT APPLE) @@ -376,6 +381,9 @@ if(ICHOR_USE_BOOST_BEAST) #beast target_include_directories(ichor PUBLIC ${Boost_INCLUDE_DIRS}) target_link_directories(ichor PUBLIC ${Boost_LIBRARY_DIRS}) target_link_libraries(ichor PUBLIC ${Boost_LIBRARIES}) + find_package(OpenSSL REQUIRED) + target_include_directories(ichor PUBLIC ${OPENSSL_INCLUDE_DIR}) + target_link_libraries(ichor PUBLIC ${OPENSSL_LIBRARIES}) #_SILENCE_ALL_CXX23_DEPRECATION_WARNINGS -> MSVC gives warnings on things like std::aligned_storage, which is still valid in C++20. target_compile_definitions(ichor PUBLIC BOOST_ASIO_NO_DEPRECATED _SILENCE_ALL_CXX23_DEPRECATION_WARNINGS) @@ -424,8 +432,8 @@ if(ICHOR_USE_ETCD) endif() # Detection for backward-cpp, works without external libraries on windows and apple -# On linux, it still works without these libraries (on gcc at least), but provides less functionality. -if(ICHOR_USE_BACKWARD) +# On linux, it still works without these libraries (on gcc + glibc at least), but provides less functionality. +if(ICHOR_USE_BACKWARD AND NOT ICHOR_MUSL) target_compile_definitions(ichor PUBLIC ICHOR_USE_BACKWARD=1) if(NOT WIN32 AND NOT APPLE) find_package(PkgConfig REQUIRED) diff --git a/Dockerfile b/Dockerfile index 6493c6bf..806d8141 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,13 +7,16 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60 RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 60 RUN update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-12 60 +# Run all downloads first to be able to use Docker's layers as cache and prevent excessive redownloads + WORKDIR /opt +RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2 +RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz #Build a new enough boost, apt only contains 1.74 which is too old. -RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.81.0/source/boost_1_81_0.tar.bz2 -RUN tar xf boost_1_81_0.tar.bz2 +RUN tar xf boost_1_83_0.tar.bz2 -WORKDIR /opt/boost_1_81_0 +WORKDIR /opt/boost_1_83_0 RUN ./bootstrap.sh --prefix=/usr RUN ./b2 variant=release link=static threading=multi @@ -22,7 +25,6 @@ RUN ./b2 variant=release link=static threading=multi install WORKDIR /opt #Build latest hiredis containing sdevent support, not available yet in apt -RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz RUN tar xf v1.2.0.tar.gz RUN mkdir /opt/hiredis-1.2.0/build @@ -36,4 +38,4 @@ WORKDIR /opt/ichor/build ENTRYPOINT ["/bin/bash", "-c"] -CMD ["cd /opt/ichor/build && cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DICHOR_USE_SANITIZERS=0 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=ON -DICHOR_USE_SPDLOG=ON /opt/ichor/src && ninja"] +CMD ["cd /opt/ichor/build && cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DICHOR_USE_SANITIZERS=0 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=1 -DICHOR_USE_SPDLOG=1 /opt/ichor/src && ninja"] diff --git a/Dockerfile-asan b/Dockerfile-asan index c88ad0be..649c1829 100644 --- a/Dockerfile-asan +++ b/Dockerfile-asan @@ -7,13 +7,19 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60 RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 60 RUN update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-12 60 +# Run all downloads first to be able to use Docker's layers as cache and prevent excessive redownloads + WORKDIR /opt +RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.83.0/source/boost_1_83_0.tar.bz2 +RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz + +ENV CFLAGS="-Og -fsanitize=address,undefined" +ENV CXXFLAGS="-Og -fsanitize=address,undefined" #Build boost with support for asan -RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.81.0/source/boost_1_81_0.tar.bz2 -RUN tar xf boost_1_81_0.tar.bz2 +RUN tar xf boost_1_83_0.tar.bz2 -WORKDIR /opt/boost_1_81_0 +WORKDIR /opt/boost_1_83_0 RUN ./bootstrap.sh --prefix=/usr RUN ./b2 cxxflags="-fsanitize=address,undefined -Og -std=c++17 -DBOOST_USE_ASAN -DBOOST_USE_UCONTEXT" linkflags="-lasan -lubsan" variant=debug link=static threading=multi context-impl=ucontext @@ -22,7 +28,6 @@ RUN ./b2 cxxflags="-fsanitize=address,undefined -Og -std=c++17 -DBOOST_USE_ASAN WORKDIR /opt #Build latest hiredis containing sdevent support, not available yet in apt -RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz RUN tar xf v1.2.0.tar.gz RUN mkdir /opt/hiredis-1.2.0/build @@ -34,6 +39,9 @@ RUN mkdir -p /opt/ichor/build WORKDIR /opt/ichor/build +RUN unset CFLAGS +RUN unset CXXFLAGS + ENTRYPOINT ["/bin/bash", "-c"] -CMD ["cd /opt/ichor/build && cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DICHOR_USE_SANITIZERS=1 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=ON -DICHOR_USE_SPDLOG=ON /opt/ichor/src && ninja"] +CMD ["cd /opt/ichor/build && cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DICHOR_USE_SANITIZERS=1 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=1 -DICHOR_USE_SPDLOG=1 /opt/ichor/src && ninja"] diff --git a/Dockerfile-musl b/Dockerfile-musl index db1254aa..77c31560 100644 --- a/Dockerfile-musl +++ b/Dockerfile-musl @@ -1,17 +1,26 @@ FROM alpine:3.17 RUN apk update -RUN apk add gcc g++ build-base cmake openssl-dev git wget make nano sed linux-headers +RUN apk add gcc g++ build-base cmake git wget make nano sed linux-headers perl # Run all downloads first to be able to use Docker's layers as cache and prevent excessive redownloads WORKDIR /opt RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.81.0/source/boost_1_81_0.tar.bz2 RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz +RUN wget https://www.openssl.org/source/openssl-3.0.11.tar.gz + +ENV LDFLAGS="-static-libgcc -static-libstdc++ -static" +#Build openssl statically, alpine (and probably most distros) only provide shared libraries. Might be a security thing? +RUN tar xf openssl-3.0.11.tar.gz +WORKDIR /opt/openssl-3.0.11 +RUN ./Configure --prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared +RUN make -j +RUN make -j install WORKDIR /opt -#Build a new enough boost, apt only contains 1.74 which is too old. +#Build boost statically RUN tar xf boost_1_81_0.tar.bz2 WORKDIR /opt/boost_1_81_0 @@ -27,7 +36,6 @@ RUN tar xf v1.2.0.tar.gz RUN mkdir /opt/hiredis-1.2.0/build WORKDIR /opt/hiredis-1.2.0/build -ENV LDFLAGS="-static-libgcc -static-libstdc++ -static" RUN cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_TESTS=1 -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_SSL=1 -DBUILD_SHARED_LIBS=0 .. RUN make -j && make install RUN mkdir -p /opt/ichor/build @@ -36,4 +44,4 @@ WORKDIR /opt/ichor/build ENTRYPOINT ["/bin/sh", "-c"] -CMD ["cd /opt/ichor/build && cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DICHOR_USE_SANITIZERS=0 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=1 -DICHOR_USE_SPDLOG=ON -DICHOR_MUSL=1 /opt/ichor/src && make -j"] +CMD ["cd /opt/ichor/build && cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DICHOR_USE_SANITIZERS=0 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=1 -DICHOR_USE_SPDLOG=1 -DICHOR_MUSL=1 /opt/ichor/src && make -j"] diff --git a/Dockerfile-musl-aarch64 b/Dockerfile-musl-aarch64 new file mode 100644 index 00000000..34d7a726 --- /dev/null +++ b/Dockerfile-musl-aarch64 @@ -0,0 +1,47 @@ +FROM arm64v8/alpine:3.17 + +RUN apk update +RUN apk add gcc g++ build-base cmake git wget make nano sed linux-headers perl + +# Run all downloads first to be able to use Docker's layers as cache and prevent excessive redownloads + +WORKDIR /opt +RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.81.0/source/boost_1_81_0.tar.bz2 +RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz +RUN wget https://www.openssl.org/source/openssl-3.0.11.tar.gz + +ENV LDFLAGS="-static-libgcc -static-libstdc++ -static" +#Build openssl statically, alpine (and probably most distros) only provide shared libraries. Might be a security thing? +RUN tar xf openssl-3.0.11.tar.gz +WORKDIR /opt/openssl-3.0.11 +RUN ./Configure --prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared +RUN make -j +RUN make -j install + +WORKDIR /opt + +#Build boost statically +RUN tar xf boost_1_81_0.tar.bz2 + +WORKDIR /opt/boost_1_81_0 + +RUN ./bootstrap.sh --prefix=/usr +RUN ./b2 variant=release link=static threading=multi cxxflags="-O2 -std=c++17 -static -fstack-protector-strong -fstack-clash-protection" linkflags="-static-libgcc -static-libstdc++ -static" +RUN ./b2 variant=release link=static threading=multi cxxflags="-O2 -std=c++17 -static -fstack-protector-strong -fstack-clash-protection" linkflags="-static-libgcc -static-libstdc++ -static" install + +WORKDIR /opt + +#Build latest hiredis containing sdevent support, not available yet in apt +RUN tar xf v1.2.0.tar.gz +RUN mkdir /opt/hiredis-1.2.0/build + +WORKDIR /opt/hiredis-1.2.0/build +RUN cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_TESTS=1 -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_SSL=1 -DBUILD_SHARED_LIBS=0 .. +RUN make -j && make install +RUN mkdir -p /opt/ichor/build + +WORKDIR /opt/ichor/build + +ENTRYPOINT ["/bin/sh", "-c"] + +CMD ["cd /opt/ichor/build && cmake -DCMAKE_BUILD_TYPE=Debug -DICHOR_USE_SANITIZERS=0 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=1 -DICHOR_USE_SPDLOG=1 -DICHOR_MUSL=1 -DICHOR_AARCH64=1 /opt/ichor/src && make -j"] diff --git a/Dockerfile-tsan b/Dockerfile-tsan new file mode 100644 index 00000000..c6b2930c --- /dev/null +++ b/Dockerfile-tsan @@ -0,0 +1,85 @@ +FROM ubuntu:jammy + +RUN apt update +RUN apt install -y g++-12 gcc-12 build-essential cmake pkg-config git wget ninja-build nano libzip-dev + +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 60 +RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 60 +RUN update-alternatives --install /usr/bin/cpp cpp /usr/bin/cpp-12 60 + +# Run all downloads first to be able to use Docker's layers as cache and prevent excessive redownloads + +WORKDIR /opt +RUN wget https://github.com/redis/hiredis/archive/refs/tags/v1.2.0.tar.gz +RUN wget https://boostorg.jfrog.io/artifactory/main/release/1.81.0/source/boost_1_81_0.tar.bz2 +RUN wget https://www.openssl.org/source/openssl-3.0.11.tar.gz + +ENV CFLAGS="-Og -fsanitize=thread" +ENV CXXFLAGS="-Og -fsanitize=thread" +ENV LDFLAGS="-fsanitize=thread -static-libtsan -static-libgcc -static-libstdc++" + +RUN tar xf openssl-3.0.11.tar.gz +WORKDIR /opt/openssl-3.0.11 +RUN ./Configure --prefix=/usr --openssldir=/etc/ssl --libdir=lib no-shared +RUN make -j +RUN make -j install + +WORKDIR /opt + +#Build boost with support for asan +RUN tar xf boost_1_81_0.tar.bz2 + +WORKDIR /opt/boost_1_81_0 + +RUN <> patch << EOR +diff -r -u boost-1.81.0/boost/asio/detail/std_fenced_block.hpp boost-1.81.0-fencecd/boost/asio/detail/std_fenced_block.hpp +--- ./boost/asio/detail/std_fenced_block.hpp 2022-12-14 16:15:35.000000000 +0100 ++++ ./boost/asio/detail/std_fenced_block.hpp 2023-10-03 20:17:11.701435674 +0200 +@@ -43,14 +43,16 @@ + // Constructor for a full fenced block. + explicit std_fenced_block(full_t) + { +- std::atomic_thread_fence(std::memory_order_acquire); ++ _fence.load(std::memory_order_acquire); + } + + // Destructor. + ~std_fenced_block() + { +- std::atomic_thread_fence(std::memory_order_release); ++ _fence.store(true, std::memory_order_release); + } ++ ++ std::atomic _fence; + }; + + } // namespace detail +EOR +EOF +RUN patch ./boost/asio/detail/std_fenced_block.hpp < patch + +RUN ./bootstrap.sh --prefix=/usr +RUN ./b2 cxxflags="-fsanitize=thread -Og -std=c++17 -DBOOST_USE_TSAN" linkflags="-static-libtsan -static-libgcc -static-libstdc++" variant=debug link=static threading=multi +RUN ./b2 cxxflags="-fsanitize=thread -Og -std=c++17 -DBOOST_USE_TSAN" linkflags="-static-libtsan -static-libgcc -static-libstdc++" variant=debug link=static threading=multi install + +WORKDIR /opt + +#Build latest hiredis containing sdevent support, not available yet in apt +RUN tar xf v1.2.0.tar.gz +RUN mkdir /opt/hiredis-1.2.0/build + +WORKDIR /opt/hiredis-1.2.0/build +RUN cmake -GNinja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DDISABLE_TESTS=1 -DCMAKE_INSTALL_PREFIX=/usr -DENABLE_SSL=1 .. +RUN ninja && ninja install + +RUN mkdir -p /opt/ichor/build + +WORKDIR /opt/ichor/build + +RUN unset CFLAGS +RUN unset CXXFLAGS + +ENTRYPOINT ["/bin/bash", "-c"] + +CMD ["cd /opt/ichor/build && cmake -GNinja -DCMAKE_BUILD_TYPE=Debug -DICHOR_USE_SANITIZERS=0 -DICHOR_USE_THREAD_SANITIZER=1 -DICHOR_USE_HIREDIS=1 -DICHOR_USE_BOOST_BEAST=1 -DICHOR_USE_SPDLOG=1 -DICHOR_USE_BACKWARD=0 /opt/ichor/src && ninja"] diff --git a/README.md b/README.md index bc18df28..f345cc4f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ In which case, you're on your own. ### Dependency Injection? -To support software lifecycles measured in decades, engineers try to encapsulate functionality. Using the [Dependency Injection](https://www.youtube.com/watch?v=yVogS4NbL6U) approach allows engineers to insulate updates to parts of the code. +To support software lifecycles measured in decades, engineers try to encapsulate functionality. Using the [Dependency Injection](./docs/02-DependencyInjection.md) approach allows engineers to insulate updates to parts of the code. There exist many Dependency Injection libraries for C++ already, but where those usually only provide Dependency Injection, Ichor also provides service lifecycle management and thread confinement. If a dependency goes away at runtime, e.g. a network client, then all the services depending on it will be cleaned up at that moment. @@ -31,14 +31,14 @@ The [realtime example](examples/realtime_example/main.cpp) shows a trivial progr More examples can be found in the [examples directory](examples). ## Supported OSes -* Linux (including musl-based distros like alpine linux) +* Linux (including aarch64 and musl-based distros like alpine linux) * Windows -* Partial support for OSX Monterey (using `brew install llvm@15`, ASAN and boost beast don't seem to work) +* Partial support for OSX Monterey (using `brew install llvm`, ASAN in combination with boost beast may result in false positives) ## Supported Compilers * Gcc 11.3 or newer (see [this gcc bug](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=95137) for why) * Clang 14 or newer -* MSVC 17.4+ (though some workarounds for [compiler bugs](https://developercommunity.visualstudio.com/t/c20-Friend-definition-of-class-with-re/10197302) are used) +* MSVC 17.4+ (though some workarounds for [some](https://developercommunity.visualstudio.com/t/C2039:-promise_type:-is-not-a-member-o/10505491) [compiler](https://developercommunity.visualstudio.com/t/c20-Friend-definition-of-class-with-re/10197302) [bugs](https://developercommunity.visualstudio.com/t/certain-coroutines-cause-error-C7587:-/10311276?q=%22task+runner%22+gulp+duration&sort=newest) are used) ## Currently Unsupported * Baremetal, might change if someone puts in the effort to modify Ichor to work with freestanding implementations of C++20 @@ -71,11 +71,11 @@ The framework provides several core features and optional services behind cmake * Event-based message passing * Dependency Injection * Service lifecycle management (sort of like OSGi-lite services) -* data race free communication between event loops +* data race free communication between event loops on multiple threads Optional services: * Websocket service through Boost.BEAST -* HTTP client and server services through Boost.BEAST +* HTTP/HTTPS client and server services through Boost.BEAST * Spdlog logging service * TCP communication service * JSON serialization services examples @@ -92,7 +92,6 @@ Optional services: * Shell Commands / REPL * Tracing interface * Opentracing? Jaeger? -* Docker integration/compilation * "Remote" services, where services are either in a different thread or a different machine * Code generator to reduce boilerplate * ... diff --git a/build.sh b/build.sh index 92bd9d60..9b376122 100755 --- a/build.sh +++ b/build.sh @@ -8,6 +8,26 @@ cleanup () trap cleanup SIGINT SIGTERM +POSITIONAL_ARGS=() +DOCKER=1 + +while [[ $# -gt 0 ]]; do + case $1 in + --no-docker) + DOCKER=0 + shift # past value + ;; + -*|--*) + echo "Unknown option $1" + exit 1 + ;; + *) + POSITIONAL_ARGS+=("$1") # save positional arg + shift # past argument + ;; + esac +done + ccompilers=("clang-14" "clang-16" "gcc-11" "gcc-12") cppcompilers=("clang++-14" "clang++-16" "g++-11" "g++-12") @@ -52,20 +72,41 @@ run_benchmarks () } -rm -rf ./* ../bin/* -docker build -f ../Dockerfile -t ichor . || exit 1 -docker run -v $(pwd)/../:/opt/ichor/src -it ichor || exit 1 -run_examples +if [[ $DOCKER -eq 1 ]]; then + rm -rf ./* ../bin/* + docker build -f ../Dockerfile -t ichor . || exit 1 + docker run -v $(pwd)/../:/opt/ichor/src -it ichor || exit 1 + run_examples + + rm -rf ./* ../bin/* + docker build -f ../Dockerfile-musl -t ichor-musl . || exit 1 + docker run -v $(pwd)/../:/opt/ichor/src -it ichor-musl || exit 1 + run_examples + + rm -rf ./* ../bin/* + docker build -f ../Dockerfile-asan -t ichor-asan . || exit 1 + docker run -v $(pwd)/../:/opt/ichor/src -it ichor-asan || exit 1 + run_examples -rm -rf ./* ../bin/* -docker build -f ../Dockerfile-musl -t ichor-musl . || exit 1 -docker run -v $(pwd)/../:/opt/ichor/src -it ichor-musl || exit 1 -run_examples + # tsan is purposefully not run automatically, because it usually contains false positives. -rm -rf ./* ../bin/* -docker build -f ../Dockerfile-asan -t ichor-asan . || exit 1 -docker run -v $(pwd)/../:/opt/ichor/src -it ichor-asan || exit 1 -run_examples + rm -rf ./* ../bin/* + docker run --rm --privileged multiarch/qemu-user-static --reset -p yes || exit 1 + docker build -f ../Dockerfile-musl-aarch64 -t ichor-musl-aarch64 . || exit 1 + docker run -v $(pwd)/../:/opt/ichor/src -it ichor-musl-aarch64 || exit 1 + cat >> ../bin/run_aarch64_examples_and_tests.sh << EOF +#!/bin/sh +FILES=/opt/ichor/src/bin/* +for f in \$FILES; do + if [[ "\$f" != *"Redis"* ]] && [[ "\$f" != *"benchmark"* ]] && [[ "\$f" != *"minimal"* ]] && [[ "\$f" != *"ping"* ]] && [[ "\$f" != *"pong"* ]] && [[ "\$f" != *"Started"* ]] && [[ "\$f" != *".sh" ]] && [[ -x "\$f" ]] ; then + echo "Running \$f" + \$f || echo "Error" && exit 1 + fi +done +EOF + chmod +x ../bin/run_aarch64_examples_and_tests.sh + docker run -v $(pwd)/../:/opt/ichor/src --privileged -it ichor-musl-aarch64 "sh -c 'ulimit -r unlimited && /opt/ichor/src/bin/run_aarch64_examples_and_tests.sh'" || exit 1 +fi for i in ${!ccompilers[@]}; do rm -rf ./* ../bin/* diff --git a/docs/01-GettingStarted.md b/docs/01-GettingStarted.md index 3ca79ddf..7edc8ad9 100644 --- a/docs/01-GettingStarted.md +++ b/docs/01-GettingStarted.md @@ -86,6 +86,15 @@ Then add the following [system variables](https://www.alphr.com/set-environment- To run the examples/tests that use boost, copy the dlls in `C:\SDKs\boost_1_81_0\lib64-msvc-14.3` (or where you installed boost) into the generated `bin` folder. +Something similar goes for openssl. Download the latest [prebuilt binaries here](https://github.com/CristiFati/Prebuilt-Binaries/tree/master/OpenSSL) and unpack it into `C:\SDKs\openssl_3.1.3` so that `C:\SDKs\openssl_3.1.3´\include` exists, skipping a few subdirectories. Then add the following environment variables: +``` + OPENSSL_INCLUDE_DIR C:\SDKs\openssl_3.1.3\include + OPENSSL_LIBRARYDIR C:\SDKs\openssl_3.1.3\lib + OPENSSL_ROOT C:\SDKs\openssl_3.1.3 +``` + +Don't forget to copy `C:\SDKs\openssl_3.1.3\bin\*.dll` to the Ichor `bin` directory after compiling Ichor. + #### OSX Monterey Work in progress, initial support available, sanitizers with boost seem to get false positives. @@ -192,6 +201,8 @@ int main() { ### Requesting Dependencies +For more information on Dependency Injection, please see the [relevant doc](02-DependencyInjection.md). + In general, the arguments in a constructor are reflected upon on compile-time and are all considered to be requests. That means that there are no custom arguments possible. e.g. ```c++ diff --git a/docs/02-DependencyInjection.md b/docs/02-DependencyInjection.md new file mode 100644 index 00000000..f0633ca5 --- /dev/null +++ b/docs/02-DependencyInjection.md @@ -0,0 +1,118 @@ +# Dependency Injection (DI) + +Ichor heavily uses Dependency Injection to ensure easier long term maintainability as well as make it easier to modify Ichor to suit the users use-cases. + +## What is DI? + +DI is a design pattern that decouples the creation of objects from using them. Instead of knowing about objects/functions directly, they are 'hidden' behind a virtual interface and the code using the interface doesn't know anything about its internals. + +The benefits of this pattern are generally: + +* Refactoring costs go down in big projects +* Better seperation of concerns, i.e. easier to reason about code +* Easier to test parts of the code in isolation + +Obviously, the cost is a bit of boilerplate. + +There's a lot of resources on DI in C++ already, please consult the Extra Resources at the bottom of the page to get a more detailled explanation of this pattern. + +## Why use DI in C++? + +C++ users traditionally are looking to get as close to the metal as possible. Even going so far as to look down upon using virtual method calls, as that introduces vtable lookups, pointer indirection and prevents inlining. + +However, as compilers have been getting better (e.g. [LTO](https://en.wikipedia.org/wiki/Interprocedural_optimization)), processors get better at branch prediction and have more L1/L2/L3 cache, the impact of virtual methods have been reduced. Moreover, software developers don't need the maximum performance everywhere, just in hot code paths or there where the profiler shows a lot of CPU time. + +Everywhere else, developers should prefer increasing development productivity and code quality. DI is a pattern that helps achieve those goals. + +## How is DI implemented in Ichor? + +Ichor provides two methods of using DI: + +### Constructor Injection + +Ichor contains some template deductions to figure out what interfaces the code requests from a constructor. This is the simplest form of using DI, but doesn't allow you to specify if the dependency is required/optional nor does it support multiple instances of the same type. + +e.g. here is a service that adds a REST API endpoint to the running HTTP host service: + +```c++ +class BasicService final { +public: + BasicService(IHttpHostService *hostService) { + _routeRegistration = hostService->addRoute(HttpMethod::get, "/basic", [this, serializer](HttpRequest &req) -> AsyncGenerator { + co_return HttpResponse{false, HttpStatus::ok, "application/text, "This is my basic webpage", {}}; + }); + } + +private: + std::unique_ptr _routeRegistration{}; +}; +``` + +Of course, Ichor needs to know about this type and this is done by registering it during initialization: + +```c++ +auto queue = std::make_unique(spinlock); +auto &dm = queue->createManager(); +dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(80))}}); +dm.createServiceManager(); +queue->start(CaptureSigInt); +``` + +As soon as `HttpHostService` is initialized, Ichor notices that all requested dependencies of `BasicService` have been met and constructs it. + +### Runtime Injection + +The second option Ichor provides is to requests dependencies through a special constructor and allow the programmer to respond to a dependency becoming available or going away. This is especially useful when a piece of code is interested in multiple instances of the same type or if the dependency is optional. The [optional dependency example](../examples/optional_dependency_example) is a good show case of this. + +The code generally follows this pattern: + +```c++ +class MyService final : public AdvancedService { +public: + // Creating instances of a service includes properties, and these need to be stored in the parent class. Be careful to move them each time and don't use the props variable but instead call getProperties(), if you need them. + MyService(DependencyRegister ®, Properties props) : AdvancedService(std::move(props)) { + reg.registerDependency(this, true); // Request ILogger as a required dependency (the start function will only be called if all required dependencies have at least 1 instance available) + reg.registerDependency(this, false); // Request IOptionalService as an optional dependency. This does not influence the start and stop functions. + } + ~MyService() final = default; + +private: + Task> start() final { + // this function is called when all required dependencies have been met with at least 1 instance + co_return {}; + } + + Task stop() final { + // this function is called when the MyService instance is in a started state but one or more of the required dependencies are not available anymore. This function is guaranteed to be called before the removeDependencyInstance function is called for the dependency that is going away. + co_return; + } + + void addDependencyInstance(ILogger &, IService &) { + // This function has this exact signature (so non-const reference parameters) and is called every time an ILogger instance is succesfully started. + } + + void removeDependencyInstance(ILogger&, IService&) { + // This function has this exact signature (so non-const reference parameters) and is called every time an ILogger instance is stopping. + } + + void addDependencyInstance(IOptionalService&, IService &isvc) { + // This function has this exact signature (so non-const reference parameters) and is called every time an IOptionalService instance is succesfully started. + } + + void removeDependencyInstance(IOptionalService&, IService&) { + // This function has this exact signature (so non-const reference parameters) and is called every time an IOptionalService instance is stopping. + } + + // The dependency register needs to access the private functions to inject the dependencies + // If you prefer, you can make the 6 functions above all public and then this won't be necessary. That's purely a stylistic choice, as the interface that other objects use won't have these functions mentioned at all. + friend DependencyRegister; +}; +``` + +## Extra Resources + +[How to Use C++ Dependency Injection to Write Maintainable Software - Francesco Zoffoli CppCon 2022](https://www.youtube.com/watch?v=l6Y9PqyK1Mc) + +[Cody Morterud's blog](https://www.codymorterud.com/design/2018/09/07/dependency-injection-cpp.html) + +[C++Now 2019: Kris Jusiak “Dependency Injection - a 25-dollar term for a 5-cent concept”](https://www.youtube.com/watch?v=yVogS4NbL6U) \ No newline at end of file diff --git a/docs/02-CMakeOptions.md b/docs/03-CMakeOptions.md similarity index 71% rename from docs/02-CMakeOptions.md rename to docs/03-CMakeOptions.md index 7e1fb4a3..ce6d19ff 100644 --- a/docs/02-CMakeOptions.md +++ b/docs/03-CMakeOptions.md @@ -2,15 +2,15 @@ ## BUILD_EXAMPLES -Builds the examples in the [examples directory](../examples). Takes some time to compile. +Turned on by default. Builds the examples in the [examples directory](../examples). Takes some time to compile. ## BUILD_BENCHMARKS -Builds the benchmarks in the [benchmarks directory](../benchmarks). +Turned on by default. Builds the benchmarks in the [benchmarks directory](../benchmarks). ## BUILD_TESTS -Builds the tests in the [test directory](../test). +Turned on by default. Builds the tests in the [test directory](../test). ## ICHOR_ENABLE_INTERNAL_DEBUGGING @@ -22,7 +22,7 @@ Enables verbose logging for coroutines in the Ichor framework. Recommended only ## ICHOR_USE_SANITIZERS -Compiles everything (including the optionally enabled submodules) with the AddressSanitizer and UndefinedBehaviourSanitizer. Recommended when debugging. Cannot be combined with the ThreadSanitizer +Turned on by default. Compiles everything (including the optionally enabled submodules) with the AddressSanitizer and UndefinedBehaviourSanitizer. Recommended when debugging. Cannot be combined with the ThreadSanitizer ## ICHOR_USE_THREAD_SANITIZER @@ -38,7 +38,7 @@ Ichor's logging macros by default adds the current filename and line number to e ## ICHOR_USE_HARDENING -Uses compiler-specific flags which add stack protection and similar features, as well as adding safety checks in Ichor itself. +Turned on by default. Uses compiler-specific flags which add stack protection and similar features, as well as adding safety checks in Ichor itself. ## ICHOR_USE_SPDLOG (optional dependency) @@ -75,7 +75,7 @@ Adds and installs the backward header. Useful for showing backtraces on-demand o ## ICHOR_DISABLE_RTTI -Disables `dynamic_cast<>()` in most cases as well as `typeid`. Ichor is an opinionated piece of software and we strongly encourage you to disable RTTI. We believe `dynamic_cast<>()` is wrong in almost all instances. Virtual methods and double dispatch should be used instead. If, however, you really want to use RTTI, use this option to re-enable it. +Turned on by default. Disables `dynamic_cast<>()` in most cases as well as `typeid`. Ichor is an opinionated piece of software and we strongly encourage you to disable RTTI. We believe `dynamic_cast<>()` is wrong in almost all instances. Virtual methods and double dispatch should be used instead. If, however, you really want to use RTTI, use this option to re-enable it. ## ICHOR_USE_MIMALLOC @@ -84,3 +84,11 @@ If `ICHOR_USE_SANITIZERS` is turned OFF, Ichor by default compiles itself with m ## ICHOR_USE_SYSTEM_MIMALLOC If `ICHOR_USE_MIMALLOC` is turned ON, this option can be used to use the system installed mimalloc instead of the Ichor provided one. + +## ICHOR_MUSL + +This option results in statically linking libgcc and libstdc++ as well as turning off some glibc-specific pthread usages. + +## ICHOR_AARCH64 + +Control flow protection is not available on Aarch64, enable this to allow (cross-)compiling for that architecture. diff --git a/docs/03-ComparisonOtherLibraries.md b/docs/04-ComparisonOtherLibraries.md similarity index 100% rename from docs/03-ComparisonOtherLibraries.md rename to docs/04-ComparisonOtherLibraries.md diff --git a/docs/04-Downsides.md b/docs/05-Downsides.md similarity index 100% rename from docs/04-Downsides.md rename to docs/05-Downsides.md diff --git a/docs/06-HttpsConnections.md b/docs/06-HttpsConnections.md new file mode 100644 index 00000000..6055debd --- /dev/null +++ b/docs/06-HttpsConnections.md @@ -0,0 +1,171 @@ +# Https Connections + +Ichor supports setting up and connecting to servers using SSL. + +## Host + +### Create certificate + +First, let's create a certificate without a password: + +```shell +$ openssl req -newkey rsa:2048 -new -nodes -keyout key.pem -out csr.pem -x509 -days 365 -subj "/C=NL/L=Amsterdam/O=Ichor/CN=www.example.com" +$ cat csr.pem +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUQEtUTmX2HbHwOpDPgFkYx66ei/swDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCTkwxEjAQBgNVBAcMCUFtc3RlcmRhbTEOMAwGA1UECgwF +SWNob3IxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMzEwMDIwODI1NTVa +Fw0yNDEwMDEwODI1NTVaMEsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlBbXN0ZXJk +YW0xDjAMBgNVBAoMBUljaG9yMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIisWpB9FSc/OIYsxznQJgioL2 +aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ+VNc63blCPAI +iL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFFelbLf7JTW80C +nNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi1QbWD3G1lkTQ +cCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGoswxe5IF+yB6f +WFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0yv+K0iUBAgMB +AAGjUzBRMB0GA1UdDgQWBBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAfBgNVHSMEGDAW +gBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQC1QKNwlqrvE5a7M5v151tA/JvIKNBfesIRCg/IG6tB3teSJjAq +PRsSajMcyvDtlDMkWL093B8Epsc8yt/vN1GgsCfQsMeIWRGvRYM6Qo2WoABaLTuC +TxH89YrXa9uwUnbesGwKSkpxzj8g6YuzUpLnAAV70LefBn8ZgBJktFVW/ANWwQqD +KiglduSDGX3NiLByn7nGKejg2dPw6kWpOTVtLsWtgG/5T4vw+INQzPJqy+pUvBeQ +OrV7cQyq12DJJPxboIRh1KqF8C25NAOnXVtCav985RixHCMaNaXyOXBJeEEASnXg +pdDrObCgMze03IaHQ1pj3LeMu0OvEMbsRrYW +-----END CERTIFICATE----- +$ cat key.pem +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDIisWpB9FSc/OI +YsxznQJgioL2aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ ++VNc63blCPAIiL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFF +elbLf7JTW80CnNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi +1QbWD3G1lkTQcCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGo +swxe5IF+yB6fWFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0 +yv+K0iUBAgMBAAECggEAB2lJgMOvMrLiTyoHkEY/Lj4wNNcNW8g0aQRWlmng7SdE +84mh1QEspJegaUe7eyNTsTY8TNwzET43jEFQmWCCVliMNmSHWWWF99sgmuL5W5aN +Ec+4LPnCWDR+pxAKcCEoN4yzhfLTKNzmsqCg0Ih5mKcN3ojZMLodpCfH3WczDkay +1KrJvIKIovIV8Yv8NJ4K9hDx7a+xGpHXqh5NLey9K6XNpDqbWwN97FygLLEoedhx +XDjPzBiGJuwIYQI/V2gYVXn3nPcglDFqHpZA8peX3+DwXojqDwzbNhf2CANpMgaD +5OQkGymb+FumKPh1UMEL+IfqtXWwwg2Ix7DtI6i/IQKBgQDasFuUC43F0NM61TFQ +hWuH4mRMp8B4YkhO9M+0/mitHI76cnU63EnJpMYvnp3CLRpVzs/sidEcbfC9gOma +Ui+xxtKers+RQDI4lrwgqyYIzNNpGO7ItlDYs3guTvX5Mvc5x/zIIHhcjhTudzHK +Utgho8PIXvrrF3fSHdL0QJb1tQKBgQDqwdGImGbF3lxsB8t4T/8fozIBwo2dWp4O +YgcVyDpq7Nl+J9Yy5fqTw/sIn6Fa6ZpqAzq0zPSoYJzU7l9+5CcpkkdPEulyER3D +maQaVMUv0cpnXUQSBRNDZJDdbC4eKocxxotyTuLjCVrUwYWuqYWo3aGx42VXWsWm +n4+9AwDBnQKBgBStumscUJKU9XRJtnkLtKhLsvpAnoWDnZzBr2ZI7DL6UVbDPeyL +6fpEN21HTVmQFD5q6ORP/9L1Xl888lniTZo816ujkgMFE/qf3jgkltscKx1z+xhF +jQ2AouuWEdI3jIMNMwzlbRwrXzVRVgbwoHlF1/x5ZraWKIFYyprIBL5FAoGAfXcM +12YsN0A6QPqBglGu1mfQCCTErv6JTsKRatDSd+cR7ly4HAfRvjuV5Ov7vqzu/A2x +yINplrvb1el4XEbvr0YgmmBPJ8mCENICZJg9sur6s/eis8bGntQWoGB63WB5VN76 +FCOZGyIay26KVekAKFobWwlfViqLTBwnJCuAsfkCgYAgrbUKCf+BGgz4epv3T3Ps +kJD2XX1eeQr/bw1IMqY7ZLKCjg7oxM1jjp2xyujY3ZNViaJsnj7ngCIbaOSJNa3t +YS8GDLTL4efNTmPEVzgxhnJdSeYVgMLDwu66M9EYhkw+0Y1PQsNS1+6SO89ySRzP +pFbool/8ZDecmB4ZSa03aw== +-----END PRIVATE KEY----- +``` + +Now we need to get these into the code. We can do this by reading a file using standard C++, but for convenience, this doc will just copy and paste them into code: + +```c++ +auto queue = std::make_unique(); +auto &dm = queue->createManager(); + +std::string key = "-----BEGIN PRIVATE KEY-----\n" + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDIisWpB9FSc/OI\n" + "YsxznQJgioL2aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ\n" + "+VNc63blCPAIiL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFF\n" + "elbLf7JTW80CnNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi\n" + "1QbWD3G1lkTQcCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGo\n" + "swxe5IF+yB6fWFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0\n" + "yv+K0iUBAgMBAAECggEAB2lJgMOvMrLiTyoHkEY/Lj4wNNcNW8g0aQRWlmng7SdE\n" + "84mh1QEspJegaUe7eyNTsTY8TNwzET43jEFQmWCCVliMNmSHWWWF99sgmuL5W5aN\n" + "Ec+4LPnCWDR+pxAKcCEoN4yzhfLTKNzmsqCg0Ih5mKcN3ojZMLodpCfH3WczDkay\n" + "1KrJvIKIovIV8Yv8NJ4K9hDx7a+xGpHXqh5NLey9K6XNpDqbWwN97FygLLEoedhx\n" + "XDjPzBiGJuwIYQI/V2gYVXn3nPcglDFqHpZA8peX3+DwXojqDwzbNhf2CANpMgaD\n" + "5OQkGymb+FumKPh1UMEL+IfqtXWwwg2Ix7DtI6i/IQKBgQDasFuUC43F0NM61TFQ\n" + "hWuH4mRMp8B4YkhO9M+0/mitHI76cnU63EnJpMYvnp3CLRpVzs/sidEcbfC9gOma\n" + "Ui+xxtKers+RQDI4lrwgqyYIzNNpGO7ItlDYs3guTvX5Mvc5x/zIIHhcjhTudzHK\n" + "Utgho8PIXvrrF3fSHdL0QJb1tQKBgQDqwdGImGbF3lxsB8t4T/8fozIBwo2dWp4O\n" + "YgcVyDpq7Nl+J9Yy5fqTw/sIn6Fa6ZpqAzq0zPSoYJzU7l9+5CcpkkdPEulyER3D\n" + "maQaVMUv0cpnXUQSBRNDZJDdbC4eKocxxotyTuLjCVrUwYWuqYWo3aGx42VXWsWm\n" + "n4+9AwDBnQKBgBStumscUJKU9XRJtnkLtKhLsvpAnoWDnZzBr2ZI7DL6UVbDPeyL\n" + "6fpEN21HTVmQFD5q6ORP/9L1Xl888lniTZo816ujkgMFE/qf3jgkltscKx1z+xhF\n" + "jQ2AouuWEdI3jIMNMwzlbRwrXzVRVgbwoHlF1/x5ZraWKIFYyprIBL5FAoGAfXcM\n" + "12YsN0A6QPqBglGu1mfQCCTErv6JTsKRatDSd+cR7ly4HAfRvjuV5Ov7vqzu/A2x\n" + "yINplrvb1el4XEbvr0YgmmBPJ8mCENICZJg9sur6s/eis8bGntQWoGB63WB5VN76\n" + "FCOZGyIay26KVekAKFobWwlfViqLTBwnJCuAsfkCgYAgrbUKCf+BGgz4epv3T3Ps\n" + "kJD2XX1eeQr/bw1IMqY7ZLKCjg7oxM1jjp2xyujY3ZNViaJsnj7ngCIbaOSJNa3t\n" + "YS8GDLTL4efNTmPEVzgxhnJdSeYVgMLDwu66M9EYhkw+0Y1PQsNS1+6SO89ySRzP\n" + "pFbool/8ZDecmB4ZSa03aw==\n" + "-----END PRIVATE KEY-----"; + +std::string cert = "-----BEGIN CERTIFICATE-----\n" + "MIIDdzCCAl+gAwIBAgIUQEtUTmX2HbHwOpDPgFkYx66ei/swDQYJKoZIhvcNAQEL\n" + "BQAwSzELMAkGA1UEBhMCTkwxEjAQBgNVBAcMCUFtc3RlcmRhbTEOMAwGA1UECgwF\n" + "SWNob3IxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMzEwMDIwODI1NTVa\n" + "Fw0yNDEwMDEwODI1NTVaMEsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlBbXN0ZXJk\n" + "YW0xDjAMBgNVBAoMBUljaG9yMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggEi\n" + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIisWpB9FSc/OIYsxznQJgioL2\n" + "aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ+VNc63blCPAI\n" + "iL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFFelbLf7JTW80C\n" + "nNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi1QbWD3G1lkTQ\n" + "cCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGoswxe5IF+yB6f\n" + "WFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0yv+K0iUBAgMB\n" + "AAGjUzBRMB0GA1UdDgQWBBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAfBgNVHSMEGDAW\n" + "gBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQC1QKNwlqrvE5a7M5v151tA/JvIKNBfesIRCg/IG6tB3teSJjAq\n" + "PRsSajMcyvDtlDMkWL093B8Epsc8yt/vN1GgsCfQsMeIWRGvRYM6Qo2WoABaLTuC\n" + "TxH89YrXa9uwUnbesGwKSkpxzj8g6YuzUpLnAAV70LefBn8ZgBJktFVW/ANWwQqD\n" + "KiglduSDGX3NiLByn7nGKejg2dPw6kWpOTVtLsWtgG/5T4vw+INQzPJqy+pUvBeQ\n" + "OrV7cQyq12DJJPxboIRh1KqF8C25NAOnXVtCav985RixHCMaNaXyOXBJeEEASnXg\n" + "pdDrObCgMze03IaHQ1pj3LeMu0OvEMbsRrYW\n" + "-----END CERTIFICATE-----"; + +dm.createServiceManager(); +dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(443))}, {"SslKey", Ichor::make_any(key)}, {"SslCert", Ichor::make_any(cert)}}); + +queue->start(CaptureSigInt); +``` + +This will create a Https-only Host on 127.0.0.1:443. Be mindful that ports below 1000 usually require some sort of root or privileged user to use it. If you want to support Http as well, simply create another Host Service: + +```c++ +dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(8000))}}); +``` + +## Client + +Connecting to a Https webserver is somewhat similar. Using the same certificate from above, we need to tell the client to trust it: + +```c++ +auto queue = std::make_unique(); +auto &dm = queue->createManager(); + +std::string cert = "-----BEGIN CERTIFICATE-----\n" + "MIIDdzCCAl+gAwIBAgIUQEtUTmX2HbHwOpDPgFkYx66ei/swDQYJKoZIhvcNAQEL\n" + "BQAwSzELMAkGA1UEBhMCTkwxEjAQBgNVBAcMCUFtc3RlcmRhbTEOMAwGA1UECgwF\n" + "SWNob3IxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMzEwMDIwODI1NTVa\n" + "Fw0yNDEwMDEwODI1NTVaMEsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlBbXN0ZXJk\n" + "YW0xDjAMBgNVBAoMBUljaG9yMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggEi\n" + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIisWpB9FSc/OIYsxznQJgioL2\n" + "aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ+VNc63blCPAI\n" + "iL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFFelbLf7JTW80C\n" + "nNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi1QbWD3G1lkTQ\n" + "cCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGoswxe5IF+yB6f\n" + "WFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0yv+K0iUBAgMB\n" + "AAGjUzBRMB0GA1UdDgQWBBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAfBgNVHSMEGDAW\n" + "gBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQC1QKNwlqrvE5a7M5v151tA/JvIKNBfesIRCg/IG6tB3teSJjAq\n" + "PRsSajMcyvDtlDMkWL093B8Epsc8yt/vN1GgsCfQsMeIWRGvRYM6Qo2WoABaLTuC\n" + "TxH89YrXa9uwUnbesGwKSkpxzj8g6YuzUpLnAAV70LefBn8ZgBJktFVW/ANWwQqD\n" + "KiglduSDGX3NiLByn7nGKejg2dPw6kWpOTVtLsWtgG/5T4vw+INQzPJqy+pUvBeQ\n" + "OrV7cQyq12DJJPxboIRh1KqF8C25NAOnXVtCav985RixHCMaNaXyOXBJeEEASnXg\n" + "pdDrObCgMze03IaHQ1pj3LeMu0OvEMbsRrYW\n" + "-----END CERTIFICATE-----"; + +dm.createServiceManager(); +dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(443))}, {"ConnectOverSsl", Ichor::make_any(true)}, {"RootCA", Ichor::make_any(cert)}}); + +queue->start(CaptureSigInt); +``` + +And you should be good to go. diff --git a/docs/07-SharingState.md b/docs/07-SharingState.md new file mode 100644 index 00000000..c5bdbd05 --- /dev/null +++ b/docs/07-SharingState.md @@ -0,0 +1,57 @@ +# Sharing State + +Programs would not be worth much unless they also modify some state. However, shared state also means having to think about access patterns. There are a couple of factors that impact the decision on how to protect the state: + +* Concurrency +* Interrupts +* Coroutines + +As Ichor adds coroutines to the mix, we might encounter situations where the standard approaches are not enough. Let's go over them: + +## Existing approaches in today's programs + +### Single Threaded, no coroutines/interrupts + +If your program only contains a single thread and you have shared state, but are not using coroutines and interrupts, then you're off easy. No protection is needed. + +### Multi Threaded, no coroutines/interrupts + +The other common pattern out there in programs today is where there are multiple threads accessing the same data. This is usually solved using mutexes or thread-safe data structures or using concurrent queues. + +## What patterns Ichor offers + +### Bog-standard `std::mutex` + +It is definitely possible to use a `std::mutex` in combination with Ichor. If the mutex is just guarding some data without needing to do I/O, this will work fine and is unlikely to cause bottlenecks. However, if your code is waiting on I/O to complete, the entire thread is blocked until the mutex stops blocking. This can result in + +### Ownership per thread combined with queues, a.k.a. Rust's Channels + +Ichor prefers this approach, where data is not shared between threads (optionally shared within the same thread). If another thread needs to read of write to data, an event should be placed into the other thread's Ichor::Queue and resolved there. That way, the only point where concurrent reads/writes occur are in Ichor's event queue, which conveniently is thread-safe. + +However, what happens if one needs state, residing in the same thread, to be consistent accross coroutines? For that, Ichor offers an `AsyncSingleThreadedMutex`: + +```c++ +struct MySharedData { + // your important data + int completed_io_operations{}; +}; +struct IMyService { + virtual ~IMyService() = default; + virtual Task my_async_call() = 0; +}; +class MyService final : public IMyService { +public: + Task my_async_call() final { + AsyncSingleThreadedLockGuard lg = co_await _m.lock(); // unliked std::mutex, this call does not block the thread. + // ... co_await some I/O while still holding the lock ... + _data.completed_io_operations++; + co_return {}; + // async mutex unlocks when lg goes out of scope + } +private: + AsyncSingleThreadedMutex _m; + MySharedData _data; +}; +``` + +The benefit of the `AsyncSingleThreadedMutex` is that unlike `std::mutex`, it does not block the thread. Instead, whenever the `AsyncSingleThreadedMutex` is locked, it queues up the function wanting to lock it and continues running that function only when the mutex is unlocked. diff --git a/docs/08-AsyncIO.md b/docs/08-AsyncIO.md new file mode 100644 index 00000000..4e5647f4 --- /dev/null +++ b/docs/08-AsyncIO.md @@ -0,0 +1,74 @@ +# Async I/O + +Although early access, Ichor offers a couple of async file I/O operations on Linux, Windows and OS X: + +* Reading files +* Copying files +* Removing files and +* Writing files + +These operations can take a relatively long time. As a quick reminder, [these are the latency numbers everyone should know](https://gist.github.com/jboner/2841832). +The abstraction Ichor provides allows you to queue up file I/O on another thread without blocking the current. + +## Example + +Here's an example showcasing reading and writing a file: + +```c++ +auto queue = std::make_unique(); +auto &dm = queue->createManager(); +uint64_t ioSvcId{}; + +// set up and run the Ichor Thread +std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); +}); + +// Create a file the "old" way to ensure it exists for this example +{ + std::ofstream out("AsyncFileIO.txt"); + out << "This is a test"; + out.close(); +} + +// Simulate running an important task manually for this example. Normally one would use dependency injection. +queue->pushEvent(0, [&]() -> AsyncGenerator { + // get a handle to the AsyncFileIO implementation + auto async_io_svc = dm.getService(ioSvcId); + + // enqueue reading the file on another thread and co_await its result + // not using auto to show the type in example. Using auto would be a lot easier here. + tl::expected ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + + if(!ret || ret != "This is a test") { + fmt::print("Couldn't read file\n"); + co_return {}; + } + + // enqueue writing to file (automatically overwrites if already exists) + tl::expected ret2 = co_await async_io_svc->first->write_file("AsyncFileIO.txt", "Overwrite"); + + if(!ret2) { + fmt::print("Couldn't write file\n"); + co_return {}; + } + + ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + + if(!ret || ret != "Overwrite") { + fmt::print("Couldn't read file\n"); + co_return {}; + } + + // Quit Ichor thread + queue->pushEvent(0); + co_return {}; +}); + +t.join(); +``` + +## Possible different implementations + +Ichor currently provides only one type of implementation due to time constraints: one I/O thread shared over all Ichor threads. Obviously, this won't fit all use cases. Creating a new implementation is possible that, for example, uses multiple I/O threads and schedules requests round robin. As long as the implementation adheres to the `IAsyncFileIO` interface, it is possible to swap. \ No newline at end of file diff --git a/examples/realtime_example/GlobalRealtimeSettings.cpp b/examples/realtime_example/GlobalRealtimeSettings.cpp index 67413dd4..8db6a2c2 100644 --- a/examples/realtime_example/GlobalRealtimeSettings.cpp +++ b/examples/realtime_example/GlobalRealtimeSettings.cpp @@ -22,10 +22,12 @@ GlobalRealtimeSettings::GlobalRealtimeSettings() { int target_latency = 0; dma_latency_file = open("/dev/cpu_dma_latency", O_RDWR); if(dma_latency_file == -1) { + fmt::print("dma_latency_file\n"); std::terminate(); } auto ret = write(dma_latency_file, &target_latency, 4); if(ret == -1) { + fmt::print("dma_latency_file write\n"); std::terminate(); } } @@ -33,6 +35,7 @@ GlobalRealtimeSettings::GlobalRealtimeSettings() { // tell linux not to swap out the memory used by this process int rc = mlockall(MCL_CURRENT | MCL_FUTURE); if (rc != 0) { + fmt::print("mlockall {} {}\n", rc, errno); std::terminate(); } @@ -42,31 +45,40 @@ GlobalRealtimeSettings::GlobalRealtimeSettings() { std::array control_read{}; smt_file = open("/sys/devices/system/cpu/smt/control", O_RDWR); if(smt_file == -1) { - std::terminate(); + fmt::print("Could not disable SMT\n"); + reenable_smt = false; + close(smt_file); + goto end_smt; } auto size = read(smt_file, control_read.data(), 4); if(size == -1) { + fmt::print("smt_file read\n"); std::terminate(); } if(lseek(smt_file, 0, SEEK_SET) == -1) { + fmt::print("smt_file lseek\n"); std::terminate(); } - if(strncmp(control_read.data(), "off", 3) == 0) { + if(strncmp(control_read.data(), "off", 3) == 0 || strncmp(control_read.data(), "noti", 4) == 0) { reenable_smt = false; close(smt_file); } else { auto ret = write(smt_file, target_control, 3); if (ret == -1) { + fmt::print("smt_file write\n"); std::terminate(); } } + } else { + fmt::print("SMT missing\n"); } + end_smt: if(setpriority(PRIO_PROCESS, 0, -20) != 0) { - std::terminate(); + fmt::print("setpriority failed\n"); } #endif } @@ -77,6 +89,7 @@ GlobalRealtimeSettings::~GlobalRealtimeSettings() { #else int rc = munlockall(); if (rc != 0) { + fmt::print("munlockall\n"); std::terminate(); } @@ -84,6 +97,7 @@ GlobalRealtimeSettings::~GlobalRealtimeSettings() { const char *target_control = "on"; auto ret = write(smt_file, target_control, 3); if (ret == -1) { + fmt::print("reenable_smt write\n"); std::terminate(); } close(smt_file); diff --git a/examples/realtime_example/TestService.h b/examples/realtime_example/TestService.h index f74b1b52..acb6a1b6 100644 --- a/examples/realtime_example/TestService.h +++ b/examples/realtime_example/TestService.h @@ -11,7 +11,11 @@ using namespace Ichor; #define ITERATIONS 20'000 +#ifdef ICHOR_AARCH64 +#define MAXIMUM_DURATION_USEC 1500 +#else #define MAXIMUM_DURATION_USEC 500 +#endif struct ExecuteTaskEvent final : public Event { ExecuteTaskEvent(uint64_t _id, uint64_t _originatingService, uint64_t _priority) noexcept : Event(TYPE, NAME, _id, _originatingService, _priority) {} diff --git a/examples/realtime_example/main.cpp b/examples/realtime_example/main.cpp index 1830a061..6db67c72 100644 --- a/examples/realtime_example/main.cpp +++ b/examples/realtime_example/main.cpp @@ -17,7 +17,6 @@ using namespace std::string_literals; -std::atomic idCounter = 0; std::string_view progName; void* run_example(void*) { @@ -63,7 +62,8 @@ int main(int argc, char *argv[]) { uid_t euid = geteuid(); if (uid !=0 || uid!=euid) { - throw std::runtime_error("No permissions to set realtime scheduling. Consider running under sudo/root."); + fmt::print("This example requires running under sudo/root."); + throw std::runtime_error("This example requires running under sudo/root."); } #endif @@ -72,6 +72,7 @@ int main(int argc, char *argv[]) { #if (defined(WIN32) || defined(_WIN32) || defined(__WIN32) || defined(__APPLE__)) && !defined(__CYGWIN__) //TODO realtime settings run_example(nullptr); + int ret = 0; #else // create a thread with realtime priority to run the program on pthread_t thread{}; @@ -85,7 +86,8 @@ int main(int argc, char *argv[]) { auto ret = pthread_create (&thread, &attr, run_example, nullptr); if(ret == EPERM) { - throw std::runtime_error("No permissions to set realtime scheduling. Consider running under sudo/root."); + fmt::print("No permissions to set realtime thread scheduling. Consider running under sudo/root."); + throw std::runtime_error("No permissions to set realtime thread scheduling. Consider running under sudo/root."); } if(ret == 0) { @@ -93,5 +95,5 @@ int main(int argc, char *argv[]) { } #endif - return 0; + return ret == 0 ? 0 : 1; } diff --git a/include/ichor/Common.h b/include/ichor/Common.h index d0682c1f..e88c148a 100644 --- a/include/ichor/Common.h +++ b/include/ichor/Common.h @@ -26,6 +26,12 @@ static constexpr bool DO_INTERNAL_COROUTINE_DEBUG = true; static constexpr bool DO_INTERNAL_COROUTINE_DEBUG = false; #endif +#ifdef ICHOR_ENABLE_INTERNAL_IO_DEBUGGING +static constexpr bool DO_INTERNAL_IO_DEBUG = true; +#else +static constexpr bool DO_INTERNAL_IO_DEBUG = false; +#endif + #ifdef ICHOR_USE_HARDENING static constexpr bool DO_HARDENING = true; #else @@ -50,6 +56,15 @@ static_assert(true, "") } \ static_assert(true, "") +#define INTERNAL_IO_DEBUG(...) \ + if constexpr(DO_INTERNAL_IO_DEBUG) { \ + fmt::print("[{:L}] ", std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); \ + fmt::print("[{}:{}] ", __FILE__, __LINE__); \ + fmt::print(__VA_ARGS__); \ + fmt::print("\n"); \ + } \ +static_assert(true, "") + // GNU C Library contains defines in sys/sysmacros.h. However, for compatibility reasons, this header is included in sys/types.h. Which is used by std. #undef major #undef minor diff --git a/include/ichor/ConstevalHash.h b/include/ichor/ConstevalHash.h index 88c134c8..d7141269 100644 --- a/include/ichor/ConstevalHash.h +++ b/include/ichor/ConstevalHash.h @@ -5,9 +5,12 @@ // Adapted from wyhash v3 + +#if (!defined(WIN32) && !defined(_WIN32) && !defined(__WIN32)) || defined(__CYGWIN__) #pragma GCC diagnostic push // If SMHasher, BigCrush and practrand are not finding quality issues, then the compiler won't either. So let's disable this warning. #pragma GCC diagnostic ignored "-Wconversion" +#endif const uint64_t _wyp0 = 0xa0761d6478bd642full, _wyp1 = 0xe7037ed1a0b428dbull, _wyp2 = 0x8ebc6af09c88c6e3ull, _wyp3 = 0x589965cc75374cc3ull, _wyp4 = 0x1d8e4e27c47d124full; static consteval uint64_t consteval_wyrotr(uint64_t v, unsigned k) { return (v >> k) | (v << (64 - k)); } @@ -142,4 +145,6 @@ namespace Ichor { } } +#if (!defined(WIN32) && !defined(_WIN32) && !defined(__WIN32)) || defined(__CYGWIN__) #pragma GCC diagnostic pop +#endif diff --git a/include/ichor/ScopeGuard.h b/include/ichor/ScopeGuard.h new file mode 100644 index 00000000..615ecc82 --- /dev/null +++ b/include/ichor/ScopeGuard.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include + +namespace Ichor { + + template + class [[nodiscard]] ScopeGuard final + { + public: + ScopeGuard(ScopeGuard&& o) noexcept(std::is_nothrow_constructible_v) : _callback(std::forward(o._callback)), _active(o._active) { + o._active = false; + } + explicit ScopeGuard(Callback&& callback) noexcept(std::is_nothrow_constructible_v) : _callback(std::forward(callback)), _active(true) { + + } + + ~ScopeGuard() noexcept { + if(_active) { + _callback(); + } + } + + ScopeGuard() = delete; + ScopeGuard(const ScopeGuard&) = delete; + ScopeGuard& operator=(const ScopeGuard&) = delete; + ScopeGuard& operator=(ScopeGuard&&) = delete; + + private: + Callback _callback; + bool _active; + }; +} diff --git a/include/ichor/coroutines/AsyncGeneratorPromiseBase.h b/include/ichor/coroutines/AsyncGeneratorPromiseBase.h index ef794e8b..b76ba1cf 100644 --- a/include/ichor/coroutines/AsyncGeneratorPromiseBase.h +++ b/include/ichor/coroutines/AsyncGeneratorPromiseBase.h @@ -186,6 +186,7 @@ namespace Ichor::Detail { void return_value(value_type &&value) noexcept(std::is_nothrow_constructible_v); T& value() noexcept { + INTERNAL_COROUTINE_DEBUG("request value {}, {}", _id, _currentValue.has_value()); return _currentValue.value(); } diff --git a/include/ichor/coroutines/Task.h b/include/ichor/coroutines/Task.h index 8942a2ce..b4df5ce3 100644 --- a/include/ichor/coroutines/Task.h +++ b/include/ichor/coroutines/Task.h @@ -11,7 +11,7 @@ #include #include #include - +#include namespace Ichor { diff --git a/include/ichor/dependency_management/ConstructorInjectionService.h b/include/ichor/dependency_management/ConstructorInjectionService.h index 7040f02d..bed0a6cd 100644 --- a/include/ichor/dependency_management/ConstructorInjectionService.h +++ b/include/ichor/dependency_management/ConstructorInjectionService.h @@ -167,7 +167,7 @@ namespace Ichor { void createServiceSpecialSauce(std::optional> = {}) { try { new (buf) T(std::get(_deps[typeNameHash>()])...); - } catch (std::exception const &e) { + } catch (std::exception const &) { std::terminate(); } } diff --git a/include/ichor/event_queues/IEventQueue.h b/include/ichor/event_queues/IEventQueue.h index 2a167679..932a0778 100644 --- a/include/ichor/event_queues/IEventQueue.h +++ b/include/ichor/event_queues/IEventQueue.h @@ -15,6 +15,7 @@ namespace Ichor { [[nodiscard]] virtual bool empty() const = 0; [[nodiscard]] virtual uint64_t size() const = 0; + [[nodiscard]] virtual bool is_running() const noexcept = 0; /// Starts the event loop, consumes the current thread until a QuitEvent occurs /// \param captureSigInt If true, exit on CTRL+C/SigInt diff --git a/include/ichor/event_queues/MultimapQueue.h b/include/ichor/event_queues/MultimapQueue.h index 84239da3..07ce7b5f 100644 --- a/include/ichor/event_queues/MultimapQueue.h +++ b/include/ichor/event_queues/MultimapQueue.h @@ -26,6 +26,7 @@ namespace Ichor { [[nodiscard]] bool empty() const noexcept final; [[nodiscard]] uint64_t size() const noexcept final; + [[nodiscard]] bool is_running() const noexcept final; void start(bool captureSigInt) final; [[nodiscard]] bool shouldQuit() final; diff --git a/include/ichor/event_queues/SdeventQueue.h b/include/ichor/event_queues/SdeventQueue.h index 13242307..22a36a12 100644 --- a/include/ichor/event_queues/SdeventQueue.h +++ b/include/ichor/event_queues/SdeventQueue.h @@ -26,6 +26,7 @@ namespace Ichor { [[nodiscard]] bool empty() const final; [[nodiscard]] uint64_t size() const final; + [[nodiscard]] bool is_running() const noexcept final; [[nodiscard]] sd_event* createEventLoop(); void useEventLoop(sd_event *loop); diff --git a/include/ichor/services/io/IAsyncFileIO.h b/include/ichor/services/io/IAsyncFileIO.h new file mode 100644 index 00000000..f4aa12a7 --- /dev/null +++ b/include/ichor/services/io/IAsyncFileIO.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace Ichor { + enum class FileIOError { + FAILED, + FILE_DOES_NOT_EXIST, + NO_PERMISSION, + IS_DIR_SHOULD_BE_FILE + }; + + class IAsyncFileIO { + public: + + /* + * Reads the entire contents of a file into a std::string. + */ + virtual Task> read_whole_file(std::filesystem::path const &file) = 0; + + /* + * Copies the contents of one file to another. This function will also copy the permission bits of the original file to the destination file. + * This function will overwrite the contents of to. + * Note that if from and to both point to the same file, then the file will likely get truncated by this operation. + */ + virtual Task> copy_file(std::filesystem::path const &from, std::filesystem::path const &to) = 0; + + /* + * Removes a file from the filesystem. + * Note that there is no guarantee that the file is immediately deleted (e.g., depending on platform, other open file descriptors may prevent immediate removal). + */ + virtual Task> remove_file(std::filesystem::path const &file) = 0; + + /* + * Write a string as the entire contents of a file. + * This function will create a file if it does not exist, and will entirely replace its contents if it does. + * Depending on the platform, this function may fail if the full directory path does not exist. + */ + virtual Task> write_file(std::filesystem::path const &file, std::string_view contents) = 0; + + protected: + ~IAsyncFileIO() = default; + }; +} diff --git a/include/ichor/services/io/SharedOverThreadsAsyncFileIO.h b/include/ichor/services/io/SharedOverThreadsAsyncFileIO.h new file mode 100644 index 00000000..c4306779 --- /dev/null +++ b/include/ichor/services/io/SharedOverThreadsAsyncFileIO.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace Ichor { + struct io_operation_submission final { + Ichor::AsyncManualResetEvent evt; + tl::expected result; + std::function fn; + }; + + class SharedOverThreadsAsyncFileIO final : public IAsyncFileIO, public AdvancedService { + public: + SharedOverThreadsAsyncFileIO(Properties props); + + Task> read_whole_file(std::filesystem::path const &file) final; + Task> copy_file(std::filesystem::path const &from, std::filesystem::path const &to) final; + Task> remove_file(std::filesystem::path const &file) final; + Task> write_file(std::filesystem::path const &file, std::string_view contents) final; + + private: + Task> start() final; + Task stop() final; + + static std::atomic _initialized; + static std::mutex _io_mutex; + static std::queue> _evts; + bool _should_stop{}; + std::optional _io_thread; + }; +} diff --git a/include/ichor/services/network/http/HttpConnectionService.h b/include/ichor/services/network/http/HttpConnectionService.h index 8edeb710..9f40aca8 100644 --- a/include/ichor/services/network/http/HttpConnectionService.h +++ b/include/ichor/services/network/http/HttpConnectionService.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -55,11 +56,14 @@ namespace Ichor { friend DependencyRegister; std::unique_ptr _httpStream{}; + std::unique_ptr> _sslStream{}; + std::unique_ptr _sslContext{}; std::atomic _priority{INTERNAL_EVENT_PRIORITY}; std::atomic _quit{}; std::atomic _connecting{}; std::atomic _connected{}; std::atomic _tcpNoDelay{}; + std::atomic _useSsl{}; std::atomic _finishedListenAndRead{}; std::atomic _logger{}; IAsioContextService *_asioContextService{}; diff --git a/include/ichor/services/network/http/HttpHostService.h b/include/ichor/services/network/http/HttpHostService.h index 90cfde38..32d7523d 100644 --- a/include/ichor/services/network/http/HttpHostService.h +++ b/include/ichor/services/network/http/HttpHostService.h @@ -8,20 +8,25 @@ #include #include #include +#include #include #include namespace beast = boost::beast; // from namespace http = beast::http; // from namespace net = boost::asio; // from +namespace ssl = boost::asio::ssl; // from using tcp = boost::asio::ip::tcp; // from namespace Ichor { namespace Detail { using HostOutboxMessage = http::response, http::basic_fields>>; + + template struct Connection { Connection(tcp::socket &&_socket) : socket(std::move(_socket)) {} - beast::tcp_stream socket; + Connection(tcp::socket &&_socket, net::ssl::context &ctx) : socket(std::move(_socket), ctx) {} + SocketT socket; boost::circular_buffer outbox{10}; RealtimeMutex mutex{}; }; @@ -49,19 +54,26 @@ namespace Ichor { void fail(beast::error_code, char const* what, bool stopSelf); void listen(tcp::endpoint endpoint, net::yield_context yield); + + template void read(tcp::socket socket, net::yield_context yield); - void sendInternal(std::shared_ptr &connection, http::response, http::basic_fields>> &&res); + + template + void sendInternal(std::shared_ptr> &connection, http::response, http::basic_fields>> &&res); friend DependencyRegister; std::unique_ptr _httpAcceptor{}; - unordered_map> _httpStreams{}; + unordered_map>> _httpStreams{}; + unordered_map>>> _sslStreams{}; + std::unique_ptr _sslContext{}; RealtimeMutex _streamsMutex{}; std::atomic _priority{INTERNAL_EVENT_PRIORITY}; std::atomic _quit{}; std::atomic _goingToCleanupStream{}; std::atomic _finishedListenAndRead{}; std::atomic _tcpNoDelay{}; + std::atomic _useSsl{}; uint64_t _streamIdCounter{}; bool _sendServerHeader{true}; std::atomic _logger{}; diff --git a/include/ichor/services/network/http/HttpScopeGuards.h b/include/ichor/services/network/http/HttpScopeGuards.h index 92d699a0..e4d3d157 100644 --- a/include/ichor/services/network/http/HttpScopeGuards.h +++ b/include/ichor/services/network/http/HttpScopeGuards.h @@ -13,20 +13,13 @@ namespace Ichor { ~ScopeGuardAtomicCount() noexcept { _var.fetch_sub(1, std::memory_order_acq_rel); } - private: - std::atomic &_var; - }; - - template - class ScopeGuardFunction final { - public: - explicit ScopeGuardFunction(T &&fn) noexcept : _fn(std::forward(fn)) { - } - ~ScopeGuardFunction() noexcept { - _fn(); - } + ScopeGuardAtomicCount() = delete; + ScopeGuardAtomicCount(const ScopeGuardAtomicCount &) = delete; + ScopeGuardAtomicCount(ScopeGuardAtomicCount &&) = delete; + ScopeGuardAtomicCount& operator=(const ScopeGuardAtomicCount&) = delete; + ScopeGuardAtomicCount& operator=(ScopeGuardAtomicCount&&) = delete; private: - T _fn; + std::atomic &_var; }; } diff --git a/include/ichor/stl/AsyncSingleThreadedMutex.h b/include/ichor/stl/AsyncSingleThreadedMutex.h new file mode 100644 index 00000000..ec87761d --- /dev/null +++ b/include/ichor/stl/AsyncSingleThreadedMutex.h @@ -0,0 +1,98 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +// Mutex that prevents access between coroutines on the same thread + +namespace Ichor { + class AsyncSingleThreadedMutex; + + class AsyncSingleThreadedLockGuard final { + explicit AsyncSingleThreadedLockGuard(AsyncSingleThreadedMutex &m) : _m{&m} { + } + public: + AsyncSingleThreadedLockGuard(const AsyncSingleThreadedLockGuard &) = delete; + AsyncSingleThreadedLockGuard(AsyncSingleThreadedLockGuard &&o) noexcept : _m{o._m}, _has_lock(o._has_lock) { + o._has_lock = false; + } + AsyncSingleThreadedLockGuard& operator=(const AsyncSingleThreadedLockGuard &o) = delete; + AsyncSingleThreadedLockGuard& operator=(AsyncSingleThreadedLockGuard &&o) noexcept { + _m = o._m; + _has_lock = o._has_lock; + o._has_lock = false; + return *this; + } + ~AsyncSingleThreadedLockGuard(); + + void unlock(); + private: + AsyncSingleThreadedMutex *_m; + bool _has_lock{true}; + friend class AsyncSingleThreadedMutex; + }; + + class AsyncSingleThreadedMutex final { + public: + Task lock() { +#if defined(ICHOR_USE_HARDENING) || defined(ICHOR_ENABLE_INTERNAL_DEBUGGING) + if(!_thread_id) { + _thread_id = std::this_thread::get_id(); + } else if (_thread_id != std::this_thread::get_id()) { + std::terminate(); + } +#endif + if(_locked) { + _evts.push(std::make_unique()); + auto *evt = _evts.front().get(); + co_await *evt; + } + + _locked = true; + co_return AsyncSingleThreadedLockGuard{*this}; + } + + std::optional non_blocking_lock() { +#if defined(ICHOR_USE_HARDENING) || defined(ICHOR_ENABLE_INTERNAL_DEBUGGING) + if(!_thread_id) { + _thread_id = std::this_thread::get_id(); + } else if (_thread_id != std::this_thread::get_id()) { + std::terminate(); + } +#endif + + if(_locked) { + return std::nullopt; + } + + _locked = true; + return AsyncSingleThreadedLockGuard{*this}; + } + + void unlock() { +#if defined(ICHOR_USE_HARDENING) || defined(ICHOR_ENABLE_INTERNAL_DEBUGGING) + if (_thread_id && _thread_id != std::this_thread::get_id()) { + std::terminate(); + } +#endif + if(_evts.empty()) { + _locked = false; + return; + } + + auto evt = std::move(_evts.front()); + _evts.pop(); + evt->set(); + } + private: + bool _locked{}; + std::queue> _evts{}; +#if defined(ICHOR_USE_HARDENING) || defined(ICHOR_ENABLE_INTERNAL_DEBUGGING) + std::optional _thread_id; +#endif + }; +} diff --git a/src/ichor/event_queues/MultimapQueue.cpp b/src/ichor/event_queues/MultimapQueue.cpp index 71c8c7ed..c6eb8eef 100644 --- a/src/ichor/event_queues/MultimapQueue.cpp +++ b/src/ichor/event_queues/MultimapQueue.cpp @@ -54,6 +54,10 @@ namespace Ichor { return static_cast(_eventQueue.size()); } + bool MultimapQueue::is_running() const noexcept { + return !_quit.load(std::memory_order_acquire); + } + void MultimapQueue::start(bool captureSigInt) { if(!_dm) [[unlikely]] { throw std::runtime_error("Please create a manager first!"); diff --git a/src/ichor/event_queues/SdeventQueue.cpp b/src/ichor/event_queues/SdeventQueue.cpp index a4aaaedd..585484b5 100644 --- a/src/ichor/event_queues/SdeventQueue.cpp +++ b/src/ichor/event_queues/SdeventQueue.cpp @@ -139,6 +139,10 @@ namespace Ichor { return state == SD_EVENT_INITIAL || state == SD_EVENT_ARMED || state == SD_EVENT_FINISHED ? 0 : 1; } + bool SdeventQueue::is_running() const noexcept { + return !_quit.load(std::memory_order_acquire); + } + [[nodiscard]] sd_event* SdeventQueue::createEventLoop() { auto ret = sd_event_default(&_eventQueue); diff --git a/src/ichor/stl/AsyncSingleThreadedMutex.cpp b/src/ichor/stl/AsyncSingleThreadedMutex.cpp new file mode 100644 index 00000000..e528d53c --- /dev/null +++ b/src/ichor/stl/AsyncSingleThreadedMutex.cpp @@ -0,0 +1,14 @@ +#include + +Ichor::AsyncSingleThreadedLockGuard::~AsyncSingleThreadedLockGuard() { + if(_has_lock) { + _m->unlock(); + } +} + +void Ichor::AsyncSingleThreadedLockGuard::unlock() { + if(_has_lock) { + _has_lock = false; + _m->unlock(); + } +} diff --git a/src/services/io/SharedOverThreadsAsyncFileIO.cpp b/src/services/io/SharedOverThreadsAsyncFileIO.cpp new file mode 100644 index 00000000..5bf97cd4 --- /dev/null +++ b/src/services/io/SharedOverThreadsAsyncFileIO.cpp @@ -0,0 +1,326 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32)) && !defined(__CYGWIN__) +#include +#include +#elif defined(__APPLE__) +#include +#else +#include +#include +#include +#endif + +using namespace std::chrono_literals; + +std::atomic Ichor::SharedOverThreadsAsyncFileIO::_initialized; +std::mutex Ichor::SharedOverThreadsAsyncFileIO::_io_mutex; +std::queue> Ichor::SharedOverThreadsAsyncFileIO::_evts; + +Ichor::SharedOverThreadsAsyncFileIO::SharedOverThreadsAsyncFileIO(Ichor::Properties props) : AdvancedService(std::move(props)) { + +} + +Ichor::Task> Ichor::SharedOverThreadsAsyncFileIO::start() { + INTERNAL_IO_DEBUG("setup_thread"); + if (!_initialized.exchange(true, std::memory_order_acq_rel)) { + auto &queue = Ichor::GetThreadLocalEventQueue(); + + _io_thread = std::thread([this, &queue = queue]() { + INTERNAL_IO_DEBUG("io_thread"); + while(!_should_stop) { + INTERNAL_IO_DEBUG("checking"); + { + std::unique_lock lg{_io_mutex}; + while (!_evts.empty()) { + INTERNAL_IO_DEBUG("processing submission"); + auto submission = std::move(_evts.front()); + _evts.pop(); + lg.unlock(); + submission->fn(submission->result); + queue.pushEvent(0, [submission = std::move(submission)]() mutable { + submission->evt.set(); + }); + } + } + std::this_thread::sleep_for(10ms); + INTERNAL_IO_DEBUG("checking post-sleep"); + } + INTERNAL_IO_DEBUG("io_thread done"); + }); + } + co_return {}; +} + +Ichor::Task Ichor::SharedOverThreadsAsyncFileIO::stop() { + if(_io_thread && _io_thread->joinable()) { + _should_stop = true; + _io_thread->join(); + _initialized.store(false, std::memory_order_release); + } + co_return; +} + +Ichor::Task> Ichor::SharedOverThreadsAsyncFileIO::read_whole_file(std::filesystem::path const &file_path) { + INTERNAL_IO_DEBUG("read_whole_file()"); + + auto submission = std::make_shared(); + std::string contents; + submission->fn = [&file_path, &contents](decltype(io_operation_submission::result) &res) { + INTERNAL_IO_DEBUG("submission->fn()"); + std::ifstream file(file_path); + + if(!file) { + if(!std::filesystem::exists(file_path)) { + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + INTERNAL_IO_DEBUG("!file"); + res = tl::unexpected(FileIOError::FAILED); + return; + } + + file.seekg(0, std::ios::end); + contents.resize(static_cast(file.tellg())); + file.seekg(0, std::ios::beg); + file.read(contents.data(), static_cast(contents.size())); + file.close(); + + INTERNAL_IO_DEBUG("submission->fn() contents: {}", contents); + }; + { + std::unique_lock lg{_io_mutex}; + _evts.push(submission); + } + INTERNAL_IO_DEBUG("co_await"); + co_await submission->evt; + + if(!submission->result) { + INTERNAL_IO_DEBUG("!result"); + co_return tl::unexpected(submission->result.error()); + } + INTERNAL_IO_DEBUG("contents: {}", contents); + + co_return contents; +} + +Ichor::Task> Ichor::SharedOverThreadsAsyncFileIO::copy_file(const std::filesystem::path &from, const std::filesystem::path &to) { + INTERNAL_IO_DEBUG("copy_file()"); + + auto submission = std::make_shared(); + submission->fn = [&from, &to](decltype(io_operation_submission::result) &res) { + INTERNAL_IO_DEBUG("submission->fn()"); +#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32)) && !defined(__CYGWIN__) + BOOL success = CopyFileExA(from.string().c_str(), to.string().c_str(), nullptr, nullptr, nullptr, 0); + if(success == 0) { + DWORD err = GetLastError(); + INTERNAL_IO_DEBUG("CopyFileExA failed: {}", err); + if(err == ERROR_FILE_NOT_FOUND) { + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + + INTERNAL_IO_DEBUG("CopyFileExA failed: {}", GetLastError()); + res = tl::unexpected(FileIOError::FAILED); + return; + } +#elif defined(__APPLE__) + auto state = copyfile_state_alloc(); + ScopeGuard sg_state([&state]() { + copyfile_state_free(state); + }); + auto ret = copyfile(from.string().c_str(), to.string().c_str(), state, COPYFILE_ALL); + auto err = errno; + if(ret < 0) { + INTERNAL_IO_DEBUG("copyfile ret: {}, errno: {}", ret, err); + if(err == EPERM) { + res = tl::unexpected(FileIOError::NO_PERMISSION); + return; + } + if(err == EISDIR) { + res = tl::unexpected(FileIOError::IS_DIR_SHOULD_BE_FILE); + return; + } + if(err == ENOENT) { + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + res = tl::unexpected(FileIOError::FAILED); + return; + } +#else + struct stat stat; + off64_t len, ret; + int fd_in = open(from.c_str(), O_RDONLY | O_CLOEXEC); + if (fd_in == -1) { + INTERNAL_IO_DEBUG("open from errno {}", errno); + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + ScopeGuard sg_fd_in{[fd_in]() { + close(fd_in); + }}; + + if (fstat(fd_in, &stat) == -1) { + INTERNAL_IO_DEBUG("fstat from errno {}", errno); + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + + len = stat.st_size; + + int fd_out = open(to.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0644); + if (fd_out == -1) { + INTERNAL_IO_DEBUG("open to errno {}", errno); + res = tl::unexpected(FileIOError::FAILED); + return; + } + ScopeGuard sg_fd_out{[fd_out]() { + close(fd_out); + }}; + + do { + ret = copy_file_range(fd_in, nullptr, fd_out, nullptr, static_cast(len), 0); + if (ret == -1) { + int err = errno; + INTERNAL_IO_DEBUG("copy_file_range failed: {}", err); + if(err == EPERM) { + res = tl::unexpected(FileIOError::NO_PERMISSION); + return; + } + if(err == EISDIR) { + res = tl::unexpected(FileIOError::IS_DIR_SHOULD_BE_FILE); + return; + } + res = tl::unexpected(FileIOError::FAILED); + return; + } + + len -= ret; + } while (len > 0 && ret > 0); + + INTERNAL_IO_DEBUG("submission->fn() copied bytes: {}", stat.st_size); +#endif + }; + { + std::unique_lock lg{_io_mutex}; + _evts.push(submission); + } + INTERNAL_IO_DEBUG("co_await"); + co_await submission->evt; + + if(!submission->result) { + INTERNAL_IO_DEBUG("!result"); + co_return tl::unexpected(submission->result.error()); + } + + co_return {}; +} + +Ichor::Task> Ichor::SharedOverThreadsAsyncFileIO::remove_file(const std::filesystem::path &file) { + INTERNAL_IO_DEBUG("remove_file()"); + + auto submission = std::make_shared(); + submission->fn = [&file](decltype(io_operation_submission::result) &res) { +#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32)) && !defined(__CYGWIN__) + BOOL ret = DeleteFile(file.string().c_str()); + + if(ret == 0) { + DWORD err = GetLastError(); + INTERNAL_IO_DEBUG("DeleteFile failed: {}", err); + if(err == ERROR_FILE_NOT_FOUND) { + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + if(err == ERROR_ACCESS_DENIED) { + res = tl::unexpected(FileIOError::NO_PERMISSION); + return; + } + res = tl::unexpected(FileIOError::FAILED); + } +#else + int ret = unlink(file.string().c_str()); + + if(ret != 0) { + int err = errno; + INTERNAL_IO_DEBUG("unlink failed: {}", err); + if(err == EACCES || err == EPERM) { + res = tl::unexpected(FileIOError::NO_PERMISSION); + return; + } + if(err == ENOENT) { + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + if(err == EISDIR) { + res = tl::unexpected(FileIOError::IS_DIR_SHOULD_BE_FILE); + return; + } + res = tl::unexpected(FileIOError::FAILED); + } +#endif + }; + { + std::unique_lock lg{_io_mutex}; + _evts.push(submission); + } + INTERNAL_IO_DEBUG("co_await"); + co_await submission->evt; + + if(!submission->result) { + INTERNAL_IO_DEBUG("!result"); + co_return tl::unexpected(submission->result.error()); + } + + co_return {}; +} + +Ichor::Task> Ichor::SharedOverThreadsAsyncFileIO::write_file(const std::filesystem::path &file, std::string_view contents) { + + INTERNAL_IO_DEBUG("write_file()"); + + auto submission = std::make_shared(); + submission->fn = [&file, contents](decltype(io_operation_submission::result) &res) { + try { + auto out = fmt::output_file(file.string()); + out.print("{}", contents); + } catch (const std::system_error &e) { + INTERNAL_IO_DEBUG("write_file failed: {} {}", e.code().value(), e.code().message()); + int err = e.code().value(); + if(err == EACCES || err == EPERM) { + res = tl::unexpected(FileIOError::NO_PERMISSION); + return; + } + if(err == ENOENT) { + res = tl::unexpected(FileIOError::FILE_DOES_NOT_EXIST); + return; + } + if(err == EISDIR) { + res = tl::unexpected(FileIOError::IS_DIR_SHOULD_BE_FILE); + return; + } + res = tl::unexpected(FileIOError::FAILED); + } + }; + { + std::unique_lock lg{_io_mutex}; + _evts.push(submission); + } + INTERNAL_IO_DEBUG("co_await"); + co_await submission->evt; + + if(!submission->result) { + INTERNAL_IO_DEBUG("!result"); + co_return tl::unexpected(submission->result.error()); + } + + co_return {}; +} diff --git a/src/services/network/http/HttpConnectionService.cpp b/src/services/network/http/HttpConnectionService.cpp index fa8e0822..722bbfff 100644 --- a/src/services/network/http/HttpConnectionService.cpp +++ b/src/services/network/http/HttpConnectionService.cpp @@ -5,6 +5,7 @@ #include #include #include +#include Ichor::HttpConnectionService::HttpConnectionService(DependencyRegister ®, Properties props) : AdvancedService(std::move(props)) { reg.registerDependency(this, true); @@ -14,13 +15,13 @@ Ichor::HttpConnectionService::HttpConnectionService(DependencyRegister ®, Pro Ichor::Task> Ichor::HttpConnectionService::start() { _queue = &GetThreadLocalEventQueue(); - if(!_asioContextService->fibersShouldStop() && !_connecting.load(std::memory_order_acquire) && !_connected.load(std::memory_order_acquire)) { + if (!_asioContextService->fibersShouldStop() && !_connecting.load(std::memory_order_acquire) && !_connected.load(std::memory_order_acquire)) { _quit.store(false, std::memory_order_release); - if (getProperties().contains("Priority")) { + if(getProperties().contains("Priority")) { _priority.store(Ichor::any_cast(getProperties()["Priority"]), std::memory_order_release); } - if (!getProperties().contains("Port") || !getProperties().contains("Address")) { + if(!getProperties().contains("Port") || !getProperties().contains("Address")) { ICHOR_LOG_ERROR_ATOMIC(_logger, "Missing port or address when starting HttpConnectionService"); co_return tl::unexpected(StartError::FAILED); } @@ -30,16 +31,26 @@ Ichor::Task> Ichor::HttpConnectionService: } // fmt::print("connecting to {}\n", Ichor::any_cast(getProperties()["Address"])); - auto address = net::ip::make_address(Ichor::any_cast(getProperties()["Address"])); + boost::system::error_code ec; + auto address = net::ip::make_address(Ichor::any_cast(getProperties()["Address"]), ec); auto port = Ichor::any_cast(getProperties()["Port"]); + if(ec) { + ICHOR_LOG_ERROR_ATOMIC(_logger, "Couldn't parse address \"{}\": {} {}", Ichor::any_cast(getProperties()["Address"]), ec.value(), ec.message()); + co_return tl::unexpected(StartError::FAILED); + } + + if (getProperties().contains("ConnectOverSsl")) { + _useSsl.store(Ichor::any_cast(getProperties()["ConnectOverSsl"]), std::memory_order_release); + } + _connecting.store(true, std::memory_order_release); net::spawn(*_asioContextService->getContext(), [this, address = std::move(address), port](net::yield_context yield) { connect(tcp::endpoint{address, port}, std::move(yield)); }ASIO_SPAWN_COMPLETION_TOKEN); } - if(_asioContextService->fibersShouldStop()) { + if (_asioContextService->fibersShouldStop()) { co_return tl::unexpected(StartError::FAILED); } @@ -119,7 +130,7 @@ Ichor::Task Ichor::HttpConnectionService::sendAsync(Ichor:: auto next = _outbox.front(); lg.unlock(); - ScopeGuardFunction const coroutineGuard{[this, &event, &lg]() { + ScopeGuard const coroutineGuard{[this, &event, &lg]() { // use service id 0 to ensure event gets run, even if service is stopped. Otherwise, the coroutine will never complete. // Similarly, use priority 0 to ensure these events run before any dependency changes, otherwise the service might be destroyed // before we can finish all the coroutines. @@ -150,11 +161,19 @@ Ichor::Task Ichor::HttpConnectionService::sendAsync(Ichor:: req.prepare_payload(); req.keep_alive(); - // Set the timeout for this operation. - // _httpStream should only be modified from the boost thread - _httpStream->expires_after(30s); - INTERNAL_DEBUG("http::write"); - http::async_write(*_httpStream, req, yield[ec]); + if(_useSsl.load(std::memory_order_acquire)) { + // Set the timeout for this operation. + // _sslStream should only be modified from the boost thread + beast::get_lowest_layer(*_sslStream).expires_after(30s); + INTERNAL_DEBUG("http::write"); + http::async_write(*_sslStream, req, yield[ec]); + } else { + // Set the timeout for this operation. + // _httpStream should only be modified from the boost thread + _httpStream->expires_after(30s); + INTERNAL_DEBUG("http::write"); + http::async_write(*_httpStream, req, yield[ec]); + } if (ec) { fail(ec, "HttpConnectionService::sendAsync write"); continue; @@ -172,8 +191,13 @@ Ichor::Task Ichor::HttpConnectionService::sendAsync(Ichor:: http::response, http::basic_fields>> res; INTERNAL_DEBUG("http::read"); + // Receive the HTTP response - http::async_read(*_httpStream, b, res, yield[ec]); + if(_useSsl.load(std::memory_order_acquire)) { + http::async_read(*_sslStream, b, res, yield[ec]); + } else { + http::async_read(*_httpStream, b, res, yield[ec]); + } if (ec) { fail(ec, "HttpConnectionService::sendAsync read"); continue; @@ -185,7 +209,11 @@ Ichor::Task Ichor::HttpConnectionService::sendAsync(Ichor:: INTERNAL_DEBUG("received HTTP response {}", std::string_view(reinterpret_cast(res.body().data()), res.body().size())); // unset the timeout for the next operation. - _httpStream->expires_never(); + if(_useSsl.load(std::memory_order_acquire)) { + beast::get_lowest_layer(*_sslStream).expires_never(); + } else { + _httpStream->expires_never(); + } next.response->status = (HttpStatus) (int) res.result(); next.response->headers.reserve(static_cast(std::distance(std::begin(res), std::end(res)))); @@ -204,14 +232,24 @@ Ichor::Task Ichor::HttpConnectionService::sendAsync(Ichor:: } Ichor::Task Ichor::HttpConnectionService::close() { - if(!_httpStream) { - co_return; + if(_useSsl.load(std::memory_order_acquire)) { + if(!_sslStream) { + co_return; + } + } else { + if(!_httpStream) { + co_return; + } } if(!_connecting.exchange(true, std::memory_order_acq_rel)) { net::spawn(*_asioContextService->getContext(), [this](net::yield_context) { - // _httpStream should only be modified from the boost thread - _httpStream->cancel(); + // _httpStream and _sslStream should only be modified from the boost thread + if(_useSsl.load(std::memory_order_acquire)) { + beast::get_lowest_layer(*_sslStream).cancel(); + } else { + _httpStream->cancel(); + } _queue->pushEvent(getServiceId(), [this]() { _startStopEvent.set(); @@ -229,6 +267,7 @@ Ichor::Task Ichor::HttpConnectionService::close() { _connected.store(false, std::memory_order_release); _connecting.store(false, std::memory_order_release); _httpStream = nullptr; + _sslStream = nullptr; co_return; } @@ -243,32 +282,75 @@ void Ichor::HttpConnectionService::connect(tcp::endpoint endpoint, net::yield_co beast::error_code ec; tcp::resolver resolver(*_asioContextService->getContext()); - // _httpStream should only be modified from the boost thread - _httpStream = std::make_unique(*_asioContextService->getContext()); auto const results = resolver.resolve(endpoint, ec); if(ec) { return fail(ec, "HttpConnectionService::connect resolve"); } - // Never expire until we actually have an operation. - _httpStream->expires_never(); - - // Make the connection on the IP address we get from a lookup - int attempts{}; - while(!_quit.load(std::memory_order_acquire) && !_asioContextService->fibersShouldStop() && attempts < 5) { - _httpStream->async_connect(results, yield[ec]); - if (ec) { - attempts++; - net::steady_timer t{*_asioContextService->getContext()}; - t.expires_after(250ms); - t.async_wait(yield); - } else { - break; + if (_useSsl.load(std::memory_order_acquire)) { + // _sslContext and _sslStream should only be modified from the boost thread + _sslContext = std::make_unique(net::ssl::context::tlsv12); + _sslContext->set_verify_mode(net::ssl::verify_peer); + + if(getProperties().contains("RootCA")) { + std::string &ca = Ichor::any_cast(getProperties()["RootCA"]); + _sslContext->add_certificate_authority(boost::asio::const_buffer(ca.c_str(), ca.size()), ec); } - } - _httpStream->socket().set_option(tcp::no_delay(_tcpNoDelay)); + _sslStream = std::make_unique>(*_asioContextService->getContext(), *_sslContext); + + if(!SSL_set_tlsext_host_name(_sslStream->native_handle(), endpoint.address().to_string().c_str())) { + ec.assign(static_cast(::ERR_get_error()), net::error::get_ssl_category()); + return fail(ec, "HttpConnectionService::connect ssl set host name"); + } + + // Never expire until we actually have an operation. + beast::get_lowest_layer(*_sslStream).expires_never(); + + // Make the connection on the IP address we get from a lookup + int attempts{}; + while(!_quit.load(std::memory_order_acquire) && !_asioContextService->fibersShouldStop() && attempts < 5) { + beast::get_lowest_layer(*_sslStream).async_connect(results, yield[ec]); + if (ec) { + attempts++; + net::steady_timer t{*_asioContextService->getContext()}; + t.expires_after(250ms); + t.async_wait(yield); + } else { + break; + } + } + + beast::get_lowest_layer(*_sslStream).socket().set_option(tcp::no_delay(_tcpNoDelay.load(std::memory_order_acquire))); + + _sslStream->async_handshake(net::ssl::stream_base::client, yield[ec]); + if(ec) { + return fail(ec, "HttpConnectionService::connect ssl handshake"); + } + } else { + // _httpStream should only be modified from the boost thread + _httpStream = std::make_unique(*_asioContextService->getContext()); + + // Never expire until we actually have an operation. + _httpStream->expires_never(); + + // Make the connection on the IP address we get from a lookup + int attempts{}; + while(!_quit.load(std::memory_order_acquire) && !_asioContextService->fibersShouldStop() && attempts < 5) { + _httpStream->async_connect(results, yield[ec]); + if (ec) { + attempts++; + net::steady_timer t{*_asioContextService->getContext()}; + t.expires_after(250ms); + t.async_wait(yield); + } else { + break; + } + } + + _httpStream->socket().set_option(tcp::no_delay(_tcpNoDelay.load(std::memory_order_acquire))); + } if(ec) { // see below for why _connecting has to be set last diff --git a/src/services/network/http/HttpHostService.cpp b/src/services/network/http/HttpHostService.cpp index fda0c818..1d0aa293 100644 --- a/src/services/network/http/HttpHostService.cpp +++ b/src/services/network/http/HttpHostService.cpp @@ -28,12 +28,28 @@ Ichor::Task> Ichor::HttpHostService::start _tcpNoDelay.store(Ichor::any_cast(getProperties()["NoDelay"]), std::memory_order_release); } + if(getProperties().contains("SslCert")) { + _useSsl.store(true, std::memory_order_release); + } + + if((_useSsl.load(std::memory_order_acquire) && !getProperties().contains("SslKey")) || + (!_useSsl.load(std::memory_order_acquire) && getProperties().contains("SslKey"))) { + ICHOR_LOG_ERROR_ATOMIC(_logger, "Both SslCert and SslKey properties are required when using ssl"); + co_return tl::unexpected(StartError::FAILED); + } + _queue = &GetThreadLocalEventQueue(); - auto address = net::ip::make_address(Ichor::any_cast(getProperties()["Address"])); + boost::system::error_code ec; + auto address = net::ip::make_address(Ichor::any_cast(getProperties()["Address"]), ec); auto port = Ichor::any_cast(getProperties()["Port"]); - net::spawn(*_asioContextService->getContext(), [this, address = std::move(address), port](net::yield_context yield){ + if(ec) { + ICHOR_LOG_ERROR_ATOMIC(_logger, "Couldn't parse address \"{}\": {} {}", Ichor::any_cast(getProperties()["Address"]), ec.value(), ec.message()); + co_return tl::unexpected(StartError::FAILED); + } + + net::spawn(*_asioContextService->getContext(), [this, address = std::move(address), port](net::yield_context yield) { listen(tcp::endpoint{address, port}, std::move(yield)); }ASIO_SPAWN_COMPLETION_TOKEN); @@ -45,7 +61,7 @@ Ichor::Task Ichor::HttpHostService::stop() { _quit.store(true, std::memory_order_release); if(!_goingToCleanupStream.exchange(true, std::memory_order_acq_rel) && _httpAcceptor) { - if (_httpAcceptor->is_open()) { + if(_httpAcceptor->is_open()) { _httpAcceptor->close(); } net::spawn(*_asioContextService->getContext(), [this](net::yield_context _yield) { @@ -53,9 +69,11 @@ Ichor::Task Ichor::HttpHostService::stop() { { std::unique_lock lg{_streamsMutex}; for (auto &[id, stream]: _httpStreams) { -// stream->quit.store(true, std::memory_order_release); stream->socket.cancel(); } + for (auto &[id, stream]: _sslStreams) { + beast::get_lowest_layer(stream->socket).cancel(); + } } _httpAcceptor = nullptr; @@ -145,6 +163,29 @@ void Ichor::HttpHostService::listen(tcp::endpoint endpoint, net::yield_context y ScopeGuardAtomicCount guard{_finishedListenAndRead}; beast::error_code ec; + if(_useSsl.load(std::memory_order_acquire)) { + _sslContext = std::make_unique(net::ssl::context::tlsv12); + + if(getProperties().contains("SslPassword")) { + _sslContext->set_password_callback( + [password = Ichor::any_cast(getProperties()["SslPassword"])](std::size_t, boost::asio::ssl::context_base::password_purpose) { + return password; + }); + } + + _sslContext->set_options(boost::asio::ssl::context::default_workarounds | + boost::asio::ssl::context::no_tlsv1 | + boost::asio::ssl::context::no_tlsv1_1 | + boost::asio::ssl::context::no_sslv2 | + boost::asio::ssl::context::no_sslv3); + + //pretty sure that Boost.Beast uses references to the data/size, so we should make sure to keep this in memory until we're done. + auto& cert = Ichor::any_cast(getProperties()["SslCert"]); + auto& key = Ichor::any_cast(getProperties()["SslKey"]); + _sslContext->use_certificate_chain(boost::asio::buffer(cert.data(), cert.size())); + _sslContext->use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context::file_format::pem); + } + _httpAcceptor = std::make_unique(*_asioContextService->getContext()); _httpAcceptor->open(endpoint.protocol(), ec); if(ec) { @@ -185,7 +226,11 @@ void Ichor::HttpHostService::listen(tcp::endpoint endpoint, net::yield_context y return; } - read(std::move(socket), std::move(_yield)); + if(_useSsl) { + read>(std::move(socket), std::move(_yield)); + } else { + read(std::move(socket), std::move(_yield)); + } }ASIO_SPAWN_COMPLETION_TOKEN); } @@ -195,68 +240,92 @@ void Ichor::HttpHostService::listen(tcp::endpoint endpoint, net::yield_context y } } +template void Ichor::HttpHostService::read(tcp::socket socket, net::yield_context yield) { - ScopeGuardAtomicCount guard{_finishedListenAndRead}; + ScopeGuardAtomicCount guard{ _finishedListenAndRead }; beast::error_code ec; auto addr = socket.remote_endpoint().address().to_string(); - std::shared_ptr connection; + std::shared_ptr> connection; uint64_t streamId; { std::lock_guard lg(_streamsMutex); streamId = _streamIdCounter++; - connection = _httpStreams.emplace(streamId, std::make_shared(std::move(socket))).first->second; + if constexpr (std::is_same_v) { + connection = _httpStreams.emplace(streamId, std::make_shared>(std::move(socket))).first->second; + } else { + connection = _sslStreams.emplace(streamId, std::make_shared>(std::move(socket), *_sslContext)).first->second; + } + } + + if constexpr (!std::is_same_v) { + connection->socket.async_handshake(ssl::stream_base::server, yield[ec]); + } + + if (ec) { + fail(ec, "HttpHostService::read ssl handshake", false); + return; } // This buffer is required to persist across reads - beast::basic_flat_buffer buffer{std::allocator{}}; + beast::basic_flat_buffer buffer{ std::allocator{} }; - while(!_quit.load(std::memory_order_acquire) && !_asioContextService->fibersShouldStop()) + while (!_quit.load(std::memory_order_acquire) && !_asioContextService->fibersShouldStop()) { // Set the timeout. - connection->socket.expires_after(30s); + if constexpr (std::is_same_v) { + connection->socket.expires_after(30s); + } else { + beast::get_lowest_layer(connection->socket).expires_after(30s); + } // Read a request http::request, http::basic_fields>> req; http::async_read(connection->socket, buffer, req, yield[ec]); - if(ec == http::error::end_of_stream) { + if (ec == http::error::end_of_stream) { fail(ec, "HttpHostService::read end of stream", false); break; } - if(ec == net::error::operation_aborted) { + if (ec == net::error::operation_aborted) { fail(ec, "HttpHostService::read operation aborted", false); break; } - if(ec == net::error::timed_out) { + if (ec == net::error::timed_out) { fail(ec, "HttpHostService::read operation timed out", false); break; } - if(ec == net::error::bad_descriptor) { + if (ec == net::error::bad_descriptor) { fail(ec, "HttpHostService::read bad descriptor", false); break; } - if(ec) { + if (ec) { fail(ec, "HttpHostService::read read", false); continue; } - ICHOR_LOG_TRACE_ATOMIC(_logger, "New request for {} {}", (int) req.method(), req.target()); + ICHOR_LOG_TRACE_ATOMIC(_logger, "New request for {} {}", (int)req.method(), req.target()); std::vector headers{}; headers.reserve(static_cast(std::distance(std::begin(req), std::end(req)))); - for (auto const &field : req) { + for (auto const& field : req) { headers.emplace_back(field.name_string(), field.value()); } // rapidjson f.e. expects a null terminator - if(!req.body().empty() && *req.body().rbegin() != 0) { + if (!req.body().empty() && *req.body().rbegin() != 0) { req.body().push_back(0); } - HttpRequest httpReq{std::move(req.body()), static_cast(req.method()), std::string{req.target()}, addr, std::move(headers)}; - + HttpRequest httpReq{ std::move(req.body()), static_cast(req.method()), std::string{req.target()}, addr, std::move(headers) }; + // Compiler bug prevents using named captures for now: https://www.reddit.com/r/cpp_questions/comments/17lc55f/coroutine_msvc_compiler_bug/ +#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32)) && !defined(__CYGWIN__) + auto version = req.version(); + auto keep_alive = req.keep_alive(); + _queue->pushEvent(getServiceId(), [this, connection, httpReq, version, keep_alive]() mutable -> AsyncGenerator { +#else _queue->pushEvent(getServiceId(), [this, connection, httpReq = std::move(httpReq), version = req.version(), keep_alive = req.keep_alive()]() mutable -> AsyncGenerator { +#endif auto routes = _handlers.find(static_cast(httpReq.method)); - if(_quit.load(std::memory_order_acquire) || _asioContextService->fibersShouldStop()) { - co_return {}; + if (_quit.load(std::memory_order_acquire) || _asioContextService->fibersShouldStop()) { + co_return{}; } if (routes != std::end(_handlers)) { @@ -265,34 +334,36 @@ void Ichor::HttpHostService::read(tcp::socket socket, net::yield_context yield) if (handler != std::end(routes->second)) { // using reference here leads to heap use after free. Not sure why. - HttpResponse httpRes = std::move(*co_await handler->second(httpReq).begin()); - http::response, http::basic_fields>> res{static_cast(httpRes.status), - version}; - if(_sendServerHeader) { + auto gen = handler->second(httpReq); + auto it = co_await gen.begin(); + HttpResponse httpRes = std::move(*it); + http::response, http::basic_fields>> res{ static_cast(httpRes.status), + version }; + if (_sendServerHeader) { res.set(http::field::server, BOOST_BEAST_VERSION_STRING); } - if(!httpRes.contentType) { + if (!httpRes.contentType) { res.set(http::field::content_type, "text/plain"); } else { res.set(http::field::content_type, *httpRes.contentType); } - for (auto const &header: httpRes.headers) { + for (auto const& header : httpRes.headers) { res.set(header.value, header.name); } res.keep_alive(keep_alive); ICHOR_LOG_TRACE_ATOMIC(_logger, "sending http response {} - {}", (int)httpRes.status, - std::string_view(reinterpret_cast(httpRes.body.data()), httpRes.body.size())); + std::string_view(reinterpret_cast(httpRes.body.data()), httpRes.body.size())); res.body() = std::move(httpRes.body); res.prepare_payload(); sendInternal(connection, std::move(res)); - co_return {}; + co_return{}; } } - http::response, http::basic_fields>> res{http::status::not_found, version}; - if(_sendServerHeader) { + http::response, http::basic_fields>> res{ http::status::not_found, version }; + if (_sendServerHeader) { res.set(http::field::server, BOOST_BEAST_VERSION_STRING); } res.set(http::field::content_type, "text/plain"); @@ -300,20 +371,25 @@ void Ichor::HttpHostService::read(tcp::socket socket, net::yield_context yield) res.prepare_payload(); sendInternal(connection, std::move(res)); - co_return {}; + co_return{}; }); } { std::lock_guard lg(_streamsMutex); - _httpStreams.erase(streamId); + if constexpr (std::is_same_v) { + _httpStreams.erase(streamId); + } else { + _sslStreams.erase(streamId); + } } // At this point the connection is closed gracefully ICHOR_LOG_WARN_ATOMIC(_logger, "finished read() {} {}", _quit.load(std::memory_order_acquire), _asioContextService->fibersShouldStop()); } -void Ichor::HttpHostService::sendInternal(std::shared_ptr &connection, http::response, http::basic_fields>> &&res) { +template +void Ichor::HttpHostService::sendInternal(std::shared_ptr> &connection, http::response, http::basic_fields>> &&res) { static_assert(std::is_move_assignable_v, "HostOutboxMessage should be move assignable"); if(_quit.load(std::memory_order_acquire) || _asioContextService->fibersShouldStop()) { @@ -339,16 +415,20 @@ void Ichor::HttpHostService::sendInternal(std::shared_ptr &c // Move message, should be trivially copyable and prevents iterator invalidation auto next = std::move(connection->outbox.front()); lg.unlock(); - connection->socket.expires_after(30s); + if constexpr (std::is_same_v) { + connection->socket.expires_after(30s); + } else { + beast::get_lowest_layer(connection->socket).expires_after(30s); + } beast::error_code ec; http::async_write(connection->socket, next, yield[ec]); - if (ec == http::error::end_of_stream) { + if(ec == http::error::end_of_stream) { fail(ec, "HttpHostService::sendInternal end of stream", false); - } else if (ec == net::error::operation_aborted) { + } else if(ec == net::error::operation_aborted) { fail(ec, "HttpHostService::sendInternal operation aborted", false); - } else if (ec == net::error::bad_descriptor) { + } else if(ec == net::error::bad_descriptor) { fail(ec, "HttpHostService::sendInternal bad descriptor", false); - } else if (ec) { + } else if(ec) { fail(ec, "HttpHostService::sendInternal write", false); } lg.lock(); diff --git a/src/services/network/ws/WsConnectionService.cpp b/src/services/network/ws/WsConnectionService.cpp index 9431597d..bfa4731a 100644 --- a/src/services/network/ws/WsConnectionService.cpp +++ b/src/services/network/ws/WsConnectionService.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include template @@ -162,7 +163,7 @@ tl::expected Ichor::WsConnectionService::sendA while(!_outbox.empty()) { auto next = std::move(_outbox.front()); - ScopeGuardFunction const coroutineGuard{[this]() { + ScopeGuard const coroutineGuard{[this]() { _outbox.pop_front(); }}; @@ -206,7 +207,7 @@ void Ichor::WsConnectionService::accept(net::yield_context yield) { beast::error_code ec; { - ScopeGuardFunction const coroutineGuard{[this]() { + ScopeGuard const coroutineGuard{[this]() { _queue->pushPrioritisedEvent(getServiceId(), _priority.load(std::memory_order_acquire), [this]() { // fmt::print("{}:{} rfe accept\n", getServiceId(), getServiceName()); _startStopEvent.set(); @@ -273,7 +274,7 @@ void Ichor::WsConnectionService::connect(net::yield_context yield) { _ws = std::make_shared>(*_asioContextService->getContext()); { - ScopeGuardFunction const coroutineGuard{[this]() { + ScopeGuard const coroutineGuard{[this]() { _queue->pushPrioritisedEvent(getServiceId(), _priority.load(std::memory_order_acquire), [this]() { // fmt::print("{}:{} rfe connect\n", getServiceId(), getServiceName()); _startStopEvent.set(); diff --git a/src/services/timer/Timer.cpp b/src/services/timer/Timer.cpp index 8b19aaae..b68bed9f 100644 --- a/src/services/timer/Timer.cpp +++ b/src/services/timer/Timer.cpp @@ -1,6 +1,7 @@ #include #include #if (defined(WIN32) || defined(_WIN32) || defined(__WIN32)) && !defined(__CYGWIN__) +#include #include #include #endif diff --git a/test/AsyncAutoResetEventTests.cpp b/test/AsyncAutoResetEventTests.cpp deleted file mode 100644 index d10426ef..00000000 --- a/test/AsyncAutoResetEventTests.cpp +++ /dev/null @@ -1,10 +0,0 @@ -#include "Common.h" -#include - -TEST_CASE("AsyncAutoResetEventTests") { - - SECTION("default constructor") - { - Ichor::AsyncAutoResetEvent event; - } -} \ No newline at end of file diff --git a/test/AsyncFileIOTests.cpp b/test/AsyncFileIOTests.cpp new file mode 100644 index 00000000..1286a54b --- /dev/null +++ b/test/AsyncFileIOTests.cpp @@ -0,0 +1,270 @@ +#include "Common.h" +#include +#include +#include +#include +#include + + +TEST_CASE("AsyncFileIOTests") { + + SECTION("Reading non-existent file should error") { + fmt::print("section 1\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + tl::expected ret = co_await async_io_svc->first->read_whole_file("NonExistentFile.txt"); + fmt::print("require\n"); + REQUIRE(!ret); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Read whole file") { + fmt::print("section 2\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + { + std::ofstream out("AsyncFileIO.txt"); + out << "This is a test"; + out.close(); + } + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + tl::expected ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + fmt::print("require\n"); + REQUIRE(ret == "This is a test"); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Copying non-existent file should error") { + fmt::print("section 3\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + tl::expected ret = co_await async_io_svc->first->copy_file("NonExistentFile.txt", "DestinationNull.txt"); + fmt::print("require\n"); + REQUIRE(!ret); + REQUIRE(ret.error() == FileIOError::FILE_DOES_NOT_EXIST); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Copying file") { + fmt::print("section 4\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + { + std::ofstream out("AsyncFileIO.txt"); + out << "This is a test"; + out.close(); + + std::remove("Destination.txt"); // ignore error, each OS handles this differently and most of the time it's just because it didn't exist in the first place + } + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + { + tl::expected ret = co_await async_io_svc->first->copy_file("AsyncFileIO.txt", "Destination.txt"); + fmt::print("require\n"); + REQUIRE(ret); + } + tl::expected ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + REQUIRE(ret == "This is a test"); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Removing non-existing file should error") { + fmt::print("section 5\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + tl::expected ret = co_await async_io_svc->first->remove_file("MissingFile.txt"); + REQUIRE(!ret); + REQUIRE(ret.error() == FileIOError::FILE_DOES_NOT_EXIST); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Removing file") { + fmt::print("section 6\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + { + std::ofstream out("AsyncFileIO.txt"); + out << "This is a test"; + out.close(); + } + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + { + tl::expected ret = co_await async_io_svc->first->remove_file("AsyncFileIO.txt"); + fmt::print("require\n"); + REQUIRE(ret); + } + tl::expected ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + REQUIRE(!ret); + REQUIRE(ret.error() == FileIOError::FILE_DOES_NOT_EXIST); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Writing file") { + fmt::print("section 7\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + { + std::remove("AsyncFileIO.txt"); // ignore error, each OS handles this differently and most of the time it's just because it didn't exist in the first place + } + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + { + tl::expected ret = co_await async_io_svc->first->write_file("AsyncFileIO.txt", "This is a test"); + fmt::print("require\n"); + REQUIRE(ret); + } + tl::expected ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + REQUIRE(ret); + REQUIRE(ret == "This is a test"); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } + + SECTION("Writing file - overwrite") { + fmt::print("section 8\n"); + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + uint64_t ioSvcId{}; + + std::thread t([&]() { + ioSvcId = dm.createServiceManager()->getServiceId(); + queue->start(CaptureSigInt); + }); + + { + std::ofstream out("AsyncFileIO.txt"); + out << "This is a test"; + out.close(); + } + + waitForRunning(dm); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + fmt::print("run function co_await\n"); + auto async_io_svc = dm.getService(ioSvcId); + tl::expected ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + REQUIRE(ret); + { + tl::expected ret2 = co_await async_io_svc->first->write_file("AsyncFileIO.txt", "Overwrite"); + fmt::print("require\n"); + REQUIRE(ret2); + } + ret = co_await async_io_svc->first->read_whole_file("AsyncFileIO.txt"); + REQUIRE(ret); + REQUIRE(ret == "Overwrite"); + queue->pushEvent(0); + co_return {}; + }); + + t.join(); + } +} diff --git a/test/AsyncSingleThreadedMutexTests.cpp b/test/AsyncSingleThreadedMutexTests.cpp new file mode 100644 index 00000000..639b4ce2 --- /dev/null +++ b/test/AsyncSingleThreadedMutexTests.cpp @@ -0,0 +1,56 @@ +#include "Common.h" +#include +#include +#include + + +TEST_CASE("AsyncSingleThreadedMutexTests") { + AsyncSingleThreadedMutex m; + AsyncSingleThreadedLockGuard *lg{}; + + SECTION("Lock/unlock") { + auto queue = std::make_unique(); + auto &dm = queue->createManager(); + bool started_async_func{}; + bool unlocked{}; + + std::thread t([&]() { + AsyncSingleThreadedLockGuard lg2 = m.non_blocking_lock().value(); + lg = &lg2; + queue->start(CaptureSigInt); + }); + + waitForRunning(dm); + + queue->pushEvent(0, [&]() { + auto val = m.non_blocking_lock(); + REQUIRE(!val); + }); + + dm.runForOrQueueEmpty(); + + queue->pushEvent(0, [&]() -> AsyncGenerator { + started_async_func = true; + AsyncSingleThreadedLockGuard lg3 = co_await m.lock(); + unlocked = true; + co_return {}; + }); + + dm.runForOrQueueEmpty(); + + REQUIRE(started_async_func); + REQUIRE(!unlocked); + + queue->pushEvent(0, [&]() { + lg->unlock(); + }); + + dm.runForOrQueueEmpty(); + + REQUIRE(unlocked); + + queue->pushEvent(0); + + t.join(); + } +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 4bfdc4c4..3f54aae8 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -13,10 +13,5 @@ foreach(filename ${PROJECT_TEST_SOURCES}) target_link_libraries(${testname} ichor) target_link_libraries(${testname} Catch2::Catch2WithMain) target_compile_definitions(${testname} PUBLIC CATCH_CONFIG_FAST_COMPILE) - - if(ICHOR_USE_SANITIZERS) - target_link_libraries(${testname}) - endif() - catch_discover_tests(${testname}) -endforeach() \ No newline at end of file +endforeach() diff --git a/test/HttpTests.cpp b/test/HttpTests.cpp index 1337ab97..67d42ae4 100644 --- a/test/HttpTests.cpp +++ b/test/HttpTests.cpp @@ -52,6 +52,94 @@ TEST_CASE("HttpTests") { REQUIRE(Ichor::Detail::_local_dm == &dm); REQUIRE(testThreadId != std::this_thread::get_id()); REQUIRE(dmThreadId == std::this_thread::get_id()); + fmt::print("1 _evt set\n"); + _evt->set(); + }); + + t.join(); + } + + SECTION("Https events on same thread") { + testThreadId = std::this_thread::get_id(); + _evt = std::make_unique(); + auto queue = std::make_unique(true); + auto &dm = queue->createManager(); + evtGate = false; + + std::thread t([&]() { + dmThreadId = std::this_thread::get_id(); + + std::string key = "-----BEGIN PRIVATE KEY-----\n" + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDIisWpB9FSc/OI\n" + "YsxznQJgioL2aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ\n" + "+VNc63blCPAIiL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFF\n" + "elbLf7JTW80CnNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi\n" + "1QbWD3G1lkTQcCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGo\n" + "swxe5IF+yB6fWFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0\n" + "yv+K0iUBAgMBAAECggEAB2lJgMOvMrLiTyoHkEY/Lj4wNNcNW8g0aQRWlmng7SdE\n" + "84mh1QEspJegaUe7eyNTsTY8TNwzET43jEFQmWCCVliMNmSHWWWF99sgmuL5W5aN\n" + "Ec+4LPnCWDR+pxAKcCEoN4yzhfLTKNzmsqCg0Ih5mKcN3ojZMLodpCfH3WczDkay\n" + "1KrJvIKIovIV8Yv8NJ4K9hDx7a+xGpHXqh5NLey9K6XNpDqbWwN97FygLLEoedhx\n" + "XDjPzBiGJuwIYQI/V2gYVXn3nPcglDFqHpZA8peX3+DwXojqDwzbNhf2CANpMgaD\n" + "5OQkGymb+FumKPh1UMEL+IfqtXWwwg2Ix7DtI6i/IQKBgQDasFuUC43F0NM61TFQ\n" + "hWuH4mRMp8B4YkhO9M+0/mitHI76cnU63EnJpMYvnp3CLRpVzs/sidEcbfC9gOma\n" + "Ui+xxtKers+RQDI4lrwgqyYIzNNpGO7ItlDYs3guTvX5Mvc5x/zIIHhcjhTudzHK\n" + "Utgho8PIXvrrF3fSHdL0QJb1tQKBgQDqwdGImGbF3lxsB8t4T/8fozIBwo2dWp4O\n" + "YgcVyDpq7Nl+J9Yy5fqTw/sIn6Fa6ZpqAzq0zPSoYJzU7l9+5CcpkkdPEulyER3D\n" + "maQaVMUv0cpnXUQSBRNDZJDdbC4eKocxxotyTuLjCVrUwYWuqYWo3aGx42VXWsWm\n" + "n4+9AwDBnQKBgBStumscUJKU9XRJtnkLtKhLsvpAnoWDnZzBr2ZI7DL6UVbDPeyL\n" + "6fpEN21HTVmQFD5q6ORP/9L1Xl888lniTZo816ujkgMFE/qf3jgkltscKx1z+xhF\n" + "jQ2AouuWEdI3jIMNMwzlbRwrXzVRVgbwoHlF1/x5ZraWKIFYyprIBL5FAoGAfXcM\n" + "12YsN0A6QPqBglGu1mfQCCTErv6JTsKRatDSd+cR7ly4HAfRvjuV5Ov7vqzu/A2x\n" + "yINplrvb1el4XEbvr0YgmmBPJ8mCENICZJg9sur6s/eis8bGntQWoGB63WB5VN76\n" + "FCOZGyIay26KVekAKFobWwlfViqLTBwnJCuAsfkCgYAgrbUKCf+BGgz4epv3T3Ps\n" + "kJD2XX1eeQr/bw1IMqY7ZLKCjg7oxM1jjp2xyujY3ZNViaJsnj7ngCIbaOSJNa3t\n" + "YS8GDLTL4efNTmPEVzgxhnJdSeYVgMLDwu66M9EYhkw+0Y1PQsNS1+6SO89ySRzP\n" + "pFbool/8ZDecmB4ZSa03aw==\n" + "-----END PRIVATE KEY-----"; + + std::string cert = "-----BEGIN CERTIFICATE-----\n" + "MIIDdzCCAl+gAwIBAgIUQEtUTmX2HbHwOpDPgFkYx66ei/swDQYJKoZIhvcNAQEL\n" + "BQAwSzELMAkGA1UEBhMCTkwxEjAQBgNVBAcMCUFtc3RlcmRhbTEOMAwGA1UECgwF\n" + "SWNob3IxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMzEwMDIwODI1NTVa\n" + "Fw0yNDEwMDEwODI1NTVaMEsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlBbXN0ZXJk\n" + "YW0xDjAMBgNVBAoMBUljaG9yMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggEi\n" + "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIisWpB9FSc/OIYsxznQJgioL2\n" + "aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ+VNc63blCPAI\n" + "iL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFFelbLf7JTW80C\n" + "nNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi1QbWD3G1lkTQ\n" + "cCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGoswxe5IF+yB6f\n" + "WFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0yv+K0iUBAgMB\n" + "AAGjUzBRMB0GA1UdDgQWBBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAfBgNVHSMEGDAW\n" + "gBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\n" + "DQEBCwUAA4IBAQC1QKNwlqrvE5a7M5v151tA/JvIKNBfesIRCg/IG6tB3teSJjAq\n" + "PRsSajMcyvDtlDMkWL093B8Epsc8yt/vN1GgsCfQsMeIWRGvRYM6Qo2WoABaLTuC\n" + "TxH89YrXa9uwUnbesGwKSkpxzj8g6YuzUpLnAAV70LefBn8ZgBJktFVW/ANWwQqD\n" + "KiglduSDGX3NiLByn7nGKejg2dPw6kWpOTVtLsWtgG/5T4vw+INQzPJqy+pUvBeQ\n" + "OrV7cQyq12DJJPxboIRh1KqF8C25NAOnXVtCav985RixHCMaNaXyOXBJeEEASnXg\n" + "pdDrObCgMze03IaHQ1pj3LeMu0OvEMbsRrYW\n" + "-----END CERTIFICATE-----"; + + dm.createServiceManager({}, 10); + dm.createServiceManager, ILoggerFactory>(); + dm.createServiceManager>(); + dm.createServiceManager(); + dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(8001))}, {"SslKey", Ichor::make_any(key)}, {"SslCert", Ichor::make_any(cert)}}); + dm.createServiceManager>(); + dm.createServiceManager(Properties{{"Address", Ichor::make_any("127.0.0.1")}, {"Port", Ichor::make_any(static_cast(8001))}, {"ConnectOverSsl", Ichor::make_any(true)}, {"RootCA", Ichor::make_any(cert)}}); + + queue->start(CaptureSigInt); + }); + + while(!evtGate) { + std::this_thread::sleep_for(500us); + } + + queue->pushEvent(0, [&]() { + REQUIRE(Ichor::Detail::_local_dm == &dm); + REQUIRE(testThreadId != std::this_thread::get_id()); + REQUIRE(dmThreadId == std::this_thread::get_id()); + fmt::print("2 _evt set\n"); _evt->set(); }); @@ -87,6 +175,7 @@ TEST_CASE("HttpTests") { REQUIRE(Ichor::Detail::_local_dm == &dm); REQUIRE(testThreadId != std::this_thread::get_id()); REQUIRE(dmThreadId == std::this_thread::get_id()); + fmt::print("3 _evt set\n"); _evt->set(); }); @@ -94,4 +183,14 @@ TEST_CASE("HttpTests") { } } +#else + +#include "Common.h" + +TEST_CASE("HttpTests") { + SECTION("Empty Test so that Catch2 exits with 0") { + REQUIRE(true); + } +} + #endif diff --git a/test/RedisTests.cpp b/test/RedisTests.cpp index e7743167..c18a9608 100644 --- a/test/RedisTests.cpp +++ b/test/RedisTests.cpp @@ -30,5 +30,15 @@ TEST_CASE("RedisTests") { } } +#else + +#include "Common.h" + +TEST_CASE("RedisTests") { + SECTION("Empty Test so that Catch2 exits with 0") { + REQUIRE(true); + } +} + #endif diff --git a/test/TestServices/HttpThreadService.h b/test/TestServices/HttpThreadService.h index 4820666f..1a2daa86 100644 --- a/test/TestServices/HttpThreadService.h +++ b/test/TestServices/HttpThreadService.h @@ -116,6 +116,7 @@ class HttpThreadService final : public AdvancedService { if(response.status == HttpStatus::ok) { auto msg = _serializer->deserialize(std::move(response.body)); + fmt::print("Received {}:{}\n", msg->id, msg->val); } else { throw std::runtime_error("Status not ok"); } diff --git a/test/certs/csr.pem b/test/certs/csr.pem new file mode 100644 index 00000000..b587a113 --- /dev/null +++ b/test/certs/csr.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIUQEtUTmX2HbHwOpDPgFkYx66ei/swDQYJKoZIhvcNAQEL +BQAwSzELMAkGA1UEBhMCTkwxEjAQBgNVBAcMCUFtc3RlcmRhbTEOMAwGA1UECgwF +SWNob3IxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMzEwMDIwODI1NTVa +Fw0yNDEwMDEwODI1NTVaMEsxCzAJBgNVBAYTAk5MMRIwEAYDVQQHDAlBbXN0ZXJk +YW0xDjAMBgNVBAoMBUljaG9yMRgwFgYDVQQDDA93d3cuZXhhbXBsZS5jb20wggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIisWpB9FSc/OIYsxznQJgioL2 +aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ+VNc63blCPAI +iL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFFelbLf7JTW80C +nNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi1QbWD3G1lkTQ +cCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGoswxe5IF+yB6f +WFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0yv+K0iUBAgMB +AAGjUzBRMB0GA1UdDgQWBBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAfBgNVHSMEGDAW +gBRgTmWQZDeiFMXtL8tyQSdD/HRYPjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQC1QKNwlqrvE5a7M5v151tA/JvIKNBfesIRCg/IG6tB3teSJjAq +PRsSajMcyvDtlDMkWL093B8Epsc8yt/vN1GgsCfQsMeIWRGvRYM6Qo2WoABaLTuC +TxH89YrXa9uwUnbesGwKSkpxzj8g6YuzUpLnAAV70LefBn8ZgBJktFVW/ANWwQqD +KiglduSDGX3NiLByn7nGKejg2dPw6kWpOTVtLsWtgG/5T4vw+INQzPJqy+pUvBeQ +OrV7cQyq12DJJPxboIRh1KqF8C25NAOnXVtCav985RixHCMaNaXyOXBJeEEASnXg +pdDrObCgMze03IaHQ1pj3LeMu0OvEMbsRrYW +-----END CERTIFICATE----- diff --git a/test/certs/key.pem b/test/certs/key.pem new file mode 100644 index 00000000..5d84c5b9 --- /dev/null +++ b/test/certs/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDIisWpB9FSc/OI +YsxznQJgioL2aKlqf0fKzrP6IVSq1ApxlMF+hVpz340Yv5rVYizfb/zx46pTbPPJ ++VNc63blCPAIiL7h3NdYsMaAe+dTfdmQd2/m1kiBQFraceOw5TB7+Y9lFXfUCUFF +elbLf7JTW80CnNOP4xPmXCusMD8m4LL8z7e7BqKr4VFrrAFXB0EBqpmHA8HXbKAi +1QbWD3G1lkTQcCrq8c7pw5oME0RNjfrevlGHATJ2a6zCAeUsIfsDGFpNHFIYqGGo +swxe5IF+yB6fWFWMjtbbRv93pZfViTFcOUWQuhLFsSN41bt9/XimlxM+ZtX5JDE0 +yv+K0iUBAgMBAAECggEAB2lJgMOvMrLiTyoHkEY/Lj4wNNcNW8g0aQRWlmng7SdE +84mh1QEspJegaUe7eyNTsTY8TNwzET43jEFQmWCCVliMNmSHWWWF99sgmuL5W5aN +Ec+4LPnCWDR+pxAKcCEoN4yzhfLTKNzmsqCg0Ih5mKcN3ojZMLodpCfH3WczDkay +1KrJvIKIovIV8Yv8NJ4K9hDx7a+xGpHXqh5NLey9K6XNpDqbWwN97FygLLEoedhx +XDjPzBiGJuwIYQI/V2gYVXn3nPcglDFqHpZA8peX3+DwXojqDwzbNhf2CANpMgaD +5OQkGymb+FumKPh1UMEL+IfqtXWwwg2Ix7DtI6i/IQKBgQDasFuUC43F0NM61TFQ +hWuH4mRMp8B4YkhO9M+0/mitHI76cnU63EnJpMYvnp3CLRpVzs/sidEcbfC9gOma +Ui+xxtKers+RQDI4lrwgqyYIzNNpGO7ItlDYs3guTvX5Mvc5x/zIIHhcjhTudzHK +Utgho8PIXvrrF3fSHdL0QJb1tQKBgQDqwdGImGbF3lxsB8t4T/8fozIBwo2dWp4O +YgcVyDpq7Nl+J9Yy5fqTw/sIn6Fa6ZpqAzq0zPSoYJzU7l9+5CcpkkdPEulyER3D +maQaVMUv0cpnXUQSBRNDZJDdbC4eKocxxotyTuLjCVrUwYWuqYWo3aGx42VXWsWm +n4+9AwDBnQKBgBStumscUJKU9XRJtnkLtKhLsvpAnoWDnZzBr2ZI7DL6UVbDPeyL +6fpEN21HTVmQFD5q6ORP/9L1Xl888lniTZo816ujkgMFE/qf3jgkltscKx1z+xhF +jQ2AouuWEdI3jIMNMwzlbRwrXzVRVgbwoHlF1/x5ZraWKIFYyprIBL5FAoGAfXcM +12YsN0A6QPqBglGu1mfQCCTErv6JTsKRatDSd+cR7ly4HAfRvjuV5Ov7vqzu/A2x +yINplrvb1el4XEbvr0YgmmBPJ8mCENICZJg9sur6s/eis8bGntQWoGB63WB5VN76 +FCOZGyIay26KVekAKFobWwlfViqLTBwnJCuAsfkCgYAgrbUKCf+BGgz4epv3T3Ps +kJD2XX1eeQr/bw1IMqY7ZLKCjg7oxM1jjp2xyujY3ZNViaJsnj7ngCIbaOSJNa3t +YS8GDLTL4efNTmPEVzgxhnJdSeYVgMLDwu66M9EYhkw+0Y1PQsNS1+6SO89ySRzP +pFbool/8ZDecmB4ZSa03aw== +-----END PRIVATE KEY-----