diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..b9af422 --- /dev/null +++ b/.clang-format @@ -0,0 +1,4 @@ +--- +BasedOnStyle: Google +IndentWidth: '4' +... diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..9802bda --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,23 @@ +--- +Checks: + -*, + bugprone-*, + clang-*, + google-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -google-build-using-namespace, + -modernize-use-trailing-return-type, + -readability-identifier-length, + -readability-magic-numbers, + -bugprone-easily-swappable-parameters + +HeaderFilterRegex: 'src' +FormatStyle: file + +CheckOptions: + - key: misc-include-cleaner.MissingIncludes + value: false +... diff --git a/.github/workflows/conf-build-test.yml b/.github/workflows/conf-build-test.yml index b2a6e1f..929e486 100644 --- a/.github/workflows/conf-build-test.yml +++ b/.github/workflows/conf-build-test.yml @@ -44,6 +44,7 @@ jobs: - name: Clang-tidy if: ${{ github.event_name == 'pull_request' }} + continue-on-error: true run: | pip3 install pyyaml COMPILE_COMMANDS=$(find . -name compile_commands.json | head -n 1) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37bf44f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.vscode +build* +doc +tags +CMakeUserPresets.json +dbus_xml/net.connman/*.h +dbus_xml/net.connman/*.c diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0b92653 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,118 @@ +cmake_minimum_required(VERSION 3.24) + +option(BUILD_EXAMPLES "Build Examples" OFF) +option(BUILD_TESTS "Build Tests" OFF) +option(BUILD_CONNMAN "Build Connman Proxy" ON) + +include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/VersionFromGit.cmake) +version_from_git(LOG OFF TIMESTAMP "%Y%m%d%H%M%S") + +project( + GDbusCpp + VERSION ${VERSION} + DESCRIPTION "" + LANGUAGES C CXX) + +set_property(GLOBAL PROPERTY USE_FOLDERS ON) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) +include(GNUInstallDirs) + +add_library(GDbusProxy include/amarula/dbus/gproxy.hpp src/dbus/gproxy.cpp) +add_library(Amarula::GDbusProxy ALIAS GDbusProxy) +target_include_directories( + GDbusProxy PUBLIC $ + "$") +install( + TARGETS GDbusProxy + EXPORT ${PROJECT_NAME}-config + COMPONENT ${PROJECT_NAME} + ARCHIVE COMPONENT ${PROJECT_NAME}-dev) +install( + DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} + COMPONENT ${PROJECT_NAME}-dev) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GIO_UNIX REQUIRED IMPORTED_TARGET gio-unix-2.0) +target_link_libraries(GDbusProxy PUBLIC PkgConfig::GIO_UNIX) + +add_subdirectory(dbus_xml) + +if(BUILD_CONNMAN) + add_library( + GConnmanDbus + $ src/dbus/gconnman_definitions.hpp + include/amarula/dbus/gconnman.hpp include/amarula/dbus/gconnman_clock.hpp + src/dbus/gconnman_clock.cpp src/dbus/gconnman.cpp) + add_library(Amarula::GConnmanDbus ALIAS GConnmanDbus) + get_target_property(CONNMAN_PROXY_INCLUDES gconnmanproxy + INTERFACE_INCLUDE_DIRECTORIES) + target_include_directories( + GConnmanDbus PUBLIC $ + "$") + target_include_directories(GConnmanDbus PRIVATE ${CONNMAN_PROXY_INCLUDES}) + target_link_libraries(GConnmanDbus PUBLIC GDbusProxy) + + install( + TARGETS GConnmanDbus + EXPORT ${PROJECT_NAME}-config + COMPONENT ${PROJECT_NAME} + ARCHIVE COMPONENT ${PROJECT_NAME}-dev) +endif(BUILD_CONNMAN) + +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + if(BUILD_TESTS) + include(CTest) + add_subdirectory(tests) + endif(BUILD_TESTS) + if(BUILD_EXAMPLES) + include(CTest) + add_subdirectory(examples) + endif(BUILD_EXAMPLES) + if(BUILD_DOCS) + find_package(Doxygen) + if(DOXYGEN_FOUND) + set(DOXYGEN_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/doc") + set(DOXYGEN_EXCLUDE_PATTERNS "*/doc/*" "*/build/*" "${CMAKE_CURRENT_BINARY_DIR}/*") + doxygen_add_docs(doxygen_docs ${PROJECT_SOURCE_DIR} + COMMENT "Generate man pages") + endif(DOXYGEN_FOUND) + endif(BUILD_DOCS) + set(cpack_file_name + "${PROJECT_NAME}-v${SEMVER}-${CMAKE_SYSTEM_NAME}_${CMAKE_SYSTEM_VERSION}-${CMAKE_SYSTEM_PROCESSOR}-${CMAKE_CXX_COMPILER_ID}" + ) + set(CPACK_PACKAGE_FILE_NAME ${cpack_file_name}) + include(CPack) + include(CPackIFW) + cpack_add_component(${PROJECT_NAME}) +endif(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + +install( + EXPORT ${PROJECT_NAME}-config + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} + NAMESPACE Amarula:: + COMPONENT ${PROJECT_NAME}-dev) +install( + FILES ${CMAKE_SOURCE_DIR}/LICENSE + DESTINATION ${CMAKE_INSTALL_DATADIR}/Amarula/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}) +include(CMakePackageConfigHelpers) +configure_package_config_file( + ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Config.cmake.in + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}) +write_basic_package_version_file( + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake + VERSION ${VERSION} + COMPATIBILITY SameMajorVersion) +install( + FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake + DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME} + COMPONENT ${PROJECT_NAME}-dev) +export( + EXPORT ${PROJECT_NAME}-config + NAMESPACE Amarula:: + FILE ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}-config.cmake) diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..107697b --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,104 @@ +{ + "version": 6, + "configurePresets": [ + { + "name": "default-release", + "displayName": "Default Release", + "description": "Default configuration for release with runtime components only", + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "default-develop", + "displayName": "Default Config for development", + "description": "Default configuration for development, release runtime and dev components, build tests ", + "inherits": "default-release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "BUILD_TESTS": "ON", + "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", + "BUILD_DOCS": "ON", + "BUILD_EXAMPLES": "ON" + } + } + ], + "buildPresets": [ + { + "name": "default-release", + "configurePreset": "default-release" + }, + { + "name": "default-develop", + "configurePreset": "default-develop" + }, + { + "name": "default-documentation", + "configurePreset": "default-develop", + "targets": "doxygen_docs" + } + ], + "testPresets": [ + { + "name": "default-develop", + "configurePreset": "default-develop", + "output": { + "outputOnFailure": true + } + } + ], + "packagePresets": [ + { + "name": "default-develop", + "configurePreset": "default-develop", + "generators": [ + "TGZ" + ], + "variables": { + "CPACK_COMPONENTS_GROUPING": "ALL_COMPONENTS_IN_ONE", + "CPACK_PACKAGE_CONTACT": "develop@amarulasolutions.com", + "CPACK_PACKAGE_VENDOR": "amarulasolutions" + }, + "packageDirectory": "packages-${presetName}" + } + ], + "workflowPresets": [ + { + "name": "default-develop", + "steps": [ + { + "type": "configure", + "name": "default-develop" + }, + { + "type": "build", + "name": "default-develop" + }, + { + "type": "test", + "name": "default-develop" + }, + { + "type": "package", + "name": "default-develop" + + } + ] + }, + { + "name": "default-documentation", + "steps": [ + { + "type": "configure", + "name": "default-develop" + }, + { + "type": "build", + "name": "default-documentation" + } + ] + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e69de29 diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in new file mode 100644 index 0000000..bcfcf7e --- /dev/null +++ b/cmake/Config.cmake.in @@ -0,0 +1,5 @@ +@PACKAGE_INIT@ +include(CMakeFindDependencyMacro) +find_dependency(PkgConfig) +pkg_check_modules(GIO_UNIX REQUIRED IMPORTED_TARGET gio-unix-2.0) +include("${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-config.cmake") \ No newline at end of file diff --git a/cmake/VersionFromGit.cmake b/cmake/VersionFromGit.cmake new file mode 100644 index 0000000..9120b63 --- /dev/null +++ b/cmake/VersionFromGit.cmake @@ -0,0 +1,169 @@ +# The MIT License (MIT) +# +# Copyright (c) 2016-2017 Theo Willows +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +cmake_minimum_required(VERSION 3.11 FATAL_ERROR) + +include( CMakeParseArguments ) + +function( version_from_git ) + # Parse arguments + set( options OPTIONAL FAST ) + set( oneValueArgs + GIT_EXECUTABLE + INCLUDE_HASH + LOG + TIMESTAMP + ) + set( multiValueArgs ) + cmake_parse_arguments( ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN} ) + + # Defaults + if( NOT DEFINED ARG_INCLUDE_HASH ) + set( ARG_INCLUDE_HASH ON ) + endif() + + if( DEFINED ARG_GIT_EXECUTABLE ) + set( GIT_EXECUTABLE "${ARG_GIT_EXECUTABLE}" ) + else () + # Find Git or bail out + find_package( Git ) + if( NOT GIT_FOUND ) + message( FATAL_ERROR "[VersionFromGit] Git not found" ) + endif( NOT GIT_FOUND ) + endif() + + # Git describe + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE git_result + OUTPUT_VARIABLE git_describe + ERROR_VARIABLE git_error + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ) + if( NOT git_result EQUAL 0 ) + message( FATAL_ERROR + "[VersionFromGit] Failed to execute Git: ${git_error}" + ) + endif() + + # Get Git tag + execute_process( + COMMAND "${GIT_EXECUTABLE}" describe --tags --abbrev=0 + WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}" + RESULT_VARIABLE git_result + OUTPUT_VARIABLE git_tag + ERROR_VARIABLE git_error + OUTPUT_STRIP_TRAILING_WHITESPACE + ERROR_STRIP_TRAILING_WHITESPACE + ) + if( NOT git_result EQUAL 0 ) + message( FATAL_ERROR + "[VersionFromGit] Failed to execute Git: ${git_error}" + ) + endif() + + if( git_tag MATCHES "^v(0|[1-9][0-9]*)[.](0|[1-9][0-9]*)[.](0|[1-9][0-9]*)(-[.0-9A-Za-z-]+)?([+][.0-9A-Za-z-]+)?$" ) + set( version_major "${CMAKE_MATCH_1}" ) + set( version_minor "${CMAKE_MATCH_2}" ) + set( version_patch "${CMAKE_MATCH_3}" ) + set( identifiers "${CMAKE_MATCH_4}" ) + set( metadata "${CMAKE_MATCH_5}" ) + else() + message( FATAL_ERROR + "[VersionFromGit] Git tag isn't valid semantic version: [${git_tag}]" + ) + endif() + + if( "${git_tag}" STREQUAL "${git_describe}" ) + set( git_at_a_tag ON ) + endif() + + if( NOT git_at_a_tag ) + # Extract the Git hash (if one exists) + string( REGEX MATCH "g[0-9a-f]+$" git_hash "${git_describe}" ) + endif() + + # Construct the version variables + set( version ${version_major}.${version_minor}.${version_patch} ) + set( semver ${version} ) + + # Identifiers + if( identifiers MATCHES ".+" ) + string( SUBSTRING "${identifiers}" 1 -1 identifiers ) + set( semver "${semver}-${identifiers}") + endif() + + # Metadata + # TODO Split and join (add Git hash inbetween) + if( metadata MATCHES ".+" ) + string( SUBSTRING "${metadata}" 1 -1 metadata ) + # Split + string( REPLACE "." ";" metadata "${metadata}" ) + endif() + + if( NOT git_at_a_tag ) + + if( ARG_INCLUDE_HASH ) + list( APPEND metadata "${git_hash}" ) + endif( ARG_INCLUDE_HASH ) + + # Timestamp + if( DEFINED ARG_TIMESTAMP ) + string( TIMESTAMP timestamp "${ARG_TIMESTAMP}" ${ARG_UTC} ) + list( APPEND metadata "${timestamp}" ) + endif( DEFINED ARG_TIMESTAMP ) + + endif() + + # Join + string( REPLACE ";" "." metadata "${metadata}" ) + + if( metadata MATCHES ".+" ) + set( semver "${semver}+${metadata}") + endif() + + # Log the results + if( ARG_LOG ) + message( STATUS + "[VersionFromGit] Version: ${version} + Git tag: [${git_tag}] + Git hash: [${git_hash}] + Decorated: [${git_describe}] + Identifiers: [${identifiers}] + Metadata: [${metadata}] + SemVer: [${semver}]" + ) + endif( ARG_LOG ) + + # Set parent scope variables + set( GIT_TAG ${git_tag} PARENT_SCOPE ) + set( SEMVER ${semver} PARENT_SCOPE ) + set( VERSION ${version} PARENT_SCOPE ) + set( VERSION_MAJOR ${version_major} PARENT_SCOPE ) + set( VERSION_MINOR ${version_minor} PARENT_SCOPE ) + set( VERSION_PATCH ${version_patch} PARENT_SCOPE ) + +endfunction( version_from_git ) + + diff --git a/dbus_xml/CMakeLists.txt b/dbus_xml/CMakeLists.txt new file mode 100644 index 0000000..db04ed2 --- /dev/null +++ b/dbus_xml/CMakeLists.txt @@ -0,0 +1,25 @@ +find_program( + GDBUS_CODEGEN + NAMES gdbus-codegen + DOC "gdbus-codegen executable") +if(NOT GDBUS_CODEGEN) + message(FATAL_ERROR "Executable gdbus-codegen not found") +endif(NOT GDBUS_CODEGEN) + +if(BUILD_CONNMAN) + set(CONNMAN_PROXY_DIR ${CMAKE_CURRENT_SOURCE_DIR}/net.connman) + add_custom_command( + OUTPUT ${CONNMAN_PROXY_DIR}/net_connman_proxy_gdbus_generated.h + ${CONNMAN_PROXY_DIR}/net_connman_proxy_gdbus_generated.c + COMMAND + gdbus-codegen --c-generate-autocleanup all --output-directory + ${CONNMAN_PROXY_DIR}/ --generate-c-code net_connman_proxy_gdbus_generated + ${CONNMAN_PROXY_DIR}/clock.xml + DEPENDS ${CONNMAN_PROXY_DIR}/clock.xml) + add_library( + gconnmanproxy OBJECT + ${CONNMAN_PROXY_DIR}/net_connman_proxy_gdbus_generated.h + ${CONNMAN_PROXY_DIR}/net_connman_proxy_gdbus_generated.c) + target_include_directories(gconnmanproxy PUBLIC ${CONNMAN_PROXY_DIR}) + target_link_libraries(gconnmanproxy PUBLIC PkgConfig::GIO_UNIX) +endif(BUILD_CONNMAN) diff --git a/dbus_xml/net.connman/clock.xml b/dbus_xml/net.connman/clock.xml new file mode 100644 index 0000000..a91ca26 --- /dev/null +++ b/dbus_xml/net.connman/clock.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/include/amarula/dbus/gconnman.hpp b/include/amarula/dbus/gconnman.hpp new file mode 100644 index 0000000..0b2f2e9 --- /dev/null +++ b/include/amarula/dbus/gconnman.hpp @@ -0,0 +1,19 @@ +#pragma once +#include +#include + +namespace Amarula::DBus { + +class Connman : public GProxy { + ConnmanClock* clock_{nullptr}; + + void cleanup() override { delete clock_; } + + void init(); + + public: + Connman(); + [[nodiscard]] auto clock() const -> ConnmanClock* { return clock_; } +}; + +} // namespace Amarula::DBus diff --git a/include/amarula/dbus/gconnman_clock.hpp b/include/amarula/dbus/gconnman_clock.hpp new file mode 100644 index 0000000..0eb845b --- /dev/null +++ b/include/amarula/dbus/gconnman_clock.hpp @@ -0,0 +1,82 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +namespace Amarula::DBus { +class Connman; + +class ConnmanClock { + public: + struct ClockProperties { + uint32_t time = 0; + std::string timeUpdates; + std::string timezone; + std::string timezoneUpdates; + std::vector timeservers; + bool timeServerSynced; + }; + constexpr static const char* PROP_TIMEUPDATES_AUTO = "auto"; + constexpr static const char* PROP_TIMEUPDATES_MANUAL = "manual"; + using ClockPropertiesCallback = std::function; + using ClockPropertiesSetCallback = std::function; + + private: + size_t callback_counter_{0U}; + std::mutex mtx_; + gpointer clock_proxy_{nullptr}; + GProxy* gproxy_{nullptr}; + std::map properties_callbacks_; + template + auto prepareCallback(T callback) { + std::unique_lock const lock(mtx_); + gproxy_->on_any_async_start(); + std::optional call_counter = std::nullopt; + if (callback != nullptr) { + size_t index = ++callback_counter_; + call_counter = index; + properties_callbacks_[index] = std::move(callback); + } + return call_counter; + } + void updateProperties(GVariant* properties, + const std::optional& counter); + static void get_property_cb(GObject* proxy, GAsyncResult* res, + gpointer user_data); + static void set_property_cb(GObject* proxy, GAsyncResult* res, + gpointer user_data); + static void on_properties_changed_cb( + GDBusProxy* proxy, GVariant* changed_properties, + const gchar* const* invalidated_properties, gpointer user_data); + explicit ConnmanClock(GProxy* proxy); + + public: + ConnmanClock(const ConnmanClock&) = delete; + auto operator=(const ConnmanClock&) = delete; + ConnmanClock(ConnmanClock&&) = delete; + auto operator=(ConnmanClock&&) = delete; + + ~ConnmanClock(); + + void setTime(unsigned int time, + ClockPropertiesSetCallback callback = nullptr); + void setTimeZone(const std::string& timezone, + ClockPropertiesSetCallback callback = nullptr); + void setTimeUpdates(const std::string& timeUpdates, + ClockPropertiesSetCallback callback = nullptr); + void setTimeZoneUpdates(const std::string& timeZoneUpdates, + ClockPropertiesSetCallback callback = nullptr); + void setTimeServers(const std::vector& servers, + ClockPropertiesSetCallback callback = nullptr); + void getProperties(ClockPropertiesCallback callback = nullptr); + void onPropertiesChanged(ClockPropertiesCallback callback); + [[nodiscard]] auto getGProxy() const { return gproxy_; } + + friend class Connman; +}; + +} // namespace Amarula::DBus diff --git a/include/amarula/dbus/gproxy.hpp b/include/amarula/dbus/gproxy.hpp new file mode 100644 index 0000000..a62fa01 --- /dev/null +++ b/include/amarula/dbus/gproxy.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +namespace Amarula::DBus { + +class GProxy { + std::mutex mtx_; + std::condition_variable cv_; + bool running_{false}; + std::thread glib_thread_; + unsigned int pending_calls_{0}; + const std::string bus_name_; + const std::string object_path_; + GDBusConnection* connection_ = nullptr; + GMainLoop* loop_{nullptr}; + GError* error_ = nullptr; + + public: + GProxy(const std::string& bus_name, const std::string& object_path); + + virtual ~GProxy(); + + void start(); + void stop(); + void on_any_async_done(); + void on_any_async_start(); + + [[nodiscard]] auto getConnection() const { return connection_; } + + protected: + virtual void cleanup() {}; +}; + +} // namespace Amarula::DBus diff --git a/src/dbus/gconnman.cpp b/src/dbus/gconnman.cpp new file mode 100644 index 0000000..a50e115 --- /dev/null +++ b/src/dbus/gconnman.cpp @@ -0,0 +1,11 @@ +#include + +#include "gconnman_definitions.hpp" +#include "net_connman_proxy_gdbus_generated.h" +namespace Amarula::DBus { + +Connman::Connman() + : GProxy(GConnman::SERVICE, "/net/connman"), + clock_(new ConnmanClock(this)) {} + +} // namespace Amarula::DBus \ No newline at end of file diff --git a/src/dbus/gconnman_clock.cpp b/src/dbus/gconnman_clock.cpp new file mode 100644 index 0000000..0dd5378 --- /dev/null +++ b/src/dbus/gconnman_clock.cpp @@ -0,0 +1,249 @@ +// clang-format off +#include "gconnman_definitions.hpp" +// clang-format on +#include +#include + +#include "net_connman_proxy_gdbus_generated.h" + +namespace Amarula::DBus { + +ConnmanClock::~ConnmanClock() { + if (clock_proxy_ != nullptr) { + g_object_unref(clock_proxy_); + } +} + +ConnmanClock::ConnmanClock(GProxy* proxy) : gproxy_{proxy} { + GError* err = nullptr; + clock_proxy_ = net_connman_clock_proxy_new_sync( + gproxy_->getConnection(), G_DBUS_PROXY_FLAGS_NONE, GConnman::SERVICE, + GConnman::MANAGER_PATH, nullptr, &err); + if (clock_proxy_ == nullptr) { + throw std::runtime_error("Failed to create Connman Clock proxy: " + + std::string(err->message)); + g_error_free(err); + } +} + +void ConnmanClock::updateProperties(GVariant* properties, + const std::optional& counter) { + if (g_variant_is_of_type(properties, G_VARIANT_TYPE("a{sv}")) == 0U) { + std::cerr << "Invalid GVariant type\n"; + return; + } + GVariantIter* iter = g_variant_iter_new(properties); + + GVariant* prop = nullptr; + ClockProperties props; + + while ((prop = g_variant_iter_next_value(iter)) != nullptr) { + const gchar* key = + g_variant_get_string(g_variant_get_child_value(prop, 0), nullptr); + GVariant* value = g_variant_get_child_value(prop, 1); + GVariant* variant = g_variant_get_variant(value); + if (g_strcmp0(key, GConnman::PROP_TIME_STR) == 0) { + props.time = g_variant_get_uint64(variant); + + } else if (g_strcmp0(key, GConnman::PROP_TIMEUPDATES_STR) == 0U) { + props.timeUpdates = g_variant_get_string(variant, nullptr); + } else if (g_strcmp0(key, GConnman::PROP_TIMEZONE_STR) == 0U) { + props.timezone = g_variant_get_string(variant, nullptr); + } else if (g_strcmp0(key, GConnman::PROP_TIMEZONEUPDATES_STR) == 0U) { + props.timezoneUpdates = g_variant_get_string(variant, nullptr); + } else if (g_strcmp0(key, GConnman::PROP_TIMESERVERS_STR) == 0U) { + const gchar* str = nullptr; + GVariantIter strIter; + if (g_variant_is_of_type(variant, G_VARIANT_TYPE("as")) != 0U) { + g_variant_iter_init(&strIter, variant); + GVariant* server = nullptr; + while ((server = g_variant_iter_next_value(&strIter)) != + nullptr) { + const gchar* server_str = + g_variant_get_string(server, nullptr); + props.timeservers.emplace_back(server_str); + g_variant_unref(server); + } + } + } else if (g_strcmp0(key, GConnman::PROP_TIMESERVERSYNCED_STR) == 0U) { + props.timeServerSynced = g_variant_get_boolean(variant) == 1U; + } else { + std::cerr << "Unknown property: " << key << '\n'; + } + g_variant_unref(variant); + g_variant_unref(value); + g_variant_unref(prop); + } + if (counter) { + std::unique_lock lock(mtx_); + const auto callback = std::any_cast( + properties_callbacks_[counter.value()]); + lock.unlock(); + callback(props); + if (counter.value() != 0U) { + lock.lock(); + properties_callbacks_.erase(counter.value()); + } + } + + g_variant_iter_free(iter); +} + +void ConnmanClock::set_property_cb(GObject* proxy, GAsyncResult* res, + gpointer user_data) { + std::unique_ptr> data( + static_cast*>(user_data)); + ConnmanClock* self = data->getSelf(); + const auto counter = data->getCounter(); + + GError* error = nullptr; + const bool success = net_connman_clock_call_set_property_finish( + NET_CONNMAN_CLOCK(proxy), res, &error) == 1U; + if (!success) { + std::cerr << "Failed to set clock property: " << error->message << '\n'; + g_error_free(error); + } + if (counter) { + std::unique_lock lock(self->mtx_); + const auto callback = std::any_cast( + self->properties_callbacks_[counter.value()]); + lock.unlock(); + callback(success); + + if (counter.value() != 0U) { + lock.lock(); + self->properties_callbacks_.erase(counter.value()); + } + } + std::unique_lock const lock(self->mtx_); + self->gproxy_->on_any_async_done(); +} + +void ConnmanClock::get_property_cb(GObject* proxy, GAsyncResult* res, + gpointer user_data) { + std::unique_ptr> data( + static_cast*>(user_data)); + ConnmanClock* self = data->getSelf(); + const auto counter = data->getCounter(); + GError* error = nullptr; + GVariant* out_properties = nullptr; + if (net_connman_clock_call_get_properties_finish( + NET_CONNMAN_CLOCK(proxy), &out_properties, res, &error) == 0U) { + std::cerr << "Failed to get clock properties: " << error->message + << '\n'; + g_error_free(error); + } else { + self->updateProperties(out_properties, counter); + g_variant_unref(out_properties); + } + std::unique_lock const lock(self->mtx_); + self->gproxy_->on_any_async_done(); +} + +void ConnmanClock::on_properties_changed_cb( + GDBusProxy* /*proxy*/, GVariant* changed_properties, + const gchar* const* /*invalidated_properties*/, gpointer user_data) { + auto* self = static_cast(user_data); + // The ClockProperties passed to the callback will only have the changed + // properties correctly set. The rest will be default initialized. This have + // to be rethinked if we want to use this callback + self->updateProperties(changed_properties, 0U); +} + +void ConnmanClock::setTime(unsigned int time, + ClockPropertiesSetCallback callback) { + const auto call_counter = prepareCallback(std::move(callback)); + auto data = std::make_unique>( + this, call_counter); + + net_connman_clock_call_set_property( + NET_CONNMAN_CLOCK(clock_proxy_), GConnman::PROP_TIME_STR, + g_variant_new_variant(g_variant_new_uint64(time)), nullptr, + static_cast(&ConnmanClock::set_property_cb), + data.release()); +} + +void ConnmanClock::setTimeZone(const std::string& timezone, + ClockPropertiesSetCallback callback) { + const auto call_counter = prepareCallback(std::move(callback)); + auto data = std::make_unique>( + this, call_counter); + net_connman_clock_call_set_property( + NET_CONNMAN_CLOCK(clock_proxy_), GConnman::PROP_TIMEZONE_STR, + g_variant_new_variant(g_variant_new_string(timezone.c_str())), nullptr, + static_cast(&ConnmanClock::set_property_cb), + data.release()); +} + +void ConnmanClock::setTimeUpdates(const std::string& timeUpdates, + ClockPropertiesSetCallback callback) { + const auto call_counter = prepareCallback(std::move(callback)); + auto data = std::make_unique>( + this, call_counter); + net_connman_clock_call_set_property( + NET_CONNMAN_CLOCK(clock_proxy_), GConnman::PROP_TIMEUPDATES_STR, + g_variant_new_variant(g_variant_new_string(timeUpdates.c_str())), + nullptr, + static_cast(&ConnmanClock::set_property_cb), + data.release()); +} + +void ConnmanClock::setTimeZoneUpdates(const std::string& timeZoneUpdates, + ClockPropertiesSetCallback callback) { + const auto call_counter = prepareCallback(std::move(callback)); + auto data = std::make_unique>( + this, call_counter); + net_connman_clock_call_set_property( + NET_CONNMAN_CLOCK(clock_proxy_), GConnman::PROP_TIMEZONEUPDATES_STR, + g_variant_new_variant(g_variant_new_string(timeZoneUpdates.c_str())), + nullptr, + static_cast(&ConnmanClock::set_property_cb), + data.release()); +} + +void ConnmanClock::setTimeServers(const std::vector& servers, + ClockPropertiesSetCallback callback) { + const auto call_counter = prepareCallback(std::move(callback)); + auto data = std::make_unique>( + this, call_counter); + GVariantBuilder builder; + g_variant_builder_init(&builder, G_VARIANT_TYPE("as")); + for (const auto& server : servers) { + GVariant* str_variant = g_variant_new_string(server.c_str()); + g_variant_builder_add_value(&builder, str_variant); + } + GVariant* servers_variant = g_variant_builder_end(&builder); + net_connman_clock_call_set_property( + NET_CONNMAN_CLOCK(clock_proxy_), GConnman::PROP_TIMESERVERS_STR, + g_variant_new_variant(servers_variant), nullptr, + static_cast(&ConnmanClock::set_property_cb), + data.release()); + g_variant_builder_clear(&builder); +} + +void ConnmanClock::getProperties(ClockPropertiesCallback callback) { + const auto call_counter = prepareCallback(std::move(callback)); + auto data = std::make_unique>( + this, call_counter); + net_connman_clock_call_get_properties( + NET_CONNMAN_CLOCK(clock_proxy_), nullptr, + static_cast(&ConnmanClock::get_property_cb), + data.release()); +} + +void ConnmanClock::onPropertiesChanged(ClockPropertiesCallback callback) { + if (callback == nullptr) { + std::cerr << "Callback for properties changed cannot be null.\n"; + return; + } + + { + std::unique_lock const lock(mtx_); + properties_callbacks_[0U] = std::move(callback); + } + + g_signal_connect(NET_CONNMAN_CLOCK(clock_proxy_), "g-properties-changed", + G_CALLBACK(&ConnmanClock::on_properties_changed_cb), this); +} + +} // namespace Amarula::DBus \ No newline at end of file diff --git a/src/dbus/gconnman_definitions.hpp b/src/dbus/gconnman_definitions.hpp new file mode 100644 index 0000000..1d6fc11 --- /dev/null +++ b/src/dbus/gconnman_definitions.hpp @@ -0,0 +1,31 @@ +#include +#include + +namespace Amarula::DBus::GConnman { + +constexpr const char* SERVICE = "net.connman"; +constexpr const char* CLOCK_INTERFACE = "net.connman.Clock"; +constexpr const char* MANAGER_PATH = "/"; +constexpr const char* PROP_TIME_STR = "Time"; +constexpr const char* PROP_TIMEUPDATES_STR = "TimeUpdates"; +constexpr const char* PROP_TIMEZONE_STR = "Timezone"; +constexpr const char* PROP_TIMEZONEUPDATES_STR = "TimezoneUpdates"; +constexpr const char* PROP_TIMESERVERS_STR = "Timeservers"; +constexpr const char* PROP_TIMESERVERSYNCED_STR = "TimeserverSynced"; + +template +struct CallbackData { + private: + T* self; + std::optional counter{std::nullopt}; + + public: + CallbackData(T* selfPtr, std::optional count) + : self{selfPtr}, counter{count} {} + auto getSelf() const -> T* { return self; } + [[nodiscard]] auto getCounter() const -> std::optional { + return counter; + } +}; + +} // namespace Amarula::DBus::GConnman diff --git a/src/dbus/gproxy.cpp b/src/dbus/gproxy.cpp new file mode 100644 index 0000000..dc3237e --- /dev/null +++ b/src/dbus/gproxy.cpp @@ -0,0 +1,103 @@ +#include "amarula/dbus/gproxy.hpp" + +#include + +namespace Amarula::DBus { + +void GProxy::on_any_async_done() { + if (pending_calls_-- == 1) { + g_main_loop_quit(loop_); + } +} + +void GProxy::on_any_async_start() { ++pending_calls_; } + +GProxy::GProxy(const std::string& bus_name, const std::string& object_path) + : bus_name_(bus_name), object_path_(object_path) { + connection_ = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &error_); + if (connection_ == nullptr) { + throw std::runtime_error("Failed to connect to DBus: " + + std::string(error_->message)); + } + + GDBusProxy* proxy = g_dbus_proxy_new_sync( + connection_, G_DBUS_PROXY_FLAGS_NONE, nullptr, bus_name_.c_str(), + object_path_.c_str(), "org.freedesktop.DBus.Introspectable", nullptr, + &error_); + + if (proxy == nullptr) { + throw std::runtime_error("Failed to create proxy for bus name: " + + std::string(error_->message)); + } + + GVariant* result = + g_dbus_proxy_call_sync(proxy, "Introspect", nullptr, + G_DBUS_CALL_FLAGS_NONE, -1, nullptr, &error_); + + g_object_unref(proxy); + + if (result == nullptr) { + throw std::runtime_error( + "Failed to introspect object path or interface: " + + std::string(error_->message)); + } + + g_variant_unref(result); + start(); +} + +GProxy::~GProxy() { + if (connection_ != nullptr) { + g_object_unref(connection_); + } + if (error_ != nullptr) { + g_error_free(error_); + } + + { + std::unique_lock lock(mtx_); + cv_.wait(lock, [this] { return running_; }); + } + + if (loop_ != nullptr && (g_main_loop_is_running(loop_) == TRUE) && + pending_calls_ == 0U) { + g_main_loop_quit(loop_); + } + + if (glib_thread_.joinable()) { + glib_thread_.join(); + } + cleanup(); +} + +void GProxy::start() { + if (!running_) { + glib_thread_ = std::thread([this]() { + { + std::lock_guard const lock(mtx_); + running_ = true; + loop_ = g_main_loop_new(nullptr, FALSE); + } + cv_.notify_all(); + g_main_loop_run(loop_); + g_main_loop_unref(loop_); + }); + } +} +void GProxy::stop() { + { + std::unique_lock lock(mtx_); + cv_.wait(lock, [this] { return running_; }); + running_ = false; + } + + if (loop_ != nullptr) { + g_main_loop_quit(loop_); + loop_ = nullptr; + } + if (glib_thread_.joinable()) { + glib_thread_.join(); + } +} + +} // namespace Amarula::DBus \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8df824e --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,20 @@ +include(FetchContent) +FetchContent_Declare( + googletest + GIT_REPOSITORY https://github.com/google/googletest.git + GIT_TAG v1.16.0) +set(INSTALL_GTEST OFF) +set(BUILD_GMOCK OFF) +FetchContent_MakeAvailable(googletest) +include(GoogleTest) + +add_executable(gdbusproxypp_test gdbusproxypp_test.cpp) +target_link_libraries(gdbusproxypp_test PRIVATE GDbusProxy gtest_main) + +add_executable(gconnmanclock_test gconnmanclock_test.cpp) +target_link_libraries(gconnmanclock_test PRIVATE GConnmanDbus gtest_main) + +install( + TARGETS gdbusproxypp_test gconnmanclock_test + EXPORT ${PROJECT_NAME}-config + COMPONENT ${PROJECT_NAME}-dev) diff --git a/tests/gconnmanclock_test.cpp b/tests/gconnmanclock_test.cpp new file mode 100644 index 0000000..306913f --- /dev/null +++ b/tests/gconnmanclock_test.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include +using Amarula::DBus::Connman; +using Amarula::DBus::ConnmanClock; + +constexpr guint kTestTime = 1633036800; +constexpr const char* kTestTimeZone = "America/Vancouver"; +constexpr int kSleepDurationSeconds = 30; + +namespace { +void print_clock_properties( + Amarula::DBus::ConnmanClock::ClockProperties& props) { + std::cout << "Time: " << props.time << " ("; + const auto timeValue = static_cast(props.time); + std::cout << std::put_time(std::localtime(&timeValue), "%Y-%m-%d %H:%M:%S") + << ")\n"; + std::cout << "Time Updates: " << props.timeUpdates << '\n'; + std::cout << "Timezone: " << props.timezone << '\n'; + std::cout << "Timezone Updates: " << props.timezoneUpdates << '\n'; + std::cout << "Time Server Synced: " + << (props.timeServerSynced ? "Yes" : "No") << '\n'; + std::cout << "Timeservers: "; + for (const auto& server : props.timeservers) { + std::cout << server << ' '; + } + std::cout << '\n'; +} +} // namespace + +TEST(Connman, ClockSetTime) { + const Connman connman; + const guint time = kTestTime; + connman.clock()->setTimeUpdates( + ConnmanClock::PROP_TIMEUPDATES_MANUAL, [&connman](auto success) { + std::cout << "Time updates set to manual: " + << (success ? "Success" : "Failure") << '\n'; + connman.clock()->setTime(time, [&connman](auto success) { + std::cout << "Time set to " << kTestTime << ": " + << (success ? "Success" : "Failure") << '\n'; + connman.clock()->getProperties( + [](auto& props) { print_clock_properties(props); }); + }); + }); +} + +TEST(Connman, ClockSetTimeZone) { + const Connman connman; + connman.clock()->setTimeZoneUpdates( + ConnmanClock::PROP_TIMEUPDATES_MANUAL, [&connman](auto success) { + std::cout << "TimeZone updates set to manual: " + << (success ? "Success" : "Failure") << '\n'; + connman.clock()->setTimeZone( + kTestTimeZone, [&connman](auto success) { + std::cout << "TimeZone set to " << kTestTimeZone << ": " + << (success ? "Success" : "Failure") << '\n'; + connman.clock()->getProperties( + [](auto& props) { print_clock_properties(props); }); + }); + }); +} + +TEST(Connman, ClockSetTimeServers) { + const Connman connman; + const std::vector servers = {"time1.example.com", + "time2.example.com"}; + connman.clock()->setTimeServers(servers, [&connman](auto success) { + std::cout << "Time servers set: " << (success ? "Success" : "Failure") + << '\n'; + connman.clock()->getProperties( + [](auto& props) { print_clock_properties(props); }); + }); +} + +TEST(Connman, ClockProxyInitialization) { + EXPECT_NO_THROW({ const Connman connman; }); +} +TEST(Connman, clockGetPropertiesNull) { + const Connman connman; + connman.clock()->getProperties(); +} + +TEST(Connman, ClockGetProperties) { + const Connman connman; + connman.clock()->getProperties( + [](auto& props) { print_clock_properties(props); }); +} + +TEST(Connman, ClockOnPropertiesChanged) { + const Connman connman; + + connman.clock()->onPropertiesChanged( + [](auto& props) { print_clock_properties(props); }); + std::this_thread::sleep_for(std::chrono::seconds(kSleepDurationSeconds)); +} diff --git a/tests/gdbusproxypp_test.cpp b/tests/gdbusproxypp_test.cpp new file mode 100644 index 0000000..8337d31 --- /dev/null +++ b/tests/gdbusproxypp_test.cpp @@ -0,0 +1,16 @@ +#include + +#include + +TEST(GProxy, Initialization) { + EXPECT_NO_THROW({ + Amarula::DBus::GProxy dbus("org.freedesktop.DBus", + "/org/freedesktop/DBus"); + }); + EXPECT_THROW( + { + const Amarula::DBus::GProxy dbus("invalid.bus.name", + "/invalid/object/path"); + }, + std::runtime_error); +}