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.
This commit is contained in:
@@ -21,7 +21,9 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
# libdpm-core.so — the library (C ABI)
|
# libdpm-core.so — the library (C ABI)
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
add_library(dpm-core SHARED
|
add_library(dpm-core SHARED
|
||||||
|
src/core/conf.cpp
|
||||||
src/core/context.cpp
|
src/core/context.cpp
|
||||||
|
src/core/logging.cpp
|
||||||
src/core/modules.cpp
|
src/core/modules.cpp
|
||||||
src/core/version.cpp
|
src/core/version.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
61
include/internal/conf.hpp
Normal file
61
include/internal/conf.hpp
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @file conf.hpp
|
||||||
|
* @brief Reading the .conf files, and interpreting the values they carry
|
||||||
|
*
|
||||||
|
* @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/>.
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct dpm_ctx;
|
||||||
|
|
||||||
|
namespace dpm_core {
|
||||||
|
/**
|
||||||
|
* @brief Reads every .conf file in a context's configuration directory
|
||||||
|
*
|
||||||
|
* Each file becomes one namespace in the store, named after the file
|
||||||
|
* without its extension, so core.conf fills "core" and mymodule.conf
|
||||||
|
* fills "mymodule". Files with any other extension are ignored.
|
||||||
|
*
|
||||||
|
* A missing or unreadable directory leaves the store empty, which
|
||||||
|
* leaves every setting at its default.
|
||||||
|
*
|
||||||
|
* @param ctx The libdpm-core.so context
|
||||||
|
*/
|
||||||
|
void load_config_dir(dpm_ctx* ctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Interprets a configured boolean value
|
||||||
|
*
|
||||||
|
* @param v The configured value
|
||||||
|
* @param fallback Returned when the value matches no known spelling
|
||||||
|
* @return The interpreted boolean, or the fallback
|
||||||
|
*/
|
||||||
|
bool parse_bool(const std::string& v, bool fallback);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Maps a configured log level name to its numeric level
|
||||||
|
*
|
||||||
|
* @param v The level name read from configuration
|
||||||
|
* @param fallback Returned when the name is unrecognized
|
||||||
|
* @return A DPM_LOG_* value, or the fallback
|
||||||
|
*/
|
||||||
|
int log_level_str2enum(const std::string& v, int fallback);
|
||||||
|
} // namespace dpm_core
|
||||||
@@ -90,14 +90,4 @@ namespace dpm_core {
|
|||||||
* @param msg The failure description
|
* @param msg The failure description
|
||||||
*/
|
*/
|
||||||
void set_error(dpm_ctx* ctx, const std::string& msg);
|
void set_error(dpm_ctx* ctx, const std::string& msg);
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Loads all configuration files into the context
|
|
||||||
*
|
|
||||||
* Parses every .conf file in the context's configuration directory
|
|
||||||
* into the context's configuration store.
|
|
||||||
*
|
|
||||||
* @param ctx The libdpm-core.so context
|
|
||||||
*/
|
|
||||||
void load_config_dir(dpm_ctx* ctx);
|
|
||||||
} // namespace dpm_core
|
} // namespace dpm_core
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ namespace {
|
|||||||
* @param name The level name (case-insensitive)
|
* @param name The level name (case-insensitive)
|
||||||
* @return The DPM_LOG_* level, or -1 if unrecognized
|
* @return The DPM_LOG_* level, or -1 if unrecognized
|
||||||
*/
|
*/
|
||||||
int level_from_name(const char* name) {
|
int log_level_str2enum(const char* name) {
|
||||||
if (strcasecmp(name, "FATAL") == 0) { return DPM_LOG_FATAL; }
|
if (strcasecmp(name, "FATAL") == 0) { return DPM_LOG_FATAL; }
|
||||||
if (strcasecmp(name, "ERROR") == 0) { return DPM_LOG_ERROR; }
|
if (strcasecmp(name, "ERROR") == 0) { return DPM_LOG_ERROR; }
|
||||||
if (strcasecmp(name, "WARN") == 0) { return DPM_LOG_WARN; }
|
if (strcasecmp(name, "WARN") == 0) { return DPM_LOG_WARN; }
|
||||||
@@ -202,7 +202,7 @@ int main(int argc, char** argv) {
|
|||||||
} else if (option_matches(arg, "-L", "--log-level", &inline_value)) {
|
} else if (option_matches(arg, "-L", "--log-level", &inline_value)) {
|
||||||
const char* v = take_value();
|
const char* v = take_value();
|
||||||
if (!v) { return 1; }
|
if (!v) { return 1; }
|
||||||
int level = level_from_name(v);
|
int level = log_level_str2enum(v);
|
||||||
if (level < 0) {
|
if (level < 0) {
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"dpm: unknown log level '%s' (use FATAL, ERROR, "
|
"dpm: unknown log level '%s' (use FATAL, ERROR, "
|
||||||
|
|||||||
244
src/core/conf.cpp
Normal file
244
src/core/conf.cpp
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* @file conf.cpp
|
||||||
|
* @brief The .conf file format: reading it, storing it, interpreting it
|
||||||
|
*
|
||||||
|
* Owns everything about configuration files. The format is sectioned
|
||||||
|
* key/value text:
|
||||||
|
*
|
||||||
|
* @code
|
||||||
|
* [logging]
|
||||||
|
* log_level = INFO
|
||||||
|
* write_to_log = false
|
||||||
|
* @endcode
|
||||||
|
*
|
||||||
|
* A line is trimmed, then classified. Empty lines and lines opening with
|
||||||
|
* '#' or ';' are comments. A line wrapped in square brackets opens a
|
||||||
|
* section. Anything else is split at the first '=' into a key and a
|
||||||
|
* value, each trimmed. A line fitting none of those shapes is skipped,
|
||||||
|
* so a malformed line costs that line and no more.
|
||||||
|
*
|
||||||
|
* Values land in the context's store under three keys: the namespace,
|
||||||
|
* which is the file's name without its extension; the section; and the
|
||||||
|
* key. Keys appearing before any section header file under "main".
|
||||||
|
* load_config_dir reads every .conf in the configuration directory, so
|
||||||
|
* core.conf fills the "core" namespace and mymodule.conf fills
|
||||||
|
* "mymodule". dpm_config_get reads back out of it.
|
||||||
|
*
|
||||||
|
* Everything in the store is text, so reading a setting out of it ends
|
||||||
|
* in a conversion. The converters live here beside the parser: each
|
||||||
|
* takes the configured spelling and a fallback, and a value it cannot
|
||||||
|
* interpret yields that fallback rather than a guess.
|
||||||
|
*
|
||||||
|
* @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/conf.hpp"
|
||||||
|
|
||||||
|
#include "internal/context.hpp"
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helpers private to this translation unit
|
||||||
|
*
|
||||||
|
* The line-level work behind parsing. 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 {
|
||||||
|
/**
|
||||||
|
* @brief Strips leading and trailing whitespace
|
||||||
|
*
|
||||||
|
* Applied to every line, section name, key, and value, so that
|
||||||
|
* surrounding spaces and the line's terminator never reach the store.
|
||||||
|
*
|
||||||
|
* @param s The string to trim
|
||||||
|
* @return The trimmed string; empty when the input is entirely whitespace
|
||||||
|
*/
|
||||||
|
std::string trim(const std::string& s) {
|
||||||
|
const char* ws = " \t\r\n\f\v";
|
||||||
|
|
||||||
|
size_t start = s.find_first_not_of(ws);
|
||||||
|
|
||||||
|
// Every character is whitespace, so nothing survives trimming.
|
||||||
|
if (start == std::string::npos) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t end = s.find_last_not_of(ws);
|
||||||
|
return s.substr(start, end - start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reads one configuration file into the store
|
||||||
|
*
|
||||||
|
* A file that cannot be opened contributes nothing and is not an
|
||||||
|
* error: configuration is optional, and defaults cover its absence.
|
||||||
|
*
|
||||||
|
* @param ctx The context whose store receives the values
|
||||||
|
* @param file Path of the file to read
|
||||||
|
* @param module_name The namespace the values are filed under
|
||||||
|
*/
|
||||||
|
void parse_config_file(dpm_ctx* ctx, const fs::path& file,
|
||||||
|
const std::string& module_name) {
|
||||||
|
std::ifstream in(file);
|
||||||
|
if (!in.is_open()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
|
||||||
|
// Keys read before the first section header belong to "main".
|
||||||
|
std::string section = "main";
|
||||||
|
|
||||||
|
while (std::getline(in, line)) {
|
||||||
|
line = trim(line);
|
||||||
|
|
||||||
|
// Blank line or comment.
|
||||||
|
if (line.empty() || line[0] == '#' || line[0] == ';') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Section header: everything after it files under this name.
|
||||||
|
if (line.front() == '[' && line.back() == ']') {
|
||||||
|
std::string name = trim(line.substr(1, line.length() - 2));
|
||||||
|
section = name.empty() ? "main" : name;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anything without a separator is not a key/value pair.
|
||||||
|
size_t eq = line.find('=');
|
||||||
|
if (eq == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string key = trim(line.substr(0, eq));
|
||||||
|
std::string value = trim(line.substr(eq + 1));
|
||||||
|
|
||||||
|
// A separator with nothing before it names no key.
|
||||||
|
if (key.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->config[module_name][section][key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
namespace dpm_core {
|
||||||
|
/**
|
||||||
|
* @brief Interprets a configured boolean value
|
||||||
|
*
|
||||||
|
* Accepts the spellings a configuration file is likely to carry, in
|
||||||
|
* any case. A value that matches none of them leaves the setting at
|
||||||
|
* the caller's fallback rather than guessing.
|
||||||
|
*/
|
||||||
|
bool parse_bool(const std::string& v, bool fallback) {
|
||||||
|
// Compare case-insensitively by folding a copy to lowercase.
|
||||||
|
std::string lower;
|
||||||
|
for (char c : v) {
|
||||||
|
lower += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (lower == "false" || lower == "no" || lower == "0" || lower == "off") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Maps a configured log level name to its numeric level
|
||||||
|
*
|
||||||
|
* The comparison is exact and case-sensitive, matching the spelling
|
||||||
|
* the shipped core.conf uses.
|
||||||
|
*/
|
||||||
|
int log_level_str2enum(const std::string& v, int fallback) {
|
||||||
|
if (v == "FATAL") { return DPM_LOG_FATAL; }
|
||||||
|
if (v == "ERROR") { return DPM_LOG_ERROR; }
|
||||||
|
if (v == "WARN") { return DPM_LOG_WARN; }
|
||||||
|
if (v == "INFO") { return DPM_LOG_INFO; }
|
||||||
|
if (v == "DEBUG") { return DPM_LOG_DEBUG; }
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
void load_config_dir(dpm_ctx* ctx) {
|
||||||
|
std::error_code ec;
|
||||||
|
if (!fs::is_directory(ctx->config_dir, ec)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) {
|
||||||
|
// Stop on a directory read error rather than iterating further.
|
||||||
|
if (ec) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!entry.is_regular_file()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.path().extension() != ".conf") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The namespace is the filename without its extension.
|
||||||
|
parse_config_file(ctx, entry.path(), entry.path().stem().string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace dpm_core
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
/**
|
||||||
|
* @brief Reads a configured value
|
||||||
|
*
|
||||||
|
* Values are addressed by the three levels the store is keyed on: the
|
||||||
|
* namespace a file supplied, a section within it, and a key within
|
||||||
|
* that section. A level that is absent yields NULL, which the caller
|
||||||
|
* reads as unset.
|
||||||
|
*/
|
||||||
|
const char* dpm_config_get(dpm_ctx* ctx, const char* module,
|
||||||
|
const char* section, const char* key) {
|
||||||
|
if (!ctx || !module || !section || !key) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto mod_it = ctx->config.find(module);
|
||||||
|
if (mod_it == ctx->config.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto sec_it = mod_it->second.find(section);
|
||||||
|
if (sec_it == mod_it->second.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto key_it = sec_it->second.find(key);
|
||||||
|
if (key_it == sec_it->second.end()) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key_it->second.c_str();
|
||||||
|
}
|
||||||
|
} /* extern "C" */
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* @file context.cpp
|
* @file context.cpp
|
||||||
* @brief Context lifecycle, configuration, logging, and services
|
* @brief Context lifecycle, configuration, and services
|
||||||
*
|
*
|
||||||
* @copyright Copyright (c) 2026 SILO GROUP LLC
|
* @copyright Copyright (c) 2026 SILO GROUP LLC
|
||||||
* @author Chris Punches <chris.punches@silogroup.org>
|
* @author Chris Punches <chris.punches@silogroup.org>
|
||||||
@@ -22,12 +22,9 @@
|
|||||||
*/
|
*/
|
||||||
#include "internal/context.hpp"
|
#include "internal/context.hpp"
|
||||||
|
|
||||||
#include "internal/version.hpp"
|
#include "internal/conf.hpp"
|
||||||
|
|
||||||
#include <cstdio>
|
|
||||||
#include <ctime>
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <fstream>
|
|
||||||
#include <new>
|
#include <new>
|
||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
@@ -35,10 +32,10 @@ namespace fs = std::filesystem;
|
|||||||
/**
|
/**
|
||||||
* @brief Helpers private to this translation unit
|
* @brief Helpers private to this translation unit
|
||||||
*
|
*
|
||||||
* Configuration reading and the value conversions it needs. An unnamed
|
* The built-in defaults and the path normalization applied to the
|
||||||
* namespace gives them internal linkage, so they are unreachable from
|
* directories a context resolves. An unnamed namespace gives them
|
||||||
* the library's other files and cannot collide with a same-named helper
|
* internal linkage, so they are unreachable from the library's other
|
||||||
* in one of them.
|
* files and cannot collide with a same-named helper in one of them.
|
||||||
*/
|
*/
|
||||||
namespace {
|
namespace {
|
||||||
/** Configuration directory used when no override is supplied. */
|
/** Configuration directory used when no override is supplied. */
|
||||||
@@ -50,30 +47,6 @@ namespace {
|
|||||||
/** Log file used when configuration enables logging without naming a path. */
|
/** Log file used when configuration enables logging without naming a path. */
|
||||||
const char* DEFAULT_LOG_FILE = "/var/log/dpm/dpm.log";
|
const char* DEFAULT_LOG_FILE = "/var/log/dpm/dpm.log";
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Strips leading and trailing whitespace
|
|
||||||
*
|
|
||||||
* Applied to every configuration line, section name, key, and value,
|
|
||||||
* so that surrounding spaces and the line's terminator never reach
|
|
||||||
* the configuration store.
|
|
||||||
*
|
|
||||||
* @param s The string to trim
|
|
||||||
* @return The trimmed string; empty when the input is entirely whitespace
|
|
||||||
*/
|
|
||||||
std::string trim(const std::string& s) {
|
|
||||||
const char* ws = " \t\r\n\f\v";
|
|
||||||
|
|
||||||
size_t start = s.find_first_not_of(ws);
|
|
||||||
|
|
||||||
// Every character is whitespace, so nothing survives trimming.
|
|
||||||
if (start == std::string::npos) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t end = s.find_last_not_of(ws);
|
|
||||||
return s.substr(start, end - start + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Appends a trailing slash when one is absent
|
* @brief Appends a trailing slash when one is absent
|
||||||
*
|
*
|
||||||
@@ -91,132 +64,6 @@ namespace {
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Interprets a configured boolean value
|
|
||||||
*
|
|
||||||
* Accepts the spellings a configuration file is likely to carry, in
|
|
||||||
* any case. A value that matches none of them leaves the setting at
|
|
||||||
* the caller's fallback rather than guessing.
|
|
||||||
*
|
|
||||||
* @param v The configured value
|
|
||||||
* @param fallback Returned when the value matches no known spelling
|
|
||||||
* @return The interpreted boolean, or the fallback
|
|
||||||
*/
|
|
||||||
bool parse_bool(const std::string& v, bool fallback) {
|
|
||||||
// Compare case-insensitively by folding a copy to lowercase.
|
|
||||||
std::string lower;
|
|
||||||
for (char c : v) {
|
|
||||||
lower += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (lower == "false" || lower == "no" || lower == "0" || lower == "off") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Maps a configured log level name to its numeric level
|
|
||||||
*
|
|
||||||
* The comparison is exact and case-sensitive, matching the spelling
|
|
||||||
* the shipped core.conf uses.
|
|
||||||
*
|
|
||||||
* @param v The level name read from configuration
|
|
||||||
* @param fallback Returned when the name is unrecognized
|
|
||||||
* @return A DPM_LOG_* value, or the fallback
|
|
||||||
*/
|
|
||||||
int level_from_string(const std::string& v, int fallback) {
|
|
||||||
if (v == "FATAL") { return DPM_LOG_FATAL; }
|
|
||||||
if (v == "ERROR") { return DPM_LOG_ERROR; }
|
|
||||||
if (v == "WARN") { return DPM_LOG_WARN; }
|
|
||||||
if (v == "INFO") { return DPM_LOG_INFO; }
|
|
||||||
if (v == "DEBUG") { return DPM_LOG_DEBUG; }
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Names a numeric log level for display
|
|
||||||
*
|
|
||||||
* Used to label each message on the console and in the log file.
|
|
||||||
*
|
|
||||||
* @param level A DPM_LOG_* value
|
|
||||||
* @return The level's name, or "UNKNOWN" for a value outside the set
|
|
||||||
*/
|
|
||||||
const char* level_name(int level) {
|
|
||||||
switch (level) {
|
|
||||||
case DPM_LOG_FATAL: return "FATAL";
|
|
||||||
case DPM_LOG_ERROR: return "ERROR";
|
|
||||||
case DPM_LOG_WARN: return "WARN";
|
|
||||||
case DPM_LOG_INFO: return "INFO";
|
|
||||||
case DPM_LOG_DEBUG: return "DEBUG";
|
|
||||||
default: return "UNKNOWN";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Parses one configuration file into the context's store
|
|
||||||
*
|
|
||||||
* Reads a sectioned key/value file. Keys appearing before any
|
|
||||||
* section header belong to the section named "main". Blank lines,
|
|
||||||
* lines opening with '#' or ';', and lines carrying no '=' are
|
|
||||||
* skipped, so a malformed line costs that line alone.
|
|
||||||
*
|
|
||||||
* A file that cannot be opened contributes nothing and is not an
|
|
||||||
* error: configuration is optional, and defaults cover its absence.
|
|
||||||
*
|
|
||||||
* @param ctx The context whose configuration store receives the values
|
|
||||||
* @param file Path of the file to read
|
|
||||||
* @param module_name The namespace the values are filed under, which
|
|
||||||
* is the file's name without its extension
|
|
||||||
*/
|
|
||||||
void parse_config_file(dpm_ctx* ctx, const fs::path& file,
|
|
||||||
const std::string& module_name) {
|
|
||||||
std::ifstream in(file);
|
|
||||||
if (!in.is_open()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string line;
|
|
||||||
|
|
||||||
// Keys read before the first section header belong to "main".
|
|
||||||
std::string section = "main";
|
|
||||||
|
|
||||||
while (std::getline(in, line)) {
|
|
||||||
line = trim(line);
|
|
||||||
|
|
||||||
// Blank line or comment.
|
|
||||||
if (line.empty() || line[0] == '#' || line[0] == ';') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Section header: everything after it files under this name.
|
|
||||||
if (line.front() == '[' && line.back() == ']') {
|
|
||||||
std::string name = trim(line.substr(1, line.length() - 2));
|
|
||||||
section = name.empty() ? "main" : name;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anything without a separator is not a key/value pair.
|
|
||||||
size_t eq = line.find('=');
|
|
||||||
if (eq == std::string::npos) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string key = trim(line.substr(0, eq));
|
|
||||||
std::string value = trim(line.substr(eq + 1));
|
|
||||||
|
|
||||||
// A separator with nothing before it names no key.
|
|
||||||
if (key.empty()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx->config[module_name][section][key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace dpm_core {
|
namespace dpm_core {
|
||||||
@@ -231,39 +78,6 @@ namespace dpm_core {
|
|||||||
ctx->last_error = msg;
|
ctx->last_error = msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Loads every configuration file in the context's config directory
|
|
||||||
*
|
|
||||||
* Each .conf file becomes one configuration namespace named after the
|
|
||||||
* file, so core.conf fills the "core" namespace and mymodule.conf
|
|
||||||
* fills "mymodule". Files with any other extension are ignored.
|
|
||||||
*
|
|
||||||
* A missing or unreadable directory leaves the store empty, which
|
|
||||||
* leaves every setting at its default.
|
|
||||||
*/
|
|
||||||
void load_config_dir(dpm_ctx* ctx) {
|
|
||||||
std::error_code ec;
|
|
||||||
if (!fs::is_directory(ctx->config_dir, ec)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) {
|
|
||||||
// Stop on a directory read error rather than iterating further.
|
|
||||||
if (ec) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!entry.is_regular_file()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (entry.path().extension() != ".conf") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The namespace is the filename without its extension.
|
|
||||||
parse_config_file(ctx, entry.path(), entry.path().stem().string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // namespace dpm_core
|
} // namespace dpm_core
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -316,10 +130,10 @@ extern "C" {
|
|||||||
ctx->log_file = DEFAULT_LOG_FILE;
|
ctx->log_file = DEFAULT_LOG_FILE;
|
||||||
|
|
||||||
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_level")) {
|
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_level")) {
|
||||||
ctx->log_level = level_from_string(v, DPM_LOG_INFO);
|
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")) {
|
if (const char* v = dpm_config_get(ctx, "core", "logging", "write_to_log")) {
|
||||||
ctx->write_to_log = parse_bool(v, false);
|
ctx->write_to_log = dpm_core::parse_bool(v, false);
|
||||||
}
|
}
|
||||||
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_file")) {
|
if (const char* v = dpm_config_get(ctx, "core", "logging", "log_file")) {
|
||||||
ctx->log_file = v;
|
ctx->log_file = v;
|
||||||
@@ -369,83 +183,6 @@ extern "C" {
|
|||||||
delete ctx;
|
delete ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Reads a configured value
|
|
||||||
*
|
|
||||||
* Values are addressed by the three levels the configuration store
|
|
||||||
* is keyed on: the namespace a file supplied, a section within it,
|
|
||||||
* and a key within that section. A level that is absent yields NULL,
|
|
||||||
* which the caller reads as unset.
|
|
||||||
*/
|
|
||||||
const char* dpm_config_get(dpm_ctx* ctx, const char* module,
|
|
||||||
const char* section, const char* key) {
|
|
||||||
if (!ctx || !module || !section || !key) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto mod_it = ctx->config.find(module);
|
|
||||||
if (mod_it == ctx->config.end()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto sec_it = mod_it->second.find(section);
|
|
||||||
if (sec_it == mod_it->second.end()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto key_it = sec_it->second.find(key);
|
|
||||||
if (key_it == sec_it->second.end()) {
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return key_it->second.c_str();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @brief Writes a message to the context's log targets
|
|
||||||
*
|
|
||||||
* Messages at or below the configured level are written; the rest
|
|
||||||
* are dropped. Errors and warnings go to stderr so they survive a
|
|
||||||
* caller redirecting stdout, and the remaining levels go to stdout
|
|
||||||
* as ordinary output.
|
|
||||||
*/
|
|
||||||
void dpm_log(dpm_ctx* ctx, int level, const char* message) {
|
|
||||||
if (!ctx || !message) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A level outside the known set is treated as ordinary reporting.
|
|
||||||
if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) {
|
|
||||||
level = DPM_LOG_INFO;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level > ctx->log_level) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (level <= DPM_LOG_WARN) {
|
|
||||||
std::fprintf(stderr, "%s: %s\n", level_name(level), message);
|
|
||||||
} else {
|
|
||||||
std::fprintf(stdout, "%s\n", message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx->write_to_log) {
|
|
||||||
std::ofstream out(ctx->log_file, std::ios::app);
|
|
||||||
if (out.is_open()) {
|
|
||||||
char stamp[32];
|
|
||||||
std::time_t now = std::time(nullptr);
|
|
||||||
std::strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S",
|
|
||||||
std::localtime(&now));
|
|
||||||
out << stamp << " [" << level_name(level) << "] "
|
|
||||||
<< message << "\n";
|
|
||||||
} else {
|
|
||||||
// Log-file trouble never fails the operation; disable the
|
|
||||||
// file target and carry on writing to the console.
|
|
||||||
ctx->write_to_log = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Reports the module directory this context resolved
|
* @brief Reports the module directory this context resolved
|
||||||
*
|
*
|
||||||
|
|||||||
112
src/core/logging.cpp
Normal file
112
src/core/logging.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
/**
|
||||||
|
* @file logging.cpp
|
||||||
|
* @brief Writing a message to a context's log targets
|
||||||
|
*
|
||||||
|
* A context has two log targets: the console, which is always written,
|
||||||
|
* and a log file, written when configuration enables one. Both carry the
|
||||||
|
* same messages, and a message is written to both or to neither.
|
||||||
|
*
|
||||||
|
* Severity decides two things. A message above the context's configured
|
||||||
|
* level is dropped before either target is touched. A message at or
|
||||||
|
* below it is written, and its level picks the console stream: errors
|
||||||
|
* and warnings go to stderr so they survive a caller redirecting stdout,
|
||||||
|
* and the rest go to stdout as ordinary output.
|
||||||
|
*
|
||||||
|
* @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 <cstdio>
|
||||||
|
#include <ctime>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Helpers private to this translation unit
|
||||||
|
*
|
||||||
|
* The name a log level carries in output. An unnamed namespace gives it
|
||||||
|
* internal linkage, so it is unreachable from the library's other files
|
||||||
|
* and cannot collide with a same-named helper in one of them.
|
||||||
|
*/
|
||||||
|
namespace {
|
||||||
|
/**
|
||||||
|
* @brief Names a numeric log level for display
|
||||||
|
*
|
||||||
|
* Used to label each message on the console and in the log file.
|
||||||
|
*
|
||||||
|
* @param level A DPM_LOG_* value
|
||||||
|
* @return The level's name, or "UNKNOWN" for a value outside the set
|
||||||
|
*/
|
||||||
|
const char* log_level_enum2str(int level) {
|
||||||
|
switch (level) {
|
||||||
|
case DPM_LOG_FATAL: return "FATAL";
|
||||||
|
case DPM_LOG_ERROR: return "ERROR";
|
||||||
|
case DPM_LOG_WARN: return "WARN";
|
||||||
|
case DPM_LOG_INFO: return "INFO";
|
||||||
|
case DPM_LOG_DEBUG: return "DEBUG";
|
||||||
|
default: return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
/**
|
||||||
|
* @brief Writes a message to the context's log targets
|
||||||
|
*
|
||||||
|
* Messages at or below the configured level are written; the rest
|
||||||
|
* are dropped. Errors and warnings go to stderr so they survive a
|
||||||
|
* caller redirecting stdout, and the remaining levels go to stdout
|
||||||
|
* as ordinary output.
|
||||||
|
*/
|
||||||
|
void dpm_log(dpm_ctx* ctx, int level, const char* message) {
|
||||||
|
if (!ctx || !message) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A level outside the known set is treated as ordinary reporting.
|
||||||
|
if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) {
|
||||||
|
level = DPM_LOG_INFO;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level > ctx->log_level) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (level <= DPM_LOG_WARN) {
|
||||||
|
std::fprintf(stderr, "%s: %s\n", log_level_enum2str(level), message);
|
||||||
|
} else {
|
||||||
|
std::fprintf(stdout, "%s\n", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ctx->write_to_log) {
|
||||||
|
std::ofstream out(ctx->log_file, std::ios::app);
|
||||||
|
if (out.is_open()) {
|
||||||
|
char stamp[32];
|
||||||
|
std::time_t now = std::time(nullptr);
|
||||||
|
std::strftime(stamp, sizeof(stamp), "%Y-%m-%d %H:%M:%S",
|
||||||
|
std::localtime(&now));
|
||||||
|
out << stamp << " [" << log_level_enum2str(level) << "] "
|
||||||
|
<< message << "\n";
|
||||||
|
} else {
|
||||||
|
// Log-file trouble never fails the operation; disable the
|
||||||
|
// file target and carry on writing to the console.
|
||||||
|
ctx->write_to_log = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* extern "C" */
|
||||||
Reference in New Issue
Block a user