├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── AUTHORS ├── CMakeLists.txt ├── CONTRIBUTING.rst ├── COPYING ├── README.md ├── cmake └── modules │ ├── BuildFunctions.cmake │ ├── CodeCoverage.cmake │ ├── ConfigurationSetting.cmake │ ├── ListSplit.cmake │ └── ZooKeeper.cmake ├── config ├── dev-env ├── docker │ ├── debian-10 │ │ └── Dockerfile │ ├── opensuse-15 │ │ └── Dockerfile │ ├── ubuntu-16.04 │ │ └── Dockerfile │ ├── ubuntu-18.04 │ │ └── Dockerfile │ └── ubuntu-20.04 │ │ └── Dockerfile ├── make-doxygen ├── make-package ├── make-packages ├── publish-doxygen ├── run-tests ├── travisci_rsa.enc └── upload-coverage ├── doc └── Doxyfile ├── install └── deb │ ├── libzkpp-dev │ └── control.in │ ├── libzkpp-server-dev │ └── control.in │ ├── libzkpp-server │ ├── control.in │ └── postinst.in │ └── libzkpp │ ├── control.in │ ├── postinst.in │ └── shlibs.in └── src └── zk ├── acl.cpp ├── acl.hpp ├── acl_tests.cpp ├── buffer.hpp ├── client.cpp ├── client.hpp ├── client_tests.cpp ├── config.hpp ├── connection.cpp ├── connection.hpp ├── connection_tests.cpp ├── connection_zk.cpp ├── connection_zk.hpp ├── error.cpp ├── error.hpp ├── error_tests.cpp ├── exceptions.cpp ├── exceptions.hpp ├── forwards.hpp ├── future.hpp ├── multi.cpp ├── multi.hpp ├── multi_tests.cpp ├── optional.hpp ├── optional_tests.cpp ├── results.cpp ├── results.hpp ├── server ├── classpath.cpp ├── classpath.hpp ├── classpath_registration_template.cpp.in ├── classpath_tests.cpp ├── configuration.cpp ├── configuration.hpp ├── configuration_tests.cpp ├── detail │ ├── close.cpp │ ├── close.hpp │ ├── event_handle.cpp │ ├── event_handle.hpp │ ├── pipe.cpp │ ├── pipe.hpp │ ├── pipe_tests.cpp │ ├── subprocess.cpp │ ├── subprocess.hpp │ └── subprocess_tests.cpp ├── package_registry.cpp ├── package_registry.hpp ├── package_registry_tests.cpp ├── package_registry_tests.hpp ├── server.cpp ├── server.hpp ├── server_group.cpp ├── server_group.hpp ├── server_group_tests.cpp ├── server_tests.cpp └── server_tests.hpp ├── string_view.hpp ├── tests ├── main.cpp ├── test.cpp └── test.hpp ├── types.cpp ├── types.hpp └── types_tests.cpp /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Don't forget the label! 2 | 3 | - If you have a question, use the "question" tag. 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | *.orig 4 | *.kdev4 5 | build* 6 | .kdev* 7 | *.kdev4 8 | packages/ 9 | .vs 10 | x64/ 11 | *sdf 12 | *.pc 13 | 14 | # Compiled source # 15 | ################### 16 | *.com 17 | *.class 18 | *.dll 19 | *.exe 20 | *.o 21 | *.so 22 | 23 | # Packages # 24 | ############ 25 | # it's better to unpack these files and commit the raw source 26 | # git has its own built in compression methods 27 | *.7z 28 | *.dmg 29 | *.gz 30 | *.iso 31 | *.jar 32 | *.rar 33 | *.tar 34 | *.zip 35 | 36 | # Logs and databases # 37 | ###################### 38 | *.log 39 | *.sql 40 | *.sqlite 41 | 42 | # OS generated files # 43 | ###################### 44 | .DS_Store 45 | .DS_Store? 46 | ._* 47 | .Spotlight-V100 48 | .Trashes 49 | ehthumbs.db 50 | Thumbs.db 51 | ## my files and cmake 52 | out 53 | out.* 54 | 55 | # IDE files # 56 | ############# 57 | nbproject 58 | .~lock.* 59 | .buildpath 60 | .idea 61 | .project 62 | .settings 63 | composer.lock 64 | 65 | #Cmake 66 | CMakeCache.txt 67 | CMakeFiles 68 | CMakeScripts 69 | Makefile 70 | cmake_install.cmake 71 | install_manifest.txt 72 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | sudo: required 3 | dist: trusty 4 | env: 5 | matrix: 6 | - DISTRO=ubuntu-16.04 CONFIG=Debug 7 | - DISTRO=ubuntu-16.04 CONFIG=Release 8 | - DISTRO=ubuntu-18.04 CONFIG=Debug 9 | - DISTRO=ubuntu-18.04 CONFIG=Release 10 | - DISTRO=ubuntu-20.04 CONFIG=Debug 11 | - DISTRO=ubuntu-20.04 CONFIG=Release 12 | - DISTRO=debian-10 CONFIG=Debug 13 | - DISTRO=debian-10 CONFIG=Release 14 | - DISTRO=opensuse-15 CONFIG=Debug 15 | services: 16 | - docker 17 | before_install: 18 | - docker build config/docker/${DISTRO} -t dev/zookeeper-cpp/${DISTRO} 19 | script: 20 | - echo ${COVERALLS_REPO_TOKEN} > ${TRAVIS_BUILD_DIR}/coveralls-repo-token 21 | - if [[ ${CONFIG} == "Debug" ]]; then ./config/dev-env ${DISTRO} -- ./config/run-tests; fi 22 | - if [[ ${CONFIG} == "Release" ]]; then ./config/make-package --dockerize ${DISTRO}; fi 23 | after_success: 24 | - if [[ ${DISTRO} != "ubuntu-20.04" ]]; then echo "Skipping documentation publishing due to non-main build environment"; exit 0; fi 25 | - if [[ ${CONFIG} != "Debug" ]]; then echo "Skipp documentation publishing due to non-debug build"; exit 0; fi 26 | - GIT_CURRENT_HASH=$(git rev-parse HEAD) 27 | - GIT_MASTER_HASH=$(git rev-parse master) 28 | - GIT_REMOTE_NAME=$(git remote) 29 | - GIT_REMOTE_FETCH_PATH=$(git remote --verbose | grep -P '^'${GIT_REMOTE_NAME}'.*\(fetch\)$' 30 | | awk '{print $2}') 31 | - GIT_EXPECTED_PATH=https://github.com/tgockel/zookeeper-cpp.git 32 | - echo "GIT_CURRENT_HASH=${GIT_CURRENT_HASH} GIT_REMOTE_NAME=${GIT_REMOTE_NAME} GIT_REMOTE_FETCH_PATH=${GIT_REMOTE_FETCH_PATH}" 33 | - if [[ ${GIT_CURRENT_HASH} != ${GIT_MASTER_HASH} ]]; then echo "Skipping documentation publishing due to non-master ${GIT_CURRENT_HASH} (master=${GIT_MASTER_HASH})"; exit 0; fi 34 | - if [[ ${GIT_REMOTE_FETCH_PATH} != ${GIT_EXPECTED_PATH} ]]; then echo "Skipping documentation publishing due to non-mainline remote ${GIT_REMOTE_FETCH_PATH}"; exit 0; fi 35 | - sudo add-apt-repository --yes ppa:libreoffice/ppa 36 | - sudo apt-get update 37 | - sudo apt-get install --yes doxygen graphviz texlive-full 38 | - openssl aes-256-cbc -K $encrypted_513b1ad04072_key -iv $encrypted_513b1ad04072_iv -in config/travisci_rsa.enc -out config/travisci_rsa -d 39 | - chmod 0600 config/travisci_rsa 40 | - cp config/travisci_rsa ~/.ssh/id_rsa 41 | - "./config/publish-doxygen" 42 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ============ 3 | 4 | Travis Gockel 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing Guide 2 | ================== 3 | 4 | So you want to contribute to the ZooKeeper C++ library? 5 | I'd love your contribution! 6 | Please help me. 7 | 8 | Building 9 | -------- 10 | 11 | Building the system only requires `CMake `_ and the standard-issue C++ compilation tools. 12 | 13 | Docker 14 | ^^^^^^ 15 | 16 | Docker is the official mechanism for supporting multiple Linux distributions (see the 17 | `TravisCI `_ build). 18 | If you would like to do this at home, simply use the ``dev-env`` script:: 19 | 20 | $> cd /path/to/zookeeper-cpp 21 | $> ./config/dev-env ubuntu-18.04 22 | 23 | This will create a Docker image named something like ``dev/zookeeper-cpp/ubuntu-18.04`` and run that image with the 24 | project's working directory mapped to ``~/zookeeper-cpp`` with you in control of a shell. 25 | Inside Docker, you can now build:: 26 | 27 | root@0ae2f54b152b:~/zookeeper-cpp# mkdir build-debug 28 | root@0ae2f54b152b:~/zookeeper-cpp# cd build-debug 29 | root@0ae2f54b152b:~/zookeeper-cpp/build-debug# cmake -GNinja .. 30 | ... output ... 31 | root@0ae2f54b152b:~/zookeeper-cpp/build-debug# ninja test 32 | ... output ... 33 | 34 | This experience is pretty decent. 35 | The biggest annoyance is editing within the Docker image makes files you touch owned by *root* (I suspect there is a way 36 | to prevent this, but I am far from competent at Docker). 37 | If you use `KDevelop `_, you can use the IDE to build and debug inside of these images with 38 | `KDevelop Runtimes `_. 39 | 40 | Process 41 | ------- 42 | 43 | This library follows the `GitHub Fork + Pull Model `_. 44 | Below are the more project-specific steps. 45 | 46 | Issue Tracker 47 | ^^^^^^^^^^^^^ 48 | 49 | All work *must* be tracked in the `Issue Tracker `_, otherwise the 50 | maintainer will have no idea what is going on. 51 | Try to find an existing bug in the list of issues -- if you can't find it, open a new issue with a descriptive title and 52 | descriptive description. 53 | If you are unclear on if it should be a bug or not, mark it with a *Question* tag or just send me an 54 | `email `_. 55 | Assign the issue to yourself so I don't forget who is working on it. 56 | 57 | For more granular tracking, the issue should move across the 58 | `GitHub Project board `_. 59 | The columns of the project should be somewhat intuitive: 60 | 61 | :Backlog: 62 | Things we are planning on doing soon. 63 | 64 | :Design: 65 | System is being designed. 66 | What this usually means is the API is being written. 67 | *Please* write your API first -- it can save a lot of time in the long run. 68 | 69 | :Implementation: 70 | The component is currently being implemented. 71 | 72 | :Pull Request: 73 | There is an open pull request. 74 | 75 | :Done: 76 | Work is complete! 77 | 78 | Developing 79 | ^^^^^^^^^^ 80 | 81 | 1. Fork the repository. 82 | 2. Branch in your fork (not actually required, but generally considered a Good Idea). 83 | 3. Write your code. 84 | 4. If this is your first contribution, add yourself to ``AUTHORS`` (alphabetically). 85 | 5. Commit your code (somewhere in the commit message, be sure to mention "Issue #NN", where "NN" is the issue number you 86 | were working on). 87 | 6. Watch your tests pass for all environments in TravisCI. 88 | 7. Issue a pull request from your branch to the master branch of the main repository. 89 | 8. Close the branch in your repository (not actually required, but clean repos are nice). 90 | 91 | Sign Your Commits 92 | """"""""""""""""" 93 | 94 | When committing code, please `sign commits with GPG `_. 95 | This lets me know that work submitted by you was really created by you (security or something like that). 96 | If you always want to sign commits instead of specifying ``-S`` on the command line every time, add it to your global 97 | configuration:: 98 | 99 | $> git config --global user.signingkey ${YOUR_KEY_ID} 100 | $> git config --global commit.gpgsign true 101 | -------------------------------------------------------------------------------- /cmake/modules/BuildFunctions.cmake: -------------------------------------------------------------------------------- 1 | include(CMakeParseArguments) 2 | include(ListSplit) 3 | 4 | # build_option 5 | # Creates a build option, which is configurable via a CMake option. If the option is set to anything non-default, a 6 | # macro with the name `ZKPP_ENABLE_${NAME}` is exported with the value of `0` or `1`. 7 | # 8 | # - NAME: The name of the build option. 9 | # - DOC: Documentation to place in the configuration GUI. 10 | # - DEFAULT: The default value of the configuration (ON or OFF). 11 | # - CONFIGS_ON[]: List of build configurations this option should be ON for. 12 | # - CONFIGS_OFF[]: List of build configurations this option should be OFF for. 13 | function(build_option) 14 | set(options) 15 | set(oneValueArgs NAME DOC DEFAULT) 16 | set(multiValueArgs CONFIGS_ON CONFIGS_OFF) 17 | cmake_parse_arguments(OPT "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 18 | 19 | if(NOT DEFINED VALID_BUILD_TYPES) 20 | message(SEND_ERROR "VALID_BUILD_TYPES not set -- future build_option errors will be inaccurrate") 21 | endif() 22 | 23 | foreach(bt IN LISTS OPT_CONFIGS_ON OPT_CONFIGS_OFF) 24 | if(NOT ${bt} IN_LIST VALID_BUILD_TYPES) 25 | message(WARNING "Specified configuration for invalid CMAKE_BUILD_TYPE=${bt}") 26 | endif() 27 | endforeach() 28 | 29 | if(${CMAKE_BUILD_TYPE} IN_LIST OPT_CONFIGS_ON) 30 | set(ENABLED ON) 31 | elseif(${CMAKE_BUILD_TYPE} IN_LIST OPT_CONFIGS_OFF) 32 | set(ENABLED OFF) 33 | else() 34 | set(ENABLED ${OPT_DEFAULT}) 35 | endif() 36 | 37 | set(ZKPP_BUILD_OPTION_${OPT_NAME} ${ENABLED} 38 | CACHE BOOL "${OPT_DOC}" 39 | ) 40 | 41 | if(ZKPP_BUILD_OPTION_${OPT_NAME}) 42 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZKPP_ENABLE_${OPT_NAME}=1" PARENT_SCOPE) 43 | else() 44 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZKPP_ENABLE_${OPT_NAME}=0" PARENT_SCOPE) 45 | endif() 46 | 47 | message(STATUS " ${OPT_NAME}: ${ENABLED}") 48 | endfunction() 49 | 50 | # build_module 51 | # Adds a module to build. 52 | # 53 | # NAME: The name of this module. 54 | # PATH: The path to find the source files for this module. It is legal to specify more than one PATH in this list. 55 | # LINK_LIBRARIES: A list of libraries to link to 56 | # PROTOTYPE: If set, the module should be considered a "prototype." It will not be built by default and does not 57 | # consider warnings as errors. 58 | # NO_RECURSE: Do not search recursively. 59 | function(build_module) 60 | set(options PROTOTYPE NO_RECURSE) 61 | set(oneValueArgs NAME) 62 | set(multiValueArgs LINK_LIBRARIES PATH) 63 | cmake_parse_arguments(MODULE "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 64 | 65 | message(STATUS "${MODULE_NAME} : ${MODULE_PATH}") 66 | 67 | if(MODULE_PROTOTYPE) 68 | set(BUILD_PROTOTYPE_${MODULE_NAME} OFF 69 | CACHE BOOL "Build the '${MODULE_NAME}' program?" 70 | ) 71 | message(STATUS " ! prototype ${BUILD_PROTOTYPE_${MODULE_NAME}}") 72 | if(NOT BUILD_PROTOTYPE_${MODULE_NAME}) 73 | return() 74 | endif() 75 | endif() 76 | 77 | if(MODULE_NO_RECURSE) 78 | set(CPP_SEARCH GLOB) 79 | else() 80 | set(CPP_SEARCH GLOB_RECURSE) 81 | endif() 82 | 83 | set(all_library_cpps) 84 | set(all_library_hpps) 85 | set(main_name) 86 | foreach(subpath ${MODULE_PATH}) 87 | file(${CPP_SEARCH} local_library_cpps RELATIVE_PATH "." "${subpath}/*.cpp") 88 | file(${CPP_SEARCH} local_library_hpps RELATIVE_PATH "." "${subpath}/*.hpp") 89 | file(GLOB local_main_name RELATIVE_PATH "." "${subpath}/main.cpp") 90 | 91 | if(local_main_name) 92 | if(main_name) 93 | message(SEND_ERROR "Found main.cpp in different paths for ${MODULE_NAME} (${main_name} and ${local_main_name})") 94 | endif() 95 | 96 | set(main_name ${local_main_name}) 97 | list(REMOVE_ITEM local_library_cpps ${local_main_name}) 98 | endif() 99 | list(APPEND all_library_cpps ${local_library_cpps}) 100 | list(APPEND all_library_hpps ${local_library_hpps}) 101 | endforeach() 102 | 103 | list_split(library_test_cpps library_cpps "${all_library_cpps}" "_tests.cpp") 104 | list_split(library_test_hpps library_notest_hpps "${all_library_hpps}" "_tests.hpp") 105 | list_split(library_detail_hpps library_hpps "${library_notest_hpps}" "detail") 106 | list(APPEND library_cpps ${library_detail_hpps}) 107 | set(MODULE_TARGETS) 108 | 109 | if(main_name) 110 | message(STATUS " + executable") 111 | list(APPEND MODULE_TARGETS ${MODULE_NAME}_prog) 112 | add_executable(${MODULE_NAME}_prog ${main_name}) 113 | target_link_libraries(${MODULE_NAME}_prog ${MODULE_LINK_LIBRARIES}) 114 | set_target_properties(${MODULE_NAME}_prog 115 | PROPERTIES 116 | OUTPUT_NAME ${MODULE_NAME} 117 | ) 118 | set(${MODULE_NAME}_MAIN_SOURCES main_name PARENT_SCOPE) 119 | endif() 120 | 121 | if(library_cpps) 122 | list(LENGTH library_cpps library_cpps_length) 123 | message(STATUS " + library (${library_cpps_length})") 124 | list(APPEND MODULE_TARGETS ${MODULE_NAME}) 125 | add_library(${MODULE_NAME} SHARED ${library_cpps}) 126 | set_target_properties(${MODULE_NAME} 127 | PROPERTIES 128 | SOVERSION ${PROJECT_SO_VERSION} 129 | VERSION ${PROJECT_SO_VERSION} 130 | ) 131 | target_link_libraries(${MODULE_NAME} ${MODULE_LINK_LIBRARIES}) 132 | if(main_name) 133 | target_link_libraries(${MODULE_NAME}_prog ${MODULE_NAME}) 134 | endif() 135 | set(${MODULE_NAME}_LIBRARY_SOURCES ${library_cpps} PARENT_SCOPE) 136 | set(${MODULE_NAME}_LIBRARY_HEADERS ${library_hpps} PARENT_SCOPE) 137 | endif() 138 | 139 | if(library_test_cpps) 140 | list(LENGTH library_test_cpps library_test_cpps_length) 141 | message(STATUS " + test library (${library_test_cpps_length})") 142 | list(APPEND MODULE_TARGETS ${MODULE_NAME}_tests) 143 | add_library(${MODULE_NAME}_tests SHARED ${library_test_cpps}) 144 | set_target_properties(${MODULE_NAME}_tests 145 | PROPERTIES 146 | SOVERSION ${PROJECT_SO_VERSION} 147 | VERSION ${PROJECT_SO_VERSION} 148 | ) 149 | target_link_libraries(${MODULE_NAME}_tests zkpp-tests) 150 | if(library_cpps) 151 | target_link_libraries(${MODULE_NAME}_tests ${MODULE_NAME}) 152 | endif() 153 | target_link_libraries(zkpp-tests_prog ${MODULE_NAME}_tests) 154 | endif() 155 | 156 | if(MODULE_PROTOTYPE) 157 | foreach(target ${MODULE_TARGETS}) 158 | set_target_properties(${target} 159 | PROPERTIES 160 | COMPILE_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error" 161 | ) 162 | endforeach() 163 | endif() 164 | endfunction() 165 | -------------------------------------------------------------------------------- /cmake/modules/CodeCoverage.cmake: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 - 2015, Lars Bilke 2 | # All rights reserved. 3 | # 4 | # Redistribution and use in source and binary forms, with or without modification, 5 | # are permitted provided that the following conditions are met: 6 | # 7 | # 1. Redistributions of source code must retain the above copyright notice, this 8 | # list of conditions and the following disclaimer. 9 | # 10 | # 2. Redistributions in binary form must reproduce the above copyright notice, 11 | # this list of conditions and the following disclaimer in the documentation 12 | # and/or other materials provided with the distribution. 13 | # 14 | # 3. Neither the name of the copyright holder nor the names of its contributors 15 | # may be used to endorse or promote products derived from this software without 16 | # specific prior written permission. 17 | # 18 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | # 29 | # 30 | # 31 | # 2012-01-31, Lars Bilke 32 | # - Enable Code Coverage 33 | # 34 | # 2013-09-17, Joakim Söderberg 35 | # - Added support for Clang. 36 | # - Some additional usage instructions. 37 | # 38 | # USAGE: 39 | 40 | # 0. (Mac only) If you use Xcode 5.1 make sure to patch geninfo as described here: 41 | # http://stackoverflow.com/a/22404544/80480 42 | # 43 | # 1. Copy this file into your cmake modules path. 44 | # 45 | # 2. Add the following line to your CMakeLists.txt: 46 | # INCLUDE(CodeCoverage) 47 | # 48 | # 3. Set compiler flags to turn off optimization and enable coverage: 49 | # SET(CMAKE_CXX_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") 50 | # SET(CMAKE_C_FLAGS "-g -O0 -fprofile-arcs -ftest-coverage") 51 | # 52 | # 3. Use the function SETUP_TARGET_FOR_COVERAGE to create a custom make target 53 | # which runs your test executable and produces a lcov code coverage report: 54 | # Example: 55 | # SETUP_TARGET_FOR_COVERAGE( 56 | # my_coverage_target # Name for custom target. 57 | # test_driver # Name of the test driver executable that runs the tests. 58 | # # NOTE! This should always have a ZERO as exit code 59 | # # otherwise the coverage generation will not complete. 60 | # coverage # Name of output directory. 61 | # ) 62 | # 63 | # 4. Build a Debug build: 64 | # cmake -DCMAKE_BUILD_TYPE=Debug .. 65 | # make 66 | # make my_coverage_target 67 | # 68 | # 69 | 70 | # Check prereqs 71 | FIND_PROGRAM( GCOV_PATH gcov ) 72 | FIND_PROGRAM( LCOV_PATH lcov ) 73 | FIND_PROGRAM( GENHTML_PATH genhtml ) 74 | FIND_PROGRAM( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/tests) 75 | 76 | IF(NOT GCOV_PATH) 77 | MESSAGE(FATAL_ERROR "gcov not found! Aborting...") 78 | ENDIF() # NOT GCOV_PATH 79 | 80 | IF(NOT CMAKE_COMPILER_IS_GNUCXX) 81 | # Clang version 3.0.0 and greater now supports gcov as well. 82 | MESSAGE(WARNING "Compiler is not GNU gcc! Clang Version 3.0.0 and greater supports gcov as well, but older versions don't.") 83 | 84 | IF(NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") 85 | MESSAGE(FATAL_ERROR "Compiler is not GNU gcc! Aborting...") 86 | ENDIF() 87 | ENDIF() # NOT CMAKE_COMPILER_IS_GNUCXX 88 | 89 | SET(CMAKE_CXX_FLAGS_COVERAGE 90 | "-g -O0 --coverage -fprofile-arcs -ftest-coverage" 91 | CACHE STRING "Flags used by the C++ compiler during coverage builds." 92 | FORCE ) 93 | SET(CMAKE_C_FLAGS_COVERAGE 94 | "-g -O0 --coverage -fprofile-arcs -ftest-coverage" 95 | CACHE STRING "Flags used by the C compiler during coverage builds." 96 | FORCE ) 97 | SET(CMAKE_EXE_LINKER_FLAGS_COVERAGE 98 | "" 99 | CACHE STRING "Flags used for linking binaries during coverage builds." 100 | FORCE ) 101 | SET(CMAKE_SHARED_LINKER_FLAGS_COVERAGE 102 | "" 103 | CACHE STRING "Flags used by the shared libraries linker during coverage builds." 104 | FORCE ) 105 | MARK_AS_ADVANCED( 106 | CMAKE_CXX_FLAGS_COVERAGE 107 | CMAKE_C_FLAGS_COVERAGE 108 | CMAKE_EXE_LINKER_FLAGS_COVERAGE 109 | CMAKE_SHARED_LINKER_FLAGS_COVERAGE ) 110 | 111 | IF ( NOT (CMAKE_BUILD_TYPE STREQUAL "Debug" OR CMAKE_BUILD_TYPE STREQUAL "Coverage")) 112 | MESSAGE( WARNING "Code coverage results with an optimized (non-Debug) build may be misleading" ) 113 | ENDIF() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug" 114 | 115 | 116 | # Param _targetname The name of new the custom make target 117 | # Param _testrunner The name of the target which runs the tests. 118 | # MUST return ZERO always, even on errors. 119 | # If not, no coverage report will be created! 120 | # Param _outputname lcov output is generated as _outputname.info 121 | # HTML report is generated in _outputname/index.html 122 | # Optional fourth parameter is passed as arguments to _testrunner 123 | # Pass them in list form, e.g.: "-j;2" for -j 2 124 | FUNCTION(SETUP_TARGET_FOR_COVERAGE _targetname _testrunner _outputname) 125 | 126 | IF(NOT LCOV_PATH) 127 | MESSAGE(FATAL_ERROR "lcov not found! Aborting...") 128 | ENDIF() # NOT LCOV_PATH 129 | 130 | IF(NOT GENHTML_PATH) 131 | MESSAGE(FATAL_ERROR "genhtml not found! Aborting...") 132 | ENDIF() # NOT GENHTML_PATH 133 | 134 | # Setup target 135 | ADD_CUSTOM_TARGET(${_targetname} 136 | 137 | # Cleanup lcov 138 | ${LCOV_PATH} --directory . --zerocounters 139 | 140 | # Run tests 141 | COMMAND ${_testrunner} ${ARGV3} 142 | 143 | # Capturing lcov counters and generating report 144 | COMMAND ${LCOV_PATH} --directory . --capture --output-file ${_outputname}.info 145 | COMMAND ${LCOV_PATH} --remove ${_outputname}.info '*_tests.cpp' '*generated*' '/usr/*' --output-file ${_outputname}.info.cleaned 146 | COMMAND ${GENHTML_PATH} -o ${_outputname} ${_outputname}.info.cleaned 147 | COMMAND ${CMAKE_COMMAND} -E remove ${_outputname}.info ${_outputname}.info.cleaned 148 | 149 | WORKING_DIRECTORY ${CMAKE_BINARY_DIR} 150 | COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report." 151 | ) 152 | 153 | # Show info where to find the report 154 | ADD_CUSTOM_COMMAND(TARGET ${_targetname} POST_BUILD 155 | COMMAND ; 156 | COMMENT "Open ./${_outputname}/index.html in your browser to view the coverage report." 157 | ) 158 | 159 | ENDFUNCTION() # SETUP_TARGET_FOR_COVERAGE 160 | -------------------------------------------------------------------------------- /cmake/modules/ConfigurationSetting.cmake: -------------------------------------------------------------------------------- 1 | include(CMakeParseArguments) 2 | 3 | # configuration_setting 4 | # Creates a configuration setting, which is configurable via a CMake option. If the option is set to anything 5 | # non-default, a macro with the name `ZKPP_${NAME}_USE_${OPTION_VALUE}` is exported as `1`. 6 | # 7 | # - NAME: The name of the configuration setting. 8 | # - DOC: Documentation to place in the CMake configuration GUI. 9 | # - DEFUALT: The default value of the configuration setting (some value from OPTIONS). This value must be synced with 10 | # the C++ code or the behavior will be nonsense. 11 | # - SET: Set the value to this. If unspecified, this will simply be the same as DEFUALT. However, this can be useful in 12 | # cases where you wish to specify a non-default based on system information. 13 | # - OPTIONS[]: List of valid options to set. 14 | function(configuration_setting) 15 | set(options) 16 | set(oneValueArgs NAME DOC DEFAULT SET) 17 | set(multiValueArgs OPTIONS) 18 | cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 19 | 20 | if(NOT ARG_SET) 21 | set(ARG_SET "${ARG_DEFAULT}") 22 | endif() 23 | 24 | set(ZKPP_BUILD_SETTING_${ARG_NAME} "${ARG_SET}" 25 | CACHE STRING "${ARG_DOC}" 26 | ) 27 | if(NOT ${ZKPP_BUILD_SETTING_${ARG_NAME}} IN_LIST ARG_OPTIONS) 28 | message(SEND_ERROR "Invalid setting for ${ARG_NAME}: ${ZKPP_BUILD_SETTING_${ARG_NAME}}") 29 | endif() 30 | 31 | if(NOT ${ZKPP_BUILD_SETTING_${ARG_NAME}} STREQUAL ${ARG_DEFAULT}) 32 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DZKPP_${ARG_NAME}_USE_${ZKPP_BUILD_SETTING_${ARG_NAME}}=1" PARENT_SCOPE) 33 | endif() 34 | 35 | message(STATUS " ${ARG_NAME}: ${ZKPP_BUILD_SETTING_${ARG_NAME}}") 36 | endfunction() 37 | -------------------------------------------------------------------------------- /cmake/modules/ListSplit.cmake: -------------------------------------------------------------------------------- 1 | # list_split 2 | # Split a list into values matching or not matching a given expression. 3 | macro(list_split matching non_matching orig expression) 4 | foreach(val ${orig}) 5 | if(${val} MATCHES ${expression}) 6 | list(APPEND ${matching} ${val}) 7 | else() 8 | list(APPEND ${non_matching} ${val}) 9 | endif() 10 | endforeach() 11 | endmacro() 12 | 13 | -------------------------------------------------------------------------------- /cmake/modules/ZooKeeper.cmake: -------------------------------------------------------------------------------- 1 | # Utilities for configuring and running a ZooKeeper Server. 2 | cmake_minimum_required(VERSION 3.5) 3 | 4 | include(CMakeParseArguments) 5 | 6 | find_package(Java COMPONENTS Runtime) 7 | if(NOT Java_Runtime_FOUND) 8 | message(FATAL_ERROR "Could not find Java Runtime") 9 | endif() 10 | 11 | # execute_jar 12 | # Similar to execute_process, but drops "java -jar" in front for you so you can execute a JAR file in the same way you 13 | # would a process. 14 | macro(execute_jar) 15 | execute_process(COMMAND "${Java_JAVA_EXECUTABLE}" "-jar" ${ARGN}) 16 | endmacro() 17 | 18 | # execute_java_cp 19 | # Similar to execute_process, but drops "java -cp" in front for you so you can execute a collection of Java locations in 20 | # the same way you would a process (albeit more annoyingly). 21 | macro(execute_java_cp) 22 | execute_process(COMMAND "${Java_JAVA_EXECUTABLE}" "-cp" ${ARGN}) 23 | endmacro() 24 | 25 | find_program(IVY_JAR 26 | NAMES ivy.jar 27 | PATHS "/usr/share/java" 28 | ) 29 | if(NOT IVY_JAR) 30 | message(FATAL_ERROR "Could not find Apache Ivy") 31 | endif() 32 | 33 | # find_zookeeper_server 34 | # Get the ZooKeeper server JARs from Ivy. 35 | # 36 | # VERSION: ZooKeeper version to fetch. This can be any Ivy pattern (for example, "3.5+"). 37 | # OUTPUT_CLASSPATH: A variable to output a classpath that can be used to run the server. 38 | # 39 | # Example: 40 | # 41 | # find_zookeeper_server(VERSION "3.5+" OUTPUT_CLASSPATH ZOOKEEPER_SERVER_CLASSPATH) 42 | # execute_java_cp("${ZOOKEEPER_SERVER_CLASSPATH}" "org.apache.zookeeper.server.quorum.QuorumPeerMain" 2181 zk-data) 43 | function(find_zookeeper_server) 44 | set(options) 45 | set(oneValueArgs VERSION OUTPUT_CLASSPATH) 46 | set(multiValueArgs) 47 | cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 48 | if(NOT ARG_VERSION) 49 | message(FATAL_ERROR "You must specify a VERSION to fetch") 50 | endif() 51 | if(NOT ARG_OUTPUT_CLASSPATH) 52 | message(FATAL_ERROR "You must specify an OUTPUT_CLASSPATH") 53 | endif() 54 | 55 | file(MAKE_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ivy-cache") 56 | set(TEMP_CLASSPATH_FILE "${CMAKE_CURRENT_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/ivy-cache/zookeeper-${ARG_VERSION}.txt") 57 | 58 | # There does not appear to be a good way to tell Ivy to be reasonable about progress reporting, so we'll silence it 59 | # completely and hope the ellipsis in the status message will prevent people from thinking the system hung. 60 | message(STATUS "Fetching ZooKeeper Server ${ARG_VERSION}...") 61 | execute_jar(${IVY_JAR} "-dependency" "org.apache.zookeeper" "zookeeper" "${ARG_VERSION}" 62 | "-cachepath" "${TEMP_CLASSPATH_FILE}" 63 | OUTPUT_VARIABLE IVY_FETCH_OUTPUT 64 | ERROR_VARIABLE IVY_FETCH_ERROR 65 | ) 66 | if(EXISTS "${TEMP_CLASSPATH_FILE}") 67 | file(READ "${TEMP_CLASSPATH_FILE}" CLASSPATH) 68 | string(STRIP "${CLASSPATH}" CLASSPATH) 69 | message(STATUS " > SUCCESS!") 70 | set(${ARG_OUTPUT_CLASSPATH} "${CLASSPATH}" PARENT_SCOPE) 71 | else() 72 | message(SEND_ERROR "Could not fetch ZooKeeper Server ${ARG_VERSION}\n" 73 | "Ivy fetch output:\n${IVY_FETCH_OUTPUT}\n" 74 | "Ivy fetch errors:\n${IVY_FETCH_ERROR}" 75 | ) 76 | endif() 77 | endfunction() 78 | -------------------------------------------------------------------------------- /config/dev-env: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Create a build environment. 3 | 4 | PROJECT_ROOT=$(readlink -f $(dirname $0)/..) 5 | 6 | COMMAND=/bin/bash 7 | DISTRO= 8 | 9 | function show_usage { 10 | echo "$0: Run a program in a Docker environment." 11 | echo "" 12 | echo "Options:" 13 | echo "" 14 | echo " --distro DISTRO:" 15 | echo " The Linux distro to create the base container." 16 | echo "" 17 | echo " Available options:" 18 | printf " " 19 | printf " %s" $(ls $(dirname $0)/docker) 20 | echo "" 21 | echo " --:" 22 | echo " Stop processing arguments and pass the remaining arguments to the" 23 | echo " container." 24 | echo "" 25 | echo "Example:" 26 | echo "" 27 | echo " Enter into a bash shell:" 28 | echo " $> $0 $(ls $(dirname $0)/docker | sort --random-sort | head -1)" 29 | echo "" 30 | echo " Build the package for a given distro:" 31 | echo " $> $0 --distro=$(ls $(dirname $0)/docker | sort --random-sort | head -1) -- ./config/make-package" 32 | } 33 | 34 | if [[ $# -lt 1 ]]; then 35 | show_usage 36 | exit 1 37 | else 38 | UNRECOGNIZED=0 39 | while [[ $# -gt 0 ]]; do 40 | key="$1" 41 | 42 | case $key in 43 | --distro) 44 | DISTRO="$2" 45 | shift 2 46 | ;; 47 | --distro=*) 48 | DISTRO="${key/--distro=/}" 49 | shift 50 | ;; 51 | --help) 52 | show_usage 53 | exit 1 54 | ;; 55 | --) 56 | shift 57 | COMMAND=$@ 58 | break 59 | ;; 60 | *) 61 | if [[ -z "${DISTRO}" ]]; then 62 | DISTRO="$key" 63 | else 64 | echo "Unrecognized option: $key" 65 | UNRECOGNIZED=1 66 | fi 67 | shift 68 | ;; 69 | esac 70 | done 71 | fi 72 | 73 | if [[ -z "${DISTRO}" ]]; then 74 | echo "Must set distro." 75 | show_usage 76 | exit 1 77 | fi 78 | 79 | IMAGE_NAME=dev/zookeeper-cpp/${DISTRO} 80 | DOCKER_DIR=${PROJECT_ROOT}/config/docker/${DISTRO} 81 | 82 | if ! [[ -e ${DOCKER_DIR}/Dockerfile ]]; then 83 | echo "Specified distro \"${DISTRO}\" does not have a Dockerfile" 84 | echo "" 85 | show_usage 86 | exit 1 87 | fi 88 | 89 | docker build ${DOCKER_DIR} -t ${IMAGE_NAME} 90 | exec docker run \ 91 | --rm \ 92 | -v ${PROJECT_ROOT}:/root/zookeeper-cpp \ 93 | -w /root/zookeeper-cpp \ 94 | --security-opt seccomp=unconfined \ 95 | -it ${IMAGE_NAME} \ 96 | ${COMMAND} 97 | -------------------------------------------------------------------------------- /config/docker/debian-10/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:10 2 | LABEL maintainer="Travis Gockel " 3 | 4 | RUN apt-get update \ 5 | && apt-get install --yes \ 6 | cmake \ 7 | g++ \ 8 | grep \ 9 | googletest \ 10 | ivy \ 11 | lcov \ 12 | libgtest-dev \ 13 | libzookeeper-mt-dev \ 14 | ninja-build 15 | 16 | CMD ["/root/zookeeper-cpp/config/run-tests"] 17 | -------------------------------------------------------------------------------- /config/docker/opensuse-15/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM opensuse/leap:15 2 | LABEL maintainer="Travis Gockel " 3 | 4 | RUN zypper addrepo -f "https://download.opensuse.org/repositories/server:/database/openSUSE_Leap_15.2/" "serverdatabase" \ 5 | && zypper --no-gpg-checks \ 6 | install -y \ 7 | apache-ivy \ 8 | cmake \ 9 | grep \ 10 | gcc-c++ \ 11 | git \ 12 | googletest-devel \ 13 | java-1_8_0-openjdk \ 14 | lcov \ 15 | libzookeeper2-devel \ 16 | ninja 17 | 18 | CMD ["/root/zookeeper-cpp/config/run-tests"] 19 | -------------------------------------------------------------------------------- /config/docker/ubuntu-16.04/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:16.04 2 | LABEL maintainer="Travis Gockel " 3 | 4 | RUN apt-get update 5 | RUN apt-get install --yes software-properties-common 6 | RUN add-apt-repository --yes ppa:ubuntu-toolchain-r/test 7 | 8 | # You might ask why g++-6 is installed, even when we intend to build with GCC 7. It turns out that Coveralls won't work 9 | # if it isn't installed due to...something. Instead of root causing the issue, we're going to ignore it and hope it gets 10 | # fixed in a future release. 11 | RUN apt-get update \ 12 | && apt-get install --yes \ 13 | cmake \ 14 | grep \ 15 | g++-6 \ 16 | g++-7 \ 17 | ivy \ 18 | libgtest-dev \ 19 | libzookeeper-mt-dev \ 20 | ninja-build 21 | 22 | # Code Coverage 23 | RUN apt-get install --yes \ 24 | git \ 25 | lcov \ 26 | python-pip 27 | RUN pip install --upgrade pip 28 | RUN pip install \ 29 | cpp-coveralls \ 30 | pyyaml 31 | 32 | RUN update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-7 99 33 | 34 | CMD ["/root/zookeeper-cpp/config/run-tests"] 35 | -------------------------------------------------------------------------------- /config/docker/ubuntu-18.04/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | LABEL maintainer="Travis Gockel " 3 | 4 | RUN apt-get update \ 5 | && apt-get install --yes \ 6 | cmake \ 7 | grep \ 8 | googletest \ 9 | g++-7 \ 10 | ivy \ 11 | lcov \ 12 | libgtest-dev \ 13 | libzookeeper-mt-dev \ 14 | ninja-build 15 | 16 | RUN update-alternatives --install /usr/bin/c++ c++ /usr/bin/g++-7 99 17 | 18 | CMD ["/root/zookeeper-cpp/config/run-tests"] 19 | -------------------------------------------------------------------------------- /config/docker/ubuntu-20.04/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | LABEL maintainer="Travis Gockel " 3 | 4 | RUN apt-get update \ 5 | && DEBIAN_FRONTEND=noninteractive \ 6 | apt-get install --yes \ 7 | cmake \ 8 | g++ \ 9 | grep \ 10 | googletest \ 11 | ivy \ 12 | lcov \ 13 | libgtest-dev \ 14 | libzookeeper-mt-dev \ 15 | ninja-build 16 | 17 | CMD ["/root/zookeeper-cpp/config/run-tests"] 18 | -------------------------------------------------------------------------------- /config/make-doxygen: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | mkdir -p build/doc 3 | doxygen doc/Doxyfile 4 | -------------------------------------------------------------------------------- /config/make-package: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | PROJECT_ROOT=$(readlink -f $(dirname $0)/..) 4 | BUILD_DIR="$BUILD_DIR" 5 | COPY_TO="" 6 | DISTRO="$DISTRO" 7 | PACKAGE_PREFIX="" 8 | 9 | function show_usage { 10 | echo "$0: Create a package for this operating system." 11 | echo "" 12 | echo "Usage: $0 [OPTION]..." 13 | echo "" 14 | echo "Options:" 15 | echo "" 16 | echo " --build-dir BUILD_DIR:" 17 | echo " Set the build output directory. By default, this is build-release or" 18 | echo " build-release-\${DISTRO} if --distro was set." 19 | echo " --copy-to COPY_TO:" 20 | echo " Copy the generated packages to the specified folder." 21 | echo " --distro DISTRO:" 22 | echo " Set the distro name." 23 | echo " --dockerize DISTRO:" 24 | echo " Perform the build inside of a dev-env-created Docker container. If used," 25 | echo " this must be the first option. The --distro flag is automatically set to" 26 | echo " the same value." 27 | echo " --package-prefix PREFIX:" 28 | echo " Generated packages will be renamed with this prefix. By default, this" 29 | echo " will be \"\${DISTRO}-\" if --distro was set." 30 | echo "" 31 | echo "Examples:" 32 | echo "" 33 | echo " Create a package inside an Ubuntu 18.04 container, copying the DEBs to a" 34 | echo " packages folder." 35 | echo " $> $0 --dockerize=ubuntu-18.04 --copy-to=packages" 36 | } 37 | 38 | UNRECOGNIZED=0 39 | DOCKERIZE=0 40 | FIRST=1 41 | while [[ $# -gt 0 ]]; do 42 | key="$1" 43 | 44 | case $key in 45 | --build-dir) 46 | BUILD_DIR="$2" 47 | shift 2 48 | ;; 49 | --build-dir=*) 50 | BUILD_DIR="${key/--build-dir=/}" 51 | shift 52 | ;; 53 | --copy-to) 54 | COPY_TO="$2" 55 | shift 2 56 | ;; 57 | --copy-to=*) 58 | COPY_TO="${key/--copy-to=/}" 59 | shift 60 | ;; 61 | --distro) 62 | DISTRO="$2" 63 | shift 2 64 | ;; 65 | --distro=*) 66 | DISTRO="${key/--distro=/}" 67 | shift 68 | ;; 69 | --dockerize) 70 | DISTRO="$2" 71 | DOCKERIZE=1 72 | shift 2 73 | break 74 | ;; 75 | --dockerize=*) 76 | DISTRO="${key/--dockerize=/}" 77 | DOCKERIZE=1 78 | shift 79 | break 80 | ;; 81 | --help) 82 | show_usage 83 | exit 1 84 | ;; 85 | --package-prefix) 86 | PACKAGE_PREFIX="$2" 87 | shift 2 88 | ;; 89 | --package-prefix=*) 90 | PACKAGE_PREFIX="${key/--package-prefix=/}" 91 | shift 92 | ;; 93 | *) 94 | echo "Unrecognized option: $key" 95 | UNRECOGNIZED=1 96 | shift 97 | ;; 98 | esac 99 | FIRST=0 100 | done 101 | 102 | if [[ ${UNRECOGNIZED} -ne 0 ]]; then 103 | show_usage 104 | exit 1 105 | fi 106 | 107 | if [[ ${DOCKERIZE} -eq 1 ]]; then 108 | if [[ ${FIRST} -eq 0 ]]; then 109 | echo "If using --dockerize, it must be the first argument." 110 | exit 1 111 | fi 112 | 113 | exec ${PROJECT_ROOT}/config/dev-env "${DISTRO}" -- \ 114 | ./config/make-package \ 115 | --distro="${DISTRO}" \ 116 | $@ 117 | fi 118 | 119 | if [[ -z "${BUILD_DIR}" ]]; then 120 | if [[ -z "${DISTRO}" ]]; then 121 | BUILD_DIR="build-release" 122 | else 123 | BUILD_DIR="build-release-${DISTRO}" 124 | fi 125 | fi 126 | 127 | if [[ -z "${PACKAGE_PREFIX}" ]]; then 128 | if [[ -n "${DISTRO}" ]]; then 129 | PACKAGE_PREFIX="${DISTRO}-" 130 | fi 131 | fi 132 | 133 | if $(hash rpm 2>/dev/null); then 134 | PACKAGE_SYSTEM=RPM 135 | PACKAGE_SUFFIX=rpm 136 | 137 | function show_contents { 138 | rpm -qlp "$1" 139 | } 140 | elif $(hash dpkg 2>/dev/null); then 141 | PACKAGE_SYSTEM=DEB 142 | PACKAGE_SUFFIX=deb 143 | 144 | function show_contents { 145 | dpkg --contents "${pkg_file}" 146 | } 147 | else 148 | echo "Unknown packaging system for this operating system" 149 | exit 2 150 | fi 151 | 152 | if [[ -e ${BUILD_DIR} ]]; then 153 | echo "Build directory (${BUILD_DIR}) unclean -- deleting it" 154 | rm -rf ${BUILD_DIR} 155 | fi 156 | mkdir ${BUILD_DIR} 157 | 158 | cd ${BUILD_DIR} 159 | cmake .. -DZKPP_PACKAGE_SYSTEM=${PACKAGE_SYSTEM} -DCMAKE_BUILD_TYPE=Release -GNinja 160 | ninja package 161 | cd - 162 | 163 | for pkg_file in $(find "${BUILD_DIR}/install" -maxdepth 1 -name "*.${PACKAGE_SUFFIX}"); do 164 | if [[ -n "${PACKAGE_PREFIX}" ]]; then 165 | new_pkg_file="$(dirname -- ${pkg_file})/${PACKAGE_PREFIX}$(basename -- ${pkg_file})" 166 | mv "${pkg_file}" "${new_pkg_file}" 167 | pkg_file="${new_pkg_file}" 168 | fi 169 | 170 | echo "PACKAGE ${pkg_file}" 171 | show_contents "${pkg_file}" 172 | 173 | if [[ -n "${COPY_TO}" ]]; then 174 | cp "${pkg_file}" "${COPY_TO}" 175 | fi 176 | done 177 | -------------------------------------------------------------------------------- /config/make-packages: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | PROJECT_ROOT=$(readlink -f $(dirname $0)/..) 4 | 5 | function show_usage { 6 | echo "$0: Create multiple packages." 7 | echo "" 8 | echo "Usage: $0 [OPTION]... [DISTRO]..." 9 | echo "" 10 | echo "Options:" 11 | echo "" 12 | echo " --all" 13 | echo " Create packages for all known distributions. If this is specified, you" 14 | echo " are not allowed to specify individual distributions." 15 | echo " --copy-to COPY_TO:" 16 | echo " Copy the generated packages to the specified folder." 17 | echo " --:" 18 | echo " Stop processing and pass the remaining arguments to make-package." 19 | } 20 | 21 | COPY_TO= 22 | DISTROS=() 23 | ALL=0 24 | while [[ $# -gt 0 ]]; do 25 | key="$1" 26 | 27 | case $key in 28 | --all) 29 | ALL=1 30 | shift 31 | ;; 32 | --copy-to) 33 | COPY_TO="$2" 34 | shift 2 35 | ;; 36 | --copy-to=*) 37 | COPY_TO="${key/--copy-to=/}" 38 | shift 39 | ;; 40 | --help) 41 | show_usage 42 | exit 1 43 | ;; 44 | --) 45 | # No shift: want to use the -- in the call 46 | break 47 | ;; 48 | *) 49 | DISTROS+=(${key}) 50 | shift 51 | ;; 52 | esac 53 | done 54 | 55 | if [[ ${ALL} -eq 1 ]]; then 56 | if [[ ${#DISTROS[@]} -ne 0 ]]; then 57 | echo "Specified --all, but also distributions: ${DISTROS[@]}" 58 | echo "" 59 | show_usage 60 | exit 1 61 | fi 62 | 63 | # All distros with working package systems. 64 | DISTROS=(ubuntu-16.04 ubuntu-17.10 ubuntu-18.04 ubuntu-18.10 debian-9) 65 | fi 66 | 67 | if [[ -n "${COPY_TO}" ]]; then 68 | mkdir -p "${COPY_TO}" 69 | COPY_TO="--copy-to=${COPY_TO}" 70 | fi 71 | 72 | for distro in ${DISTROS[@]}; do 73 | echo "BUILDING PACKAGE FOR $distro" 74 | ${PROJECT_ROOT}/config/make-package --dockerize=${distro} ${COPY_TO} $@ 75 | done 76 | -------------------------------------------------------------------------------- /config/publish-doxygen: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # Settings 4 | REPO_PATH=git@github.com:tgockel/zookeeper-cpp.git 5 | HTML_PATH=build/doc/html 6 | COMMIT_USER="Documentation Builder" 7 | COMMIT_EMAIL="travis@gockelhut.com" 8 | CHANGESET=$(git rev-parse --verify HEAD) 9 | 10 | # Get a clean version of the HTML documentation repo. 11 | rm -rf ${HTML_PATH} 12 | mkdir -p ${HTML_PATH} 13 | git clone -b gh-pages "${REPO_PATH}" --single-branch ${HTML_PATH} 14 | 15 | # rm all the files through git to prevent stale files. 16 | cd ${HTML_PATH} 17 | git rm -rf *.html *.js *.png *.css search 18 | cd - 19 | 20 | # Generate the HTML documentation. 21 | ./config/make-doxygen 22 | 23 | # Create and commit the documentation repo. 24 | cd ${HTML_PATH} 25 | git add . 26 | git config user.name "${COMMIT_USER}" 27 | git config user.email "${COMMIT_EMAIL}" 28 | git commit -m "Automated documentation build for changeset ${CHANGESET}." 29 | git push origin gh-pages 30 | cd - 31 | -------------------------------------------------------------------------------- /config/run-tests: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | PROJECT_ROOT=$(readlink -f $(dirname $0)/..) 4 | BUILD_DIR=${PROJECT_ROOT}/build-ci 5 | echo "${DISTRO} ${PROJECT_ROOT}" 6 | c++ --version 7 | 8 | if [[ -e ${BUILD_DIR} ]]; then 9 | echo "Build directory (${BUILD_DIR}) unclean -- deleting it" 10 | rm -rf ${BUILD_DIR} 11 | fi 12 | mkdir ${BUILD_DIR} 13 | cd ${BUILD_DIR} 14 | 15 | if $(hash lcov 2>/dev/null); then 16 | # TODO(https://github.com/tgockel/zookeeper-cpp/issues/42): Disabling Coveralls coverage for now. 17 | COVERAGE=0 18 | else 19 | COVERAGE=0 20 | fi 21 | 22 | cmake .. -DZKPP_BUILD_OPTION_CODE_COVERAGE=${COVERAGE} 23 | make VERBOSE=1 24 | make test 25 | 26 | if [[ ${COVERAGE} -eq 1 ]]; then 27 | ${PROJECT_ROOT}/config/upload-coverage ${BUILD_DIR} 28 | fi 29 | -------------------------------------------------------------------------------- /config/travisci_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tgockel/zookeeper-cpp/425dfe0a5f509b9f8d3165d74d1022406c3967d2/config/travisci_rsa.enc -------------------------------------------------------------------------------- /config/upload-coverage: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | # TODO: 99% sure there is a better way to do this 4 | GCC_VERSION_MAJOR=$(c++ --version | grep -Po '\d\.\d\.\d' | grep -Po '^\d+' | head -1) 5 | 6 | ls -l $1/../coveralls-repo-token 7 | COVERALLS_REPO_TOKEN=$(cat $1/../coveralls-repo-token) 8 | TRAVIS=true 9 | CI=true 10 | 11 | coveralls \ 12 | --repo-token="${COVERALLS_REPO_TOKEN}" \ 13 | --build-root $1 \ 14 | --exclude-pattern '_test\.cpp$' \ 15 | --exclude-pattern bits \ 16 | --gcov-options '\-lp' --gcov "gcov-${GCC_VERSION_MAJOR}" 17 | -------------------------------------------------------------------------------- /install/deb/libzkpp-dev/control.in: -------------------------------------------------------------------------------- 1 | Package: libzkpp@PROJECT_SO_VERSION@-dev 2 | Version: @PROJECT_PACKAGE_VERSION@ 3 | Priority: optional 4 | Maintainer: Travis Gockel 5 | Architecture: all 6 | Section: libdevel 7 | Depends: libzkpp@PROJECT_SO_VERSION@ (= @PROJECT_PACKAGE_VERSION@) 8 | Description: development files for zkpp 9 | A ZooKeeper client for C++. 10 | -------------------------------------------------------------------------------- /install/deb/libzkpp-server-dev/control.in: -------------------------------------------------------------------------------- 1 | Package: libzkpp-server@PROJECT_SO_VERSION@-dev 2 | Version: @PROJECT_PACKAGE_VERSION@ 3 | Priority: optional 4 | Maintainer: Travis Gockel 5 | Architecture: all 6 | Section: libdevel 7 | Depends: libzkpp@PROJECT_SO_VERSION@-dev (= @PROJECT_PACKAGE_VERSION@), 8 | libzkpp-server@PROJECT_SO_VERSION@ (= @PROJECT_PACKAGE_VERSION@) 9 | Description: development files for zkpp_server@PROJECT_SO_VERSION@ 10 | Control a ZooKeeper Java process on a single machine. 11 | -------------------------------------------------------------------------------- /install/deb/libzkpp-server/control.in: -------------------------------------------------------------------------------- 1 | Package: libzkpp-server@PROJECT_SO_VERSION@ 2 | Version: @PROJECT_PACKAGE_VERSION@ 3 | Priority: optional 4 | Maintainer: Travis Gockel 5 | Architecture: @PROJECT_BUILD_ARCHITECTURE@ 6 | Section: libs 7 | Depends: libzkpp@PROJECT_SO_VERSION@ (= @PROJECT_PACKAGE_VERSION@) 8 | Build-Depends: debhelper (>= 9), cmake (>= 3.5) 9 | Homepage: https://tgockel.github.io/zookeeper-cpp/ 10 | Description: Control a ZooKeeper Java process on a single machine. 11 | -------------------------------------------------------------------------------- /install/deb/libzkpp-server/postinst.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | update-alternatives --install @CMAKE_INSTALL_PREFIX@/lib/libzkpp-server.so libzkpp-server @CMAKE_INSTALL_PREFIX@/lib/libzkpp-server.so.@PROJECT_SO_VERSION@ 10 4 | -------------------------------------------------------------------------------- /install/deb/libzkpp/control.in: -------------------------------------------------------------------------------- 1 | Package: libzkpp@PROJECT_SO_VERSION@ 2 | Version: @PROJECT_PACKAGE_VERSION@ 3 | Priority: optional 4 | Maintainer: Travis Gockel 5 | Architecture: @PROJECT_BUILD_ARCHITECTURE@ 6 | Section: libs 7 | Depends: libzookeeper-mt2 (>= 3.4), libc6 (>= 2.18), libstdc++6 (>= 7-20170407) 8 | Build-Depends: debhelper (>= 9), cmake (>= 3.5), libzookeeper-mt-dev (>= 3.4) 9 | Homepage: https://tgockel.github.io/zookeeper-cpp/ 10 | Description: A ZooKeeper client for C++. 11 | -------------------------------------------------------------------------------- /install/deb/libzkpp/postinst.in: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | update-alternatives --install @CMAKE_INSTALL_PREFIX@/lib/libzkpp.so libzkpp @CMAKE_INSTALL_PREFIX@/lib/libzkpp.so.@PROJECT_SO_VERSION@ 10 4 | -------------------------------------------------------------------------------- /install/deb/libzkpp/shlibs.in: -------------------------------------------------------------------------------- 1 | libzkpp 0.2 libzkpp0.2 (>= 0.2.3) 2 | -------------------------------------------------------------------------------- /src/zk/acl.cpp: -------------------------------------------------------------------------------- 1 | #include "acl.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace zk 9 | { 10 | 11 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 12 | // permission // 13 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | 15 | std::ostream& operator<<(std::ostream& os, const permission& self) 16 | { 17 | if (self == permission::none) 18 | return os << "none"; 19 | else if (self == permission::all) 20 | return os << "all"; 21 | 22 | bool first = true; 23 | auto tick = [&] { return std::exchange(first, false) ? "" : "|"; }; 24 | if (allows(self, permission::read)) os << tick() << "read"; 25 | if (allows(self, permission::write)) os << tick() << "write"; 26 | if (allows(self, permission::create)) os << tick() << "create"; 27 | if (allows(self, permission::erase)) os << tick() << "erase"; 28 | if (allows(self, permission::admin)) os << tick() << "admin"; 29 | 30 | return os; 31 | } 32 | 33 | std::string to_string(const permission& self) 34 | { 35 | std::ostringstream os; 36 | os << self; 37 | return os.str(); 38 | } 39 | 40 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 41 | // acl_rule // 42 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 43 | 44 | acl_rule::acl_rule(std::string scheme, std::string id, permission permissions) : 45 | _scheme(std::move(scheme)), 46 | _id(std::move(id)), 47 | _permissions(permissions) 48 | { } 49 | 50 | acl_rule::~acl_rule() noexcept 51 | { } 52 | 53 | std::size_t hash(const acl_rule& self) 54 | { 55 | return std::hash{}(self.scheme()) 56 | ^ std::hash{}(self.id()) 57 | ^ std::hash{}(static_cast(self.permissions())); 58 | } 59 | 60 | bool operator==(const acl_rule& lhs, const acl_rule& rhs) 61 | { 62 | return lhs.scheme() == rhs.scheme() 63 | && lhs.id() == rhs.id() 64 | && lhs.permissions() == rhs.permissions(); 65 | } 66 | 67 | bool operator!=(const acl_rule& lhs, const acl_rule& rhs) 68 | { 69 | return !(lhs == rhs); 70 | } 71 | 72 | bool operator< (const acl_rule& lhs, const acl_rule& rhs) 73 | { 74 | return std::tie(lhs.scheme(), lhs.id(), lhs.permissions()) < std::tie(rhs.scheme(), rhs.id(), rhs.permissions()); 75 | } 76 | 77 | bool operator<=(const acl_rule& lhs, const acl_rule& rhs) 78 | { 79 | return !(rhs < lhs); 80 | } 81 | 82 | bool operator> (const acl_rule& lhs, const acl_rule& rhs) 83 | { 84 | return rhs < lhs; 85 | } 86 | 87 | bool operator>=(const acl_rule& lhs, const acl_rule& rhs) 88 | { 89 | return !(lhs < rhs); 90 | } 91 | 92 | std::ostream& operator<<(std::ostream& os, const acl_rule& self) 93 | { 94 | os << '(' << self.scheme(); 95 | if (!self.id().empty()) 96 | os << ':' << self.id(); 97 | os << ", " << self.permissions() << ')'; 98 | return os; 99 | } 100 | 101 | std::string to_string(const acl_rule& self) 102 | { 103 | std::ostringstream os; 104 | os << self; 105 | return os.str(); 106 | } 107 | 108 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 109 | // acl // 110 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 111 | 112 | acl::acl(std::vector rules) noexcept : 113 | _impl(std::move(rules)) 114 | { } 115 | 116 | acl::~acl() noexcept 117 | { } 118 | 119 | bool operator==(const acl& lhs, const acl& rhs) 120 | { 121 | return std::equal(lhs.cbegin(), lhs.cend(), rhs.cbegin(), rhs.cend()); 122 | } 123 | 124 | bool operator!=(const acl& lhs, const acl& rhs) 125 | { 126 | return !(lhs == rhs); 127 | } 128 | 129 | std::ostream& operator<<(std::ostream& os, const acl& self) 130 | { 131 | os << '['; 132 | bool first = true; 133 | for (const auto& x : self) 134 | { 135 | if (first) 136 | first = false; 137 | else 138 | os << ", "; 139 | os << x; 140 | } 141 | return os << ']'; 142 | } 143 | 144 | std::string to_string(const acl& self) 145 | { 146 | std::ostringstream os; 147 | os << self; 148 | return os.str(); 149 | } 150 | 151 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 152 | // acls // 153 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 154 | 155 | const acl& acls::creator_all() 156 | { 157 | static acl instance = { { "auth", "", permission::all } }; 158 | return instance; 159 | } 160 | 161 | const acl& acls::open_unsafe() 162 | { 163 | static acl instance = { { "world", "anyone", permission::all } }; 164 | return instance; 165 | } 166 | 167 | const acl& acls::read_unsafe() 168 | { 169 | static acl instance = { { "world", "anyone", permission::read } }; 170 | return instance; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/zk/acl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "forwards.hpp" 12 | 13 | namespace zk 14 | { 15 | 16 | /// \addtogroup Client 17 | /// \{ 18 | 19 | /// Describes the ability of a user to perform a certain action. Permissions can be mixed together like integers with 20 | /// \c | and \c &. 21 | enum class permission : unsigned int 22 | { 23 | none = 0b00000, //!< No permissions are set (server could have been configured without ACL support). 24 | read = 0b00001, //!< You can access the data of a node and can list its children. 25 | write = 0b00010, //!< You can set the data of a node. 26 | create = 0b00100, //!< You can create a child node. 27 | erase = 0b01000, //!< You can erase a child node (but not necessarily this one). 28 | admin = 0b10000, //!< You can alter permissions on this node. 29 | all = 0b11111, //!< You can do anything. 30 | }; 31 | 32 | /// \{ 33 | /// Set union operation of \ref permission. 34 | inline constexpr permission operator|(permission a, permission b) 35 | { 36 | return permission(static_cast(a) | static_cast(b)); 37 | } 38 | 39 | inline constexpr permission& operator|=(permission& self, permission mask) 40 | { 41 | return self = self | mask; 42 | } 43 | /// \} 44 | 45 | /// \{ 46 | /// Set intersection operation of \ref permission. 47 | inline constexpr permission operator&(permission a, permission b) 48 | { 49 | return permission(static_cast(a) & static_cast(b)); 50 | } 51 | 52 | inline constexpr permission& operator&=(permission& self, permission mask) 53 | { 54 | return self = self & mask; 55 | } 56 | /// \} 57 | 58 | /// Set inverse operation of \ref permission. This is not exactly the bitwise complement of \a a, as the returned value 59 | /// will only include bits set that are valid in \ref permission discriminants. 60 | inline constexpr permission operator~(permission a) 61 | { 62 | return permission(~static_cast(a)) & permission::all; 63 | } 64 | 65 | /// Check that \a self allows it \a perform all operations. For example, 66 | /// `allows(permission::read | permission::write, permission::read)` will be \c true, as `read|write` is allowed to 67 | /// \c read. 68 | inline constexpr bool allows(permission self, permission perform) 69 | { 70 | return (self & perform) == perform; 71 | } 72 | 73 | std::ostream& operator<<(std::ostream&, const permission&); 74 | 75 | std::string to_string(const permission&); 76 | 77 | /// An individual rule in an \ref acl. It consists of a \ref scheme and \ref id pair to identify the who and a 78 | /// \ref permission set to determine what they are allowed to do. 79 | /// 80 | /// See "Builtin ACL 81 | /// Schemes" in the ZooKeeper Programmer's Guide for more information. 82 | class acl_rule final 83 | { 84 | public: 85 | /// Create an ACL under the given \a scheme and \a id with the given \a permissions. 86 | acl_rule(std::string scheme, std::string id, permission permissions); 87 | 88 | acl_rule(const acl_rule&) = default; 89 | acl_rule& operator=(const acl_rule&) = default; 90 | 91 | acl_rule(acl_rule&&) = default; 92 | acl_rule& operator=(acl_rule&&) = default; 93 | 94 | ~acl_rule() noexcept; 95 | 96 | /// The authentication scheme this list is used for. The most common scheme is `"auth"`, which allows any 97 | /// authenticated user to perform actions (see \ref acls::creator_all). 98 | /// 99 | /// ZooKeeper's authentication system is extensible, but the majority of use cases are covered by the built-in 100 | /// schemes: 101 | /// 102 | /// - \c "world" -- This has a single ID \c "anyone" that represents any user of the system. The ACLs 103 | /// \ref acls::open_unsafe and \ref acls::read_unsafe use the \c "world" scheme. 104 | /// - \c "auth" -- This represents any authenticated user. The \c id field is unused. The ACL \ref acls::creator_all 105 | /// uses the \c "auth" scheme. 106 | /// - \c "digest" -- This uses a \c "${username}:${password}" string to generate MD5 hash which is then used as an 107 | /// identity. Authentication is done by sending the string in clear text. When used in the ACL, the expression 108 | /// will be the \c "${username}:${digest}", where \c digest is the base 64 encoded SHA1 digest of \c password. 109 | /// - \c "ip" -- This uses the client host IP as an identity. The \c id expression is an IP address or CIDR netmask, 110 | /// which will be matched against the client identity. 111 | const std::string& scheme() const 112 | { 113 | return _scheme; 114 | } 115 | 116 | /// The ID of the user under the \ref scheme. For example, with the \c "ip" \c scheme, this is an IP address or CIDR 117 | /// netmask. 118 | const std::string& id() const 119 | { 120 | return _id; 121 | } 122 | 123 | /// The permissions associated with this ACL. 124 | const permission& permissions() const 125 | { 126 | return _permissions; 127 | } 128 | 129 | private: 130 | std::string _scheme; 131 | std::string _id; 132 | permission _permissions; 133 | }; 134 | 135 | /// Compute a hash for the given \a rule. 136 | std::size_t hash(const acl_rule& rule); 137 | 138 | [[gnu::pure]] bool operator==(const acl_rule& lhs, const acl_rule& rhs); 139 | [[gnu::pure]] bool operator!=(const acl_rule& lhs, const acl_rule& rhs); 140 | [[gnu::pure]] bool operator< (const acl_rule& lhs, const acl_rule& rhs); 141 | [[gnu::pure]] bool operator<=(const acl_rule& lhs, const acl_rule& rhs); 142 | [[gnu::pure]] bool operator> (const acl_rule& lhs, const acl_rule& rhs); 143 | [[gnu::pure]] bool operator>=(const acl_rule& lhs, const acl_rule& rhs); 144 | 145 | std::ostream& operator<<(std::ostream&, const acl_rule&); 146 | 147 | std::string to_string(const acl_rule&); 148 | 149 | /// An access control list is a wrapper around \ref acl_rule instances. In general, the ACL system is similar to UNIX 150 | /// file access permissions, where znodes act as files. Unlike UNIX, each znode can have any number of ACLs to 151 | /// correspond with the potentially limitless (and pluggable) authentication schemes. A more surprising difference is 152 | /// that ACLs are not recursive: If \c /path is only readable by a single user, but \c /path/sub is world-readable, then 153 | /// anyone will be able to read \c /path/sub. 154 | /// 155 | /// See ZooKeeper 156 | /// Programmer's Guide for more information. 157 | /// 158 | /// \see acls 159 | class acl final 160 | { 161 | public: 162 | using iterator = std::vector::iterator; 163 | using const_iterator = std::vector::const_iterator; 164 | using size_type = std::size_t; 165 | 166 | public: 167 | /// Create an empty ACL. Keep in mind that an empty ACL is an illegal ACL. 168 | acl() = default; 169 | 170 | /// Create an instance with the provided \a rules. 171 | acl(std::vector rules) noexcept; 172 | 173 | /// Create an instance with the provided \a rules. 174 | acl(std::initializer_list rules) : 175 | acl(std::vector(rules)) 176 | { } 177 | 178 | acl(const acl&) = default; 179 | acl& operator=(const acl&) = default; 180 | 181 | acl(acl&&) = default; 182 | acl& operator=(acl&&) = default; 183 | 184 | ~acl() noexcept; 185 | 186 | /// The number of rules in this ACL. 187 | size_type size() const { return _impl.size(); } 188 | 189 | /// \{ 190 | /// Get the rule at the given \a idx. 191 | const acl_rule& operator[](size_type idx) const { return _impl[idx]; } 192 | acl_rule& operator[](size_type idx) { return _impl[idx]; } 193 | /// \} 194 | 195 | /// \{ 196 | /// Get the rule at the given \a idx. 197 | /// 198 | /// \throws std::out_of_range if the \a idx is larger than \ref size. 199 | const acl_rule& at(size_type idx) const { return _impl.at(idx); } 200 | acl_rule& at(size_type idx) { return _impl.at(idx); } 201 | /// \} 202 | 203 | /// \{ 204 | /// Get an iterator to the beginning of the rule list. 205 | iterator begin() { return _impl.begin(); } 206 | const_iterator begin() const { return _impl.begin(); } 207 | const_iterator cbegin() const { return _impl.begin(); } 208 | /// \} 209 | 210 | /// \{ 211 | /// Get an iterator to the end of the rule list. 212 | iterator end() { return _impl.end(); } 213 | const_iterator end() const { return _impl.end(); } 214 | const_iterator cend() const { return _impl.end(); } 215 | /// \} 216 | 217 | /// Increase the reserved memory block so it can store at least \a capacity rules without reallocating. 218 | void reserve(size_type capacity) { _impl.reserve(capacity); } 219 | 220 | /// Construct a rule emplace on the end of the list using \a args. 221 | /// 222 | /// \see push_back 223 | template 224 | void emplace_back(TArgs&&... args) 225 | { 226 | _impl.emplace_back(std::forward(args)...); 227 | } 228 | 229 | /// \{ 230 | /// Add the rule \a x to the end of this list. 231 | void push_back(acl_rule&& x) { emplace_back(std::move(x)); } 232 | void push_back(const acl_rule& x) { emplace_back(x); } 233 | /// \} 234 | 235 | private: 236 | std::vector _impl; 237 | }; 238 | 239 | [[gnu::pure]] bool operator==(const acl& lhs, const acl& rhs); 240 | [[gnu::pure]] bool operator!=(const acl& lhs, const acl& rhs); 241 | 242 | std::ostream& operator<<(std::ostream&, const acl&); 243 | 244 | std::string to_string(const acl& self); 245 | 246 | /// Commonly-used ACLs. 247 | class acls 248 | { 249 | public: 250 | /// This ACL gives the creators authentication id's all permissions. 251 | static const acl& creator_all(); 252 | 253 | /// This is a completely open ACL. It is also the ACL used in operations like \ref client::create if no ACL is 254 | /// specified. 255 | static const acl& open_unsafe(); 256 | 257 | /// This ACL gives the world the ability to read. 258 | static const acl& read_unsafe(); 259 | }; 260 | 261 | /// \} 262 | 263 | } 264 | 265 | namespace std 266 | { 267 | 268 | template <> 269 | class hash 270 | { 271 | public: 272 | using argument_type = zk::acl_rule; 273 | using result_type = std::size_t; 274 | 275 | result_type operator()(const argument_type& x) const 276 | { 277 | return zk::hash(x); 278 | } 279 | }; 280 | 281 | } 282 | -------------------------------------------------------------------------------- /src/zk/acl_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "acl.hpp" 4 | 5 | namespace zk 6 | { 7 | 8 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 | // permission // 10 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | GTEST_TEST(permission_tests, all) 13 | { 14 | auto all = permission::read 15 | | permission::write 16 | | permission::create 17 | | permission::erase 18 | | permission::admin; 19 | CHECK_EQ(permission::all, all); 20 | CHECK_EQ("all", to_string(all)); 21 | } 22 | 23 | GTEST_TEST(permission_tests, stringification) 24 | { 25 | CHECK_EQ("none", to_string(permission::none)); 26 | CHECK_EQ("read|create", to_string(permission::read | permission::create)); 27 | CHECK_EQ("write|admin", to_string(permission::write | permission::admin)); 28 | CHECK_EQ("read|create|erase", to_string(permission::read | permission::create | permission::erase)); 29 | CHECK_EQ("admin", to_string(permission::admin)); 30 | } 31 | 32 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 33 | // acl_rule // 34 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 35 | 36 | GTEST_TEST(acl_rule_tests, stringification) 37 | { 38 | CHECK_EQ("(ip:80.23.0.0/16, read|write)", 39 | to_string(acl_rule("ip", "80.23.0.0/16", permission::read | permission::write)) 40 | ); 41 | } 42 | 43 | GTEST_TEST(acl_rule_tests, comparisons) 44 | { 45 | acl_rule creator_all = { "auth", "", permission::all }; 46 | acl_rule world_open = { "world", "anyone", permission::all }; 47 | 48 | CHECK_EQ(creator_all, creator_all); 49 | CHECK_NE(creator_all, world_open); 50 | CHECK_LT(creator_all, world_open); 51 | CHECK_LE(creator_all, world_open); 52 | CHECK_GT(world_open, creator_all); 53 | CHECK_GE(world_open, creator_all); 54 | } 55 | 56 | GTEST_TEST(acl_rule_tests, hashing) 57 | { 58 | acl_rule creator_all = { "auth", "", permission::all }; 59 | acl_rule world_open = { "world", "anyone", permission::all }; 60 | 61 | CHECK_EQ(hash(creator_all), hash(creator_all)); 62 | } 63 | 64 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | // acl // 66 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 67 | 68 | GTEST_TEST(acl_tests, stringification) 69 | { 70 | CHECK_EQ("[(auth, all)]", to_string(acls::creator_all())); 71 | CHECK_EQ("[(world:anyone, all)]", to_string(acls::open_unsafe())); 72 | CHECK_EQ("[(world:anyone, read)]", to_string(acls::read_unsafe())); 73 | 74 | CHECK_EQ("[(auth, read), (ip:50.40.30.0/24, all)]", 75 | to_string(acl({ { "auth", "", permission::read }, { "ip", "50.40.30.0/24", permission::all } })) 76 | ); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/zk/buffer.hpp: -------------------------------------------------------------------------------- 1 | /// \file 2 | /// Controls the \c buffer type. 3 | #pragma once 4 | 5 | #include 6 | 7 | #include 8 | 9 | /// \addtogroup Client 10 | /// \{ 11 | 12 | // \def ZKPP_BUFFER_USE_STD_STRING 13 | // Set this to 1 to use \c std::string as the backing type for zk::buffer. 14 | // 15 | #ifndef ZKPP_BUFFER_USE_STD_STRING 16 | # define ZKPP_BUFFER_USE_STD_STRING 0 17 | #endif 18 | 19 | /// \def ZKPP_BUFFER_USE_CUSTOM 20 | /// Set this to 1 to use a custom definitions for \c buffer. If this is set, you must also set 21 | /// \ref ZKPP_BUFFER_INCLUDE and \ref ZKPP_BUFFER_TYPE. 22 | /// 23 | /// \def ZKPP_BUFFER_INCLUDE 24 | /// The header file to use to find the \c buffer type. This value is set automatically if using built-in configuration 25 | /// options (such as \ref ZKPP_BUFFER_USE_STD_VECTOR) and must be set manually if using \ref ZKPP_BUFFER_USE_CUSTOM. 26 | /// 27 | /// \def ZKPP_BUFFER_TYPE 28 | /// The type name to use for the \c buffer type. This value is set automatically if using built-in configuration 29 | /// options (such as \ref ZKPP_BUFFER_USE_STD_VECTOR) and must be set manually if using \ref ZKPP_BUFFER_USE_CUSTOM. 30 | #ifndef ZKPP_BUFFER_USE_CUSTOM 31 | # define ZKPP_BUFFER_USE_CUSTOM 0 32 | #endif 33 | 34 | /// \def ZKPP_BUFFER_USE_STD_VECTOR 35 | /// Set this to 1 to use \c std::vector for the implementation of \ref zk::buffer. This is the default behavior. 36 | #ifndef ZKPP_BUFFER_USE_STD_VECTOR 37 | # if ZKPP_BUFFER_USE_STD_STRING || ZKPP_BUFFER_USE_CUSTOM 38 | # define ZKPP_BUFFER_USE_STD_VECTOR 0 39 | # else 40 | # define ZKPP_BUFFER_USE_STD_VECTOR 1 41 | # endif 42 | #endif 43 | 44 | #if ZKPP_BUFFER_USE_STD_VECTOR 45 | # define ZKPP_BUFFER_INCLUDE 46 | # define ZKPP_BUFFER_TYPE std::vector 47 | #elif ZKPP_BUFFER_USE_STD_STRING 48 | # define ZKPP_BUFFER_INCLUDE 49 | # define ZKPP_BUFFER_TYPE std::string 50 | #elif ZKPP_BUFFER_USE_CUSTOM 51 | # if !defined ZKPP_BUFFER_INCLUDE || !defined ZKPP_BUFFER_TYPE 52 | # error "When ZKPP_BUFFER_USE_CUSTOM is set, you must also define ZKPP_BUFFER_INCLUDE and ZKPP_BUFFER_TYPE" 53 | # endif 54 | #else 55 | # error "Unknown type to use for zk::buffer" 56 | #endif 57 | 58 | /// \} 59 | 60 | #include ZKPP_BUFFER_INCLUDE 61 | 62 | namespace zk 63 | { 64 | 65 | /// \addtogroup Client 66 | /// \{ 67 | 68 | /// The \c buffer type. By default, this is an \c std::vector, but this can be altered by compile-time flags such 69 | /// as \ref ZKPP_BUFFER_USE_CUSTOM. The requirements for a custom buffer are minimal -- the type must fit this criteria: 70 | /// 71 | /// | expression | type | description | 72 | /// |:----------------------|:--------------------------|:-------------------------------------------------------------| 73 | /// | `buffer::value_type` | `char` | Buffers must be made of single-byte elements | 74 | /// | `buffer::size_type` | `std::size_t` | | 75 | /// | `buffer(ib, ie)` | `buffer` | Constructs a buffer with the range [`ib`, `ie`) | 76 | /// | `buffer(buffer&&)` | `buffer` | Move constructible (must be `noexcept`). | 77 | /// | `operator=(buffer&&)` | `buffer&` | Move assignable (must be `noexcept`). | 78 | /// | `size()` | `size_type` | Get the length of the buffer | 79 | /// | `data()` | `const value_type*` | Get a pointer to the beginning of the contents | 80 | using buffer = ZKPP_BUFFER_TYPE; 81 | 82 | // Check through static_assert: 83 | static_assert(sizeof(buffer::value_type) == 1U, "buffer::value_type must be single-byte elements"); 84 | static_assert(std::is_same::value, "buffer::size_type must be std::size_t"); 85 | static_assert(std::is_constructible, ptr>::value, 86 | "buffer must be constructible with two pointers" 87 | ); 88 | static_assert(std::is_move_constructible::value, "buffer must be move-constructible"); 89 | static_assert(std::is_nothrow_move_constructible::value, "buffer must be nothrow move-constructible"); 90 | static_assert(std::is_nothrow_move_assignable::value, "buffer must be nothrow move-assignable"); 91 | static_assert(std::is_same().size()), buffer::size_type>::value, 92 | "buffer::size() must return buffer::size_type" 93 | ); 94 | static_assert(std::is_constructible, 95 | decltype(std::declval().data()) 96 | >::value, 97 | "buffer::data() must return ptr" 98 | ); 99 | 100 | /// \} 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/zk/client.cpp: -------------------------------------------------------------------------------- 1 | #include "client.hpp" 2 | #include "acl.hpp" 3 | #include "connection.hpp" 4 | #include "multi.hpp" 5 | #include "exceptions.hpp" 6 | 7 | #include 8 | #include 9 | 10 | namespace zk 11 | { 12 | 13 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | // client // 15 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | client::client(const connection_params& params) : 18 | client(connection::connect(params)) 19 | { } 20 | 21 | client::client(string_view conn_string) : 22 | client(connection::connect(conn_string)) 23 | { } 24 | 25 | client::client(std::shared_ptr conn) noexcept : 26 | _conn(std::move(conn)) 27 | { } 28 | 29 | future client::connect(string_view conn_string) 30 | { 31 | return connect(connection_params::parse(conn_string)); 32 | } 33 | 34 | future client::connect(connection_params conn_params) 35 | { 36 | try 37 | { 38 | auto conn = connection::connect(conn_params); 39 | auto state_change_fut = conn->watch_state(); 40 | if (conn->state() == state::connected) 41 | { 42 | promise p; 43 | p.set_value(client(std::move(conn))); 44 | return p.get_future(); 45 | } 46 | else 47 | { 48 | // TODO: Test if future::then can be relied on and use that instead of std::async 49 | return zk::async 50 | ( 51 | zk::launch::async, 52 | [state_change_fut = std::move(state_change_fut), conn = std::move(conn)] () mutable -> client 53 | { 54 | state s(state_change_fut.get()); 55 | if (s == state::connected) 56 | return client(conn); 57 | else 58 | zk::throw_exception(std::runtime_error(std::string("Unexpected state: ") + to_string(s))); 59 | } 60 | ); 61 | } 62 | } 63 | catch (...) 64 | { 65 | promise p; 66 | p.set_exception(zk::current_exception()); 67 | return p.get_future(); 68 | } 69 | } 70 | 71 | client::~client() noexcept = default; 72 | 73 | void client::close() 74 | { 75 | _conn->close(); 76 | } 77 | 78 | future client::get(string_view path) const 79 | { 80 | return _conn->get(path); 81 | } 82 | 83 | future client::watch(string_view path) const 84 | { 85 | return _conn->watch(path); 86 | } 87 | 88 | future client::get_children(string_view path) const 89 | { 90 | return _conn->get_children(path); 91 | } 92 | 93 | future client::watch_children(string_view path) const 94 | { 95 | return _conn->watch_children(path); 96 | } 97 | 98 | future client::exists(string_view path) const 99 | { 100 | return _conn->exists(path); 101 | } 102 | 103 | future client::watch_exists(string_view path) const 104 | { 105 | return _conn->watch_exists(path); 106 | } 107 | 108 | future client::create(string_view path, 109 | const buffer& data, 110 | const acl& rules, 111 | create_mode mode 112 | ) 113 | { 114 | return _conn->create(path, data, rules, mode); 115 | } 116 | 117 | future client::create(string_view path, 118 | const buffer& data, 119 | create_mode mode 120 | ) 121 | { 122 | return create(path, data, acls::open_unsafe(), mode); 123 | } 124 | 125 | future client::set(string_view path, const buffer& data, version check) 126 | { 127 | return _conn->set(path, data, check); 128 | } 129 | 130 | future client::get_acl(string_view path) const 131 | { 132 | return _conn->get_acl(path); 133 | } 134 | 135 | future client::set_acl(string_view path, const acl& rules, acl_version check) 136 | { 137 | return _conn->set_acl(path, rules, check); 138 | } 139 | 140 | future client::erase(string_view path, version check) 141 | { 142 | return _conn->erase(path, check); 143 | } 144 | 145 | future client::load_fence() const 146 | { 147 | return _conn->load_fence(); 148 | } 149 | 150 | future client::commit(multi_op txn) 151 | { 152 | return _conn->commit(std::move(txn)); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /src/zk/client_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "client.hpp" 10 | #include "error.hpp" 11 | #include "multi.hpp" 12 | #include "string_view.hpp" 13 | 14 | namespace zk 15 | { 16 | 17 | static buffer buffer_from(string_view str) 18 | { 19 | return buffer(str.data(), str.data() + str.size()); 20 | } 21 | 22 | class client_tests : 23 | public server::single_server_fixture 24 | { }; 25 | 26 | GTEST_TEST_F(client_tests, get_root) 27 | { 28 | client c = get_connected_client(); 29 | auto res = c.get("/").get(); 30 | std::cerr << res << std::endl; 31 | c.close(); 32 | } 33 | 34 | GTEST_TEST_F(client_tests, exists) 35 | { 36 | client c = get_connected_client(); 37 | CHECK_TRUE(c.exists("/").get()); 38 | CHECK_FALSE(c.exists("/some/bogus/path").get()); 39 | } 40 | 41 | GTEST_TEST_F(client_tests, create) 42 | { 43 | client c = get_connected_client(); 44 | const char local_buf[10] = { 0 }; 45 | auto f_create = c.create("/test-node", buffer(local_buf, local_buf + sizeof local_buf)); 46 | auto name = f_create.get().name(); 47 | CHECK_EQ("/test-node", name); 48 | } 49 | 50 | GTEST_TEST_F(client_tests, create_seq_and_set) 51 | { 52 | client c = get_connected_client(); 53 | auto f_create = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential); 54 | auto name = f_create.get().name(); 55 | auto orig_stat = c.get(name).get().stat(); 56 | auto expected_version = orig_stat.data_version; 57 | ++expected_version; 58 | 59 | c.set(name, buffer_from("WORLD")).get(); 60 | auto contents = c.get(name).get(); 61 | CHECK_EQ(contents.stat().data_version, expected_version); 62 | CHECK_TRUE(contents.data() == buffer_from("WORLD")); 63 | } 64 | 65 | GTEST_TEST_F(client_tests, create_seq_and_erase) 66 | { 67 | client c = get_connected_client(); 68 | auto f_create = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential); 69 | auto name = f_create.get().name(); 70 | auto orig_stat = c.get(name).get().stat(); 71 | c.erase(name, orig_stat.data_version).get(); 72 | CHECK_THROWS(no_entry) 73 | { 74 | c.get(name).get(); 75 | }; 76 | } 77 | 78 | GTEST_TEST_F(client_tests, create_seq_and_get_children) 79 | { 80 | client c = get_connected_client(); 81 | auto f_create = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential); 82 | auto name = f_create.get().name(); 83 | auto orig_stat = c.get(name).get().stat(); 84 | 85 | c.create(name + "/a", buffer()).get(); 86 | c.create(name + "/b", buffer()).get(); 87 | c.create(name + "/c", buffer()).get(); 88 | 89 | auto result = c.get_children(name).get(); 90 | CHECK_EQ(orig_stat.data_version, result.parent_stat().data_version); 91 | CHECK_LT(orig_stat.child_version, result.parent_stat().child_version); 92 | std::vector expected_children = { "a", "b", "c" }; 93 | std::sort(result.children().begin(), result.children().end()); 94 | CHECK_TRUE(expected_children == result.children()); 95 | } 96 | 97 | GTEST_TEST_F(client_tests, acl) 98 | { 99 | client c = get_connected_client(); 100 | auto name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); 101 | 102 | // set the data of the node a few times to make sure the data_version is different value from acl_version 103 | for (std::size_t changes = 0; changes < 5; ++changes) 104 | c.set(name, buffer_from("data change")).get(); 105 | 106 | auto orig_result = c.get_acl(name).get(); 107 | CHECK_EQ(acls::open_unsafe(), orig_result.acl()); 108 | std::cerr << "HEY: " << orig_result << std::endl; 109 | 110 | c.set_acl(name, acls::read_unsafe(), orig_result.stat().acl_version).get(); 111 | auto new_result = c.get_acl(name).get(); 112 | CHECK_EQ(acls::read_unsafe(), new_result.acl()); 113 | } 114 | 115 | GTEST_TEST_F(client_tests, watch_change) 116 | { 117 | client c = get_connected_client(); 118 | auto name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); 119 | 120 | auto watch = c.watch(name).get(); 121 | CHECK_TRUE(watch.initial().data() == buffer_from("Hello!")); 122 | 123 | c.set(name, buffer_from("world")); // don't wait -- the watch won't trigger until the operation completes 124 | auto ev = watch.next().get(); 125 | CHECK_EQ(ev.type(), event_type::changed); 126 | CHECK_EQ(ev.state(), state::connected); 127 | } 128 | 129 | GTEST_TEST_F(client_tests, watch_children) 130 | { 131 | client c = get_connected_client(); 132 | auto root_name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); 133 | 134 | c.commit({ 135 | op::create(root_name + "/a", buffer_from("a")), 136 | op::create(root_name + "/b", buffer_from("b")), 137 | op::create(root_name + "/c", buffer_from("c")), 138 | op::create(root_name + "/d", buffer_from("d")), 139 | }).get(); 140 | 141 | auto watch_child_creation = c.watch_children(root_name).get(); 142 | CHECK_EQ(4U, watch_child_creation.initial().children().size()); 143 | 144 | c.create(root_name + "/e", buffer_from("e")); 145 | auto ev = watch_child_creation.next().get(); 146 | CHECK_EQ(ev.type(), event_type::child); 147 | CHECK_EQ(ev.state(), state::connected); 148 | 149 | auto watch_child_erase = c.watch_children(root_name).get(); 150 | CHECK_EQ(5U, watch_child_erase.initial().children().size()); 151 | 152 | c.erase(root_name + "/a"); 153 | auto ev2 = watch_child_erase.next().get(); 154 | CHECK_EQ(ev2.type(), event_type::child); 155 | CHECK_EQ(ev2.state(), state::connected); 156 | } 157 | 158 | GTEST_TEST_F(client_tests, watch_exists) 159 | { 160 | client c = get_connected_client(); 161 | auto root_name = c.create("/test-node-", buffer_from("Hello!"), create_mode::sequential).get().name(); 162 | 163 | auto watch_creation = c.watch_exists(root_name + "/sub").get(); 164 | CHECK_FALSE(watch_creation.initial()); 165 | 166 | c.create(root_name + "/sub", buffer_from("Blah")); 167 | auto ev = watch_creation.next().get(); 168 | CHECK_EQ(ev.type(), event_type::created); 169 | CHECK_EQ(ev.state(), state::connected); 170 | 171 | auto watch_erase = c.watch_exists(root_name + "/sub").get(); 172 | CHECK_TRUE(watch_erase.initial()); 173 | 174 | c.erase(root_name + "/sub"); 175 | auto ev2 = watch_erase.next().get(); 176 | CHECK_EQ(ev2.type(), event_type::erased); 177 | CHECK_EQ(ev2.state(), state::connected); 178 | } 179 | 180 | GTEST_TEST_F(client_tests, load_fence) 181 | { 182 | client c = get_connected_client(); 183 | // There does not appear to be a good way to actually test this -- so just make sure we don't segfault 184 | c.load_fence().get(); 185 | } 186 | 187 | GTEST_TEST_F(client_tests, watch_close) 188 | { 189 | client c = get_connected_client(); 190 | auto watch = c.watch("/").get(); 191 | 192 | c.close(); 193 | 194 | // watch should be triggered with session closed 195 | auto ev = watch.next().get(); 196 | CHECK_EQ(ev.type(), event_type::session); 197 | CHECK_EQ(ev.state(), state::closed); 198 | } 199 | 200 | class stopping_client_tests : 201 | public server::server_fixture 202 | { }; 203 | 204 | GTEST_TEST_F(stopping_client_tests, watch_server_stop) 205 | { 206 | client c = get_connected_client(); 207 | auto watch = c.watch("/").get(); 208 | 209 | this->stop_server(true); 210 | 211 | auto ev = watch.next().get(); 212 | CHECK_EQ(ev.type(), event_type::session); 213 | } 214 | 215 | } 216 | -------------------------------------------------------------------------------- /src/zk/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | /// \addtogroup Client 4 | /// \{ 5 | 6 | /// \def ZKPP_USER_CONFIG 7 | /// A user-defined configuration file to be included before all other ZooKeeper C++ content. 8 | #ifdef ZKPP_USER_CONFIG 9 | # include ZKPP_USER_CONFIG 10 | #endif 11 | 12 | #define ZKPP_VERSION_MAJOR 0 13 | #define ZKPP_VERSION_MINOR 2 14 | #define ZKPP_VERSION_PATCH 3 15 | 16 | /// \def ZKPP_DEBUG 17 | /// Was ZooKeeper C++ compiled in debug mode? This value must be the same between when the SO was built and when you are 18 | /// compiling. In general, this is not useful outside of library maintainers. 19 | /// 20 | /// \warning 21 | /// Keep in mind this value is \e always defined. Use `#if ZKPP_DEBUG`, \e not `#ifdef ZKPP_DEBUG`. 22 | #ifndef ZKPP_DEBUG 23 | # define ZKPP_DEBUG 0 24 | #endif 25 | 26 | /// \} 27 | 28 | namespace zk 29 | { 30 | 31 | /// \addtogroup Client 32 | /// \{ 33 | 34 | /// A simple, unowned pointer. It operates exactly like using \c *, but removes the question of \c * associativity and 35 | /// is easier to read when \c const qualifiers are involved. 36 | template 37 | using ptr = T*; 38 | 39 | /// \} 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/zk/connection.cpp: -------------------------------------------------------------------------------- 1 | #include "connection.hpp" 2 | #include "connection_zk.hpp" 3 | #include "error.hpp" 4 | #include "types.hpp" 5 | #include "exceptions.hpp" 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | 16 | namespace zk 17 | { 18 | 19 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 20 | // connection // 21 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 22 | 23 | connection::~connection() noexcept 24 | { } 25 | 26 | std::shared_ptr connection::connect(const connection_params& params) 27 | { 28 | return std::make_shared(params); 29 | } 30 | 31 | std::shared_ptr connection::connect(string_view conn_string) 32 | { 33 | return connect(connection_params::parse(conn_string)); 34 | } 35 | 36 | future connection::watch_state() 37 | { 38 | std::unique_lock ax(_state_change_promises_protect); 39 | _state_change_promises.emplace_back(); 40 | return _state_change_promises.rbegin()->get_future(); 41 | } 42 | 43 | void connection::on_session_event(zk::state new_state) 44 | { 45 | std::unique_lock ax(_state_change_promises_protect); 46 | auto l_state_change_promises = std::move(_state_change_promises); 47 | ax.unlock(); 48 | 49 | auto ex = new_state == zk::state::expired_session ? get_exception_ptr_of(error_code::session_expired) 50 | : new_state == zk::state::authentication_failed ? get_exception_ptr_of(error_code::authentication_failed) 51 | : zk::exception_ptr(); 52 | 53 | for (auto& p : l_state_change_promises) 54 | { 55 | if (ex) 56 | p.set_exception(ex); 57 | else 58 | p.set_value(new_state); 59 | } 60 | } 61 | 62 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 63 | // connection_params // 64 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 65 | 66 | connection_params::connection_params() noexcept : 67 | _connection_schema("zk"), 68 | _hosts({}), 69 | _chroot("/"), 70 | _randomize_hosts(true), 71 | _read_only(false), 72 | _timeout(default_timeout) 73 | { } 74 | 75 | connection_params::~connection_params() noexcept 76 | { } 77 | 78 | template 79 | static string_view sv_from_match(const TMatch& src) 80 | { 81 | return string_view(src.first, std::distance(src.first, src.second)); 82 | } 83 | 84 | template 85 | void split_each_substr(string_view src, char delim, const FAction& action) 86 | { 87 | while (!src.empty()) 88 | { 89 | // if next_div is src.end, the logic still works 90 | auto next_div = std::find(src.begin(), src.end(), delim); 91 | action(string_view(src.data(), std::distance(src.begin(), next_div))); 92 | src.remove_prefix(std::distance(src.begin(), next_div)); 93 | if (!src.empty()) 94 | src.remove_prefix(1U); 95 | } 96 | } 97 | 98 | static connection_params::host_list extract_host_list(string_view src) 99 | { 100 | connection_params::host_list out; 101 | out.reserve(std::count(src.begin(), src.end(), ',')); 102 | split_each_substr(src, ',', [&] (string_view sub) { out.emplace_back(std::string(sub)); }); 103 | return out; 104 | } 105 | 106 | static bool extract_bool(string_view key, string_view val) 107 | { 108 | if (val.empty()) 109 | zk::throw_exception(std::invalid_argument(std::string("Key ") + std::string(key) + " has blank value")); 110 | 111 | switch (val[0]) 112 | { 113 | case '1': 114 | case 't': 115 | case 'T': 116 | return true; 117 | case '0': 118 | case 'f': 119 | case 'F': 120 | return false; 121 | default: 122 | zk::throw_exception(std::invalid_argument(std::string("Invalid value for ") + std::string(key) + std::string(" \"") 123 | + std::string(val) + "\" -- expected a boolean" 124 | )); 125 | } 126 | } 127 | 128 | static std::chrono::milliseconds extract_millis(string_view key, string_view val) 129 | { 130 | if (val.empty()) 131 | zk::throw_exception(std::invalid_argument(std::string("Key ") + std::string(key) + " has blank value")); 132 | 133 | if (val[0] == 'P') 134 | { 135 | zk::throw_exception(std::invalid_argument("ISO 8601 duration is not supported (yet).")); 136 | } 137 | else 138 | { 139 | double seconds = std::stod(std::string(val)); 140 | return std::chrono::duration_cast(std::chrono::duration(seconds)); 141 | } 142 | } 143 | 144 | static void extract_advanced_options(string_view src, connection_params& out) 145 | { 146 | if (src.empty() || src.size() == 1U) 147 | return; 148 | else 149 | src.remove_prefix(1U); 150 | 151 | std::string invalid_keys_msg; 152 | auto invalid_key = [&] (string_view key) 153 | { 154 | if (invalid_keys_msg.empty()) 155 | invalid_keys_msg = "Invalid key in querystring: "; 156 | else 157 | invalid_keys_msg += ", "; 158 | 159 | invalid_keys_msg += std::string(key); 160 | }; 161 | 162 | split_each_substr(src, '&', [&] (string_view qp_part) 163 | { 164 | auto eq_it = std::find(qp_part.begin(), qp_part.end(), '='); 165 | if (eq_it == qp_part.end()) 166 | zk::throw_exception(std::invalid_argument("Invalid connection string -- query string must be specified as " 167 | "\"key1=value1&key2=value2...\"" 168 | )); 169 | 170 | auto key = qp_part.substr(0, std::distance(qp_part.begin(), eq_it)); 171 | auto val = qp_part.substr(std::distance(qp_part.begin(), eq_it) + 1); 172 | 173 | if (key == "randomize_hosts") 174 | out.randomize_hosts() = extract_bool(key, val); 175 | else if (key == "read_only") 176 | out.read_only() = extract_bool(key, val); 177 | else if (key == "timeout") 178 | out.timeout() = extract_millis(key, val); 179 | else 180 | invalid_key(key); 181 | }); 182 | 183 | if (!invalid_keys_msg.empty()) 184 | zk::throw_exception(std::invalid_argument(std::move(invalid_keys_msg))); 185 | } 186 | 187 | connection_params connection_params::parse(string_view conn_string) 188 | { 189 | static const std::regex expr(R"(([^:]+)://([^/]+)((/[^\?]*)(\?.*)?)?)", 190 | std::regex_constants::ECMAScript | std::regex_constants::optimize 191 | ); 192 | constexpr auto schema_idx = 1U; 193 | constexpr auto hostaddr_idx = 2U; 194 | constexpr auto path_idx = 4U; 195 | constexpr auto querystring_idx = 5U; 196 | 197 | std::cmatch match; 198 | if (!std::regex_match(conn_string.begin(), conn_string.end(), match, expr)) 199 | zk::throw_exception(std::invalid_argument(std::string("Invalid connection string (") + std::string(conn_string) 200 | + " -- format is \"schema://[auth@]${host_addrs}/[path][?options]\"" 201 | )); 202 | 203 | connection_params out; 204 | out.connection_schema() = match[schema_idx].str(); 205 | out.hosts() = extract_host_list(sv_from_match(match[hostaddr_idx])); 206 | out.chroot() = match[path_idx].str(); 207 | if (out.chroot().empty()) 208 | out.chroot() = "/"; 209 | 210 | extract_advanced_options(sv_from_match(match[querystring_idx]), out); 211 | 212 | return out; 213 | } 214 | 215 | bool operator==(const connection_params& lhs, const connection_params& rhs) 216 | { 217 | return lhs.connection_schema() == rhs.connection_schema() 218 | && lhs.hosts() == rhs.hosts() 219 | && lhs.chroot() == rhs.chroot() 220 | && lhs.randomize_hosts() == rhs.randomize_hosts() 221 | && lhs.read_only() == rhs.read_only() 222 | && lhs.timeout() == rhs.timeout(); 223 | } 224 | 225 | bool operator!=(const connection_params& lhs, const connection_params& rhs) 226 | { 227 | return !(lhs == rhs); 228 | } 229 | 230 | std::ostream& operator<<(std::ostream& os, const connection_params& x) 231 | { 232 | os << x.connection_schema() << "://"; 233 | bool first = true; 234 | for (const auto& host : x.hosts()) 235 | { 236 | if (first) 237 | first = false; 238 | else 239 | os << ','; 240 | 241 | os << host; 242 | } 243 | os << x.chroot(); 244 | 245 | first = true; 246 | auto query_string = [&] (ptr key, const auto& val) 247 | { 248 | if (first) 249 | { 250 | first = false; 251 | os << '?'; 252 | } 253 | else 254 | { 255 | os << '&'; 256 | } 257 | 258 | os << key << '=' << val; 259 | }; 260 | if (!x.randomize_hosts()) 261 | query_string("randomize_hosts", "false"); 262 | if (x.read_only()) 263 | query_string("read_only", "true"); 264 | if (x.timeout() != connection_params::default_timeout) 265 | query_string("timeout", std::chrono::duration(x.timeout()).count()); 266 | return os; 267 | } 268 | 269 | std::string to_string(const connection_params& x) 270 | { 271 | std::ostringstream os; 272 | os << x; 273 | return os.str(); 274 | } 275 | 276 | } 277 | -------------------------------------------------------------------------------- /src/zk/connection.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "buffer.hpp" 13 | #include "forwards.hpp" 14 | #include "future.hpp" 15 | #include "string_view.hpp" 16 | 17 | namespace zk 18 | { 19 | 20 | /// \addtogroup Client 21 | /// \{ 22 | 23 | /// An actual connection to the server. The majority of methods have the same signature and meaning as \ref client. 24 | /// 25 | /// \see connection_zk 26 | class connection 27 | { 28 | public: 29 | static std::shared_ptr connect(const connection_params&); 30 | 31 | static std::shared_ptr connect(string_view conn_string); 32 | 33 | virtual ~connection() noexcept; 34 | 35 | virtual void close() = 0; 36 | 37 | virtual future get(string_view path) = 0; 38 | 39 | virtual future watch(string_view path) = 0; 40 | 41 | virtual future get_children(string_view path) = 0; 42 | 43 | virtual future watch_children(string_view path) = 0; 44 | 45 | virtual future exists(string_view path) = 0; 46 | 47 | virtual future watch_exists(string_view path) = 0; 48 | 49 | virtual future create(string_view path, 50 | const buffer& data, 51 | const acl& rules, 52 | create_mode mode 53 | ) = 0; 54 | 55 | virtual future set(string_view path, const buffer& data, version check) = 0; 56 | 57 | virtual future erase(string_view path, version check) = 0; 58 | 59 | virtual future get_acl(string_view path) const = 0; 60 | 61 | virtual future set_acl(string_view path, const acl& rules, acl_version check) = 0; 62 | 63 | virtual future commit(multi_op&& txn) = 0; 64 | 65 | virtual future load_fence() = 0; 66 | 67 | virtual zk::state state() const = 0; 68 | 69 | /// Watch for a state change. 70 | virtual future watch_state(); 71 | 72 | protected: 73 | /// Call this from derived classes when a session event happens. This triggers the delivery of all promises of state 74 | /// changes (issued through \ref watch_state). 75 | virtual void on_session_event(zk::state new_state); 76 | 77 | private: 78 | mutable std::mutex _state_change_promises_protect; 79 | std::vector> _state_change_promises; 80 | }; 81 | 82 | /// Used to specify parameters for a \c connection. This can either be created manually or through a 83 | /// \ref ConnectionStrings "connection string". 84 | class connection_params final 85 | { 86 | public: 87 | using host_list = std::vector; 88 | 89 | public: 90 | static constexpr std::chrono::milliseconds default_timeout = std::chrono::seconds(10); 91 | 92 | public: 93 | /// Create an instance with default values. 94 | connection_params() noexcept; 95 | 96 | ~connection_params() noexcept; 97 | 98 | /// Create an instance from a connection string. 99 | /// 100 | /// \anchor ConnectionStrings 101 | /// \par Connection Strings 102 | /// Connection strings follow the standard format for URLs (`schema://host_address/path?querystring`). The `schema` 103 | /// is the type of connection (usually `"zk"`), `auth` is potential authentication, the `host_address` is the list 104 | /// of servers, the `path` is the chroot to use for this connection and the `querystring` allows for configuring 105 | /// advanced options. For example: `"zk://server-a:2181,server-b:2181,server-c:2181/?timeout=5"`. 106 | /// 107 | /// \par 108 | /// - \e schema: \ref connection_params::connection_schema 109 | /// - \e host_address: comma-separated list of \ref connection_params::hosts 110 | /// - \e path: \ref connection_params::chroot 111 | /// - \e querystring: Allows for an arbitrary amount of optional parameters to be specified. These are specified 112 | /// with the conventional HTTP-style (e.g. `"zk://localhost/?timeout=10&read_only=true"` specifies a timeout of 10 113 | /// seconds and sets a read-only client. Boolean values can be specified with \c true, \c t, or \c 1 for \c true 114 | /// or \c false, \c f, or \c 0 for \c false. It is important to note that, unlike regular HTTP URLs, query 115 | /// parameters which are not understood will result in an error. 116 | /// - `randomize_hosts`: \ref connection_params::randomize_hosts 117 | /// - `read_only`: \ref connection_params::read_only 118 | /// - `timeout`: \ref connection_params::timeout 119 | /// 120 | /// \throws std::invalid_argument if the string is malformed in some way. 121 | static connection_params parse(string_view conn_string); 122 | 123 | /// \{ 124 | /// Determines the underlying \ref zk::connection implementation to use. The valid values are \c "zk" and 125 | /// \c "fakezk". 126 | /// 127 | /// - `zk`: The standard-issue ZooKeeper connection to a real ZooKeeper cluster. This schema uses 128 | /// \ref zk::connection_zk as the underlying connection. 129 | /// - `fakezk`: Create a client connected to a fake ZooKeeper server (\ref zk::fake::server). Here, the 130 | /// `host_address` refers to the name of the in-memory DB created when the server instance was. This schema uses 131 | /// \ref zk::fake::connection_fake as the underlying connection. 132 | const std::string& connection_schema() const { return _connection_schema; } 133 | std::string& connection_schema() { return _connection_schema; } 134 | /// \} 135 | 136 | /// \{ 137 | /// Addresses for the ensemble to connect to. This can be IP addresses (IPv4 or IPv6) or hostnames, but IP 138 | /// addresses are the recommended method of specification. If the port is left unspecified, \c 2181 is assumed (as 139 | /// it is the de facto standard for ZooKeeper server). For IPv6 addresses, use the boxed format (e.g. `[::1]:2181`); 140 | /// this is required even when the port is \c 2181 to disambiguate between a host named in hexadecimal and an IPv6 141 | /// address (e.g. `"[fd2d:8413:d6c6::73b]"` or `"[::1]:2181"`). 142 | const host_list& hosts() const { return _hosts; } 143 | host_list& hosts() { return _hosts; } 144 | /// \} 145 | 146 | /// \{ 147 | /// Specifying a value for \c chroot as something aside from \c "" or \c "/" will run the client commands while 148 | /// interpreting all paths relative to the specified path. For example, specifying `"/app/a"` will make requests for 149 | /// `"/foo/bar"` sent to `"/app/a/foo/bar"` (from the server's perspective). If unspecified, the path will be 150 | /// treated as `"/"`. 151 | const std::string& chroot() const { return _chroot; } 152 | std::string& chroot() { return _chroot; } 153 | /// \} 154 | 155 | /// \{ 156 | /// Connect to a host at random (as opposed to attempting connections in order)? The default is to randomize (the 157 | /// use cases for sequential connections are usually limited to testing purposes). 158 | bool randomize_hosts() const { return _randomize_hosts; } 159 | bool& randomize_hosts() { return _randomize_hosts; } 160 | /// \} 161 | 162 | /// \{ 163 | /// Allow connections to read-only servers? The default (\c false) is to disallow. **/ 164 | bool read_only() const { return _read_only; } 165 | bool& read_only() { return _read_only; } 166 | /// \} 167 | 168 | /// \{ 169 | /// The session timeout between this client and the server. The server will attempt to respect this value, but will 170 | /// automatically use a lower timeout value if this value is too large (see the ZooKeeper Programmer's Guide for 171 | /// more information on maximum values). The default is 10 seconds. 172 | /// 173 | /// When specified in a query string, this value is specified either with floating-point seconds or as an ISO 8601 174 | /// duration (`PT8.93S` for \c 8.930 seconds). 175 | std::chrono::milliseconds timeout() const { return _timeout; } 176 | std::chrono::milliseconds& timeout() { return _timeout; } 177 | /// \} 178 | 179 | private: 180 | std::string _connection_schema; 181 | host_list _hosts; 182 | std::string _chroot; 183 | bool _randomize_hosts; 184 | bool _read_only; 185 | std::chrono::milliseconds _timeout; 186 | }; 187 | 188 | bool operator==(const connection_params& lhs, const connection_params& rhs); 189 | bool operator!=(const connection_params& lhs, const connection_params& rhs); 190 | 191 | std::string to_string(const connection_params&); 192 | std::ostream& operator<<(std::ostream&, const connection_params&); 193 | 194 | /// \} 195 | 196 | } 197 | -------------------------------------------------------------------------------- /src/zk/connection_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "connection.hpp" 4 | 5 | namespace zk 6 | { 7 | 8 | // This test is mostly to check that we still use the defaults. 9 | GTEST_TEST(connection_params_tests, defaults) 10 | { 11 | const auto res = connection_params::parse("zk://localhost/"); 12 | CHECK_EQ("zk", res.connection_schema()); 13 | CHECK_EQ(1U, res.hosts().size()); 14 | CHECK_EQ("localhost", res.hosts()[0]); 15 | CHECK_EQ("/", res.chroot()); 16 | CHECK_TRUE(res.randomize_hosts()); 17 | CHECK_FALSE(res.read_only()); 18 | CHECK_TRUE(connection_params::default_timeout == res.timeout()); 19 | 20 | connection_params manual; 21 | manual.hosts() = { "localhost" }; 22 | CHECK_EQ(manual, res); 23 | } 24 | 25 | GTEST_TEST(connection_params_tests, multi_hosts) 26 | { 27 | const auto res = connection_params::parse("zk://server-a,server-b,server-c/"); 28 | connection_params manual; 29 | manual.hosts() = { "server-a", "server-b", "server-c" }; 30 | CHECK_EQ(manual, res); 31 | } 32 | 33 | GTEST_TEST(connection_params_tests, chroot) 34 | { 35 | const auto res = connection_params::parse("zk://localhost/some/sub/path"); 36 | connection_params manual; 37 | manual.hosts() = { "localhost" }; 38 | manual.chroot() = "/some/sub/path"; 39 | CHECK_EQ(manual, res); 40 | } 41 | 42 | GTEST_TEST(connection_params_tests, randomize_hosts) 43 | { 44 | const auto res = connection_params::parse("zk://localhost/?randomize_hosts=false"); 45 | connection_params manual; 46 | manual.hosts() = { "localhost" }; 47 | manual.randomize_hosts() = false; 48 | CHECK_EQ(manual, res); 49 | } 50 | 51 | GTEST_TEST(connection_params_tests, read_only) 52 | { 53 | const auto res = connection_params::parse("zk://localhost/?read_only=true"); 54 | connection_params manual; 55 | manual.hosts() = { "localhost" }; 56 | manual.read_only() = true; 57 | CHECK_EQ(manual, res); 58 | } 59 | 60 | GTEST_TEST(connection_params_tests, randomize_and_read_only) 61 | { 62 | const auto res = connection_params::parse("zk://localhost/?randomize_hosts=false&read_only=1"); 63 | connection_params manual; 64 | manual.hosts() = { "localhost" }; 65 | manual.randomize_hosts() = false; 66 | manual.read_only() = true; 67 | CHECK_EQ(manual, res); 68 | } 69 | 70 | GTEST_TEST(connection_params_tests, timeout) 71 | { 72 | const auto res = connection_params::parse("zk://localhost/?timeout=0.5"); 73 | connection_params manual; 74 | manual.hosts() = { "localhost" }; 75 | manual.timeout() = std::chrono::milliseconds(500); 76 | CHECK_EQ(manual, res); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/zk/connection_zk.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "connection.hpp" 11 | #include "string_view.hpp" 12 | 13 | typedef struct _zhandle zhandle_t; 14 | 15 | namespace zk 16 | { 17 | 18 | /// \addtogroup Client 19 | /// \{ 20 | 21 | class connection_zk final : 22 | public connection 23 | { 24 | public: 25 | explicit connection_zk(const connection_params& params); 26 | 27 | virtual ~connection_zk() noexcept; 28 | 29 | virtual void close() override; 30 | 31 | virtual zk::state state() const override; 32 | 33 | virtual future get(string_view path) override; 34 | 35 | virtual future watch(string_view path) override; 36 | 37 | virtual future get_children(string_view path) override; 38 | 39 | virtual future watch_children(string_view path) override; 40 | 41 | virtual future exists(string_view path) override; 42 | 43 | virtual future watch_exists(string_view path) override; 44 | 45 | virtual future create(string_view path, 46 | const buffer& data, 47 | const acl& rules, 48 | create_mode mode 49 | ) override; 50 | 51 | virtual future set(string_view path, const buffer& data, version check) override; 52 | 53 | virtual future erase(string_view path, version check) override; 54 | 55 | virtual future get_acl(string_view path) const override; 56 | 57 | virtual future set_acl(string_view path, const acl& rules, acl_version check) override; 58 | 59 | virtual future commit(multi_op&& txn) override; 60 | 61 | virtual future load_fence() override; 62 | 63 | private: 64 | static void on_session_event_raw(ptr handle, 65 | int ev_type, 66 | int state, 67 | ptr path, 68 | ptr watcher_ctx 69 | ) noexcept; 70 | 71 | using watch_function = void (*)(ptr, int type_in, int state_in, ptr, ptr); 72 | 73 | class watcher; 74 | 75 | template 76 | class basic_watcher; 77 | 78 | class data_watcher; 79 | 80 | class child_watcher; 81 | 82 | class exists_watcher; 83 | 84 | /** Erase the watch tracker for the watch with the value \a p. 85 | * 86 | * \returns \c true if it was deleted (the watch should be delivered); \c false if \a p was not in the list. 87 | **/ 88 | std::shared_ptr try_extract_watch(ptr p); 89 | 90 | static void deliver_watch(ptr zh, int type_in, int state_in, ptr, ptr proms_in); 91 | 92 | private: 93 | ptr _handle; 94 | std::unordered_map, std::shared_ptr> _watches; 95 | mutable std::mutex _watches_protect; 96 | }; 97 | 98 | /// \} 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/zk/error.cpp: -------------------------------------------------------------------------------- 1 | #include "error.hpp" 2 | #include "exceptions.hpp" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace zk 10 | { 11 | 12 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 13 | // error_code // 14 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 15 | 16 | std::ostream& operator<<(std::ostream& os, const error_code& code) 17 | { 18 | switch (code) 19 | { 20 | case error_code::ok: return os << "ok"; 21 | default: return os << "error_code(" << static_cast(code) << ')'; 22 | } 23 | } 24 | 25 | std::string to_string(const error_code& code) 26 | { 27 | std::ostringstream os; 28 | os << code; 29 | return os.str(); 30 | } 31 | 32 | void throw_error(error_code code) 33 | { 34 | switch (code) 35 | { 36 | case error_code::connection_loss: zk::throw_exception( connection_loss() ); 37 | case error_code::marshalling_error: zk::throw_exception( marshalling_error() ); 38 | case error_code::not_implemented: zk::throw_exception( not_implemented("unspecified") ); 39 | case error_code::invalid_arguments: zk::throw_exception( invalid_arguments() ); 40 | case error_code::new_configuration_no_quorum: zk::throw_exception( new_configuration_no_quorum() ); 41 | case error_code::reconfiguration_in_progress: zk::throw_exception( reconfiguration_in_progress() ); 42 | case error_code::no_entry: zk::throw_exception( no_entry() ); 43 | case error_code::not_authorized: zk::throw_exception( not_authorized() ); 44 | case error_code::version_mismatch: zk::throw_exception( version_mismatch() ); 45 | case error_code::no_children_for_ephemerals: zk::throw_exception( no_children_for_ephemerals() ); 46 | case error_code::entry_exists: zk::throw_exception( entry_exists() ); 47 | case error_code::not_empty: zk::throw_exception( not_empty() ); 48 | case error_code::session_expired: zk::throw_exception( session_expired() ); 49 | case error_code::authentication_failed: zk::throw_exception( authentication_failed() ); 50 | case error_code::closed: zk::throw_exception( closed() ); 51 | case error_code::read_only_connection: zk::throw_exception( read_only_connection() ); 52 | case error_code::ephemeral_on_local_session: zk::throw_exception( ephemeral_on_local_session() ); 53 | case error_code::reconfiguration_disabled: zk::throw_exception( reconfiguration_disabled() ); 54 | case error_code::transaction_failed: zk::throw_exception( transaction_failed(error_code::transaction_failed, 0U) ); 55 | default: zk::throw_exception( error(code, "unknown") ); 56 | } 57 | } 58 | 59 | zk::exception_ptr get_exception_ptr_of(error_code code) 60 | { 61 | try 62 | { 63 | throw_error(code); 64 | } 65 | catch (...) 66 | { 67 | return zk::current_exception(); 68 | } 69 | } 70 | 71 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 72 | // error_category // 73 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 74 | 75 | class error_category_impl final : public std::error_category 76 | { 77 | public: 78 | virtual ptr name() const noexcept override { return "zookeeper"; } 79 | 80 | virtual std::string message(int condition) const override; 81 | }; 82 | 83 | std::string error_category_impl::message(int condition) const 84 | { 85 | return to_string(static_cast(condition)); 86 | } 87 | 88 | const std::error_category& error_category() 89 | { 90 | static error_category_impl instance; 91 | return instance; 92 | } 93 | 94 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 95 | // errors // 96 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 97 | 98 | static std::string format_error(error_code code, const std::string& description) 99 | { 100 | if (description.empty()) 101 | return to_string(code); 102 | else 103 | return to_string(code) + ": " + description; 104 | } 105 | 106 | error::error(error_code code, const std::string& description) : 107 | std::system_error(static_cast(code), error_category(), format_error(code, description)), 108 | _code(code) 109 | { } 110 | 111 | error::~error() noexcept = default; 112 | 113 | transport_error::transport_error(error_code code, const std::string& description) : 114 | error(code, std::move(description)) 115 | { } 116 | 117 | transport_error::~transport_error() noexcept = default; 118 | 119 | connection_loss::connection_loss() : 120 | transport_error(error_code::connection_loss, "") 121 | { } 122 | 123 | connection_loss::~connection_loss() noexcept = default; 124 | 125 | marshalling_error::marshalling_error() : 126 | transport_error(error_code::marshalling_error, "") 127 | { } 128 | 129 | marshalling_error::~marshalling_error() noexcept = default; 130 | 131 | not_implemented::not_implemented(ptr op_name) : 132 | error(error_code::not_implemented, std::string("Operation not implemented: ") + op_name) 133 | { } 134 | 135 | not_implemented::~not_implemented() noexcept = default; 136 | 137 | invalid_arguments::invalid_arguments(error_code code, const std::string& description) : 138 | error(code, description) 139 | { } 140 | 141 | invalid_arguments::invalid_arguments() : 142 | invalid_arguments(error_code::invalid_arguments, "") 143 | { } 144 | 145 | invalid_arguments::~invalid_arguments() noexcept = default; 146 | 147 | authentication_failed::authentication_failed() : 148 | invalid_arguments(error_code::authentication_failed, "") 149 | { } 150 | 151 | authentication_failed::~authentication_failed() noexcept = default; 152 | 153 | invalid_ensemble_state::invalid_ensemble_state(error_code code, const std::string& description) : 154 | error(code, description) 155 | { } 156 | 157 | invalid_ensemble_state::~invalid_ensemble_state() noexcept = default; 158 | 159 | new_configuration_no_quorum::new_configuration_no_quorum() : 160 | invalid_ensemble_state(error_code::new_configuration_no_quorum, "") 161 | { } 162 | 163 | new_configuration_no_quorum::~new_configuration_no_quorum() noexcept = default; 164 | 165 | reconfiguration_in_progress::reconfiguration_in_progress() : 166 | invalid_ensemble_state(error_code::reconfiguration_in_progress, "") 167 | { } 168 | 169 | reconfiguration_in_progress::~reconfiguration_in_progress() noexcept = default; 170 | 171 | reconfiguration_disabled::reconfiguration_disabled() : 172 | invalid_ensemble_state(error_code::reconfiguration_disabled, "") 173 | { } 174 | 175 | reconfiguration_disabled::~reconfiguration_disabled() noexcept = default; 176 | 177 | invalid_connection_state::invalid_connection_state(error_code code, const std::string& description) : 178 | error(code, description) 179 | { } 180 | 181 | invalid_connection_state::~invalid_connection_state() noexcept = default; 182 | 183 | session_expired::session_expired() : 184 | invalid_connection_state(error_code::session_expired, "") 185 | { } 186 | 187 | session_expired::~session_expired() noexcept = default; 188 | 189 | not_authorized::not_authorized() : 190 | invalid_connection_state(error_code::not_authorized, "") 191 | { } 192 | 193 | not_authorized::~not_authorized() noexcept = default; 194 | 195 | closed::closed() : 196 | invalid_connection_state(error_code::closed, "") 197 | { } 198 | 199 | closed::~closed() noexcept = default; 200 | 201 | ephemeral_on_local_session::ephemeral_on_local_session() : 202 | invalid_connection_state(error_code::ephemeral_on_local_session, "") 203 | { } 204 | 205 | ephemeral_on_local_session::~ephemeral_on_local_session() noexcept = default; 206 | 207 | read_only_connection::read_only_connection() : 208 | invalid_connection_state(error_code::read_only_connection, "") 209 | { } 210 | 211 | read_only_connection::~read_only_connection() noexcept = default; 212 | 213 | check_failed::check_failed(error_code code, const std::string& description) : 214 | error(code, description) 215 | { } 216 | 217 | check_failed::~check_failed() noexcept = default; 218 | 219 | no_entry::no_entry() : 220 | check_failed(error_code::no_entry, "") 221 | { } 222 | 223 | no_entry::~no_entry() noexcept = default; 224 | 225 | entry_exists::entry_exists() : 226 | check_failed(error_code::entry_exists, "") 227 | { } 228 | 229 | entry_exists::~entry_exists() noexcept = default; 230 | 231 | not_empty::not_empty() : 232 | check_failed(error_code::not_empty, "") 233 | { } 234 | 235 | not_empty::~not_empty() noexcept = default; 236 | 237 | version_mismatch::version_mismatch() : 238 | check_failed(error_code::version_mismatch, "") 239 | { } 240 | 241 | version_mismatch::~version_mismatch() noexcept = default; 242 | 243 | no_children_for_ephemerals::no_children_for_ephemerals() : 244 | check_failed(error_code::no_children_for_ephemerals, "") 245 | { } 246 | 247 | no_children_for_ephemerals::~no_children_for_ephemerals() noexcept = default; 248 | 249 | transaction_failed::transaction_failed(error_code underlying_cause, std::size_t op_index) : 250 | check_failed(error_code::transaction_failed, 251 | std::string("Could not commit transaction due to ") + to_string(underlying_cause) 252 | + " on operation " + std::to_string(op_index) 253 | ), 254 | _underlying_cause(underlying_cause), 255 | _op_index(op_index) 256 | { } 257 | 258 | transaction_failed::~transaction_failed() noexcept = default; 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/zk/error_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "error.hpp" 4 | 5 | namespace zk 6 | { 7 | 8 | static error_code all_error_codes[] = 9 | { 10 | error_code::connection_loss, 11 | error_code::marshalling_error, 12 | error_code::not_implemented, 13 | error_code::invalid_arguments, 14 | error_code::new_configuration_no_quorum, 15 | error_code::reconfiguration_in_progress, 16 | error_code::no_entry, 17 | error_code::not_authorized, 18 | error_code::version_mismatch, 19 | error_code::no_children_for_ephemerals, 20 | error_code::entry_exists, 21 | error_code::not_empty, 22 | error_code::session_expired, 23 | error_code::authentication_failed, 24 | error_code::closed, 25 | error_code::read_only_connection, 26 | error_code::ephemeral_on_local_session, 27 | error_code::reconfiguration_disabled, 28 | error_code::transaction_failed, 29 | }; 30 | 31 | GTEST_TEST(error_code_tests, throwing) 32 | { 33 | for (error_code code : all_error_codes) 34 | { 35 | try 36 | { 37 | try 38 | { 39 | throw_error(code); 40 | } 41 | catch (const check_failed& ex) 42 | { 43 | CHECK_EQ(code, ex.code()); 44 | CHECK_TRUE(is_check_failed(code)) << code; 45 | throw; 46 | } 47 | catch (const invalid_arguments& ex) 48 | { 49 | CHECK_EQ(code, ex.code()); 50 | CHECK_TRUE(is_invalid_arguments(code)) << code; 51 | throw; 52 | } 53 | catch (const invalid_connection_state& ex) 54 | { 55 | CHECK_EQ(code, ex.code()); 56 | CHECK_TRUE(is_invalid_connection_state(code)) << code; 57 | throw; 58 | } 59 | catch (const invalid_ensemble_state& ex) 60 | { 61 | CHECK_EQ(code, ex.code()); 62 | CHECK_TRUE(is_invalid_ensemble_state(code)) << code; 63 | throw; 64 | } 65 | catch (const not_implemented& ex) 66 | { 67 | CHECK_EQ(code, ex.code()); 68 | CHECK_EQ(error_code::not_implemented, ex.code()); 69 | throw; 70 | } 71 | catch (const transport_error& ex) 72 | { 73 | CHECK_EQ(code, ex.code()); 74 | throw; 75 | } 76 | catch (const error& ex) 77 | { 78 | // not a real error, so it will come across as unknown 79 | CHECK_EQ(error_code::ok, ex.code()); 80 | throw; 81 | } 82 | CHECK_FAIL() << "Should not have reached this point"; 83 | } 84 | catch (const error& ex) 85 | { 86 | CHECK_EQ(code, ex.code()); 87 | } 88 | } 89 | } 90 | 91 | GTEST_TEST(error_code_tests, to_string_bogus_code) 92 | { 93 | CHECK_EQ("error_code(19)", to_string(static_cast(19))); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/zk/exceptions.cpp: -------------------------------------------------------------------------------- 1 | #include "exceptions.hpp" 2 | 3 | namespace zk 4 | { 5 | 6 | exception_ptr current_exception() noexcept 7 | { 8 | #if ZKPP_FUTURE_USE_BOOST 9 | return boost::current_exception(); 10 | #else 11 | return std::current_exception(); 12 | #endif 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/zk/exceptions.hpp: -------------------------------------------------------------------------------- 1 | /** \file 2 | * Controls the throwing of exceptions and import of \c exception_ptr types and \c current_exception implementation. 3 | * These are probably \c std::exception_ptr and \c std::current_exception(), but if boost future is used it will be 4 | * boost's implementation. 5 | **/ 6 | #pragma once 7 | 8 | #include 9 | 10 | #if ZKPP_FUTURE_USE_BOOST 11 | #include 12 | #else 13 | #include 14 | #endif 15 | 16 | namespace zk 17 | { 18 | 19 | /// \addtogroup Client 20 | /// \{ 21 | 22 | 23 | #if ZKPP_FUTURE_USE_BOOST 24 | 25 | using exception_ptr = boost::exception_ptr; 26 | exception_ptr current_exception() noexcept; 27 | 28 | template 29 | [[noreturn]] inline void throw_exception(T const & e) 30 | { 31 | throw boost::enable_current_exception(e); 32 | } 33 | 34 | 35 | #else 36 | 37 | using exception_ptr = std::exception_ptr; 38 | exception_ptr current_exception() noexcept; 39 | 40 | template 41 | [[noreturn]] inline void throw_exception(T const & e) 42 | { 43 | throw e; 44 | } 45 | 46 | #endif 47 | 48 | /// \} 49 | 50 | } 51 | 52 | -------------------------------------------------------------------------------- /src/zk/forwards.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace zk 6 | { 7 | 8 | class acl; 9 | class acl_rule; 10 | struct acl_version; 11 | struct child_version; 12 | class client; 13 | class connection; 14 | class connection_params; 15 | enum class create_mode : unsigned int; 16 | class create_result; 17 | class error; 18 | class event; 19 | class exists_result; 20 | enum class error_code : int; 21 | enum class event_type : int; 22 | class get_acl_result; 23 | class get_children_result; 24 | class get_result; 25 | class multi_result; 26 | class multi_op; 27 | class op; 28 | enum class op_type : int; 29 | enum class permission : unsigned int; 30 | class set_result; 31 | enum class state : int; 32 | struct transaction_id; 33 | struct version; 34 | class watch_children_result; 35 | class watch_exists_result; 36 | class watch_result; 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/zk/future.hpp: -------------------------------------------------------------------------------- 1 | /** \file 2 | * Controls the import of \c future and \c promise types. These are probably \c std::future and \c std::promise, but 3 | * can be your custom types (as long as they behave in a manner similar enough to \c std::future and \c std::promise). 4 | **/ 5 | #pragma once 6 | 7 | #include 8 | 9 | /** \addtogroup Client 10 | * \{ 11 | **/ 12 | 13 | /** \def ZKPP_FUTURE_USE_STD_EXPERIMENTAL 14 | * Set this to 1 to use \c std::experimental::future and \c std::experimental::promise as the backing types for 15 | * \c zk::future and \c zk::promise. 16 | **/ 17 | #ifndef ZKPP_FUTURE_USE_STD_EXPERIMENTAL 18 | # define ZKPP_FUTURE_USE_STD_EXPERIMENTAL 0 19 | #endif 20 | 21 | /** \def ZKPP_FUTURE_USE_BOOST 22 | * Set this to 1 to use \c boost::future and \c boost::promise as the backing types for 23 | * \c zk::future and \c zk::promise. 24 | **/ 25 | #ifndef ZKPP_FUTURE_USE_BOOST 26 | # define ZKPP_FUTURE_USE_BOOST 0 27 | #endif 28 | 29 | /** \def ZKPP_FUTURE_USE_CUSTOM 30 | * Set this to 1 to use custom definitions of \c zk::future and \c zk::promise. If this is set, you must also set 31 | * \c ZKPP_FUTURE_TEMPLATE, \c ZKPP_PROMISE_TEMPLATE, and \c ZKPP_FUTURE_INCLUDE. 32 | * 33 | * \def ZKPP_FUTURE_TEMPLATE 34 | * The template to use for \c zk::future. By default, this is \c std::future. 35 | * 36 | * \def ZKPP_PROMISE_TEMPLATE 37 | * The template to use for \c zk::promise. This should be highly related to \c ZKPP_FUTURE_TEMPLATE 38 | * 39 | * \def ZKPP_ASYNC_TEMPLATE 40 | * The template to use for \c zk::async. This should be highly related to \c ZKPP_FUTURE_TEMPLATE and usually mapped 41 | * to std::async or boost::async. 42 | * 43 | * \def ZKPP_LAUNCH_ENUM 44 | * The enum to use for \c zk::async launch policy. This should be highly related to \c ZKPP_FUTURE_TEMPLATE and usually mapped 45 | * to std::launch::async or boost::launch::async. 46 | * 47 | * \def ZKPP_FUTURE_INCLUDE 48 | * The file to include to get the implementation for \c future and \c promise. If you define \c ZKPP_FUTURE_TEMPLATE 49 | * and \c ZKPP_PROMISE_TEMPLATE, you must also define this. 50 | **/ 51 | #ifndef ZKPP_FUTURE_USE_CUSTOM 52 | # define ZKPP_FUTURE_USE_CUSTOM 0 53 | #endif 54 | 55 | /** \def ZKPP_FUTURE_USE_STD 56 | * Set this to 1 to use \c std::future and \c std::promise as the backing types for \c zk::future and \c zk::promise. 57 | * This is the default behavior. 58 | **/ 59 | #ifndef ZKPP_FUTURE_USE_STD 60 | # if ZKPP_FUTURE_USE_BOOST || ZKPP_FUTURE_USE_STD_EXPERIMENTAL || ZKPP_FUTURE_USE_CUSTOM 61 | # define ZKPP_FUTURE_USE_STD 0 62 | # else 63 | # define ZKPP_FUTURE_USE_STD 1 64 | # endif 65 | #endif 66 | 67 | #if ZKPP_FUTURE_USE_STD 68 | # define ZKPP_FUTURE_INCLUDE 69 | # define ZKPP_FUTURE_TEMPLATE std::future 70 | # define ZKPP_PROMISE_TEMPLATE std::promise 71 | # define ZKPP_ASYNC_TEMPLATE std::async 72 | # define ZKPP_LAUNCH_ENUM std::launch 73 | #elif ZKPP_FUTURE_USE_STD_EXPERIMENTAL 74 | # define ZKPP_FUTURE_INCLUDE 75 | # define ZKPP_FUTURE_TEMPLATE std::experimental::future 76 | # define ZKPP_PROMISE_TEMPLATE std::experimental::promise 77 | # define ZKPP_ASYNC_TEMPLATE std::async 78 | # define ZKPP_LAUNCH_ENUM std::launch 79 | #elif ZKPP_FUTURE_USE_BOOST 80 | # define BOOST_THREAD_PROVIDES_FUTURE 81 | # define BOOST_THREAD_PROVIDES_FUTURE_CONTINUATION 82 | # define BOOST_THREAD_PROVIDES_FUTURE_WHEN_ALL_WHEN_ANY 83 | # define ZKPP_FUTURE_INCLUDE 84 | # define ZKPP_FUTURE_TEMPLATE boost::future 85 | # define ZKPP_PROMISE_TEMPLATE boost::promise 86 | # define ZKPP_ASYNC_TEMPLATE boost::async 87 | # define ZKPP_LAUNCH_ENUM boost::launch 88 | #elif ZKPP_FUTURE_USE_CUSTOM 89 | # if !defined ZKPP_FUTURE_TEMPLATE || !defined ZKPP_PROMISE_TEMPLATE || !defined ZKPP_FUTURE_INCLUDE 90 | # error "When ZKPP_FUTURE_USE_CUSTOM is set, you must also define ZKPP_FUTURE_TEMPLATE, ZKPP_PROMISE_TEMPLATE," 91 | # error "and ZKPP_FUTURE_INCLUDE." 92 | # endif 93 | #else 94 | # error "Unknown type to use for zk::future and zk::promise" 95 | #endif 96 | 97 | #include ZKPP_FUTURE_INCLUDE 98 | 99 | /** \} **/ 100 | 101 | namespace zk 102 | { 103 | 104 | /** \addtogroup Client 105 | * \{ 106 | **/ 107 | 108 | template 109 | using future = ZKPP_FUTURE_TEMPLATE; 110 | 111 | template 112 | using promise = ZKPP_PROMISE_TEMPLATE; 113 | 114 | using ZKPP_LAUNCH_ENUM; 115 | using ZKPP_ASYNC_TEMPLATE; 116 | 117 | /** \} **/ 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/zk/multi.cpp: -------------------------------------------------------------------------------- 1 | #include "multi.hpp" 2 | #include "exceptions.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace zk 10 | { 11 | 12 | template 13 | static std::string to_string_generic(const T& self) 14 | { 15 | std::ostringstream os; 16 | os << self; 17 | return os.str(); 18 | } 19 | 20 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 21 | // op_type // 22 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 23 | 24 | std::ostream& operator<<(std::ostream& os, const op_type& self) 25 | { 26 | switch (self) 27 | { 28 | case op_type::check: return os << "check"; 29 | case op_type::create: return os << "create"; 30 | case op_type::erase: return os << "erase"; 31 | case op_type::set: return os << "set"; 32 | default: return os << "op_type(" << static_cast(self) << ')'; 33 | } 34 | } 35 | 36 | std::string to_string(const op_type& self) 37 | { 38 | return to_string_generic(self); 39 | } 40 | 41 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | // op // 43 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 44 | 45 | op::op(any_data&& src) noexcept : 46 | _storage(std::move(src)) 47 | { } 48 | 49 | op::op(const op& src) = default; 50 | 51 | op::op(op&& src) noexcept : 52 | _storage(std::move(src._storage)) 53 | { } 54 | 55 | op::~op() noexcept = default; 56 | 57 | op_type op::type() const 58 | { 59 | return std::visit([] (const auto& x) { return x.type(); }, _storage); 60 | } 61 | 62 | template 63 | const T& op::as(ptr operation) const 64 | { 65 | try 66 | { 67 | return std::get(_storage); 68 | } 69 | catch (const std::bad_variant_access&) 70 | { 71 | zk::throw_exception(std::logic_error( std::string("Invalid op type for op::") 72 | + std::string(operation) 73 | + std::string(": ") 74 | + to_string(type()) 75 | )); 76 | } 77 | } 78 | 79 | // check 80 | 81 | op::check_data::check_data(std::string path, version check_) : 82 | path(std::move(path)), 83 | check(check_) 84 | { } 85 | 86 | std::ostream& operator<<(std::ostream& os, const op::check_data& self) 87 | { 88 | os << '{' << self.path; 89 | os << ' ' << self.check; 90 | return os << '}'; 91 | } 92 | 93 | op op::check(std::string path, version check_) 94 | { 95 | return op(check_data(std::move(path), check_)); 96 | } 97 | 98 | const op::check_data& op::as_check() const 99 | { 100 | return as("as_check"); 101 | } 102 | 103 | // create 104 | 105 | op::create_data::create_data(std::string path, buffer data, acl rules, create_mode mode) : 106 | path(std::move(path)), 107 | data(std::move(data)), 108 | rules(std::move(rules)), 109 | mode(mode) 110 | { } 111 | 112 | std::ostream& operator<<(std::ostream& os, const op::create_data& self) 113 | { 114 | os << '{' << self.path; 115 | os << ' ' << self.mode; 116 | os << ' ' << self.rules; 117 | return os << '}'; 118 | } 119 | 120 | op op::create(std::string path, buffer data, acl rules, create_mode mode) 121 | { 122 | return op(create_data(std::move(path), std::move(data), std::move(rules), mode)); 123 | } 124 | 125 | op op::create(std::string path, buffer data, create_mode mode) 126 | { 127 | return create(std::move(path), std::move(data), acls::open_unsafe(), mode); 128 | } 129 | 130 | const op::create_data& op::as_create() const 131 | { 132 | return as("as_create"); 133 | } 134 | 135 | // erase 136 | 137 | op::erase_data::erase_data(std::string path, version check) : 138 | path(std::move(path)), 139 | check(check) 140 | { } 141 | 142 | std::ostream& operator<<(std::ostream& os, const op::erase_data& self) 143 | { 144 | os << '{' << self.path; 145 | os << ' ' << self.check; 146 | return os << '}'; 147 | } 148 | 149 | op op::erase(std::string path, version check) 150 | { 151 | return op(erase_data(std::move(path), check)); 152 | } 153 | 154 | const op::erase_data& op::as_erase() const 155 | { 156 | return as("as_erase"); 157 | } 158 | 159 | // set 160 | 161 | op::set_data::set_data(std::string path, buffer data, version check) : 162 | path(std::move(path)), 163 | data(std::move(data)), 164 | check(check) 165 | { } 166 | 167 | std::ostream& operator<<(std::ostream& os, const op::set_data& self) 168 | { 169 | os << '{' << self.path; 170 | os << ' ' << self.check; 171 | return os << '}'; 172 | } 173 | 174 | op op::set(std::string path, buffer data, version check) 175 | { 176 | return op(set_data(std::move(path), std::move(data), check)); 177 | } 178 | 179 | const op::set_data& op::as_set() const 180 | { 181 | return as("as_set"); 182 | } 183 | 184 | // generic 185 | 186 | std::ostream& operator<<(std::ostream& os, const op& self) 187 | { 188 | os << self.type(); 189 | std::visit([&] (const auto& x) { os << x; }, self._storage); 190 | return os; 191 | } 192 | 193 | std::string to_string(const op& self) 194 | { 195 | std::ostringstream os; 196 | os << self; 197 | return os.str(); 198 | } 199 | 200 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 201 | // multi_op // 202 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 203 | 204 | multi_op::multi_op(std::vector ops) noexcept : 205 | _ops(std::move(ops)) 206 | { } 207 | 208 | multi_op::~multi_op() noexcept 209 | { } 210 | 211 | std::ostream& operator<<(std::ostream& os, const multi_op& self) 212 | { 213 | os << '['; 214 | bool first = true; 215 | for (const auto& x : self) 216 | { 217 | if (first) 218 | first = false; 219 | else 220 | os << ", "; 221 | os << x; 222 | } 223 | return os << ']'; 224 | } 225 | 226 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 227 | // multi_result // 228 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 229 | 230 | multi_result::part::part(op_type type, std::nullptr_t) noexcept : 231 | _type(type), 232 | _storage(std::monostate()) 233 | { } 234 | 235 | multi_result::part::part(create_result res) noexcept : 236 | _type(op_type::create), 237 | _storage(std::move(res)) 238 | { } 239 | 240 | multi_result::part::part(set_result res) noexcept : 241 | _type(op_type::set), 242 | _storage(std::move(res)) 243 | { } 244 | 245 | multi_result::part::part(const part& src) = default; 246 | 247 | multi_result::part::part(part&& src) noexcept : 248 | _type(src._type), 249 | _storage(std::move(src._storage)) 250 | { } 251 | 252 | multi_result::part::~part() noexcept = default; 253 | 254 | template 255 | const T& multi_result::part::as(ptr operation) const 256 | { 257 | try 258 | { 259 | return std::get(_storage); 260 | } 261 | catch (const std::bad_variant_access&) 262 | { 263 | zk::throw_exception(std::logic_error( std::string("Invalid op type for multi_result::") 264 | + std::string(operation) 265 | + std::string(": ") 266 | + to_string(type()) 267 | )); 268 | } 269 | } 270 | 271 | const create_result& multi_result::part::as_create() const 272 | { 273 | return as("as_create"); 274 | } 275 | 276 | const set_result& multi_result::part::as_set() const 277 | { 278 | return as("as_set"); 279 | } 280 | 281 | multi_result::multi_result(std::vector parts) noexcept : 282 | _parts(std::move(parts)) 283 | { } 284 | 285 | multi_result::~multi_result() noexcept 286 | { } 287 | 288 | std::ostream& operator<<(std::ostream& os, const multi_result::part& self) 289 | { 290 | switch (self.type()) 291 | { 292 | case op_type::create: return os << self.as_create(); 293 | case op_type::set: return os << self.as_set(); 294 | default: return os << self.type() << "_result{}"; 295 | } 296 | } 297 | 298 | std::ostream& operator<<(std::ostream& os, const multi_result& self) 299 | { 300 | os << '['; 301 | bool first = true; 302 | for (const auto& x : self) 303 | { 304 | if (first) 305 | first = false; 306 | else 307 | os << ", "; 308 | os << x; 309 | } 310 | return os << ']'; 311 | } 312 | 313 | std::string to_string(const multi_result::part& self) 314 | { 315 | return to_string_generic(self); 316 | } 317 | 318 | std::string to_string(const multi_result& self) 319 | { 320 | return to_string_generic(self); 321 | } 322 | 323 | } 324 | -------------------------------------------------------------------------------- /src/zk/multi_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "client.hpp" 9 | #include "error.hpp" 10 | #include "multi.hpp" 11 | #include "string_view.hpp" 12 | 13 | namespace zk 14 | { 15 | 16 | class multi_tests : 17 | public server::server_fixture 18 | { }; 19 | 20 | static buffer buffer_from(string_view str) 21 | { 22 | return buffer(str.data(), str.data() + str.size()); 23 | } 24 | 25 | GTEST_TEST_F(multi_tests, commit_no_fail) 26 | { 27 | client c = get_connected_client(); 28 | 29 | auto node1 = c.create("/test-", buffer_from("Going to delete"), create_mode::sequential).get().name(); 30 | auto node2 = c.create("/test-", buffer_from("First data"), create_mode::sequential).get().name(); 31 | 32 | multi_op txn = 33 | { 34 | op::check("/"), 35 | op::create("/test-", buffer_from("1"), create_mode::sequential), 36 | op::erase(node1), 37 | op::set(node2, buffer_from("Second data")), 38 | }; 39 | multi_result res = c.commit(txn).get(); 40 | CHECK_EQ(4U, res.size()); 41 | 42 | // node1 should be erased 43 | CHECK_FALSE(c.exists(node1).get()); 44 | 45 | auto node2_contents = c.get(node2).get().data(); 46 | CHECK_TRUE(node2_contents == buffer_from("Second data")); 47 | 48 | // Create a child of the node created in the multi 49 | auto name_to_make = res[1].as_create().name() + "/some-subnode"; 50 | c.create(name_to_make, buffer_from("sub")).get(); 51 | } 52 | 53 | GTEST_TEST_F(multi_tests, commit_fail_check) 54 | { 55 | client c = get_connected_client(); 56 | 57 | auto base = c.create("/test-", buffer_from("Base"), create_mode::sequential).get().name(); 58 | multi_op txn = 59 | { 60 | op::create(base + "/a", buffer_from("a")), 61 | op::check(base + "/does-not-exist"), 62 | op::create(base + "/b", buffer_from("b")), 63 | }; 64 | 65 | try 66 | { 67 | c.commit(txn).get(); 68 | } 69 | catch (const transaction_failed& ex) 70 | { 71 | CHECK_EQ(error_code::no_entry, ex.underlying_cause()); 72 | CHECK_EQ(1U, ex.failed_op_index()); 73 | } 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/zk/optional.hpp: -------------------------------------------------------------------------------- 1 | /// \file 2 | /// Imports of \c optional and \c nullopt_t types, as well as the \c nullopt \c constexpr. These are \c std::optional, 3 | /// \c std::nullopt_t, and \c std::nullopt, respectively. 4 | #pragma once 5 | 6 | #include 7 | 8 | #include 9 | 10 | namespace zk 11 | { 12 | 13 | /// \addtogroup Client 14 | /// \{ 15 | 16 | template 17 | using optional = std::optional; 18 | 19 | using nullopt_t = std::nullopt_t; 20 | 21 | using std::nullopt; 22 | 23 | /// Apply \a transform with the arguments in \a x iff all of them have a value. Otherwise, \c nullopt will be returned. 24 | template 25 | auto map(FUnary&& transform, const optional&... x) -> optional 26 | { 27 | if ((x && ...)) 28 | return transform(x.value()...); 29 | else 30 | return nullopt; 31 | } 32 | 33 | template 34 | optional some(T x) 35 | { 36 | return optional(std::move(x)); 37 | } 38 | 39 | /// \} 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/zk/optional_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "optional.hpp" 4 | 5 | namespace zk 6 | { 7 | 8 | GTEST_TEST(optional_test, integer) 9 | { 10 | optional x = nullopt; 11 | CHECK_FALSE(x); 12 | x = 1; 13 | CHECK_TRUE(x); 14 | CHECK_EQ(1, x.value()); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/zk/server/classpath.cpp: -------------------------------------------------------------------------------- 1 | #include "classpath.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | #include 15 | #include 16 | 17 | namespace zk::server 18 | { 19 | 20 | template 21 | void join(std::ostream& os, const TContainer& src, TSep sep) 22 | { 23 | bool first = true; 24 | for (const auto& x : src) 25 | { 26 | if (!std::exchange(first, false)) 27 | os << sep; 28 | os << x; 29 | } 30 | } 31 | 32 | static bool file_exists(ptr path) 33 | { 34 | struct ::stat s; 35 | if (::stat(path, &s)) 36 | { 37 | if (errno == ENOENT) 38 | return false; 39 | else 40 | throw std::system_error(errno, std::system_category()); 41 | } 42 | else 43 | { 44 | return true; 45 | } 46 | } 47 | 48 | static classpath find_system_default() 49 | { 50 | string_view locations[] = 51 | { 52 | "/usr/share/java", 53 | "/usr/local/share/java", 54 | }; 55 | string_view requirements[] = 56 | { 57 | "zookeeper.jar", 58 | "slf4j-api.jar", 59 | "slf4j-simple.jar", 60 | }; 61 | 62 | std::vector components; 63 | std::vector unfound; 64 | for (auto jar : requirements) 65 | { 66 | bool found = false; 67 | for (auto base_loc : locations) 68 | { 69 | auto potential_sz = base_loc.size() + jar.size() + 4; 70 | char potential[potential_sz]; 71 | std::snprintf(potential, potential_sz, "%s/%s", base_loc.data(), jar.data()); 72 | 73 | if (file_exists(potential)) 74 | { 75 | found = true; 76 | components.emplace_back(std::string(potential)); 77 | break; 78 | } 79 | } 80 | 81 | if (!found) 82 | unfound.emplace_back(jar); 83 | } 84 | 85 | if (unfound.empty()) 86 | { 87 | return classpath(std::move(components)); 88 | } 89 | else 90 | { 91 | std::ostringstream os; 92 | os << "Could not find requirement" << ((unfound.size() == 1U) ? "" : "s") << ": "; 93 | join(os, unfound, ", "); 94 | os << ". Searched paths: "; 95 | join(os, locations, ", "); 96 | throw std::runtime_error(os.str()); 97 | } 98 | 99 | } 100 | 101 | classpath classpath::system_default() 102 | { 103 | static const auto instance = find_system_default(); 104 | return instance; 105 | } 106 | 107 | classpath::classpath(std::vector components) noexcept : 108 | _components(std::move(components)) 109 | { } 110 | 111 | std::string classpath::command_line() const 112 | { 113 | std::ostringstream os; 114 | os << *this; 115 | return os.str(); 116 | } 117 | 118 | std::ostream& operator<<(std::ostream& os, const classpath& self) 119 | { 120 | join(os, self._components, ':'); 121 | return os; 122 | } 123 | 124 | } 125 | -------------------------------------------------------------------------------- /src/zk/server/classpath.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | namespace zk::server 10 | { 11 | 12 | /// \addtogroup Server 13 | /// \{ 14 | 15 | /// Represents a collection of JARs or other Java entities that should be provided as the `--classpath` to the JVM. This 16 | /// is used to fetch both the ZooKeeper package (`zookeeper.jar`), but the required SLF JAR. If you do not know or care 17 | /// about what that second part means, just call \ref system_default. 18 | class classpath final 19 | { 20 | public: 21 | /// Create a classpath specification from the provided \a components. 22 | explicit classpath(std::vector components) noexcept; 23 | 24 | /// Load the system-default classpath for ZooKeeper. This searches for `zookeeper.jar` nad `slf4j-simple.jar` in 25 | /// various standard locations. 26 | static classpath system_default(); 27 | 28 | /// Get the command-line representation of this classpath. This puts \c ':' characters between the components. 29 | std::string command_line() const; 30 | 31 | friend std::ostream& operator<<(std::ostream&, const classpath&); 32 | 33 | private: 34 | std::vector _components; 35 | }; 36 | 37 | /// \} 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/zk/server/classpath_registration_template.cpp.in: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | namespace zk::server 7 | { 8 | 9 | namespace 10 | { 11 | 12 | auto registration = test_package_registry::instance() 13 | .register_classpath_server 14 | ( 15 | R"(@server_version@)", 16 | classpath({R"(@server_classpath@)"}) 17 | ); 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/zk/server/classpath_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "classpath.hpp" 7 | 8 | namespace zk::server 9 | { 10 | 11 | GTEST_TEST(classpath_tests, system_default) 12 | { 13 | try 14 | { 15 | auto cp = classpath::system_default(); 16 | std::cout << "Found system-default ZooKeeper: " << cp.command_line() << std::endl; 17 | } 18 | catch (const std::runtime_error& ex) 19 | { 20 | std::cout << "Could not find system-default ZooKeeper: " << ex.what() << std::endl; 21 | } 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/zk/server/configuration_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include "configuration.hpp" 8 | 9 | namespace zk::server 10 | { 11 | 12 | static string_view configuration_source_file_example = 13 | R"(# http://hadoop.apache.org/zookeeper/docs/current/zookeeperAdmin.html 14 | 15 | tickTime=2500 16 | initLimit=10 17 | syncLimit=5 18 | dataDir=/var/lib/zookeeper 19 | 20 | # the port at which the clients will connect 21 | clientPort=2181 22 | 23 | # specify all zookeeper servers 24 | server.1=zookeeper1:2888:3888 25 | server.2=zookeeper2:2888:3888 26 | server.3=zookeeper3:2888:3888 27 | 28 | #preAllocSize=65536 29 | #snapCount=1000 30 | 31 | leaderServes=yes 32 | 33 | # A server key that we do not understand 34 | randomExtra=Value 35 | )"; 36 | 37 | GTEST_TEST(configuration_tests, from_example) 38 | { 39 | auto parsed = configuration::from_string(configuration_source_file_example); 40 | CHECK_EQ(2500U, parsed.tick_time().count()); 41 | CHECK_EQ(10U, parsed.init_limit()); 42 | CHECK_EQ(5U, parsed.sync_limit()); 43 | CHECK_EQ(2181U, parsed.client_port()); 44 | CHECK_TRUE(parsed.leader_serves()); 45 | 46 | auto servers = parsed.servers(); 47 | CHECK_EQ(3U, servers.size()); 48 | CHECK_EQ("zookeeper1:2888:3888", servers.at(server_id(1))); 49 | CHECK_EQ("zookeeper2:2888:3888", servers.at(server_id(2))); 50 | CHECK_EQ("zookeeper3:2888:3888", servers.at(server_id(3))); 51 | 52 | auto unrecognized = parsed.unknown_settings(); 53 | CHECK_EQ(1U, unrecognized.size()); 54 | CHECK_EQ("Value", unrecognized.at("randomExtra")); 55 | 56 | auto configured = configuration::make_minimal("/var/lib/zookeeper") 57 | .tick_time(std::chrono::milliseconds(2500)) 58 | .init_limit(10) 59 | .sync_limit(5) 60 | .client_port(2181) 61 | .leader_serves(true) 62 | .add_server(server_id(1), "zookeeper1") 63 | .add_server(server_id(2), "zookeeper2") 64 | .add_server(server_id(3), "zookeeper3") 65 | .add_setting("randomExtra", "Value") 66 | ; 67 | CHECK_EQ(configured, parsed); 68 | 69 | std::ostringstream os; 70 | parsed.save(os); 71 | auto reloaded = configuration::from_string(os.str()); 72 | CHECK_EQ(parsed, reloaded); 73 | } 74 | 75 | GTEST_TEST(configuration_tests, minimal) 76 | { 77 | auto minimal = configuration::make_minimal("/some/path", 2345); 78 | CHECK_EQ("/some/path", minimal.data_directory().value()); 79 | CHECK_EQ(2345, minimal.client_port()); 80 | CHECK_TRUE(minimal.is_minimal()); 81 | } 82 | 83 | static string_view configuration_source_with_four_letter_words_example = 84 | R"(# http://hadoop.apache.org/zookeeper/docs/current/zookeeperAdmin.html 85 | 86 | tickTime=2500 87 | initLimit=10 88 | syncLimit=5 89 | dataDir=/var/lib/zookeeper 90 | 4lw.commands.whitelist=stat,mntr,srvr,ruok 91 | )"; 92 | 93 | GTEST_TEST(configuration_tests, four_letter_words) 94 | { 95 | auto parsed = configuration::from_string(configuration_source_with_four_letter_words_example); 96 | 97 | CHECK_EQ(4U, parsed.four_letter_word_whitelist().size()); 98 | std::set expected = { "stat", "mntr", "srvr", "ruok" }; 99 | CHECK_TRUE(expected == parsed.four_letter_word_whitelist()); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/zk/server/detail/close.cpp: -------------------------------------------------------------------------------- 1 | #include "close.hpp" 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace zk::server::detail 10 | { 11 | 12 | void close(int fd) 13 | { 14 | if (::close(fd) == -1) 15 | { 16 | // LCOV_EXCL_START: Close will always work -- push failures to the consumer 17 | switch (errno) 18 | { 19 | case EINTR: return; // POSIX guarantees we're still closed in EINTR cases 20 | case EIO: throw std::system_error(errno, std::system_category(), "I/O error on close()"); 21 | default: throw std::system_error(errno, std::system_category(), "System error on close()"); 22 | } 23 | // LCOV_EXCL_STOP 24 | } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/zk/server/detail/close.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace zk::server::detail 6 | { 7 | 8 | /** A safe wrapper around the Linux \c close call. In particular, it transforms certain error codes that matter into 9 | * exceptions and happily ignores error codes that still close the file descriptor (\c EINTR). 10 | * 11 | * \throws system_error if an I/O error occurs (this can happen if the kernel defers writes -- although in general, you 12 | * have made a grave error by not performing a \c sync before \c close). 13 | * \throws system_error if \a fd is not a file descriptor. 14 | **/ 15 | void close(int fd); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/zk/server/detail/event_handle.cpp: -------------------------------------------------------------------------------- 1 | #include "event_handle.hpp" 2 | #include "close.hpp" 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | namespace zk::server::detail 12 | { 13 | 14 | event_handle::event_handle() : 15 | _fd(::eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)) 16 | { } 17 | 18 | event_handle::~event_handle() noexcept 19 | { 20 | close(); 21 | } 22 | 23 | void event_handle::close() noexcept 24 | { 25 | if (_fd != -1) 26 | { 27 | detail::close(_fd); 28 | _fd = -1; 29 | } 30 | } 31 | 32 | void event_handle::notify_one() 33 | { 34 | std::uint64_t x = 1; 35 | if (::write(_fd, &x, sizeof x) == -1 && errno != EAGAIN) 36 | throw std::system_error(errno, std::system_category(), "event_handle::notify_one()"); 37 | } 38 | 39 | bool event_handle::try_wait() 40 | { 41 | std::uint64_t burn; 42 | if (::read(_fd, &burn, sizeof burn) == -1) 43 | return errno == EAGAIN ? false 44 | : throw std::system_error(errno, std::system_category(), "event_handle::try_wait()"); 45 | else 46 | return true; 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/zk/server/detail/event_handle.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace zk::server::detail 6 | { 7 | 8 | class event_handle final 9 | { 10 | public: 11 | using native_handle_type = int; 12 | 13 | public: 14 | explicit event_handle(); 15 | 16 | event_handle(const event_handle&) = delete; 17 | 18 | event_handle& operator=(const event_handle&) = delete; 19 | 20 | ~event_handle() noexcept; 21 | 22 | /// Close this event for future signalling. This is automatically called from the destructor. 23 | void close() noexcept; 24 | 25 | /// Signal this handle that something has happened. 26 | void notify_one(); 27 | 28 | /// Attempt to wait for this handle to be signalled, but do not block. 29 | /// 30 | /// \returns \c true if we successfully waited for a signal (and consumed it); \c false if this handle was not 31 | /// signalled. 32 | bool try_wait(); 33 | 34 | /// Get the file descriptor backing this handle. This is generally only used when interacting with the kernel and 35 | /// should be avoided in regular use. 36 | native_handle_type native_handle() { return _fd; } 37 | 38 | private: 39 | native_handle_type _fd; 40 | }; 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/zk/server/detail/pipe.cpp: -------------------------------------------------------------------------------- 1 | #include "close.hpp" 2 | #include "pipe.hpp" 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | namespace zk::server::detail 11 | { 12 | 13 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | // pipe_closed // 15 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | pipe_closed::pipe_closed() : 18 | std::runtime_error("I/O operation on closed pipe") 19 | { } 20 | 21 | pipe_closed::~pipe_closed() noexcept 22 | { } 23 | 24 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 25 | // pipe // 26 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | static int on_exec_flags(on_exec exec) 29 | { 30 | switch (exec) 31 | { 32 | case on_exec::close: 33 | return O_CLOEXEC; 34 | case on_exec::keep_open: 35 | default: 36 | return 0; 37 | } 38 | } 39 | 40 | pipe::pipe(on_exec exec) : 41 | _read_fd(-1), 42 | _write_fd(-1) 43 | { 44 | int fds[2]; 45 | if (::pipe2(fds, O_NONBLOCK | on_exec_flags(exec))) 46 | throw std::system_error(errno, std::system_category(), "Could not create pipe"); 47 | 48 | _read_fd = fds[0]; 49 | _write_fd = fds[1]; 50 | } 51 | 52 | pipe::~pipe() noexcept 53 | { 54 | close(); 55 | } 56 | 57 | static void close_if_open(int& fd) 58 | { 59 | if (fd != -1) 60 | { 61 | close(fd); 62 | fd = -1; 63 | } 64 | } 65 | 66 | void pipe::close() 67 | { 68 | close_write(); 69 | close_read(); 70 | } 71 | 72 | void pipe::close_read() 73 | { 74 | close_if_open(_read_fd); 75 | } 76 | 77 | void pipe::close_write() 78 | { 79 | close_if_open(_write_fd); 80 | } 81 | 82 | std::string pipe::read(optional max) 83 | { 84 | if (_read_fd == -1) 85 | throw pipe_closed(); 86 | 87 | std::string out; 88 | if (max) 89 | out.reserve(max.value()); 90 | 91 | while (true) 92 | { 93 | char read_buf[4096]; 94 | 95 | ssize_t rc = ::read(_read_fd, read_buf, sizeof read_buf); 96 | if (rc < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) 97 | rc = 0; 98 | 99 | if (rc < 0 && errno == EINTR) 100 | { 101 | continue; 102 | } 103 | else if (rc < 0 && errno != EINTR) 104 | { 105 | if (out.empty()) 106 | throw std::system_error(errno, std::system_category(), "Failed to read from pipe"); 107 | else 108 | return out; 109 | } 110 | else if (rc == 0) 111 | { 112 | return out; 113 | } 114 | else 115 | { 116 | out.append(read_buf, rc); 117 | } 118 | } 119 | } 120 | 121 | void pipe::write(const std::string& contents) 122 | { 123 | if (_write_fd == -1) 124 | throw pipe_closed(); 125 | 126 | ptr write_ptr = contents.data(); 127 | while (write_ptr < contents.data() + contents.size()) 128 | { 129 | ssize_t goal_dist = std::distance(write_ptr, contents.data() + contents.size()); 130 | ssize_t rc = ::write(_write_fd, write_ptr, goal_dist); 131 | if (rc < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) 132 | rc = 0; 133 | 134 | if (rc < 0 && errno == EINTR) 135 | { 136 | continue; 137 | } 138 | else if (rc < 0 && errno == EPIPE) 139 | { 140 | throw pipe_closed(); 141 | } 142 | else if (rc < 0) 143 | { 144 | throw std::system_error(errno, std::system_category(), "Failed to write to pipe"); 145 | } 146 | else 147 | { 148 | write_ptr += rc; 149 | } 150 | } 151 | } 152 | 153 | static int dup3(int src_fd, int redir_fd, on_exec exec) 154 | { 155 | while (true) 156 | { 157 | int rc = ::dup3(src_fd, redir_fd, on_exec_flags(exec)); 158 | if (rc == -1 && errno == EINTR) 159 | continue; 160 | else if (rc == -1 && errno != EINTR) 161 | throw std::system_error(errno, std::system_category()); 162 | else 163 | return rc; 164 | } 165 | } 166 | 167 | void pipe::subsume_read(handle fd, on_exec exec) 168 | { 169 | dup3(_read_fd, fd, exec); 170 | // close now unused read side of this pipe 171 | close_read(); 172 | } 173 | 174 | void pipe::subsume_write(handle fd, on_exec exec) 175 | { 176 | dup3(_write_fd, fd, exec); 177 | close_write(); 178 | } 179 | 180 | pipe::handle pipe::native_read_handle() 181 | { 182 | return _read_fd; 183 | } 184 | 185 | pipe::handle pipe::native_write_handle() 186 | { 187 | return _write_fd; 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /src/zk/server/detail/pipe.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | namespace zk::server::detail 10 | { 11 | 12 | /** Used to specify behavior of POSIX resources when \c exec is called. **/ 13 | enum class on_exec 14 | { 15 | /** Enable the close-on-exec flag for an opened handle. This means that any subprocess created with \c exec will not 16 | * be able to access the resource of the parent process. 17 | **/ 18 | close, 19 | /** Keep the file descriptor open for any opened subprocesses. You probably do not want to use this flag unless you 20 | * intend to share a pipe with the subprocess. In general, it is safer for the subprocess to open the same file 21 | * itself. 22 | **/ 23 | keep_open, 24 | }; 25 | 26 | /** Thrown when a read or write operation is attempted on a \c pipe which is closed. **/ 27 | class pipe_closed : 28 | public std::runtime_error 29 | { 30 | public: 31 | pipe_closed(); 32 | 33 | virtual ~pipe_closed() noexcept; 34 | }; 35 | 36 | /** A unidirectional data channel that can be used for interprocess communication or as a signal-safe mechanism for 37 | * in-process communication. 38 | **/ 39 | class pipe final 40 | { 41 | public: 42 | using handle = int; 43 | 44 | public: 45 | /** Create a pipe. 46 | * 47 | * \param exec Should this pipe stay open when \c exec is called? See \c on_exec for more details. 48 | * \throws std::system_error if the pipe could not be created. 49 | **/ 50 | explicit pipe(on_exec exec = on_exec::close); 51 | 52 | pipe(const pipe&) = delete; 53 | pipe& operator=(const pipe&) = delete; 54 | 55 | ~pipe() noexcept; 56 | 57 | /** Close the read and write ends of this pipe. It is safe to call this multiple times (subsequent calls to \c close 58 | * will have no effect). 59 | **/ 60 | void close(); 61 | 62 | /** Similar to \c close, but only close the read end of the pipe. **/ 63 | void close_read(); 64 | 65 | /** Similar to \c close, but only close the write end of the pipe. **/ 66 | void close_write(); 67 | 68 | /** Read from the pipe (up to \a max). If there is nothing to read, an empty string is returned. 69 | * 70 | * \param max The maximum amount of bytes to read in a single pass. If unspecified, this will read until the pipe 71 | * appears to be empty. 72 | * \throws pipe_closed if the pipe is already closed. 73 | **/ 74 | std::string read(optional max = nullopt); 75 | 76 | /** Write the \a contents into the pipe. 77 | * 78 | * \throws pipe_closed if the pipe is already closed (this typically happens when communicating with a subprocess 79 | * which has been terminated). 80 | **/ 81 | void write(const std::string& contents); 82 | 83 | /** Redirect the provided \a fd to read from this pipe instead of what it was originally reading from. 84 | * 85 | * \example 86 | * This redirects standard input to be controlled by a pipe. See \c subprocess for the use case. 87 | * \code 88 | * pipe p; 89 | * p.subsume_read(STDIN_FILENO); 90 | * // Now p.write(...) calls will be picked up by reads to standard input 91 | * \endcode 92 | **/ 93 | void subsume_read(handle fd, on_exec exec = on_exec::close); 94 | 95 | /** Redirect the provided \a fd to write to this pipe instead of what it was originally writing to. **/ 96 | void subsume_write(handle fd, on_exec exec = on_exec::close); 97 | 98 | /** Get the native handle for the read end of this pipe. **/ 99 | handle native_read_handle(); 100 | 101 | /** Get the native handle for the write end of this pipe. **/ 102 | handle native_write_handle(); 103 | 104 | private: 105 | handle _read_fd; 106 | handle _write_fd; 107 | }; 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/zk/server/detail/pipe_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "pipe.hpp" 4 | 5 | #include 6 | 7 | namespace zk::server::detail 8 | { 9 | 10 | GTEST_TEST(pipe_tests, read_write) 11 | { 12 | std::string buff(8000, 'a'); 13 | 14 | pipe p; 15 | p.write(buff); 16 | 17 | std::string out; 18 | out.reserve(buff.size()); 19 | 20 | while (out.size() < buff.size()) 21 | out += p.read(); 22 | 23 | CHECK_EQ(buff, out); 24 | } 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/zk/server/detail/subprocess.cpp: -------------------------------------------------------------------------------- 1 | #include "subprocess.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace zk::server::detail 16 | { 17 | 18 | static pid_t create_subproc(pipe& stdin_pipe, 19 | pipe& stdout_pipe, 20 | pipe& stderr_pipe, 21 | std::string program_name, 22 | subprocess::argument_list args 23 | ) 24 | { 25 | std::vector arg_ptrs; 26 | arg_ptrs.reserve(args.size() + 2); 27 | arg_ptrs.emplace_back(program_name.c_str()); 28 | std::transform(begin(args), end(args), 29 | std::back_inserter(arg_ptrs), 30 | [] (const std::string& s) { return s.c_str(); } 31 | ); 32 | arg_ptrs.emplace_back(nullptr); 33 | 34 | pid_t pid = ::fork(); 35 | if (pid == 0) // child process 36 | { 37 | // LCOV_EXCL_START: No way to detect success at this point 38 | stdin_pipe.subsume_read(STDIN_FILENO, on_exec::keep_open); 39 | stdin_pipe.close(); 40 | stdout_pipe.subsume_write(STDOUT_FILENO, on_exec::keep_open); 41 | stdout_pipe.close(); 42 | stderr_pipe.subsume_write(STDERR_FILENO, on_exec::keep_open); 43 | stderr_pipe.close(); 44 | 45 | ::execvp(program_name.c_str(), 46 | const_cast(arg_ptrs.data()) 47 | ); 48 | // if we get here, exec failed 49 | std::exit(1); 50 | // LCOV_EXCL_STOP 51 | } 52 | else // parent process 53 | { 54 | stdin_pipe.close_read(); 55 | stdout_pipe.close_write(); 56 | stderr_pipe.close_write(); 57 | 58 | return pid; 59 | } 60 | } 61 | 62 | static void dont_leak(pipe& p) noexcept 63 | { 64 | auto fds = { p.native_read_handle(), p.native_write_handle() }; 65 | for (auto fd : fds) 66 | { 67 | if (fd == -1) 68 | continue; 69 | 70 | // ignore the return code -- the inability to set FD_CLOEXEC isn't fatal, just inconvenient 71 | ::fcntl(fd, F_SETFD, FD_CLOEXEC); 72 | } 73 | } 74 | 75 | subprocess::subprocess(std::string program_name, argument_list args) : 76 | _program_name(std::move(program_name)), 77 | _proc_id(-1), 78 | _stdin(on_exec::keep_open), 79 | _stdout(on_exec::keep_open), 80 | _stderr(on_exec::keep_open) 81 | { 82 | _proc_id = create_subproc(_stdin, _stdout, _stderr, _program_name, std::move(args)); 83 | 84 | // Set the on_exec for the pipes to no longer keep_open for future subprocesses -- there is a race condition here if 85 | // another thread creates a subprocess before we set FD_CLOEXEC. The worst thing that happens is we leak a couple of 86 | // (unused) file descriptors to the subprocess. There is no easy way to prevent this. 87 | dont_leak(_stdin); 88 | dont_leak(_stdout); 89 | dont_leak(_stderr); 90 | } 91 | 92 | subprocess::~subprocess() noexcept 93 | { 94 | terminate(); 95 | } 96 | 97 | void subprocess::terminate(duration_type time_to_abort) noexcept 98 | { 99 | auto alarm_time = [&] () -> unsigned int 100 | { 101 | if (time_to_abort.count() <= 0) 102 | return 1U; 103 | else if (time_to_abort.count() > 300) 104 | return 300U; 105 | else 106 | return static_cast(time_to_abort.count()); 107 | }(); 108 | 109 | for (unsigned attempt = 1U; _proc_id != -1; ++attempt) 110 | { 111 | auto old_sig_handler = ::signal(SIGALRM, [](int) { }); 112 | signal(attempt == 1U ? SIGTERM : SIGABRT); 113 | 114 | int rc; 115 | ::alarm(alarm_time); 116 | if (::waitpid(_proc_id, &rc, 0) > 0) 117 | { 118 | _proc_id = -1; 119 | } 120 | 121 | ::alarm(0); 122 | ::signal(SIGALRM, old_sig_handler); 123 | } 124 | } 125 | 126 | bool subprocess::signal(int sig_val) 127 | { 128 | if (_proc_id == -1) 129 | return false; 130 | 131 | pid_t pid = _proc_id; 132 | 133 | int rc = ::kill(pid, sig_val); 134 | if (rc == -1 && errno == ESRCH) 135 | return false; 136 | else if (rc == -1) 137 | throw std::system_error(errno, std::system_category()); 138 | else 139 | return true; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/zk/server/detail/subprocess.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "pipe.hpp" 12 | 13 | namespace zk::server::detail 14 | { 15 | 16 | /// Represents an owned subprocess. 17 | class subprocess 18 | { 19 | public: 20 | using handle = int; 21 | using argument_list = std::vector; 22 | using duration_type = std::chrono::seconds; 23 | 24 | public: 25 | explicit subprocess(std::string program_name, argument_list args = argument_list()); 26 | 27 | subprocess(const subprocess&) = delete; 28 | subprocess& operator=(const subprocess&) = delete; 29 | 30 | ~subprocess() noexcept; 31 | 32 | /// Send a signal to this subprocess. 33 | /// 34 | /// \returns \c true if the signal likely reached the subprocess; \c false if it might not have (this can happen if 35 | /// the subprocess has already terminated). 36 | bool signal(int sig_val); 37 | 38 | pipe& stdin() noexcept { return _stdin; } 39 | pipe& stdout() noexcept { return _stdout; } 40 | pipe& stderr() noexcept { return _stderr; } 41 | 42 | /// Terminate the process if it is still running. In the first attempt to terminate, \c SIGTERM is used. If the 43 | /// process has not terminated before \a time_to_abort has passed, the process is signalled again with \c SIGABRT. 44 | void terminate(duration_type time_to_abort = std::chrono::seconds(1U)) noexcept; 45 | 46 | private: 47 | std::string _program_name; 48 | handle _proc_id; 49 | pipe _stdin; 50 | pipe _stdout; 51 | pipe _stderr; 52 | }; 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/zk/server/detail/subprocess_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | 6 | #include "subprocess.hpp" 7 | 8 | namespace zk::server::detail 9 | { 10 | 11 | GTEST_TEST(subprocess_tests, echo) 12 | { 13 | subprocess proc("echo", { "Hello, world!" }); 14 | 15 | std::string read_str; 16 | while (read_str.size() < 14) 17 | read_str += proc.stdout().read(4096); 18 | CHECK_EQ("Hello, world!\n", read_str); 19 | } 20 | 21 | // `cat` hangs on shutdown due to never getting EOF from stdin -- however, we should still be able to kill the process 22 | // with a signal on scope exit. 23 | GTEST_TEST(subprocess_tests, cat_kill) 24 | { 25 | std::chrono::steady_clock::time_point scope_exit_time; 26 | { 27 | subprocess proc("cat"); 28 | scope_exit_time = std::chrono::steady_clock::now(); 29 | } 30 | auto elapsed = std::chrono::steady_clock::now() - scope_exit_time; 31 | CHECK(elapsed < std::chrono::milliseconds(100)); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /src/zk/server/package_registry.cpp: -------------------------------------------------------------------------------- 1 | #include "package_registry.hpp" 2 | 3 | #include 4 | 5 | namespace zk::server 6 | { 7 | 8 | struct package_registry::registration_info final 9 | { 10 | std::weak_ptr owner; 11 | std::string name; 12 | 13 | registration_info(std::shared_ptr owner, std::string name) : 14 | owner(std::move(owner)), 15 | name(std::move(name)) 16 | { } 17 | 18 | ~registration_info() noexcept 19 | { 20 | if (auto strong_owner = owner.lock()) 21 | strong_owner->unregister_server(*this); 22 | } 23 | }; 24 | 25 | package_registry::package_registry() : 26 | _lifetime(std::make_shared()) 27 | { } 28 | 29 | package_registry::~package_registry() noexcept 30 | { 31 | std::unique_lock ax(_protect); 32 | _lifetime.reset(); 33 | } 34 | 35 | package_registry::registration package_registry::register_classpath_server(std::string version, classpath packages) 36 | { 37 | std::unique_lock ax(_protect); 38 | 39 | auto ret = _registrations.insert({ std::move(version), std::move(packages) }); 40 | if (!ret.second) 41 | throw std::invalid_argument(version + " is already registered"); 42 | 43 | return std::make_shared(std::shared_ptr(_lifetime, this), ret.first->first); 44 | } 45 | 46 | bool package_registry::unregister_server(const registration_info& reg) 47 | { 48 | std::unique_lock ax(_protect); 49 | return _registrations.erase(reg.name) > 0U; 50 | } 51 | 52 | bool package_registry::unregister_server(registration reg) 53 | { 54 | if (reg) 55 | return unregister_server(*reg); 56 | else 57 | return false; 58 | } 59 | 60 | package_registry::size_type package_registry::size() const 61 | { 62 | std::unique_lock ax(_protect); 63 | return _registrations.size(); 64 | } 65 | 66 | optional package_registry::find_newest_classpath() const 67 | { 68 | std::unique_lock ax(_protect); 69 | if (_registrations.empty()) 70 | return nullopt; 71 | else 72 | return _registrations.rbegin()->second; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/zk/server/package_registry.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include "classpath.hpp" 13 | 14 | namespace zk::server 15 | { 16 | 17 | /// \addtogroup Server 18 | /// \{ 19 | 20 | /// The package registry tracks configuration of classpaths and JARs needed to run various ZooKeeper versions. 21 | /// 22 | /// \note{Thread Safety} 23 | /// Registering and unregistering configurations is thread-safe. However, it is \e not safe when a \c package_registry 24 | /// is being destroyed. 25 | class package_registry final 26 | { 27 | public: 28 | using size_type = std::size_t; 29 | 30 | struct registration_info; 31 | using registration = std::shared_ptr; 32 | 33 | public: 34 | /// Create an empty registry. 35 | package_registry(); 36 | 37 | ~package_registry() noexcept; 38 | 39 | /// Register a server that can be created via the specified Java \a classpath. 40 | /// 41 | /// \param version A version string used to look up the server when creating them. While this can be a lie, it 42 | /// should not be. 43 | /// \param packages The Java classpath used to run the server. This will be the \c cp argument to Java. 44 | /// \returns a registration that can be used to \ref unregister_server. 45 | /// \throws std::invalid_argument if \a version is already registered. 46 | registration register_classpath_server(std::string version, classpath packages); 47 | 48 | /// \{ 49 | /// Attempt to unregister the server associated with the provided registration. Unregistering will prevent future 50 | /// servers from being created with the particular setup, but will not teardown servers which might be running with 51 | /// it. 52 | /// 53 | /// \returns \c true if this call removed anything; \c false if otherwise. 54 | bool unregister_server(registration reg); 55 | bool unregister_server(const registration_info& reg); 56 | /// \} 57 | 58 | /// How many registrations have been registered? 59 | size_type size() const; 60 | 61 | /// Is this registry empty? 62 | bool empty() const 63 | { 64 | return size() == size_type(0); 65 | } 66 | 67 | /// Get the classpath for running the newest registered server version. 68 | optional find_newest_classpath() const; 69 | 70 | private: 71 | mutable std::mutex _protect; 72 | std::shared_ptr _lifetime; 73 | std::map _registrations; 74 | }; 75 | 76 | /// \} 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/zk/server/package_registry_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "classpath.hpp" 4 | #include "package_registry.hpp" 5 | #include "package_registry_tests.hpp" 6 | 7 | #include 8 | #include 9 | 10 | namespace zk::server 11 | { 12 | 13 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 14 | // test_package_registry // 15 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 16 | 17 | package_registry& test_package_registry::instance() 18 | { 19 | static auto instance_ptr = std::make_shared(); 20 | return *instance_ptr; 21 | } 22 | 23 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 24 | // Unit Tests // 25 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 26 | 27 | GTEST_TEST(package_registry_tests, test_package_registry_global_instance) 28 | { 29 | CHECK_LT(0U, test_package_registry::instance().size()); 30 | } 31 | 32 | GTEST_TEST(package_registry_tests, registration) 33 | { 34 | package_registry registry; 35 | CHECK_TRUE(registry.empty()); 36 | 37 | auto registration1 = registry.register_classpath_server("1.0", classpath({ "RANDOM" })); 38 | CHECK_EQ(1U, registry.size()); 39 | CHECK_THROWS(std::invalid_argument) { registry.register_classpath_server("1.0", classpath({ "RANDOM" })); }; 40 | 41 | auto registration2 = registry.register_classpath_server("2.0", classpath({ "RANDOM" })); 42 | CHECK_EQ(2U, registry.size()); 43 | registration2.reset(); 44 | CHECK_EQ(1U, registry.size()); 45 | CHECK_FALSE(registry.unregister_server(registration2)); 46 | 47 | CHECK_TRUE(registry.unregister_server(std::move(registration1))); 48 | CHECK_TRUE(registry.empty()); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/zk/server/package_registry_tests.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace zk::server 6 | { 7 | 8 | class package_registry; 9 | 10 | /** Global package registry for server unit tests. **/ 11 | class test_package_registry final 12 | { 13 | public: 14 | static package_registry& instance(); 15 | }; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/zk/server/server.cpp: -------------------------------------------------------------------------------- 1 | #include "server.hpp" 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "classpath.hpp" 14 | #include "configuration.hpp" 15 | #include "detail/event_handle.hpp" 16 | #include "detail/subprocess.hpp" 17 | 18 | namespace zk::server 19 | { 20 | 21 | static void validate_settings(const configuration& settings) 22 | { 23 | if (settings.is_minimal()) 24 | { 25 | return; 26 | } 27 | else if (!settings.source_file()) 28 | { 29 | throw std::invalid_argument("Configuration has not been saved to a file"); 30 | } 31 | } 32 | 33 | server::server(classpath packages, configuration settings) : 34 | _running(true), 35 | _shutdown_event(std::make_unique()) 36 | { 37 | validate_settings(settings); 38 | _worker = std::thread([this, packages = std::move(packages), settings = std::move(settings)] () 39 | { 40 | this->run_process(packages, settings); 41 | } 42 | ); 43 | } 44 | 45 | server::server(configuration settings) : 46 | server(classpath::system_default(), std::move(settings)) 47 | { } 48 | 49 | server::~server() noexcept 50 | { 51 | shutdown(true); 52 | } 53 | 54 | void server::shutdown(bool wait_for_stop) 55 | { 56 | _running.store(false, std::memory_order_release); 57 | _shutdown_event->notify_one(); 58 | 59 | if (wait_for_stop && _worker.joinable()) 60 | _worker.join(); 61 | } 62 | 63 | static void wait_for_event(int fd1, int fd2, int fd3) 64 | { 65 | // This could be implemented with epoll instead of select, but since N=3, it doesn't really matter 66 | ::fd_set read_fds; 67 | FD_ZERO(&read_fds); 68 | FD_SET(fd1, &read_fds); 69 | FD_SET(fd2, &read_fds); 70 | FD_SET(fd3, &read_fds); 71 | 72 | int nfds = std::max(std::max(fd1, fd2), fd3) + 1; 73 | int rc = ::select(nfds, &read_fds, nullptr, nullptr, nullptr); 74 | if (rc < 0) 75 | { 76 | if (errno == EINTR) 77 | return; 78 | else 79 | throw std::system_error(errno, std::system_category(), "select"); 80 | } 81 | } 82 | 83 | void server::run_process(const classpath& packages, const configuration& settings) 84 | { 85 | detail::subprocess::argument_list args = { "-cp", packages.command_line(), 86 | "org.apache.zookeeper.server.quorum.QuorumPeerMain", 87 | }; 88 | if (settings.is_minimal()) 89 | { 90 | args.emplace_back(std::to_string(settings.client_port())); 91 | args.emplace_back(settings.data_directory().value()); 92 | } 93 | else 94 | { 95 | args.emplace_back(settings.source_file().value()); 96 | } 97 | 98 | detail::subprocess proc("java", std::move(args)); 99 | 100 | auto drain_pipes = [&] () 101 | { 102 | bool read_anything = true; 103 | while (read_anything) 104 | { 105 | read_anything = false; 106 | 107 | auto out = proc.stdout().read(); 108 | if (!out.empty()) 109 | { 110 | read_anything = true; 111 | std::cout << out; 112 | } 113 | 114 | auto err = proc.stderr().read(); 115 | if (!err.empty()) 116 | { 117 | read_anything = true; 118 | std::cerr << out; 119 | } 120 | } 121 | }; 122 | 123 | while (_running.load(std::memory_order_acquire)) 124 | { 125 | wait_for_event(proc.stdout().native_read_handle(), 126 | proc.stderr().native_read_handle(), 127 | _shutdown_event->native_handle() 128 | ); 129 | 130 | drain_pipes(); 131 | } 132 | proc.terminate(); 133 | drain_pipes(); 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /src/zk/server/server.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | namespace zk::server 12 | { 13 | 14 | namespace detail 15 | { 16 | 17 | class event_handle; 18 | 19 | } 20 | 21 | /// \defgroup Server 22 | /// Control a ZooKeeper \ref server process. 23 | /// \{ 24 | 25 | class classpath; 26 | class configuration; 27 | 28 | /// Controls a ZooKeeper server process on this local machine. 29 | class server final 30 | { 31 | public: 32 | /// Create a running server process with the specified \a packages and \a settings. 33 | /// 34 | /// \param packages The classpath to use to find ZooKeeper's \c QuorumPeerMain class. 35 | /// \param settings The server settings to run with. 36 | /// \throws std::invalid_argument If `settings.is_minimal()` is \c false and `settings.source_file()` is \c nullopt. 37 | /// This is because non-minimal configurations require ZooKeeper to be launched with a file. 38 | explicit server(classpath packages, configuration settings); 39 | 40 | /// Create a running server with the specified \a settings using the system-provided default packages for ZooKeeper 41 | /// (see \ref classpath::system_default). 42 | /// 43 | /// \param settings The server settings to run with. 44 | /// \throws std::invalid_argument If `settings.is_minimal()` is \c false and `settings.source_file()` is \c nullopt. 45 | /// This is because non-minimal configurations require ZooKeeper to be launched with a file. 46 | explicit server(configuration settings); 47 | 48 | server(const server&) = delete; 49 | 50 | ~server() noexcept; 51 | 52 | /// Initiate shutting down the server process. For most usage, this is not needed, as it is called automatically 53 | /// from the destructor. 54 | /// 55 | /// \param wait_for_stop If \c true, wait for the process to run until termination instead of simply initiating the 56 | /// termination. 57 | void shutdown(bool wait_for_stop = false); 58 | 59 | private: 60 | void run_process(const classpath&, const configuration&); 61 | 62 | private: 63 | std::atomic _running; 64 | std::unique_ptr _shutdown_event; 65 | std::thread _worker; 66 | 67 | // NOTE: The configuration is NOT stored in the server object. This is because configuration can be changed by the 68 | // ZK process in cases like ensemble reconfiguration. It is the job of run_process to deal with this. 69 | }; 70 | 71 | /// \} 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/zk/server/server_group.cpp: -------------------------------------------------------------------------------- 1 | #include "server_group.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "classpath.hpp" 14 | #include "configuration.hpp" 15 | #include "server.hpp" 16 | 17 | namespace zk::server 18 | { 19 | 20 | struct server_group::info 21 | { 22 | configuration settings; //!< Settings for this server. 23 | std::string name; //!< Name of this server (string version of its ID) 24 | std::string path; //!< settings.data_directory 25 | std::uint16_t peer_port; //!< settings.peer_port 26 | std::uint16_t leader_port; //!< settings.leader_port 27 | std::shared_ptr instance; //!< Instance(if this is running) 28 | 29 | info(const configuration& base) : 30 | settings(base) 31 | { } 32 | }; 33 | 34 | server_group::server_group() noexcept 35 | { } 36 | 37 | server_group::server_group(server_group&&) noexcept = default; 38 | 39 | server_group& server_group::operator=(server_group&& src) noexcept 40 | { 41 | _servers = std::move(src._servers); 42 | _conn_string = std::move(src._conn_string); 43 | return *this; 44 | } 45 | 46 | static void create_directory(const std::string& path) 47 | { 48 | if (::mkdir(path.c_str(), 0755)) 49 | { 50 | throw std::system_error(errno, std::system_category()); 51 | } 52 | } 53 | 54 | static void save_id_file(const std::string& path, const server_id& id) 55 | { 56 | std::ofstream ofs(path.c_str()); 57 | if (!ofs) 58 | throw std::runtime_error("IO failure"); 59 | ofs << id << '\n'; 60 | ofs.flush(); 61 | } 62 | 63 | server_group server_group::make_ensemble(std::size_t size, const configuration& base_settings_in) 64 | { 65 | auto base_settings = base_settings_in; 66 | 67 | if (!base_settings.data_directory()) 68 | throw std::invalid_argument("Settings must specify a base directory"); 69 | 70 | auto base_directory = base_settings.data_directory().value(); 71 | auto base_port = base_settings.client_port() == configuration::default_client_port ? std::uint16_t(18500) 72 | : base_settings.client_port(); 73 | 74 | server_group out; 75 | for (std::size_t idx = 0U; idx < size; ++idx) 76 | { 77 | auto id = server_id(idx + 1); 78 | auto px = std::make_shared(base_settings); 79 | auto& x = *px; 80 | x.name = std::to_string(id.value); 81 | x.path = base_directory + "/" + x.name; 82 | x.settings 83 | .client_port(base_port++) 84 | .data_directory(x.path + "/data") 85 | ; 86 | x.peer_port = base_port++; 87 | x.leader_port = base_port++; 88 | 89 | out._servers.emplace(id, px); 90 | } 91 | 92 | std::ostringstream conn_str_os; 93 | conn_str_os << "zk://"; 94 | bool first = true; 95 | 96 | create_directory(base_directory); 97 | for (auto& [id, srvr] : out._servers) 98 | { 99 | for (const auto& [id2, srvr2] : out._servers) 100 | { 101 | srvr->settings.add_server(id2, "127.0.0.1", srvr2->peer_port, srvr2->leader_port); 102 | } 103 | 104 | create_directory(srvr->path); 105 | create_directory(srvr->path + "/data"); 106 | save_id_file(srvr->path + "/data/myid", id); 107 | srvr->settings.save_file(srvr->path + "/settings.cfg"); 108 | 109 | if (!std::exchange(first, false)) 110 | conn_str_os << ','; 111 | conn_str_os << "127.0.0.1:" << srvr->settings.client_port(); 112 | } 113 | conn_str_os << '/'; 114 | out._conn_string = conn_str_os.str(); 115 | 116 | return out; 117 | } 118 | 119 | const std::string& server_group::get_connection_string() 120 | { 121 | return _conn_string; 122 | } 123 | 124 | void server_group::start_all_servers(const classpath& packages) 125 | { 126 | for (auto& [name, srvr] : _servers) 127 | { 128 | static_cast(name); 129 | 130 | if (!srvr->instance) 131 | { 132 | srvr->instance = std::make_shared(packages, srvr->settings); 133 | } 134 | } 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/zk/server/server_group.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "configuration.hpp" 12 | 13 | namespace zk::server 14 | { 15 | 16 | /** \addtogroup Server 17 | * \{ 18 | **/ 19 | 20 | class classpath; 21 | class server; 22 | 23 | /// Create and manage a group of \ref server instances on this local machine (most likely in a single ensemble). This is 24 | /// exclusively for testing, as running multiple peers on a single machine is a very bad idea in production. 25 | /// 26 | /// \code 27 | /// auto servers = zk::server::server_group::make_ensemble(3U, 28 | /// zk::server::configuration::make_minimal("test-data") 29 | /// ); 30 | /// servers.start_all_servers(); 31 | /// auto client = zk::client::connect(servers.get_connection_string()).get(); 32 | /// // do things with client... 33 | /// \endcode 34 | class server_group final 35 | { 36 | public: 37 | /// Create an empty server group. 38 | server_group() noexcept; 39 | 40 | /// Move-construct a server group. 41 | server_group(server_group&&) noexcept; 42 | 43 | /// Move-assign a server group. 44 | server_group& operator=(server_group&&) noexcept; 45 | 46 | /// Create an ensemble of the given \a size. None of the servers will be started. 47 | /// 48 | /// \param size The size of the ensemble. This should be an odd number. 49 | /// \param base_settings The basic settings to use for every server. 50 | static server_group make_ensemble(std::size_t size, const configuration& base_settings); 51 | 52 | /// Get a connection string which can connect to any the servers in the group. 53 | const std::string& get_connection_string(); 54 | 55 | /// Start all servers in the group. Note that this does not wait for the servers to be up-and-running. Use a 56 | /// \ref client instance to check for connectability. 57 | void start_all_servers(const classpath& packages); 58 | 59 | /// How many servers are in this group? 60 | std::size_t size() const { return _servers.size(); } 61 | 62 | private: 63 | struct info; 64 | 65 | using server_map_type = std::map>; 66 | 67 | private: 68 | server_map_type _servers; 69 | std::string _conn_string; 70 | }; 71 | 72 | /// \} 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/zk/server/server_group_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | 7 | #include "configuration.hpp" 8 | #include "package_registry.hpp" 9 | #include "package_registry_tests.hpp" 10 | #include "server_group.hpp" 11 | 12 | namespace zk::server 13 | { 14 | 15 | void delete_directory(std::string path); 16 | 17 | GTEST_TEST(server_group_tests, ensemble) 18 | { 19 | delete_directory("ensemble"); 20 | auto group = server_group::make_ensemble(5U, configuration::make_minimal("ensemble")); 21 | group.start_all_servers(test_package_registry::instance().find_newest_classpath().value()); 22 | 23 | // connect and get data from the ensemble 24 | auto c = client::connect(group.get_connection_string()).get(); 25 | CHECK_TRUE(c.exists("/").get()); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/zk/server/server_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | #include "classpath.hpp" 14 | #include "configuration.hpp" 15 | #include "package_registry.hpp" 16 | #include "package_registry_tests.hpp" 17 | #include "server.hpp" 18 | #include "server_tests.hpp" 19 | 20 | namespace zk::server 21 | { 22 | 23 | void delete_directory(std::string path) 24 | { 25 | auto unlink_cb = [] (ptr fpath, ptr, int, ptr) -> int 26 | { 27 | return std::remove(fpath); 28 | }; 29 | 30 | if (nftw(path.c_str(), unlink_cb, 64, FTW_DEPTH | FTW_PHYS)) 31 | { 32 | if (errno == ENOENT) 33 | return; 34 | else 35 | throw std::system_error(errno, std::system_category()); 36 | } 37 | } 38 | 39 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 40 | // server_fixture // 41 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 42 | 43 | void server_fixture::SetUp() 44 | { 45 | delete_directory("zk-data"); 46 | _server = std::make_shared(test_package_registry::instance().find_newest_classpath().value(), 47 | configuration::make_minimal("zk-data") 48 | ); 49 | _conn_string = "zk://127.0.0.1:2181"; 50 | } 51 | 52 | void server_fixture::TearDown() 53 | { 54 | _server->shutdown(); 55 | _server.reset(); 56 | _conn_string.clear(); 57 | } 58 | 59 | const std::string& server_fixture::get_connection_string() const 60 | { 61 | return _conn_string; 62 | } 63 | 64 | client server_fixture::get_connected_client() const 65 | { 66 | return client(client::connect(get_connection_string()).get()); 67 | } 68 | 69 | void server_fixture::stop_server(bool wait_for_stop) 70 | { 71 | _server->shutdown(wait_for_stop); 72 | } 73 | 74 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 75 | // single_server_fixture // 76 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 77 | 78 | static std::shared_ptr single_server_server; 79 | static std::string single_server_conn_string; 80 | 81 | void single_server_fixture::SetUpTestCase() 82 | { 83 | delete_directory("zk-data"); 84 | single_server_server = std::make_shared(test_package_registry::instance().find_newest_classpath().value(), 85 | configuration::make_minimal("zk-data") 86 | ); 87 | single_server_conn_string = "zk://127.0.0.1:2181"; 88 | } 89 | 90 | void single_server_fixture::TearDownTestCase() 91 | { 92 | single_server_server->shutdown(); 93 | single_server_server.reset(); 94 | single_server_conn_string.clear(); 95 | } 96 | 97 | const std::string& single_server_fixture::get_connection_string() 98 | { 99 | return single_server_conn_string; 100 | } 101 | 102 | client single_server_fixture::get_connected_client() 103 | { 104 | return client(client::connect(get_connection_string()).get()); 105 | } 106 | 107 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 108 | // Unit Tests // 109 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 110 | 111 | GTEST_TEST(server_tests, start_stop) 112 | { 113 | server svr(test_package_registry::instance().find_newest_classpath().value(), 114 | configuration::make_minimal("zk-data") 115 | ); 116 | std::this_thread::sleep_for(std::chrono::seconds(1)); 117 | svr.shutdown(); 118 | } 119 | 120 | GTEST_TEST(server_tests, shutdown_and_wait) 121 | { 122 | server svr(test_package_registry::instance().find_newest_classpath().value(), 123 | configuration::make_minimal("zk-data") 124 | ); 125 | svr.shutdown(true); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/zk/server/server_tests.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | namespace zk::server 10 | { 11 | 12 | class server; 13 | 14 | class server_fixture : 15 | public test::test_fixture 16 | { 17 | public: 18 | virtual void SetUp() override; 19 | 20 | virtual void TearDown() override; 21 | 22 | protected: 23 | const std::string& get_connection_string() const; 24 | 25 | client get_connected_client() const; 26 | 27 | void stop_server(bool wait_for_stop = true); 28 | 29 | private: 30 | std::shared_ptr _server; 31 | std::string _conn_string; 32 | }; 33 | 34 | /// Similar to \ref server_fixture, but do not start up and tear down the server with each test. Instead, setup is run 35 | /// once at the start of a suite and torn down at the end of it. 36 | class single_server_fixture : 37 | public test::test_fixture 38 | { 39 | public: 40 | static void SetUpTestCase(); 41 | 42 | static void TearDownTestCase(); 43 | 44 | protected: 45 | static const std::string& get_connection_string(); 46 | 47 | static client get_connected_client(); 48 | }; 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/zk/string_view.hpp: -------------------------------------------------------------------------------- 1 | /// \file 2 | /// Imports the \c string_view type as \c std::string_view. 3 | #pragma once 4 | 5 | #include 6 | 7 | #include 8 | 9 | namespace zk 10 | { 11 | 12 | /// \addtogroup Client 13 | /// \{ 14 | 15 | using string_view = std::string_view; 16 | 17 | /// \} 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/zk/tests/main.cpp: -------------------------------------------------------------------------------- 1 | #include "test.hpp" 2 | 3 | int main(int argc, char** argv) 4 | { 5 | ::testing::InitGoogleTest(&argc, argv); 6 | return ::RUN_ALL_TESTS(); 7 | } 8 | -------------------------------------------------------------------------------- /src/zk/tests/test.cpp: -------------------------------------------------------------------------------- 1 | #include "test.hpp" 2 | -------------------------------------------------------------------------------- /src/zk/tests/test.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #ifdef GTEST_API_ 6 | # error "GTest was included externally -- you MUST include first" 7 | #endif 8 | 9 | // Before including GTest, tell it not to define the various generically-named macros (which clash with other common 10 | // names). 11 | #define GTEST_DONT_DEFINE_ASSERT_EQ 1 12 | #define GTEST_DONT_DEFINE_ASSERT_NE 1 13 | #define GTEST_DONT_DEFINE_ASSERT_LE 1 14 | #define GTEST_DONT_DEFINE_ASSERT_LT 1 15 | #define GTEST_DONT_DEFINE_ASSERT_GE 1 16 | #define GTEST_DONT_DEFINE_ASSERT_GT 1 17 | #define GTEST_DONT_DEFINE_TEST 1 18 | #define GTEST_DONT_DEFINE_FAIL 1 19 | #define GTEST_DONT_DEFINE_SUCCEED 1 20 | 21 | #include 22 | 23 | // Maybe some day this will be defined 24 | #ifndef GTEST_TEST_F 25 | # define GTEST_TEST_F TEST_F 26 | #endif 27 | 28 | namespace zk::test 29 | { 30 | 31 | /** \def CHECK 32 | * Check a condition and abort the test in failure if it does not hold. When performing binary comparisons, prefer to 33 | * use the check macros which accept two arguments (\c CHECK_EQ, \c CHECK_LT, etc), as these will print out the values 34 | * in failure cases. 35 | * 36 | * \example 37 | * \code 38 | * CHECK(something) << "Something isn't right!"; 39 | * \endcode 40 | **/ 41 | #define CHECK(cond) GTEST_TEST_BOOLEAN_(cond, #cond, false, true, GTEST_FATAL_FAILURE_) 42 | #define CHECK_TRUE(cond) GTEST_TEST_BOOLEAN_(!!(cond), #cond, false, true, GTEST_FATAL_FAILURE_) 43 | #define CHECK_FALSE(cond) GTEST_TEST_BOOLEAN_(!(cond), #cond, true, false, GTEST_FATAL_FAILURE_) 44 | #define CHECK_EQ GTEST_ASSERT_EQ 45 | #define CHECK_NE GTEST_ASSERT_NE 46 | #define CHECK_LT GTEST_ASSERT_LT 47 | #define CHECK_LE GTEST_ASSERT_LE 48 | #define CHECK_GT GTEST_ASSERT_GT 49 | #define CHECK_GE GTEST_ASSERT_GE 50 | #define CHECK_SUCCESS GTEST_SUCCEED 51 | #define CHECK_FAIL GTEST_FAIL 52 | 53 | using test_fixture = ::testing::Test; 54 | 55 | namespace detail 56 | { 57 | 58 | template 59 | struct check_throws_info 60 | { 61 | ptr filename; 62 | std::size_t line_no; 63 | 64 | explicit check_throws_info(ptr filename, std::size_t line_no) : 65 | filename(filename), 66 | line_no(line_no) 67 | { } 68 | 69 | friend std::ostream& operator<<(std::ostream& os, const check_throws_info& info) 70 | { 71 | return os << info.filename << ':' << info.line_no; 72 | } 73 | }; 74 | 75 | template 76 | void operator+(const check_throws_info& info, FAction&& action) 77 | { 78 | try 79 | { 80 | std::forward(action)(); 81 | CHECK_FAIL() << "At " << info << ": Action was supposed to throw, but it did not"; // LCOV_EXCL_LINE 82 | } 83 | catch (const TException&) 84 | { 85 | CHECK_SUCCESS() << "Successfully threw expected error"; 86 | } 87 | } 88 | 89 | /** \def CHECK_THROWS 90 | * Ensure that a block of code throws an exception of type \a ex. 91 | * 92 | * \code 93 | * CHECK_THROWS(std::logic_error) 94 | * { 95 | * throw std::logic_error("Some issue"); 96 | * }; // <- note the ';' 97 | * \endcode 98 | **/ 99 | #define CHECK_THROWS(ex) \ 100 | ::zk::test::detail::check_throws_info(__FILE__, __LINE__) + [&] () 101 | 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/zk/types.cpp: -------------------------------------------------------------------------------- 1 | #include "types.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace zk 8 | { 9 | 10 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | // event_type // 12 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 13 | 14 | std::ostream& operator<<(std::ostream& os, const event_type& self) 15 | { 16 | switch (self) 17 | { 18 | case event_type::error: return os << "error"; 19 | case event_type::created: return os << "created"; 20 | case event_type::erased: return os << "erased"; 21 | case event_type::changed: return os << "changed"; 22 | case event_type::child: return os << "child"; 23 | case event_type::session: return os << "session"; 24 | case event_type::not_watching: return os << "not_watching"; 25 | default: return os << "event_type(" << static_cast(self) << ')'; 26 | }; 27 | } 28 | 29 | std::string to_string(const event_type& self) 30 | { 31 | std::ostringstream os; 32 | os << self; 33 | return os.str(); 34 | } 35 | 36 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 37 | // version // 38 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 39 | 40 | std::ostream& operator<<(std::ostream& os, const version& self) 41 | { 42 | os << "version("; 43 | if (self == version::any()) 44 | os << "any"; 45 | else if (self == version::invalid()) 46 | os << "invalid"; 47 | else 48 | os << self.value; 49 | return os << ')'; 50 | } 51 | 52 | std::string to_string(const version& self) 53 | { 54 | std::ostringstream os; 55 | os << self; 56 | return os.str(); 57 | } 58 | 59 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 60 | // acl_version // 61 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 62 | 63 | std::ostream& operator<<(std::ostream& os, const acl_version& self) 64 | { 65 | os << "acl_version("; 66 | if (self == acl_version::any()) 67 | os << "any"; 68 | else if (self == acl_version::invalid()) 69 | os << "invalid"; 70 | else 71 | os << self.value; 72 | return os << ')'; 73 | } 74 | 75 | std::string to_string(const acl_version& self) 76 | { 77 | std::ostringstream os; 78 | os << self; 79 | return os.str(); 80 | } 81 | 82 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 83 | // child_version // 84 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 85 | 86 | std::ostream& operator<<(std::ostream& os, const child_version& self) 87 | { 88 | os << "child_version("; 89 | if (self == child_version::any()) 90 | os << "any"; 91 | else if (self == child_version::invalid()) 92 | os << "invalid"; 93 | else 94 | os << self.value; 95 | return os << ')'; 96 | } 97 | 98 | std::string to_string(const child_version& self) 99 | { 100 | std::ostringstream os; 101 | os << self; 102 | return os.str(); 103 | } 104 | 105 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 106 | // transaction_id // 107 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 108 | 109 | std::ostream& operator<<(std::ostream& os, const transaction_id& self) 110 | { 111 | return os << "transaction_id(" << self.value << ')'; 112 | } 113 | 114 | std::string to_string(const transaction_id& self) 115 | { 116 | std::ostringstream os; 117 | os << self; 118 | return os.str(); 119 | } 120 | 121 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 122 | // stat // 123 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 124 | 125 | std::ostream& operator<<(std::ostream& os, const stat& self) 126 | { 127 | os << "{data_version=" << self.data_version.value; 128 | os << " child_version=" << self.child_version.value; 129 | os << " acl_version=" << self.acl_version.value; 130 | os << " data_size=" << self.data_size; 131 | os << " children_count=" << self.children_count; 132 | os << " ephemeral=" << (self.is_ephemeral() ? "true" : "false"); 133 | return os << '}'; 134 | } 135 | 136 | std::string to_string(const stat& self) 137 | { 138 | std::ostringstream os; 139 | os << self; 140 | return os.str(); 141 | } 142 | 143 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 144 | // state // 145 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 146 | 147 | std::ostream& operator<<(std::ostream& os, const state& self) 148 | { 149 | switch (self) 150 | { 151 | case state::closed: return os << "closed"; 152 | case state::connecting: return os << "connecting"; 153 | case state::connected: return os << "connected"; 154 | case state::read_only: return os << "read_only"; 155 | case state::expired_session: return os << "expired_session"; 156 | case state::authentication_failed: return os << "authentication_failed"; 157 | default: return os << "state(" << static_cast(self) << ')'; 158 | } 159 | } 160 | 161 | std::string to_string(const state& self) 162 | { 163 | std::ostringstream os; 164 | os << self; 165 | return os.str(); 166 | } 167 | 168 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 169 | // create_mode // 170 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 171 | 172 | std::ostream& operator<<(std::ostream& os, const create_mode& mode) 173 | { 174 | if (mode == create_mode::normal) 175 | return os << "normal"; 176 | 177 | bool first = true; 178 | auto tick = [&] { return std::exchange(first, false) ? "" : "|"; }; 179 | if (is_set(mode, create_mode::ephemeral)) os << tick() << "ephemeral"; 180 | if (is_set(mode, create_mode::sequential)) os << tick() << "sequential"; 181 | if (is_set(mode, create_mode::container)) os << tick() << "container"; 182 | 183 | return os; 184 | } 185 | 186 | std::string to_string(const create_mode& self) 187 | { 188 | std::ostringstream os; 189 | os << self; 190 | return os.str(); 191 | } 192 | 193 | } 194 | -------------------------------------------------------------------------------- /src/zk/types_tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "types.hpp" 4 | 5 | namespace zk 6 | { 7 | 8 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 9 | // event_type // 10 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | GTEST_TEST(event_type_tests, stringification) 13 | { 14 | CHECK_EQ("error", to_string(event_type::error)); 15 | CHECK_EQ("created", to_string(event_type::created)); 16 | CHECK_EQ("erased", to_string(event_type::erased)); 17 | CHECK_EQ("changed", to_string(event_type::changed)); 18 | CHECK_EQ("child", to_string(event_type::child)); 19 | CHECK_EQ("session", to_string(event_type::session)); 20 | CHECK_EQ("not_watching", to_string(event_type::not_watching)); 21 | CHECK_EQ("event_type(764532)", to_string(static_cast(764532))); 22 | } 23 | 24 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 25 | // state // 26 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 27 | 28 | GTEST_TEST(state_tests, stringification) 29 | { 30 | CHECK_EQ("closed", to_string(state::closed)); 31 | CHECK_EQ("connecting", to_string(state::connecting)); 32 | CHECK_EQ("connected", to_string(state::connected)); 33 | CHECK_EQ("read_only", to_string(state::read_only)); 34 | CHECK_EQ("expired_session", to_string(state::expired_session)); 35 | CHECK_EQ("authentication_failed", to_string(state::authentication_failed)); 36 | CHECK_EQ("state(605983)", to_string(static_cast(605983))); 37 | } 38 | 39 | } 40 | --------------------------------------------------------------------------------