Files
radiantwave/src/config/config_manager.cpp
T
NeilClack da0b6ffd5e Kiosk: apply schedule engine in playback and add System Settings toggle
ScrollerPage now resolves the active Block from ScheduleManager (reusing
the existing stream-switch path), honoring dynamicAudio true/false and
falling back to local selection for gaps, no schedule, or toggle off.
Adds a 'Schedule' On/Off entry (default On) in System Settings persisted
in config.yaml, and config plumbing (proclamation.schedule_enabled). All
on-device schedule times remain 12h AM/PM.
2026-09-29 10:34:45 -05:00

313 lines
9.6 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);
}
if (proc_node.hasChild("schedule_enabled")) {
merged.proclamation.schedule_enabled = proc_node.getChild("schedule_enabled").asBool(defaults.proclamation.schedule_enabled);
}
}
{
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