Files
dpm-core-ng/src/core/context.cpp
Christopher M. Punches c71913ded9 Configuration and logging move out of the context source
context.cpp had grown to hold three unrelated concerns: the context
lifecycle, everything about the .conf file format, and everything about
writing a log message.

conf.cpp now owns the format end to end - parsing a file into the store,
reading values back out, and interpreting a configured string as the
boolean or log level a setting holds. logging.cpp owns the write path:
the level filter, the stream choice, and the name a level carries in
output.

context.cpp keeps what a context is: resolving each setting from the
override, the configured value, or the built-in default, and reporting
what it resolved.
2026-08-18 02:57:47 -04:00

213 lines
7.1 KiB
C++

/**
* @file context.cpp
* @brief Context lifecycle, configuration, and services
*
* @copyright Copyright (c) 2026 SILO GROUP LLC
* @author Chris Punches <chris.punches@silogroup.org>
*
* Part of the Dark Horse Linux Package Manager (DPM)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#include "internal/context.hpp"
#include "internal/conf.hpp"
#include <filesystem>
#include <new>
namespace fs = std::filesystem;
/**
* @brief Helpers private to this translation unit
*
* The built-in defaults and the path normalization applied to the
* directories a context resolves. An unnamed namespace gives them
* internal linkage, so they are unreachable from the library's other
* files and cannot collide with a same-named helper in one of them.
*/
namespace {
/** Configuration directory used when no override is supplied. */
const char* DEFAULT_CONFIG_DIR = "/etc/dpm/conf.d/";
/** Module directory used when neither an override nor configuration names one. */
const char* DEFAULT_MODULE_PATH = "/usr/lib/dpm/modules/";
/** Log file used when configuration enables logging without naming a path. */
const char* DEFAULT_LOG_FILE = "/var/log/dpm/dpm.log";
/**
* @brief Appends a trailing slash when one is absent
*
* Directory paths are stored with a trailing slash so that callers
* concatenating a filename onto them produce a valid path without
* checking the separator themselves.
*
* @param s The path to normalize
* @return The path, ending in a slash unless it is empty
*/
std::string with_trailing_slash(std::string s) {
if (!s.empty() && s.back() != '/') {
s += '/';
}
return s;
}
} // namespace
namespace dpm_core {
/**
* @brief Records a failure reason on the context
*
* Overwrites whatever reason was recorded previously, so the context
* carries the most recent failure and nothing older.
*/
void set_error(dpm_ctx* ctx, const std::string& msg) {
if (ctx) {
ctx->last_error = msg;
}
}
} // namespace dpm_core
extern "C" {
/**
* @brief Creates a context
*
* Reads configuration, resolves the module path and target root, and
* initializes logging. Loads no modules.
*
* Each setting is resolved from the most specific source available.
* The module path takes an override first, then the configured
* value, then the built-in default; logging reads configuration and
* then lets an override replace the level.
*
* Explicit overrides are validated before anything is allocated, so
* a caller that names an unusable path gets NULL rather than a
* context that fails later.
*/
dpm_ctx* dpm_open(const dpm_open_overrides* overrides) {
// Validate explicit overrides before allocating anything.
if (overrides) {
if (overrides->config_dir && *overrides->config_dir) {
std::error_code ec;
if (!fs::is_directory(overrides->config_dir, ec)) {
return nullptr;
}
}
if (overrides->log_level < -1 || overrides->log_level > DPM_LOG_DEBUG) {
return nullptr;
}
}
dpm_ctx* ctx = new (std::nothrow) dpm_ctx;
if (!ctx) {
return nullptr;
}
// Config directory: override, then default.
ctx->config_dir = DEFAULT_CONFIG_DIR;
if (overrides && overrides->config_dir && *overrides->config_dir) {
ctx->config_dir = with_trailing_slash(overrides->config_dir);
}
// Everything below reads configuration, so it loads first.
dpm_core::load_config_dir(ctx);
// Logging: defaults, then configuration, then the override.
ctx->log_level = DPM_LOG_INFO;
ctx->write_to_log = false;
ctx->log_file = DEFAULT_LOG_FILE;
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_level")) {
ctx->log_level = dpm_core::log_level_str2enum(v, DPM_LOG_INFO);
}
if (const char* v = dpm_config_get(ctx, "core", "logging", "write_to_log")) {
ctx->write_to_log = dpm_core::parse_bool(v, false);
}
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_file")) {
ctx->log_file = v;
}
if (overrides && overrides->log_level >= 0) {
ctx->log_level = overrides->log_level;
}
// Module path: default, then configuration, then the override.
ctx->module_path = DEFAULT_MODULE_PATH;
if (const char* v = dpm_config_get(ctx, "core", "modules", "path")) {
ctx->module_path = with_trailing_slash(v);
}
if (overrides && overrides->module_path && *overrides->module_path) {
ctx->module_path = with_trailing_slash(overrides->module_path);
}
// Target root: override, then default.
ctx->root = "/";
if (overrides && overrides->root && *overrides->root) {
ctx->root = overrides->root;
}
return ctx;
}
/**
* @brief Releases a context
*
* Unloads every module the context loaded, then frees the context
* itself. Every handle and string the context issued is invalid
* afterwards.
*/
void dpm_close(dpm_ctx* ctx) {
if (!ctx) {
return;
}
// A dpm_module destructor does not close its handle, so the
// registry is unloaded here and the teardown order stays visible.
for (auto& [name, mod] : ctx->modules) {
if (mod->handle) {
dpm_internal_unload(mod->handle);
}
}
delete ctx;
}
/**
* @brief Reports the module directory this context resolved
*
* The path was fixed when the context opened, so it reflects the
* override, configured value, or default that won at that point.
*/
const char* dpm_module_path(dpm_ctx* ctx) {
if (!ctx) {
return nullptr;
}
return ctx->module_path.c_str();
}
/**
* @brief Reports the most recent failure recorded on the context
*
* The reason is replaced by the next failing call, so it describes
* the most recent failure and nothing earlier. An empty reason reads
* as no failure recorded.
*/
const char* dpm_last_error(dpm_ctx* ctx) {
if (!ctx || ctx->last_error.empty()) {
return nullptr;
}
return ctx->last_error.c_str();
}
} /* extern "C" */