initial commit
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
#include "config_manager.h"
|
||||
#include "yaml_parser.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <pwd.h>
|
||||
#include <sstream>
|
||||
#include <unistd.h>
|
||||
|
||||
namespace radiantwave::config {
|
||||
|
||||
// ─── Constructor & Destructor ──────────────────────────────────────────
|
||||
|
||||
ConfigManager::ConfigManager() = default;
|
||||
|
||||
ConfigManager::~ConfigManager() {
|
||||
shutdown();
|
||||
}
|
||||
|
||||
// ─── Path Resolution ───────────────────────────────────────────────────
|
||||
|
||||
std::string ConfigManager::resolvePath(const std::string& raw_path) const {
|
||||
std::string path = raw_path;
|
||||
|
||||
if (path.empty()) {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) {
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (home) {
|
||||
path = std::string(home) + "/.local/radiantwave/config.yaml";
|
||||
} else {
|
||||
path = "/tmp/radiantwave/config.yaml";
|
||||
}
|
||||
} else if (path.front() == '~') {
|
||||
const char* home = std::getenv("HOME");
|
||||
if (!home) {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
if (pw) {
|
||||
home = pw->pw_dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (home) {
|
||||
path = std::string(home) + path.substr(1);
|
||||
}
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
// ─── Initialization ────────────────────────────────────────────────────
|
||||
|
||||
bool ConfigManager::initialize(const std::string& custom_path) {
|
||||
file_path_ = resolvePath(custom_path);
|
||||
|
||||
// Create parent directories if they don't exist
|
||||
try {
|
||||
std::filesystem::path p(file_path_);
|
||||
if (p.has_parent_path()) {
|
||||
std::filesystem::create_directories(p.parent_path());
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "[Config] Failed to create directories: " << e.what() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load and merge defaults without overwriting existing settings
|
||||
loadAndMergeDefaults();
|
||||
|
||||
// Store initial modification timestamp
|
||||
try {
|
||||
if (std::filesystem::exists(file_path_)) {
|
||||
last_write_time_ = std::filesystem::last_write_time(file_path_);
|
||||
}
|
||||
} catch (...) {}
|
||||
|
||||
// Start live hot-reload background watcher
|
||||
startWatcher();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigManager::shutdown() {
|
||||
stopWatcher();
|
||||
}
|
||||
|
||||
// ─── Read / Write Operations ───────────────────────────────────────────
|
||||
|
||||
AppConfig ConfigManager::get() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return config_;
|
||||
}
|
||||
|
||||
bool ConfigManager::update(std::function<void(AppConfig& config)> modifier) {
|
||||
std::vector<ConfigCallback> callbacks_to_notify;
|
||||
AppConfig updated_config;
|
||||
bool success = false;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
modifier(config_);
|
||||
updated_config = config_;
|
||||
success = saveUnlocked();
|
||||
}
|
||||
|
||||
if (success) {
|
||||
std::lock_guard<std::mutex> cb_lock(callback_mutex_);
|
||||
callbacks_to_notify = callbacks_;
|
||||
}
|
||||
|
||||
for (const auto& cb : callbacks_to_notify) {
|
||||
if (cb) cb(updated_config);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
bool ConfigManager::save() {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return saveUnlocked();
|
||||
}
|
||||
|
||||
bool ConfigManager::saveUnlocked() {
|
||||
std::string content = serializeConfigToYaml(config_);
|
||||
|
||||
std::ofstream file(file_path_, std::ios::out | std::ios::trunc);
|
||||
if (!file.is_open()) {
|
||||
std::cerr << "[Config] Failed to open file for writing: " << file_path_ << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
file << content;
|
||||
file.close();
|
||||
|
||||
// Update timestamp so watcher doesn't trigger a duplicate hot-reload event
|
||||
try {
|
||||
if (std::filesystem::exists(file_path_)) {
|
||||
last_write_time_ = std::filesystem::last_write_time(file_path_);
|
||||
}
|
||||
} catch (...) {}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ConfigManager::reload() {
|
||||
std::ifstream file(file_path_);
|
||||
if (!file.is_open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << file.rdbuf();
|
||||
file.close();
|
||||
|
||||
AppConfig loaded = parseConfigFromYaml(ss.str());
|
||||
|
||||
std::vector<ConfigCallback> callbacks_to_notify;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_ = loaded;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> cb_lock(callback_mutex_);
|
||||
callbacks_to_notify = callbacks_;
|
||||
}
|
||||
|
||||
for (const auto& cb : callbacks_to_notify) {
|
||||
if (cb) cb(loaded);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ConfigManager::loadAndMergeDefaults() {
|
||||
AppConfig defaults = AppConfig::defaults();
|
||||
|
||||
if (!std::filesystem::exists(file_path_)) {
|
||||
// File doesn't exist: write full defaults
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_ = defaults;
|
||||
saveUnlocked();
|
||||
return;
|
||||
}
|
||||
|
||||
// Read existing file
|
||||
std::ifstream file(file_path_);
|
||||
if (!file.is_open()) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_ = defaults;
|
||||
return;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << file.rdbuf();
|
||||
file.close();
|
||||
|
||||
std::string yaml_content = ss.str();
|
||||
YamlNode root = parseYaml(yaml_content);
|
||||
|
||||
AppConfig merged = defaults;
|
||||
|
||||
// Preserving existing values if present
|
||||
if (root.hasChild("license_key") && !root.getChild("license_key").asString().empty()) {
|
||||
merged.license_key = root.getChild("license_key").asString();
|
||||
}
|
||||
if (root.hasChild("email_address") && !root.getChild("email_address").asString().empty()) {
|
||||
merged.email_address = root.getChild("email_address").asString();
|
||||
}
|
||||
if (root.hasChild("provisioning_status") && !root.getChild("provisioning_status").asString().empty()) {
|
||||
merged.provisioning_status = root.getChild("provisioning_status").asString();
|
||||
}
|
||||
if (root.hasChild("timezone") && !root.getChild("timezone").asString().empty()) {
|
||||
merged.timezone = root.getChild("timezone").asString();
|
||||
}
|
||||
|
||||
if (root.hasChild("audio")) {
|
||||
const auto& audio_node = root.getChild("audio");
|
||||
if (audio_node.hasChild("enabled")) {
|
||||
merged.audio.enabled = audio_node.getChild("enabled").asBool(defaults.audio.enabled);
|
||||
}
|
||||
if (audio_node.hasChild("volume")) {
|
||||
merged.audio.volume = audio_node.getChild("volume").asInt(defaults.audio.volume);
|
||||
}
|
||||
if (audio_node.hasChild("output_device")) {
|
||||
merged.audio.output_device = audio_node.getChild("output_device").asString(defaults.audio.output_device);
|
||||
}
|
||||
if (audio_node.hasChild("current_track")) {
|
||||
merged.audio.current_track = audio_node.getChild("current_track").asString(defaults.audio.current_track);
|
||||
}
|
||||
if (audio_node.hasChild("dynamic_audio")) {
|
||||
merged.audio.dynamic_audio = audio_node.getChild("dynamic_audio").asBool(defaults.audio.dynamic_audio);
|
||||
}
|
||||
}
|
||||
|
||||
if (root.hasChild("proclamation")) {
|
||||
const auto& proc_node = root.getChild("proclamation");
|
||||
if (proc_node.hasChild("scroll_speed")) {
|
||||
merged.proclamation.scroll_speed = proc_node.getChild("scroll_speed").asFloat(defaults.proclamation.scroll_speed);
|
||||
}
|
||||
if (proc_node.hasChild("font_size")) {
|
||||
merged.proclamation.font_size = proc_node.getChild("font_size").asInt(defaults.proclamation.font_size);
|
||||
}
|
||||
if (proc_node.hasChild("current_id")) {
|
||||
merged.proclamation.current_id = proc_node.getChild("current_id").asString(defaults.proclamation.current_id);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
config_ = merged;
|
||||
saveUnlocked();
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Live Hot-Reloading Watcher ───────────────────────────────────────
|
||||
|
||||
void ConfigManager::onChanged(ConfigCallback callback) {
|
||||
std::lock_guard<std::mutex> lock(callback_mutex_);
|
||||
callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
std::string ConfigManager::getFilePath() const {
|
||||
return file_path_;
|
||||
}
|
||||
|
||||
void ConfigManager::startWatcher() {
|
||||
stopWatcher();
|
||||
watcher_running_ = true;
|
||||
|
||||
watcher_thread_ = std::thread([this]() {
|
||||
while (watcher_running_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
|
||||
if (!watcher_running_) break;
|
||||
|
||||
try {
|
||||
if (std::filesystem::exists(file_path_)) {
|
||||
auto current_time = std::filesystem::last_write_time(file_path_);
|
||||
if (current_time != last_write_time_) {
|
||||
last_write_time_ = current_time;
|
||||
std::cout << "[Config] Detected live change in " << file_path_ << ", hot-reloading..." << std::endl;
|
||||
reload();
|
||||
}
|
||||
}
|
||||
} catch (...) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void ConfigManager::stopWatcher() {
|
||||
if (watcher_running_) {
|
||||
watcher_running_ = false;
|
||||
if (watcher_thread_.joinable()) {
|
||||
watcher_thread_.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace radiantwave::config
|
||||
Reference in New Issue
Block a user