diff options
author | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-22 15:42:13 +0200 |
---|---|---|
committer | Fabien Proriol <fabien.proriol@kazoe.org> | 2025-05-22 15:48:00 +0200 |
commit | c842548fef050ac5f8b56a5fcb4f579820247434 (patch) | |
tree | 68bce34d0932db0157761635a010695c5e9616e3 |
Initial Commit
-rw-r--r-- | CMakeLists.txt | 71 | ||||
-rw-r--r-- | LICENSE | 21 | ||||
-rw-r--r-- | README.md | 181 | ||||
-rw-r--r-- | cmake/CPack.cmake | 8 | ||||
-rw-r--r-- | cmake/Config.cmake.in | 7 | ||||
-rw-r--r-- | cmd/ksettings.cpp | 44 | ||||
-rw-r--r-- | python/bindings.cpp | 65 | ||||
-rw-r--r-- | src/changewatcher.cpp | 134 | ||||
-rw-r--r-- | src/changewatcher.h | 39 | ||||
-rw-r--r-- | src/settings.cpp | 285 | ||||
-rw-r--r-- | src/settings.h | 180 |
11 files changed, 1035 insertions, 0 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..acb7c5d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.14) + +project(KaZoe-Settings VERSION 1.0.0 LANGUAGES CXX) + +option(WITH_PYTHON "Create python binding" ON) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package (Threads REQUIRED) + +add_library(KaZoeSettings SHARED + src/settings.h src/settings.cpp + src/changewatcher.h src/changewatcher.cpp +) +set_target_properties(KaZoeSettings PROPERTIES VERSION ${CMAKE_PROJECT_VERSION} SOVERSION 1) +set_target_properties(KaZoeSettings PROPERTIES PUBLIC_HEADER "src/settings.h") +target_link_libraries(KaZoeSettings PRIVATE Threads::Threads) + +add_executable(ksettings cmd/ksettings.cpp) +target_include_directories(ksettings PRIVATE src) +target_link_libraries(ksettings KaZoeSettings) + +install(TARGETS KaZoeSettings + EXPORT "KaZoeSettingsTargets" + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + PUBLIC_HEADER DESTINATION "${CMAKE_INSTALL_PREFIX}/include/KaZoe/" +) + +install(TARGETS ksettings + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if(WITH_PYTHON) + message(STATUS "Build with python binding") + find_package(Python3 COMPONENTS Interpreter Development REQUIRED) + set(PYTHON_MODULE_EXTENSION ".so" CACHE INTERNAL "Cross python lib extension") + find_package(pybind11 REQUIRED) + include_directories(${Python3_INCLUDE_DIRS}) + pybind11_add_module(PyKaZoeSettings python/bindings.cpp) + set_target_properties(PyKaZoeSettings PROPERTIES OUTPUT_NAME "KaZoeSettings") + target_link_libraries(PyKaZoeSettings PUBLIC KaZoeSettings ${PYTHON_LIBRARY}) + install(TARGETS PyKaZoeSettings LIBRARY DESTINATION ${Python3_SITEARCH}) +endif(WITH_PYTHON) + + +# CMake Module +include(CMakePackageConfigHelpers) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfigVersion.cmake" COMPATIBILITY SameMajorVersion +) + +configure_package_config_file( + "cmake/Config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfig.cmake" + INSTALL_DESTINATION "lib/cmake/${PROJECT_NAME}" +) + +install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfig.cmake" "${CMAKE_CURRENT_BINARY_DIR}/generated/KaZoeSettingsConfigVersion.cmake" + DESTINATION "lib/cmake/${PROJECT_NAME}" +) + +install( + EXPORT "KaZoeSettingsTargets" + DESTINATION "lib/cmake/${PROJECT_NAME}" +) + +include(cmake/CPack.cmake) @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Proriol Fabien + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a84076d --- /dev/null +++ b/README.md @@ -0,0 +1,181 @@ +# KaZoe::Settings Class Documentation + +## Overview +The `KaZoe::Settings` class provides a robust system-wide configuration management solution. It allows reading and writing settings from configuration files with category support, type safety, and change notifications. + +## Features +- Read settings from multiple configuration files +- Support for different value types (string, int, double, boolean) +- Category-based organization of settings +- File change monitoring +- Permission-based write access +- Change notification system + +## Configuration File Structure +Settings are stored in configuration files with the `.conf` extension. The file format supports: +- Categories in square brackets: `[CategoryName]` +- Key-value pairs: `key = value` +- Owner declarations: `@key = owner_id` + +### Value Types SettingValue +- Strings: `key = "value"` or `key = value` +- Numbers: `key = 42` or `key = 3.14` +- Booleans: `key = true/false` or `key = on/off` + +## Usage + +### Initialization +```cmake +find_package(KaZoeSettings REQUIRED) +include_directories(${LIBKAZOESETTINGS_INCLUDE_DIR}) + +... + +target_link_libraries(myproject PUBLIC ${LIBKAZOESETTINGS_LIBRARIES}) +``` + +```cpp +#include <settings.h> + +KaZoe::Settings settings; // Creates instance and loads configuration +``` + +### Reading Settings +```cpp +// Get value with default +auto value = settings.get("key", "category", defaultValue); + +// Get value without category +auto value = settings.get("key"); + +// Get value using category notation +auto value = settings.get("[category]key"); +``` + +### Writing Settings +```cpp +// Set value with category +settings.set("key", value, "category"); + +// Set value without category +settings.set("key", value); +``` + +### Utility with SettingValue type + +KaZoe::makeValue: provide a way to convert std::string into SettingValue managing numeric, bool and string conversion + +```cpp +#include <settings.h> + +SettingValue v1 = KaZoe::makeValue("42"); // Integer +SettingValue v2 = KaZoe::makeValue("3.14"); // Double +SettingValue v3 = KaZoe::makeValue("true"); // Boolean +SettingValue v4 = KaZoe::makeValue("text"); // String +``` + +KaZoe::valueToStr: provide a way to convert SettingValue into std::string to display +```cpp +#include <settings.h> + +std::cout << KaZoe::valueToStr(v1) << std::endl; +``` + +### Change Notifications and print helper function +```cpp +// Set a notification handler +settings.setNotifier([](const std::string& category, const std::string& key, SettingValue value) { + // Handle setting change + std::cout << category << " | " << key << " changed to " << KaZoe::valueToStr(value) << std::endl; +}); +``` + +## File Locations +The system reads configuration from: + + - /etc/kazoe.conf - System defaults (read-only) + - /etc/kazoe.conf.d/* - System configuration fragments (read-only, own by projects packages) + - /var/kazoe.conf.d/<appid>.conf - Persistant write data storage (set) + +## Permissions + +Settings must be declared writable using @key = owner_id +The owner should be the binary filename of the application +Only the declared owner can modify the setting if the program name is the good one + + +The settings command-line tool can bypass ownership restrictions but must used ONLY for debug usage + +## Value Types +The system supports these value types through the SettingValue variant: + + - std::string + - int + - double + - bool + +# KaZoeSettings Python Module Documentation +## Overview + +The KaZoeSettings module provides a Python interface for managing KaZoe settings across your application. It automatically uses the Python script name as a unique identifier. + +## Usage Class: KaZoeSettings + +```python +from KaZoeSettings import KaZoeSettings +``` + +## Constructor +```python +settings = KaZoeSettings.KaZoeSettings() +``` + +Creates a new KaZoeSettings instance using the current Python script name as identifier. + +## Methods + +### get(key, category="", default=None) + +Retrieves a setting value. + + Parameters: + key (str): The setting key + category (str, optional): Setting category + default: Default value if setting not found + Returns: The setting value or default if not found + Supported value types: str, int, float, bool + +```python +value = settings.get("my_setting", "my_category", "default_value") +``` + +### set(key, value, category="") + +Sets a setting value. + + Parameters: + key (str): The setting key + value: The value to set (str, int, float, or bool) + category (str, optional): Setting category + Returns: True if successful, False otherwise + +```python +settings.set("my_setting", "new_value", "my_category") +``` + +## Full example usage + +```python +from KaZoeSettings import KaZoeSettings + +# Create settings instance +settings = KaZoeSettings() + +# Set values +settings.set("port", 8080, "network") +settings.set("debug", True, "app") + +# Get values +port = settings.get("port", "network", 80) # returns 8080 +debug = settings.get("debug", "app", False) # returns True +``` diff --git a/cmake/CPack.cmake b/cmake/CPack.cmake new file mode 100644 index 0000000..33169b1 --- /dev/null +++ b/cmake/CPack.cmake @@ -0,0 +1,8 @@ +set(CPACK_GENERATOR "DEB") +set(CPACK_PACKAGE_NAME "KaZoe-Settings") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "python3") +set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT) +set(CPACK_PACKAGE_VERSION_PATCH "${CMAKE_PROJECT_VERSION_PATCH}") +set(CPACK_PACKAGE_DESCRIPTION "KaZoe library") +set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Fabien Proriol <fabien.proriol@kazoe.org>") +include(CPack) diff --git a/cmake/Config.cmake.in b/cmake/Config.cmake.in new file mode 100644 index 0000000..efbb7cc --- /dev/null +++ b/cmake/Config.cmake.in @@ -0,0 +1,7 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/KaZoeSettingsTargets.cmake") +set(LIBKAZOESETTINGS_INCLUDE_DIR "${PACKAGE_PREFIX_DIR}/include/@PROJECT_NAME@") +set(LIBKAZOESETTINGS_LIBRARIES "KaZoeSettings" ) + +check_required_components("@PROJECT_NAME@") diff --git a/cmd/ksettings.cpp b/cmd/ksettings.cpp new file mode 100644 index 0000000..0d29dab --- /dev/null +++ b/cmd/ksettings.cpp @@ -0,0 +1,44 @@ +#include <iostream> +#include <settings.h> + +using namespace std; + +int main(int argc, char *argv[]) +{ + KaZoe::Settings settings; + + if(argc < 2) + { + std::cout << "Current settings:" << std::endl; + + settings.forEach([](const SettingKey& key, const SettingValue& value) { + std::string head; + if(!key.first.empty()) + { + head = "[" + key.first + "]"; + } + std::cout << "> " << head << key.second << " = " << KaZoe::valueToStr(value) << std::endl; + }); + } + else + { + std::string category; + std::string gkey = argv[1]; + if(gkey.starts_with("[")) + { + category = gkey.substr(1, gkey.find(']') - 1); + gkey = gkey.substr(gkey.find(']') + 1); + } + if(argc == 2) + { + std::cout << KaZoe::valueToStr(settings.get(gkey, category)) << std::endl; + } + else if(argc == 3) + { + std::string value = argv[2]; + settings.set(gkey, KaZoe::makeValue(value), category); + } + } + + return 0; +} diff --git a/python/bindings.cpp b/python/bindings.cpp new file mode 100644 index 0000000..5c816f6 --- /dev/null +++ b/python/bindings.cpp @@ -0,0 +1,65 @@ +#include <pybind11/pybind11.h> +#include <pybind11/stl.h> +#include "../src/settings.h" + +namespace py = pybind11; +using namespace pybind11::literals; + +class PyKaZoeSettings { + KaZoe::Settings m_settings; + +public: + PyKaZoeSettings(); + std::string __repr__(); + static void add_python_binding(pybind11::module &m); + SettingValue get(std::string key, std::string category, SettingValue def); + SettingValue set(std::string key, SettingValue value, std::string category); +}; + +PyKaZoeSettings::PyKaZoeSettings() { + py::module sys = py::module::import("sys"); + py::list argv = sys.attr("argv"); + m_settings.setId(argv[0].cast<std::string>()); +} + +SettingValue PyKaZoeSettings::get(std::string key, std::string category, SettingValue def) { + return m_settings.get(key, category, def); +} + +SettingValue PyKaZoeSettings::set(std::string key, SettingValue value, std::string category) { + return m_settings.set(key, value, category); +} + + +std::string PyKaZoeSettings::__repr__() { + std::string result = "KaZoeSettings"; + return result; +} +void PyKaZoeSettings::add_python_binding(pybind11::module &m) +{ + py::class_<PyKaZoeSettings>(m, "KaZoeSettings") + .def(py::init<>()) + + .def("get", [] (PyKaZoeSettings &m, std::string key, std::string category = "", SettingValue def = SettingValue()) { + return m.get(key, category, def); + }, + py::arg("key"), + py::arg("category") = "", + py::arg("default") = SettingValue()) + .def("set", [] (PyKaZoeSettings &m, std::string key, SettingValue value, std::string category = "") { + return m.set(key, value, category); + }, + py::arg("key"), + py::arg("value"), + py::arg("category") = "") + .def("__repr__", &PyKaZoeSettings::__repr__); +} + + +PYBIND11_MODULE(KaZoeSettings, m) { + m.doc() = R"pbdoc( + Python bindings for KaZoeSettings + )pbdoc"; + + PyKaZoeSettings::add_python_binding(m); +} diff --git a/src/changewatcher.cpp b/src/changewatcher.cpp new file mode 100644 index 0000000..4762ebb --- /dev/null +++ b/src/changewatcher.cpp @@ -0,0 +1,134 @@ +#include "changewatcher.h" +#include <iostream> +#include <filesystem> +#include <sys/inotify.h> +#include <sys/eventfd.h> +#include <unistd.h> +#include <algorithm> + +ChangeWatcher::ChangeWatcher() +{ +} + +ChangeWatcher::~ChangeWatcher() +{ + setEnable(false); +} + +void ChangeWatcher::watch(const std::string &newPath) +{ + if(std::find(m_watches.begin(), m_watches.end(), newPath) == m_watches.end()) + { + m_watches.push_back(newPath); + if(m_enabled) + { + _addWatch(newPath); + } + } +} + +void ChangeWatcher::setEnable(bool newVal) +{ + if(m_enabled == newVal) + return; + + if(newVal) + { + // Start thread + m_fdnotify = inotify_init(); + for(auto &newPath: m_watches) + { + _addWatch(newPath); + } + m_fdevent = eventfd(0, 0); + m_enabled = true; + m_thread = std::thread(_runningLoop, this); + } + else + { + // Stop thread + m_enabled = false; + uint64_t value = 1; + write(m_fdevent, &value, sizeof(value)); + close(m_fdevent); + m_thread.join(); + close(m_fdnotify); + } +} + +bool ChangeWatcher::enabled() const +{ + return m_enabled; +} + +void ChangeWatcher::setFileWrited(const std::function<void (const std::string &)> &callback) +{ + m_filewrited = callback; +} + +void ChangeWatcher::setFileAdded(const std::function<void (const std::string &)> &callback) +{ + m_fileadded = callback; +} + +void ChangeWatcher::setFileRemoved(const std::function<void (const std::string &)> &callback) +{ + m_fileremoved = callback; +} + +void ChangeWatcher::_runningLoop(ChangeWatcher *m) +{ + const size_t event_size = sizeof(struct inotify_event); + const size_t buffer_size = 1024 * (event_size + 16); + char buffer[buffer_size]; + + while (m->m_enabled) { + fd_set rfds; + FD_ZERO(&rfds); + FD_SET(m->m_fdevent, &rfds); + FD_SET(m->m_fdnotify, &rfds); + int nfds = std::max(m->m_fdevent, m->m_fdnotify) + 1; + + if (select(nfds, &rfds, nullptr, nullptr, nullptr) > 0) { + if (FD_ISSET(m->m_fdevent, &rfds)) { + break; // Signal d'arrêt reçu + } + if (FD_ISSET(m->m_fdnotify, &rfds)) { + int length = read(m->m_fdnotify, buffer, buffer_size); + if (length > 0) { + size_t i = 0; + while (i < length) { + auto* event = reinterpret_cast<struct inotify_event*>(&buffer[i]); + if((event->mask & IN_CLOSE_WRITE) && (m->m_filewrited != nullptr)) + { + m->m_filewrited(m->m_wd[event->wd]); + } + if((event->mask & IN_CREATE) && (m->m_fileadded != nullptr)) + { + m->m_fileadded(m->m_wd[event->wd]); + } + if((event->mask & IN_DELETE) && (m->m_fileremoved != nullptr)) + { + m->m_fileremoved(m->m_wd[event->wd]); + } + i += event_size + event->len; + } + } + } + } + } +} + +void ChangeWatcher::_addWatch(const std::string &newPath) +{ + int wd = -1; + if(std::filesystem::is_directory(newPath)) + { + wd = inotify_add_watch(m_fdnotify, newPath.c_str(), IN_CREATE | IN_DELETE); + } + else + { + wd = inotify_add_watch(m_fdnotify, newPath.c_str(), IN_CLOSE_WRITE); + } + if(wd > -1) m_wd[wd] = newPath; +} diff --git a/src/changewatcher.h b/src/changewatcher.h new file mode 100644 index 0000000..fde6ab8 --- /dev/null +++ b/src/changewatcher.h @@ -0,0 +1,39 @@ +#ifndef CHANGEWATCHER_H +#define CHANGEWATCHER_H + +#include <functional> +#include <string> +#include <map> +#include <thread> + +class ChangeWatcher +{ + int m_fdnotify; + int m_fdevent; + bool m_enabled {false}; + std::thread m_thread; + std::vector<std::string> m_watches; + std::map<int, std::string> m_wd; + + std::function<void (const std::string &)> m_filewrited {}; + std::function<void (const std::string &)> m_fileadded; + std::function<void (const std::string &)> m_fileremoved; + +public: + explicit ChangeWatcher(); + virtual ~ChangeWatcher(); + void watch(const std::string &newPath); + + void setEnable(bool newVal); + bool enabled() const; + + void setFileWrited(const std::function<void(const std::string&)> &callback); + void setFileAdded(const std::function<void(const std::string&)> &callback); + void setFileRemoved(const std::function<void(const std::string&)> &callback); + +private: + static void _runningLoop(ChangeWatcher *m); + void _addWatch(const std::string& newPath); +}; + +#endif // CHANGEWATCHER_H diff --git a/src/settings.cpp b/src/settings.cpp new file mode 100644 index 0000000..addaa32 --- /dev/null +++ b/src/settings.cpp @@ -0,0 +1,285 @@ +#include "settings.h" +#include <iostream> +#include <fstream> +#include <map> +#include <vector> +#include <filesystem> +#include <climits> +#include <mutex> +#include <unistd.h> +#include "changewatcher.h" + +using namespace KaZoe; + +static inline void trim(std::string & str) +{ + str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](const unsigned char ch){return !std::isspace(ch);})); + str.erase(std::find_if(str.rbegin(), str.rend(), [](const unsigned char ch){return !std::isspace(ch);}).base(), str.end()); +} + +static inline bool list_contains(std::vector<std::string> list, const std::string & str) +{ + return (std::find(list.begin(), list.end(), str) != list.end()); +} + +namespace KaZoe { +class SettingsPrivate { + friend class Settings; + Settings *m_parent; + std::map<SettingKey, SettingValue> m_data; + ChangeWatcher m_watcher; + std::map<SettingKey, std::string> m_owner; + std::string m_id; + std::mutex m_mutex_data; + std::mutex m_mutex_notifier; + bool m_bypass {false}; + std::vector<std::function<void(const std::string&, const std::string&, SettingValue)>> m_notifier; + +public: + SettingsPrivate(Settings *parent) + : m_parent(parent) { + char result[PATH_MAX]; + ssize_t count = readlink("/proc/self/exe", result, PATH_MAX); + std::string binary = std::string(result, (count > 0) ? count : 0); + std::filesystem::path bin = binary; + m_id = bin.filename(); + if(m_id == "settings") + { + // Only "settings cli tools can bypass the owner protection" + m_bypass = true; + } + + m_watcher.setFileAdded([this](const std::string path){ refreshDirectory(path); }); + m_watcher.setFileRemoved([this](const std::string path){ refreshAll(); }); + m_watcher.setFileWrited([this](const std::string path){ refreshFile(path); }); + } + + void parseFile(const std::string &filename, std::map<SettingKey, SettingValue> &data, bool watch = false) + { + if(!std::filesystem::exists(filename)) + { + return; + } + std::filesystem::path pfile = filename; + if(pfile.filename().string().starts_with(".")) return; + if(pfile.extension() != ".conf") return; + + if(watch) + { + m_watcher.watch(filename); + } + + std::ifstream file(filename); + std::string line; + std::string category = ""; + + while (std::getline(file, line)) { + trim(line); + + if(line.empty() || line.starts_with("#") || line.ends_with(";")) { + continue; + } + if(line.starts_with("[") && line.ends_with("]")) { + category = line.substr(1, line.length() - 2); + if(category == "General") category.clear(); + continue; + } + size_t pos = line.find("="); + std::string key = line.substr(0, pos); + trim(key); + std::string value = line.substr(pos + 1); + trim(value); + + if(key.starts_with("@")) + { + m_owner[SettingKey(category, key.substr(1))] = value; + continue; + } + if(key.ends_with("/owner")) + { + std::cerr << "WARNING: Legacy usage for owner: " << key << " should be @" << key.substr(0, key.size() - 6) << std::endl; + m_owner[SettingKey(category, key.substr(0, key.size() - 6))] = value; + continue; + } + + data[SettingKey(category, key)] = makeValue(value); + } + } + + void parseDir(const std::string &dconf, std::map<SettingKey, SettingValue> &data, bool watch) + { + if(std::filesystem::exists(dconf) && std::filesystem::is_directory(dconf)) + { + for (const auto& entry : std::filesystem::directory_iterator(dconf)) + { + if(entry.is_directory()) continue; + parseFile(std::filesystem::absolute(entry.path()), data, watch); + } + } + } + + void parseConf(const std::string &fconf, std::map<SettingKey, SettingValue> &data, bool watch) + { + if(std::filesystem::exists(fconf)) + { + parseFile(fconf, data, watch); + } + std::string dconf = fconf + ".d"; + parseDir(dconf, data, watch); + if(watch) + { + m_watcher.watch(dconf); + } + } + + void refreshData(const std::map<SettingKey, SettingValue> &cust_data) + { + std::lock_guard<std::mutex> lock(m_mutex_data); + std::lock_guard<std::mutex> lock_notifier(m_mutex_notifier); + for(const auto& [key, value] : cust_data) { + if(!m_data.contains(key) || m_data[key] != value) + { + // New ITEM OR Data changed + m_data[key] = value; + for(auto ¬ifier: m_notifier) + { + notifier(key.first, key.second, value); + } + continue; + } + } + } + + void refreshFile(const std::string &file) + { + std::map<SettingKey, SettingValue> cust_data; + parseFile(file, cust_data); + refreshData(cust_data); + } + + void refreshDirectory(const std::string &dirname) + { + std::map<SettingKey, SettingValue> cust_data; + parseDir(dirname, cust_data, true); + refreshData(cust_data); + } + + void refreshAll() + { + std::map<SettingKey, SettingValue> cust_data; + parseConf("/etc/kazoe.conf", cust_data, false); + parseConf("/var/kazoe.conf", cust_data, true); + refreshData(cust_data); + } +}; +}; + +Settings::Settings() + : m_ptr(std::make_unique<SettingsPrivate>(this)) +{ + m_ptr->parseConf("/etc/kazoe.conf", m_ptr->m_data, false); + m_ptr->parseConf("/var/kazoe.conf", m_ptr->m_data, true); +} + +SettingValue Settings::get(const std::string &key, const std::string &category, SettingValue def) const +{ + std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data); + std::string gkey = key; + std::string gcategory = category; + if(gcategory.empty() && gkey.starts_with("[")) + { + gcategory = gkey.substr(1, gkey.find(']') - 1); + gkey = gkey.substr(gkey.find(']') + 1); + } + + SettingKey pkey(gcategory, gkey); + + if(m_ptr->m_data.count(pkey)) + { + return m_ptr->m_data[pkey]; + } + return def; +} + +bool Settings::set(const std::string &key, SettingValue value, const std::string &category) +{ + // Check if variable is writable + std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data); + SettingKey pkey(category, key); + std::string id; + + if(!m_ptr->m_owner.count(pkey)) + { + std::cerr << "Variable " << category << ">" << key << " is not writable" << std::endl; + return false; + } + + if(m_ptr->m_bypass) + { + id = m_ptr->m_owner[pkey]; + } + else + { + id = m_ptr->m_id; + if(m_ptr->m_owner[pkey] != id) + { + std::cerr << "Variable " << category << ">" << key << " is not own by " << m_ptr->m_id << std::endl; + return false; + } + } + + // Permission is OK, get old custom variable + std::map<SettingKey, SettingValue> cust_data; + m_ptr->parseFile("/var/kazoe.conf.d/"+id+".conf", cust_data); + cust_data[pkey] = value; + std::string last_category = ""; + std::ofstream file("/var/kazoe.conf.d/"+id+".conf"); + + for(const auto& [key, value] : cust_data) { + if(key.first != last_category) + { + last_category = key.first.empty() ? "General" : key.first; + file << "[" << last_category << "]\n"; + } + file << key.second << " = " << KaZoe::valueToStr(value) << "\n"; + } + + m_ptr->m_data[pkey] = value; + return true; +} + +std::string Settings::id() const +{ + return m_ptr->m_id; +} + +void Settings::setId(const std::string &newid) +{ + m_ptr->m_id = newid; +} + +void Settings::setNotifier(const std::function<void (const std::string &, const std::string &, SettingValue)> &callback) +{ + std::lock_guard<std::mutex> lock(m_ptr->m_mutex_notifier); + m_ptr->m_notifier.push_back(callback); + m_ptr->m_watcher.setEnable(true); +} + +std::size_t Settings::size() const +{ + std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data); + return m_ptr->m_data.size(); +} + +void Settings::forEach(const std::function<void (const SettingKey &, const SettingValue &)> &callback) const +{ + std::lock_guard<std::mutex> lock(m_ptr->m_mutex_data); + for (const auto& [key, value] : m_ptr->m_data) { + callback(key, value); + } +} + +Settings::~Settings() +{ + m_ptr->m_watcher.setEnable(false); +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 0000000..5dfe90b --- /dev/null +++ b/src/settings.h @@ -0,0 +1,180 @@ +#ifndef KAZOESETTINGS_H +#define KAZOESETTINGS_H + +#include <functional> +#include <memory> +#include <variant> +#include <string> +#include <algorithm> + +/** @brief Key type for settings, composed of category and name */ +using SettingKey = std::pair<std::string, std::string>; + +/** @brief Value type that can hold string, int, double or boolean */ +using SettingValue = std::variant<std::string, int, double, bool>; + + +namespace KaZoe { + +class SettingsPrivate; + +/** + * @brief Main class for managing KaZoe settings + * + * This class provides a thread-safe interface for storing and retrieving + * configuration settings with support for different value types. + */ +class Settings +{ +public: + /** @brief Constructs a new Settings instance */ + explicit Settings(); + + /** @brief Virtual destructor */ + virtual ~Settings(); + + /** + * @brief Retrieves a setting value + * @param key The setting key + * @param category The setting category (optional) + * @param def Default value if setting not found (optional) + * @return The setting value or default if not found + */ + SettingValue get(const std::string &key, const std::string &category = "", SettingValue def = SettingValue()) const; + + /** + * @brief Sets a setting value + * @param key The setting key + * @param value The value to set + * @param category The setting category (optional) + * @return true if successful, false otherwise + */ + bool set(const std::string &key, SettingValue value, const std::string &category = ""); + + /** + * @brief Gets the current instance identifier + * @return The instance ID string, by default, this is the excecutable filename + */ + std::string id() const; + + /** + * @brief Sets the instance identifier + * @param newid The new ID to set + */ + void setId(const std::string &newid); + + /** + * @brief Sets a callback for setting changes notification + * @param callback Function to call when settings change + */ + void setNotifier(const std::function<void(const std::string&, const std::string&, SettingValue)>& callback); + + /** + * @brief Gets the number of settings + * @return The total count of settings + */ + std::size_t size() const; + + /** + * @brief Iterates over all settings in a thread-safe manner + * @param callback Function called for each key-value pair + */ + void forEach(const std::function<void(const SettingKey&, const SettingValue&)>& callback) const; + +private: + std::unique_ptr<SettingsPrivate> m_ptr; ///< Private implementation pointer +}; + + +/** + * @brief Functor to convert SettingValue variants to string + * + * Provides string conversion for all supported setting value types + */ +struct settingValueFunctor { + std::string operator()(const std::string &x) const { return x; } + std::string operator()(int x) const { return std::to_string(x); } + std::string operator()(double x) const { return std::to_string(x); } + std::string operator()(bool x) const { return x ? "true" : "false"; } +}; + +/** + * @brief Convert a SettingValue into a string for display + * @param value Input SettingValue to convert + * @return std::string containing the converted value + */ +static inline std::string valueToStr(const SettingValue &value) +{ + return std::visit(settingValueFunctor(), value); +} + +/** + * @brief Check if a string represents a valid numeric value + * @param str String to check + * @return true if string is numeric, false otherwise + */ +static inline bool is_numeric(const std::string& str) { + return !str.empty() && + str.find_first_not_of("0123456789.-") == std::string::npos && + (std::count(str.begin(), str.end(), '.') <= 1) && + (std::count(str.begin(), str.end(), '-') <= 1) && + (str[0] == '-' || std::isdigit(str[0])); +} + +/** + * @brief Convert a string to appropriate SettingValue type + * @param value Input string to convert + * @return SettingValue containing the converted value + * + * Conversion rules: + * - Quoted strings -> string without quotes + * - Numeric values -> int or double + * - "true"/"on" -> boolean true + * - "false"/"off" -> boolean false + * - Other -> string + */ +static inline SettingValue makeValue(const std::string &value) +{ + if(value.starts_with('"') || value.starts_with('\'')) + { + return value.substr(1, value.length() - 2); + } + + if(is_numeric(value)) + { + // double + if(value.find('.') != value.npos) + { + return std::stod(value); + } + else + { + return std::stoi(value); + } + } + + // To Lower case + std::string lvalue = value; + std::transform(lvalue.begin(), lvalue.end(), lvalue.begin(), + [](unsigned char c){ return std::tolower(c); }); + + + // bool true + if(lvalue == "true" || lvalue == "on") + { + return true; + } + + // bool false + if(lvalue == "false" || lvalue == "off") + { + return false; + } + + // else, string without quote + return value; +} + +}; + +#endif // KAZOESETTINGS_H |