diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml new file mode 100644 index 0000000..3b86400 --- /dev/null +++ b/.github/workflows/win.yml @@ -0,0 +1,18 @@ +name: Windows + +on: [push, pull_request] +jobs: + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: dependencies + run: | + choco install cmake + - name: build + run: | + cmake -S . -B build -G "Visual Studio 16 2019" -A x64 + cmake --build build --target ALL_BUILD --config Release + - name: test + run: | + cmake --build build --target RUN_TESTS --config Release diff --git a/CMakeLists.txt b/CMakeLists.txt index e85b4c4..38287d6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,9 +5,19 @@ project(cpp-statsd-client LANGUAGES CXX DESCRIPTION "A header-only StatsD client implemented in C++" HOMEPAGE_URL "https://github.com/vthiery/cpp-statsd-client") + +option(CPP_STATSD_STANDALONE "Allows configuration of targets for verifying library functionality" ON) +option(ENABLE_TESTS "Build tests" ON) +option(ENABLE_COVERAGE "Build with coverage instrumentalisation" OFF) + +if(NOT CPP_STATSD_STANDALONE) + set(ENABLE_TESTS OFF) + set(ENABLE_COVERAGE OFF) +endif() + set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_EXTENSIONS OFF) -option(ENABLE_COVERAGE "Build with coverage instrumentalisation" OFF) + include(GNUInstallDirs) include(CMakePackageConfigHelpers) find_package(Threads) @@ -30,6 +40,9 @@ target_include_directories( INTERFACE $ $) target_link_libraries(${PROJECT_NAME} INTERFACE Threads::Threads) +if(WIN32) + target_link_libraries(${PROJECT_NAME} INTERFACE ws2_32) +endif() # The installation and pkg-config-like cmake config install(TARGETS ${PROJECT_NAME} @@ -54,13 +67,19 @@ install(FILES "${PROJECT_BINARY_DIR}/${PROJECT_NAME}Config.cmake" DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/${PROJECT_NAME}/cmake) install(DIRECTORY ${PROJECT_SOURCE_DIR}/include DESTINATION include) -# The test targets -add_executable(testStatsdClient ${CMAKE_CURRENT_SOURCE_DIR}/tests/testStatsdClient.cpp) -target_compile_options(testStatsdClient PRIVATE -Wall -Wextra -pedantic -Werror) -target_include_directories(testStatsdClient PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests) -target_link_libraries(testStatsdClient ${PROJECT_NAME}) +if(ENABLE_TESTS) + # The test targets + add_executable(testStatsdClient ${CMAKE_CURRENT_SOURCE_DIR}/tests/testStatsdClient.cpp) + if(WIN32) + target_compile_options(testStatsdClient PRIVATE -W4 -WX /external:W0) + else() + target_compile_options(testStatsdClient PRIVATE -Wall -Wextra -pedantic -Werror) + endif() + target_include_directories(testStatsdClient PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/tests) + target_link_libraries(testStatsdClient ${PROJECT_NAME}) -# The test suite -enable_testing() -add_test(ctestTestStatsdClient testStatsdClient) -add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS testStatsdClient) + # The test suite + enable_testing() + add_test(ctestTestStatsdClient testStatsdClient) + add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS testStatsdClient) +endif() diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in new file mode 100644 index 0000000..25a2902 --- /dev/null +++ b/cmake/Config.cmake.in @@ -0,0 +1,18 @@ +# Copyright (c) 2016, Ruslan Baratov +# +# Licensed under the MIT License (the "License"); you may not use this file except +# in compliance with the License. You may obtain a copy of the License at +# +# http://opensource.org/licenses/MIT +# +# Unless required by applicable law or agreed to in writing, software distributed +# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +# CONDITIONS OF ANY KIND, either express or implied. See the License for the +# specific language governing permissions and limitations under the License. + +@PACKAGE_INIT@ + +find_package(Threads REQUIRED) + +include("${CMAKE_CURRENT_LIST_DIR}/@TARGETS_EXPORT_NAME@.cmake") +check_required_components("@PROJECT_NAME@") diff --git a/include/cpp-statsd-client/StatsdClient.hpp b/include/cpp-statsd-client/StatsdClient.hpp index 4b91de4..7a5d22a 100644 --- a/include/cpp-statsd-client/StatsdClient.hpp +++ b/include/cpp-statsd-client/StatsdClient.hpp @@ -55,7 +55,7 @@ namespace Statsd { */ class StatsdClient { public: - //!@name Constructor and destructor + //!@name Constructor and destructor, non-copyable //!@{ //! Constructor diff --git a/include/cpp-statsd-client/UDPSender.hpp b/include/cpp-statsd-client/UDPSender.hpp index ed46d00..c7d667a 100644 --- a/include/cpp-statsd-client/UDPSender.hpp +++ b/include/cpp-statsd-client/UDPSender.hpp @@ -1,13 +1,21 @@ #ifndef UDP_SENDER_HPP #define UDP_SENDER_HPP +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#include +#else #include #include #include -#include #include +#endif + #include #include +#include #include #include #include @@ -15,6 +23,19 @@ #include namespace Statsd { + +#ifdef _WIN32 +using SOCKET_TYPE = SOCKET; +constexpr SOCKET_TYPE k_invalidSocket{INVALID_SOCKET}; +#define SOCKET_ERRNO WSAGetLastError() +#define SOCKET_CLOSE closesocket +#else +using SOCKET_TYPE = int; +constexpr SOCKET_TYPE k_invalidSocket{-1}; +#define SOCKET_ERRNO errno +#define SOCKET_CLOSE close +#endif + /*! * * UDP sender @@ -24,7 +45,7 @@ namespace Statsd { */ class UDPSender final { public: - //!@name Constructor and destructor + //!@name Constructor and destructor, non-copyable //!@{ //! Constructor @@ -36,6 +57,10 @@ class UDPSender final { //! Destructor ~UDPSender(); + UDPSender(const UDPSender&) = delete; + UDPSender& operator=(const UDPSender&) = delete; + UDPSender(UDPSender&&) = delete; + //!@} //!@name Methods @@ -92,7 +117,7 @@ class UDPSender final { struct sockaddr_in m_server; //! The socket to be used - int m_socket{-1}; + SOCKET_TYPE m_socket = k_invalidSocket; //!@} @@ -118,10 +143,37 @@ class UDPSender final { //! Error message (optional string) std::string m_errorMessage; +}; + +namespace detail { + +inline bool isValidSocket(const SOCKET_TYPE socket) { + return socket != k_invalidSocket; +} + +#ifdef _WIN32 +struct WinSockSingleton { + inline static const WinSockSingleton& getInstance() { + static const WinSockSingleton instance; + return instance; + } + inline bool ok() const { + return m_ok; + } + ~WinSockSingleton() { + WSACleanup(); + } - //! Bad file descriptor - static constexpr int k_invalidFd{-1}; +private: + WinSockSingleton() { + WSADATA wsa; + m_ok = WSAStartup(MAKEWORD(2, 2), &wsa) == 0; + } + bool m_ok; }; +#endif + +} // namespace detail inline UDPSender::UDPSender(const std::string& host, const uint16_t port, @@ -138,7 +190,7 @@ inline UDPSender::UDPSender(const std::string& host, // Define the batching thread m_batchingThread = std::thread([this] { // TODO: this will drop unsent stats, should we send all the unsent stats before we exit? - while (!m_mustExit.load(std::memory_order_acq_rel)) { + while (!m_mustExit.load(std::memory_order_acquire)) { std::deque stagedMessageQueue; std::unique_lock batchingLock(m_batchingMutex); @@ -165,12 +217,12 @@ inline UDPSender::~UDPSender() { // If we're running a background thread tell it to stop if (m_batchingThread.joinable()) { - m_mustExit.store(true, std::memory_order_acq_rel); + m_mustExit.store(true, std::memory_order_release); m_batchingThread.join(); } // Cleanup the socket - close(m_socket); + SOCKET_CLOSE(m_socket); } inline void UDPSender::send(const std::string& message) noexcept { @@ -207,10 +259,16 @@ inline const std::string& UDPSender::errorMessage() const noexcept { } inline bool UDPSender::initialize() noexcept { +#ifdef _WIN32 + if (!detail::WinSockSingleton::getInstance().ok()) { + m_errorMessage = "WSAStartup failed: errno=" + std::to_string(SOCKET_ERRNO); + } +#endif + // Connect the socket m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); - if (m_socket == k_invalidFd) { - m_errorMessage = std::string("socket creation failed: err=") + std::strerror(errno); + if (!detail::isValidSocket(m_socket)) { + m_errorMessage = "socket creation failed: errno=" + std::to_string(SOCKET_ERRNO); return false; } @@ -218,7 +276,7 @@ inline bool UDPSender::initialize() noexcept { m_server.sin_family = AF_INET; m_server.sin_port = htons(m_port); - if (inet_aton(m_host.c_str(), &m_server.sin_addr) == 0) { + if (inet_pton(AF_INET, m_host.c_str(), &m_server.sin_addr) == 0) { // An error code has been returned by inet_aton // Specify the criteria for selecting the socket address structure @@ -232,8 +290,8 @@ inline bool UDPSender::initialize() noexcept { const int ret{getaddrinfo(m_host.c_str(), nullptr, &hints, &results)}; if (ret != 0) { // An error code has been returned by getaddrinfo - close(m_socket); - m_socket = k_invalidFd; + SOCKET_CLOSE(m_socket); + m_socket = k_invalidSocket; m_errorMessage = "getaddrinfo failed: err=" + std::to_string(ret) + ", msg=" + gai_strerror(ret); return false; } @@ -251,16 +309,24 @@ inline bool UDPSender::initialize() noexcept { inline void UDPSender::sendToDaemon(const std::string& message) noexcept { // Try sending the message - const long int ret{ - sendto(m_socket, message.data(), message.size(), 0, (struct sockaddr*)&m_server, sizeof(m_server))}; + const auto ret = sendto(m_socket, + message.data(), +#ifdef _WIN32 + static_cast(message.size()), +#else + message.size(), +#endif + 0, + (struct sockaddr*)&m_server, + sizeof(m_server)); if (ret == -1) { - m_errorMessage = - "sendto server failed: host=" + m_host + ":" + std::to_string(m_port) + ", err=" + std::strerror(errno); + m_errorMessage = "sendto server failed: host=" + m_host + ":" + std::to_string(m_port) + + ", err=" + std::to_string(SOCKET_ERRNO); } } inline bool UDPSender::initialized() const noexcept { - return m_socket != k_invalidFd; + return m_socket != k_invalidSocket; } inline void UDPSender::flush() noexcept { diff --git a/tests/StatsdServer.hpp b/tests/StatsdServer.hpp index 5f1e87e..e87fe59 100644 --- a/tests/StatsdServer.hpp +++ b/tests/StatsdServer.hpp @@ -1,9 +1,11 @@ #ifndef STATSD_SERVER_HPP #define STATSD_SERVER_HPP -#include -#include -#include +// It might make sense to include this test class in the UDPSender header +// it includes most of the cross platform defines etc that we need for socket io +#include "cpp-statsd-client/UDPSender.hpp" + +#include #include namespace Statsd { @@ -11,9 +13,16 @@ namespace Statsd { class StatsdServer { public: StatsdServer(unsigned short port = 8125) noexcept { - // Create the fd - if (!isValidFd(m_fd = socket(AF_INET, SOCK_DGRAM, 0))) { - m_errorMessage = "Could not create socket file descriptor"; +#ifdef _WIN32 + if (!detail::WinSockSingleton::getInstance().ok()) { + m_errorMessage = "WSAStartup failed: errno=" + std::to_string(SOCKET_ERRNO); + } +#endif + + // Create the socket + m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (!detail::isValidSocket(m_socket)) { + m_errorMessage = "socket creation failed: errno=" + std::to_string(SOCKET_ERRNO); return; } @@ -24,16 +33,16 @@ class StatsdServer { address.sin_addr.s_addr = INADDR_ANY; // Try to bind - if (bind(m_fd, reinterpret_cast(&address), sizeof(address)) != 0) { - close(m_fd); - m_fd = k_invalidFd; - m_errorMessage = "Could not bind to address and port"; + if (bind(m_socket, reinterpret_cast(&address), sizeof(address)) != 0) { + SOCKET_CLOSE(m_socket); + m_socket = k_invalidSocket; + m_errorMessage = "bind failed: errno=" + std::to_string(SOCKET_ERRNO); } } ~StatsdServer() { - if (isValidFd(m_fd)) { - close(m_fd); + if (detail::isValidSocket(m_socket)) { + SOCKET_CLOSE(m_socket); } } @@ -43,32 +52,27 @@ class StatsdServer { std::string receive() noexcept { // If uninitialized then bail - if (!isValidFd(m_fd)) { + if (!detail::isValidSocket(m_socket)) { return ""; } // Try to receive (this is blocking) std::string buffer(256, '\0'); - int string_len = -1; - if ((string_len = recv(m_fd, &buffer[0], buffer.size(), 0)) < 1) { + int string_len; + if ((string_len = recv(m_socket, &buffer[0], static_cast(buffer.size()), 0)) < 1) { m_errorMessage = "Could not recv on the socket file descriptor"; return ""; } // No error return the trimmed result m_errorMessage.clear(); - buffer.resize(std::min(string_len, static_cast(buffer.size()))); + buffer.resize(std::min(static_cast(string_len), buffer.size())); return buffer; } private: - static inline bool isValidFd(const int fd) { - return fd != k_invalidFd; - } - - int m_fd; + SOCKET_TYPE m_socket; std::string m_errorMessage; - static constexpr int k_invalidFd{-1}; }; } // namespace Statsd diff --git a/tests/testStatsdClient.cpp b/tests/testStatsdClient.cpp index c58107d..cda8038 100644 --- a/tests/testStatsdClient.cpp +++ b/tests/testStatsdClient.cpp @@ -15,7 +15,7 @@ void mock(StatsdServer& server, std::vector& messages) { auto recvd = server.receive(); // Split the messages on '\n' - std::string::size_type start = -1; + auto start = std::string::npos; do { // Keep this message auto end = recvd.find('\n', ++start); @@ -31,10 +31,19 @@ void mock(StatsdServer& server, std::vector& messages) { } while (server.errorMessage().empty() && !messages.back().empty()); } -void throwOnError(const StatsdClient& client, bool expectEmpty = true, const std::string& extraMessage = "") { - if (client.errorMessage().empty() != expectEmpty) { - std::cerr << (expectEmpty ? client.errorMessage() : extraMessage) << std::endl; - throw std::runtime_error(expectEmpty ? client.errorMessage() : extraMessage); +template +void throwOnError(const SocketWrapper& wrapped, bool expectEmpty = true, const std::string& extraMessage = "") { + if (wrapped.errorMessage().empty() != expectEmpty) { + std::cerr << (expectEmpty ? wrapped.errorMessage() : extraMessage) << std::endl; + throw std::runtime_error(expectEmpty ? wrapped.errorMessage() : extraMessage); + } +} + +void throwOnWrongMessage(StatsdServer& server, const std::string& expected) { + auto actual = server.receive(); + if (actual != expected) { + std::cerr << "Expected: " << expected << " but got: " << actual << std::endl; + throw std::runtime_error("Incorrect stat received"); } } @@ -46,29 +55,22 @@ void testErrorConditions() { void testReconfigure() { StatsdServer server; + throwOnError(server); StatsdClient client("localhost", 8125, "first."); client.increment("foo"); - if (server.receive() != "first.foo:1|c") { - throw std::runtime_error("Incorrect stat received"); - } + throwOnWrongMessage(server, "first.foo:1|c"); client.setConfig("localhost", 8125, "second"); client.increment("bar"); - if (server.receive() != "second.bar:1|c") { - throw std::runtime_error("Incorrect stat received"); - } + throwOnWrongMessage(server, "second.bar:1|c"); client.setConfig("localhost", 8125, ""); client.increment("third.baz"); - if (server.receive() != "third.baz:1|c") { - throw std::runtime_error("Incorrect stat received"); - } + throwOnWrongMessage(server, "third.baz:1|c"); client.increment(""); - if (server.receive() != ":1|c") { - throw std::runtime_error("Incorrect stat received"); - } + throwOnWrongMessage(server, ":1|c"); // TODO: test what happens with the batching after resolving the question about incomplete // batches being dropped vs sent on reconfiguring