310 lines
9.4 KiB
C++
310 lines
9.4 KiB
C++
#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);
|
|
}
|
|
if (proc_node.hasChild("schedule_active")) {
|
|
merged.proclamation.schedule_active = proc_node.getChild("schedule_active").asBool(defaults.proclamation.schedule_active);
|
|
}
|
|
}
|
|
|
|
{
|
|
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
|