From 5db82885b580349ac82b18a5d930a0a3485d9de0 Mon Sep 17 00:00:00 2001 From: GuanYuankai Date: Wed, 7 Jan 2026 10:38:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(config=20manager):=20=E5=A2=9E=E5=BC=BAcon?= =?UTF-8?q?fig=20manager=E7=9A=84=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E4=BE=9B=E8=A7=82=E5=AF=9F=E8=80=85=E6=A8=A1=E5=BC=8F=E7=9A=84?= =?UTF-8?q?=E5=9B=9E=E8=B0=83=EF=BC=8C=E7=BB=99=E6=9C=AA=E6=9D=A5=E7=9A=84?= =?UTF-8?q?=E7=83=AD=E9=87=8D=E8=BD=BD=E5=81=9A=E5=87=86=E5=A4=87=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 33 ++-- src/config/config_manager.cc | 318 +++++++++++++++++++++-------------- src/config/config_manager.h | 155 +++++++++-------- 3 files changed, 298 insertions(+), 208 deletions(-) diff --git a/config/config.json b/config/config.json index fb6188c..887b6b8 100644 --- a/config/config.json +++ b/config/config.json @@ -1,17 +1,18 @@ { - "alarm_rules_path": "alarms.json", - "config_base_path": "/app/config/", - "data_cache_db_path": "edge_data_cache.db", - "data_storage_db_path": "edge_proxy_data.db", - "device_id": "rk3588-proxy-002", - "log_level": "info", - "mqtt_broker": "tcp://localhost:1883", - "mqtt_client_id_prefix": "vehicle-road-counter-", - "piper_executable_path": "/usr/bin/piper", - "piper_model_path": "/app/models/model.onnx", - "tcp_server_ports": [ - 12345 - ], - "video_config_path": "video_config.json", - "web_server_port": 8080 -} \ No newline at end of file + "alarm_rules_path": "alarms.json", + "config_base_path": "/app/config/", + "data_cache_db_path": "edge_data_cache.db", + "data_storage_db_path": "edge_proxy_data.db", + "device_id": "rk3588-proxy-002", + "log_level": "info", + "mqtt_broker": "tcp://localhost:1883", + "mqtt_client_id_prefix": "vehicle-road-counter-", + "piper_executable_path": "/usr/bin/piper", + "piper_model_path": "/app/models/model.onnx", + "tcp_server_ports": [12345], + "video_config_path": "video_config.json", + "web_server_port": 8080, + "db_user": "forlinx", + "db_pwd": "forlinx", + "db_database": "smart-car-dev" +} diff --git a/src/config/config_manager.cc b/src/config/config_manager.cc index 60f5674..be4d6c6 100644 --- a/src/config/config_manager.cc +++ b/src/config/config_manager.cc @@ -1,146 +1,216 @@ -// config_manager.cc (修改后) #include "config_manager.h" + +#include // for std::rename, std::remove #include -ConfigManager &ConfigManager::getInstance() { - static ConfigManager instance; - return instance; +ConfigManager& ConfigManager::getInstance() { + static ConfigManager instance; + return instance; +} + +// 注册监听器 +void ConfigManager::monitorKey(const std::string& key, KeyUpdateCallback cb) { + std::lock_guard lock(m_callbackMutex); + m_key_callbacks[key].push_back(cb); + spdlog::debug("Registered observer for config key: '{}'", key); +} + +// 核心逻辑:对比新旧配置,触发回调 +void ConfigManager::checkDiffAndNotify(const json& old_config, const json& new_config) { + // 复制一份监听列表,避免在遍历时加锁时间过长,或者回调中再次注册导致死锁 + std::map> callbacks_snapshot; + { + std::lock_guard lock(m_callbackMutex); + callbacks_snapshot = m_key_callbacks; + } + + int trigger_count = 0; + + for (const auto& [key, callbacks] : callbacks_snapshot) { + if (!new_config.contains(key)) + continue; + + // 获取新旧值 (如果旧配置没这个key,视为 null) + json old_val = old_config.contains(key) ? old_config[key] : json(); + json new_val = new_config[key]; + + if (old_val != new_val) { + spdlog::info("Config Hot-Reload: Key '{}' changed. Triggering {} callbacks.", key, + callbacks.size()); + + for (const auto& cb : callbacks) { + try { + cb(new_val); // 执行回调 + trigger_count++; + } catch (const std::exception& e) { + spdlog::error("Exception inside config callback for key '{}': {}", key, + e.what()); + } + } + } + } + + if (trigger_count > 0) { + spdlog::info("Config reload finished. {} callbacks triggered.", trigger_count); + } } json ConfigManager::createDefaultConfig() { - // --- 修改: 添加新的 video_service 和 video_streams 默认值 --- - return json{ - {"device_id", "default-edge-proxy-01"}, - {"config_base_path", "/app/config/"}, - {"mqtt_broker", "tcp://localhost:1883"}, - {"mqtt_client_id_prefix", "edge-proxy-"}, - {"data_storage_db_path", "edge_proxy_data.db"}, - {"data_cache_db_path", "edge_data_cache.db"}, - {"tcp_server_ports", {12345}}, - {"web_server_port", 8080}, - {"log_level", "debug"}, - {"alarm_rules_path", "alarms.json"}, - {"piper_executable_path", "/usr/bin/piper"}, - {"piper_model_path", "/app/models/model.onnx"}, - - // --- 新增: 视频服务配置 --- - {"video_config_path", "/app/config/video_config.json"}, - }; + return json{ + {"device_id", "default-edge-proxy-01"}, + {"config_base_path", "/app/config/"}, + {"mqtt_broker", "tcp://localhost:1883"}, + {"mqtt_client_id_prefix", "edge-proxy-"}, + {"data_storage_db_path", "edge_proxy_data.db"}, + {"data_cache_db_path", "edge_data_cache.db"}, + {"tcp_server_ports", {12345}}, + {"web_server_port", 8080}, + {"log_level", "debug"}, + {"alarm_rules_path", "alarms.json"}, + {"piper_executable_path", "/usr/bin/piper"}, + {"piper_model_path", "/app/models/model.onnx"}, + {"video_config_path", "/app/config/video_config.json"}, + }; } +// 原子写入实现 bool ConfigManager::save_unlocked() { - std::ofstream ofs(m_configFilePath); - if (!ofs.is_open()) { - spdlog::error("Failed to open config file '{}' for writing.", - m_configFilePath); - return false; - } - try { - ofs << m_config_json.dump(4); - return true; - } catch (const json::exception &e) { - spdlog::error("Failed to serialize config. Error: {}", e.what()); - return false; - } + std::string tmpPath = m_configFilePath + ".tmp"; + + // 1. 写入临时文件 + { + std::ofstream ofs(tmpPath); + if (!ofs.is_open()) { + spdlog::error("Failed to open temp config file '{}' for writing.", tmpPath); + return false; + } + try { + ofs << m_config_json.dump(4); + ofs.flush(); // 确保缓冲区刷入内核 + // 在某些关键系统上,可能还需要 fsync + } catch (const json::exception& e) { + spdlog::error("Failed to serialize config. Error: {}", e.what()); + return false; + } + } // ofs 关闭 + + // 2. 原子重命名 (Atomic Rename) + // 如果重命名失败,原文件不会损坏 + if (std::rename(tmpPath.c_str(), m_configFilePath.c_str()) != 0) { + spdlog::error("Failed to rename temp config file to '{}'. Errno: {}", m_configFilePath, + errno); + return false; + } + + return true; } -bool ConfigManager::load(const std::string &configFilePath) { - std::unique_lock lock(m_mutex); - m_configFilePath = configFilePath; +bool ConfigManager::load(const std::string& configFilePath) { + // 准备两个容器,用于在锁外进行对比 + json old_config_copy; + json new_config_copy; - std::ifstream ifs(m_configFilePath); - if (!ifs.is_open()) { - spdlog::warn("Config file '{}' not found. Creating with default values.", - m_configFilePath); - m_config_json = createDefaultConfig(); - if (save_unlocked()) { - spdlog::info("Default config file created at '{}'.", m_configFilePath); - return true; - } else { - spdlog::error("Failed to create default config file."); - return false; - } - } + { + std::unique_lock lock(m_mutex); // 获取写锁 + m_configFilePath = configFilePath; - try { - ifs >> m_config_json; - // **重要**:合并默认值。 - json defaults = createDefaultConfig(); - defaults.merge_patch( - m_config_json); // <-- 关键: m_config_json 会覆盖 defaults - m_config_json = defaults; // 将合并后的结果存回 + // 备份当前配置(如果是第一次加载,这里是空的) + old_config_copy = m_config_json; - spdlog::info("Successfully loaded config from '{}'. Device ID: {}", - m_configFilePath, m_config_json.value("device_id", "N/A")); + std::ifstream ifs(m_configFilePath); + if (!ifs.is_open()) { + spdlog::warn("Config file '{}' not found. Creating default.", m_configFilePath); + m_config_json = createDefaultConfig(); + // 保存默认文件 + if (!save_unlocked()) { + spdlog::error("Failed to create default config file."); + return false; + } + // 第一次加载,不需要触发“变更”回调 + return true; + } - if (save_unlocked()) { - spdlog::debug("Config file updated with new default keys if any."); - } - return true; - } catch (const json::exception &e) { - spdlog::error( - "Failed to parse config file '{}'. Error: {}. Using default values.", - m_configFilePath, e.what()); - m_config_json = createDefaultConfig(); - return false; - } + try { + json loaded_json; + ifs >> loaded_json; + + // 合并默认值 + json defaults = createDefaultConfig(); + defaults.merge_patch(loaded_json); + m_config_json = defaults; + + // 复制新配置用于后续对比 + new_config_copy = m_config_json; + + spdlog::info("Config loaded from '{}'. DeviceID: {}", m_configFilePath, + m_config_json.value("device_id", "N/A")); + + // 可选:如果缺少key,回写文件 + save_unlocked(); + + } catch (const json::exception& e) { + spdlog::error("Failed to parse config file. Error: {}. Using defaults.", e.what()); + m_config_json = createDefaultConfig(); + return false; + } + } // !!! 写锁在这里释放 !!! + + // 锁释放后,执行回调通知 + // 这样做非常安全,不会因为回调里调用了 get() 而导致死锁 + checkDiffAndNotify(old_config_copy, new_config_copy); + + return true; } -// ... (save, getDeviceID, ... getPiperModelPath 保持不变) ... bool ConfigManager::save() { - std::unique_lock lock(m_mutex); - return save_unlocked(); -} -std::string ConfigManager::getDeviceID() { - return get("device_id", "default-edge-proxy-01"); -} -std::string ConfigManager::getConfigBasePath() { - return get("config_base_path", "/app/config/"); -} -std::string ConfigManager::getMqttBroker() { - return get("mqtt_broker", "tcp://localhost:1883"); -} -std::string ConfigManager::getMqttClientID() { - return get("mqtt_client_id_prefix", "edge-proxy-") + - getDeviceID(); -} -std::string ConfigManager::getDataStorageDbPath() { - return getConfigBasePath() + - get("data_storage_db_path", "edge_proxy_data.db"); -} -std::string ConfigManager::getDataCacheDbPath() { - return getConfigBasePath() + - get("data_cache_db_path", "edge_data_cache.db"); -} -std::string ConfigManager::getDevicesConfigPath() { - return getConfigBasePath() + "devices.json"; -} -int ConfigManager::getWebServerPort() { - return get("web_server_port", 8080); -} -std::vector ConfigManager::getTcpServerPorts() { - return get>("tcp_server_ports", {12345}); -} -std::string ConfigManager::getLogLevel() { - return get("log_level", "debug"); -} -std::string ConfigManager::getAlarmRulesPath() { - return getConfigBasePath() + - get("alarm_rules_path", "alarms.json"); -} -std::string ConfigManager::getPiperExecutablePath() { - return get("piper_executable_path", "/usr/bin/piper"); -} -std::string ConfigManager::getPiperModelPath() { - return get("piper_model_path", "/app/models/model.onnx"); -} - -std::string ConfigManager::getVideoConfigPath() { - return getConfigBasePath() + - get("video_config_path", "video_config.json"); + std::unique_lock lock(m_mutex); + return save_unlocked(); } std::string ConfigManager::getConfigFilePath() const { - std::shared_lock lock(m_mutex); - return m_configFilePath; + std::shared_lock lock(m_mutex); + return m_configFilePath; +} + +std::string ConfigManager::getDeviceID() { + return get("device_id", "default-edge-proxy-01"); +} +std::string ConfigManager::getConfigBasePath() { + return get("config_base_path", "/app/config/"); +} +std::string ConfigManager::getMqttBroker() { + return get("mqtt_broker", "tcp://localhost:1883"); +} +std::string ConfigManager::getMqttClientID() { + return get("mqtt_client_id_prefix", "edge-proxy-") + getDeviceID(); +} +std::string ConfigManager::getDataStorageDbPath() { + return getConfigBasePath() + get("data_storage_db_path", "edge_proxy_data.db"); +} +std::string ConfigManager::getDataCacheDbPath() { + return getConfigBasePath() + get("data_cache_db_path", "edge_data_cache.db"); +} +std::string ConfigManager::getDevicesConfigPath() { + return getConfigBasePath() + "devices.json"; +} +int ConfigManager::getWebServerPort() { + return get("web_server_port", 8080); +} +std::vector ConfigManager::getTcpServerPorts() { + return get>("tcp_server_ports", {12345}); +} +std::string ConfigManager::getLogLevel() { + return get("log_level", "debug"); +} +std::string ConfigManager::getAlarmRulesPath() { + return getConfigBasePath() + get("alarm_rules_path", "alarms.json"); +} +std::string ConfigManager::getPiperExecutablePath() { + return get("piper_executable_path", "/usr/bin/piper"); +} +std::string ConfigManager::getPiperModelPath() { + return get("piper_model_path", "/app/models/model.onnx"); +} +std::string ConfigManager::getVideoConfigPath() { + return getConfigBasePath() + get("video_config_path", "video_config.json"); } \ No newline at end of file diff --git a/src/config/config_manager.h b/src/config/config_manager.h index 3c1d4e9..7139e11 100644 --- a/src/config/config_manager.h +++ b/src/config/config_manager.h @@ -1,9 +1,12 @@ -// config_manager.h (修改后) #pragma once +#include + +#include +#include +#include #include #include -#include #include #include #include @@ -12,82 +15,98 @@ using json = nlohmann::json; class ConfigManager { public: - struct VideoStreamConfig { - std::string id; - bool enabled; - std::string input_url; - std::string output_rtsp; + // 定义回调函数类型:接收变化后的新值 (json格式) + using KeyUpdateCallback = std::function; - std::string module_type; - json module_config; - }; + static ConfigManager& getInstance(); - static ConfigManager &getInstance(); - bool load(const std::string &configFilePath); - bool save(); + // 加载配置(支持热重载) + bool load(const std::string& configFilePath); - template T get(const std::string &key, const T &default_value) { - std::shared_lock lock(m_mutex); + // 保存配置(原子写入) + bool save(); - if (!m_config_json.contains(key)) { - spdlog::debug("Config key '{}' not found, using default value.", key); - return default_value; - } + // --- 核心新功能:监听特定 Key 的变化 --- + void monitorKey(const std::string& key, KeyUpdateCallback cb); - try { - return m_config_json.at(key).get(); - } catch (const json::type_error &e) { - spdlog::warn("Config type mismatch for key '{}'. Expected '{}', found " - "'{}'. Using default. Error: {}", - key, typeid(T).name(), m_config_json.at(key).type_name(), - e.what()); - return default_value; - } - } + // 通用 Get 方法 + template + T get(const std::string& key, const T& default_value) { + std::shared_lock lock(m_mutex); // 读锁 - template void set(const std::string &key, const T &value) { - { - std::unique_lock lock(m_mutex); - m_config_json[key] = value; - spdlog::info("Config updated: [{}] set.", key); - } + if (!m_config_json.contains(key)) { + // debug 级别,避免刷屏 + // spdlog::debug("Config key '{}' not found, using default.", key); + return default_value; + } - save(); + try { + return m_config_json.at(key).get(); + } catch (const json::type_error& e) { + spdlog::warn( + "Config type mismatch for key '{}'. Expected '{}'. Using default. Error: {}", key, + typeid(T).name(), e.what()); + return default_value; + } + } - if (key == "log_level") { - spdlog::set_level(spdlog::level::from_str(value)); - } - } + // 通用 Set 方法 + template + void set(const std::string& key, const T& value) { + { + std::unique_lock lock(m_mutex); // 写锁 + m_config_json[key] = value; + spdlog::info("Config updated in memory: [{}]", key); + } + // 注意:这里 set 后立即 save,但目前没有触发回调(通常回调用于响应外部文件的变化) + // 如果需要 set 也触发回调,可以手动调用 checkDiffAndNotify,但稍微复杂 + save(); + } - std::string getDeviceID(); - std::string getConfigBasePath(); - std::string getMqttBroker(); - std::string getMqttClientID(); - std::string getDataStorageDbPath(); - std::string getDataCacheDbPath(); - std::string getDevicesConfigPath(); - int getWebServerPort(); - std::vector getTcpServerPorts(); - std::string getLogLevel(); - std::string getAlarmRulesPath(); - std::string getPiperExecutablePath(); - std::string getPiperModelPath(); - - // bool getIsVideoServiceEnabled() const; - // std::vector getVideoStreamConfigs() const; - std::string getVideoConfigPath(); - std::string getConfigFilePath() const; + // --- 快捷 Getter (保持你原有的接口不变) --- + std::string getDeviceID(); + std::string getConfigBasePath(); + std::string getMqttBroker(); + std::string getMqttClientID(); + std::string getDataStorageDbPath(); + std::string getDataCacheDbPath(); + std::string getDevicesConfigPath(); + int getWebServerPort(); + std::vector getTcpServerPorts(); + std::string getLogLevel(); + std::string getAlarmRulesPath(); + std::string getPiperExecutablePath(); + std::string getPiperModelPath(); + std::string getVideoConfigPath(); + std::string getConfigFilePath() const; private: - ConfigManager() = default; - ~ConfigManager() = default; - ConfigManager(const ConfigManager &) = delete; - ConfigManager &operator=(const ConfigManager &) = delete; - json createDefaultConfig(); + ConfigManager() = default; + ~ConfigManager() = default; + ConfigManager(const ConfigManager&) = delete; + ConfigManager& operator=(const ConfigManager&) = delete; - bool save_unlocked(); + json createDefaultConfig(); + bool save_unlocked(); // 内部使用的无锁保存 - std::string m_configFilePath; - json m_config_json; - mutable std::shared_mutex m_mutex; -}; \ No newline at end of file + // 检查差异并通知观察者 + void checkDiffAndNotify(const json& old_config, const json& new_config); + + std::string m_configFilePath; + json m_config_json; + + mutable std::shared_mutex m_mutex; // 保护 m_config_json 的读写锁 + + // 观察者相关 + std::mutex m_callbackMutex; // 专门保护 map 的互斥锁 + std::map> m_key_callbacks; +}; + +/* +回调的示例 + config.monitorKey("log_level", [](const json& new_val) { + std::string level = new_val.get(); + spdlog::set_level(spdlog::level::from_str(level)); + spdlog::warn("System Log Level changed to: {}", level); + }); +*/ \ No newline at end of file