Document the library's implementation
Every function in context.cpp, modules.cpp, and version.cpp carries a Doxygen block, and the bodies explain the decisions the code alone does not show: why dlerror is cleared before dlsym rather than testing the returned address, what RTLD_NOW and RTLD_LOCAL buy, why missing contract symbols are collected into one report, why a candidate that fails validation is logged and skipped instead of failing the listing, and why version parsing tests the first character itself. Functions declared in a header carry a brief and implementation prose rather than a second parameter list; Doxygen was reporting duplicate documentation sections for each of them. READMEs in include/internal and src/cli state what those directories hold.
This commit is contained in:
7
include/internal/README.md
Normal file
7
include/internal/README.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Internal Headers
|
||||||
|
|
||||||
|
libdpm-core.so's own headers, used by its sources and never installed.
|
||||||
|
|
||||||
|
They declare the types and functions the library's translation units share with each other: the definitions behind the opaque handles `<dpm/core.h>` exposes, and the helpers those sources call across file boundaries.
|
||||||
|
|
||||||
|
Nothing here is part of the public interface. The install rule ships `include/dpm/` alone, so a consumer or a module compiles against `<dpm/core.h>` and never sees this directory.
|
||||||
5
src/cli/README.md
Normal file
5
src/cli/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# CLI
|
||||||
|
|
||||||
|
The `dpm` binary's source.
|
||||||
|
|
||||||
|
It parses arguments and prints. Every capability it offers comes from a module reached through libdpm-core.so, so its subcommand surface is the set of loadable modules rather than a list built into the tool.
|
||||||
@@ -33,20 +33,49 @@
|
|||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
/** Configuration directory used when no override is supplied. */
|
||||||
const char* DEFAULT_CONFIG_DIR = "/etc/dpm/conf.d/";
|
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/";
|
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";
|
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) {
|
std::string trim(const std::string& s) {
|
||||||
const char* ws = " \t\r\n\f\v";
|
const char* ws = " \t\r\n\f\v";
|
||||||
|
|
||||||
size_t start = s.find_first_not_of(ws);
|
size_t start = s.find_first_not_of(ws);
|
||||||
|
|
||||||
|
// Every character is whitespace, so nothing survives trimming.
|
||||||
if (start == std::string::npos) {
|
if (start == std::string::npos) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t end = s.find_last_not_of(ws);
|
size_t end = s.find_last_not_of(ws);
|
||||||
return s.substr(start, end - start + 1);
|
return s.substr(start, end - start + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
std::string with_trailing_slash(std::string s) {
|
||||||
if (!s.empty() && s.back() != '/') {
|
if (!s.empty() && s.back() != '/') {
|
||||||
s += '/';
|
s += '/';
|
||||||
@@ -54,20 +83,44 @@ 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) {
|
bool parse_bool(const std::string& v, bool fallback) {
|
||||||
|
// Compare case-insensitively by folding a copy to lowercase.
|
||||||
std::string lower;
|
std::string lower;
|
||||||
for (char c : v) {
|
for (char c : v) {
|
||||||
lower += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
lower += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") {
|
if (lower == "true" || lower == "yes" || lower == "1" || lower == "on") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (lower == "false" || lower == "no" || lower == "0" || lower == "off") {
|
if (lower == "false" || lower == "no" || lower == "0" || lower == "off") {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fallback;
|
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) {
|
int level_from_string(const std::string& v, int fallback) {
|
||||||
if (v == "FATAL") { return DPM_LOG_FATAL; }
|
if (v == "FATAL") { return DPM_LOG_FATAL; }
|
||||||
if (v == "ERROR") { return DPM_LOG_ERROR; }
|
if (v == "ERROR") { return DPM_LOG_ERROR; }
|
||||||
@@ -77,6 +130,14 @@ namespace {
|
|||||||
return fallback;
|
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) {
|
const char* level_name(int level) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
case DPM_LOG_FATAL: return "FATAL";
|
case DPM_LOG_FATAL: return "FATAL";
|
||||||
@@ -88,6 +149,22 @@ namespace {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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,
|
void parse_config_file(dpm_ctx* ctx, const fs::path& file,
|
||||||
const std::string& module_name) {
|
const std::string& module_name) {
|
||||||
std::ifstream in(file);
|
std::ifstream in(file);
|
||||||
@@ -96,21 +173,26 @@ namespace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
|
|
||||||
|
// Keys read before the first section header belong to "main".
|
||||||
std::string section = "main";
|
std::string section = "main";
|
||||||
|
|
||||||
while (std::getline(in, line)) {
|
while (std::getline(in, line)) {
|
||||||
line = trim(line);
|
line = trim(line);
|
||||||
|
|
||||||
|
// Blank line or comment.
|
||||||
if (line.empty() || line[0] == '#' || line[0] == ';') {
|
if (line.empty() || line[0] == '#' || line[0] == ';') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Section header: everything after it files under this name.
|
||||||
if (line.front() == '[' && line.back() == ']') {
|
if (line.front() == '[' && line.back() == ']') {
|
||||||
std::string name = trim(line.substr(1, line.length() - 2));
|
std::string name = trim(line.substr(1, line.length() - 2));
|
||||||
section = name.empty() ? "main" : name;
|
section = name.empty() ? "main" : name;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anything without a separator is not a key/value pair.
|
||||||
size_t eq = line.find('=');
|
size_t eq = line.find('=');
|
||||||
if (eq == std::string::npos) {
|
if (eq == std::string::npos) {
|
||||||
continue;
|
continue;
|
||||||
@@ -118,6 +200,8 @@ namespace {
|
|||||||
|
|
||||||
std::string key = trim(line.substr(0, eq));
|
std::string key = trim(line.substr(0, eq));
|
||||||
std::string value = trim(line.substr(eq + 1));
|
std::string value = trim(line.substr(eq + 1));
|
||||||
|
|
||||||
|
// A separator with nothing before it names no key.
|
||||||
if (key.empty()) {
|
if (key.empty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -128,12 +212,28 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace dpm_core {
|
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) {
|
void set_error(dpm_ctx* ctx, const std::string& msg) {
|
||||||
if (ctx) {
|
if (ctx) {
|
||||||
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) {
|
void load_config_dir(dpm_ctx* ctx) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!fs::is_directory(ctx->config_dir, ec)) {
|
if (!fs::is_directory(ctx->config_dir, ec)) {
|
||||||
@@ -141,6 +241,7 @@ namespace dpm_core {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) {
|
for (const auto& entry : fs::directory_iterator(ctx->config_dir, ec)) {
|
||||||
|
// Stop on a directory read error rather than iterating further.
|
||||||
if (ec) {
|
if (ec) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -150,14 +251,31 @@ namespace dpm_core {
|
|||||||
if (entry.path().extension() != ".conf") {
|
if (entry.path().extension() != ".conf") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The namespace is the filename without its extension.
|
||||||
parse_config_file(ctx, entry.path(), entry.path().stem().string());
|
parse_config_file(ctx, entry.path(), entry.path().stem().string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace dpm_core
|
} // namespace dpm_core
|
||||||
|
|
||||||
extern "C" {
|
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) {
|
dpm_ctx* dpm_open(const dpm_open_overrides* overrides) {
|
||||||
/* Validate explicit overrides before allocating anything. */
|
// Validate explicit overrides before allocating anything.
|
||||||
if (overrides) {
|
if (overrides) {
|
||||||
if (overrides->config_dir && *overrides->config_dir) {
|
if (overrides->config_dir && *overrides->config_dir) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
@@ -175,15 +293,16 @@ extern "C" {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Config directory: override > default. */
|
// Config directory: override, then default.
|
||||||
ctx->config_dir = DEFAULT_CONFIG_DIR;
|
ctx->config_dir = DEFAULT_CONFIG_DIR;
|
||||||
if (overrides && overrides->config_dir && *overrides->config_dir) {
|
if (overrides && overrides->config_dir && *overrides->config_dir) {
|
||||||
ctx->config_dir = with_trailing_slash(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);
|
dpm_core::load_config_dir(ctx);
|
||||||
|
|
||||||
/* Logging: config, then override. */
|
// Logging: defaults, then configuration, then the override.
|
||||||
ctx->log_level = DPM_LOG_INFO;
|
ctx->log_level = DPM_LOG_INFO;
|
||||||
ctx->write_to_log = false;
|
ctx->write_to_log = false;
|
||||||
ctx->log_file = DEFAULT_LOG_FILE;
|
ctx->log_file = DEFAULT_LOG_FILE;
|
||||||
@@ -201,7 +320,7 @@ extern "C" {
|
|||||||
ctx->log_level = overrides->log_level;
|
ctx->log_level = overrides->log_level;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Module path: override > config > default. */
|
// Module path: default, then configuration, then the override.
|
||||||
ctx->module_path = DEFAULT_MODULE_PATH;
|
ctx->module_path = DEFAULT_MODULE_PATH;
|
||||||
if (const char* v = dpm_config_get(ctx, "core", "modules", "path")) {
|
if (const char* v = dpm_config_get(ctx, "core", "modules", "path")) {
|
||||||
ctx->module_path = with_trailing_slash(v);
|
ctx->module_path = with_trailing_slash(v);
|
||||||
@@ -210,7 +329,7 @@ extern "C" {
|
|||||||
ctx->module_path = with_trailing_slash(overrides->module_path);
|
ctx->module_path = with_trailing_slash(overrides->module_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Target root: override > default. */
|
// Target root: override, then default.
|
||||||
ctx->root = "/";
|
ctx->root = "/";
|
||||||
if (overrides && overrides->root && *overrides->root) {
|
if (overrides && overrides->root && *overrides->root) {
|
||||||
ctx->root = overrides->root;
|
ctx->root = overrides->root;
|
||||||
@@ -219,20 +338,37 @@ extern "C" {
|
|||||||
return ctx;
|
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) {
|
void dpm_close(dpm_ctx* ctx) {
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
/* dpm_module destructors do not dlclose; do it here so the
|
|
||||||
registry teardown order is explicit. */
|
// 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) {
|
for (auto& [name, mod] : ctx->modules) {
|
||||||
if (mod->handle) {
|
if (mod->handle) {
|
||||||
dpm_internal_unload(mod->handle);
|
dpm_internal_unload(mod->handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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* dpm_config_get(dpm_ctx* ctx, const char* module,
|
||||||
const char* section, const char* key) {
|
const char* section, const char* key) {
|
||||||
if (!ctx || !module || !section || !key) {
|
if (!ctx || !module || !section || !key) {
|
||||||
@@ -257,13 +393,24 @@ extern "C" {
|
|||||||
return key_it->second.c_str();
|
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) {
|
void dpm_log(dpm_ctx* ctx, int level, const char* message) {
|
||||||
if (!ctx || !message) {
|
if (!ctx || !message) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A level outside the known set is treated as ordinary reporting.
|
||||||
if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) {
|
if (level < DPM_LOG_FATAL || level > DPM_LOG_DEBUG) {
|
||||||
level = DPM_LOG_INFO;
|
level = DPM_LOG_INFO;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (level > ctx->log_level) {
|
if (level > ctx->log_level) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -284,13 +431,19 @@ extern "C" {
|
|||||||
out << stamp << " [" << level_name(level) << "] "
|
out << stamp << " [" << level_name(level) << "] "
|
||||||
<< message << "\n";
|
<< message << "\n";
|
||||||
} else {
|
} else {
|
||||||
/* Log-file trouble never fails the operation; disable and
|
// Log-file trouble never fails the operation; disable the
|
||||||
continue on console alone. */
|
// file target and carry on writing to the console.
|
||||||
ctx->write_to_log = false;
|
ctx->write_to_log = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
const char* dpm_module_path(dpm_ctx* ctx) {
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -298,6 +451,13 @@ extern "C" {
|
|||||||
return ctx->module_path.c_str();
|
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) {
|
const char* dpm_last_error(dpm_ctx* ctx) {
|
||||||
if (!ctx || ctx->last_error.empty()) {
|
if (!ctx || ctx->last_error.empty()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|||||||
@@ -39,8 +39,13 @@
|
|||||||
|
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
/* dlclose wrapper referenced from context.cpp so handle release stays
|
/**
|
||||||
in one translation unit with the loader. */
|
* @brief Closes a dlopen handle
|
||||||
|
*
|
||||||
|
* Called from context.cpp during context teardown. Handle release lives
|
||||||
|
* here so that dlopen and dlclose stay in the translation unit that owns
|
||||||
|
* the loader.
|
||||||
|
*/
|
||||||
void dpm_internal_unload(void* handle) {
|
void dpm_internal_unload(void* handle) {
|
||||||
if (handle) {
|
if (handle) {
|
||||||
dlclose(handle);
|
dlclose(handle);
|
||||||
@@ -48,13 +53,30 @@ void dpm_internal_unload(void* handle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
/** Signature of a module's command entry point. */
|
||||||
using execute_fn = int (*)(dpm_ctx*, const char*, int, char**);
|
using execute_fn = int (*)(dpm_ctx*, const char*, int, char**);
|
||||||
|
|
||||||
|
/** Signature of a module's version and description probes. */
|
||||||
using string_fn = const char* (*)(void);
|
using string_fn = const char* (*)(void);
|
||||||
|
|
||||||
/** dlsym with dlerror() discipline; returns nullptr on any error. */
|
/**
|
||||||
|
* @brief Resolves a symbol, treating any dlerror as failure
|
||||||
|
*
|
||||||
|
* A symbol can legitimately resolve to a null address, so the return
|
||||||
|
* value alone cannot report failure. dlerror is cleared first and
|
||||||
|
* then checked, which distinguishes the two cases.
|
||||||
|
*
|
||||||
|
* @param handle The dlopen handle to search
|
||||||
|
* @param symbol The symbol name to resolve
|
||||||
|
* @return The symbol's address, or nullptr when resolution failed
|
||||||
|
*/
|
||||||
void* resolve(void* handle, const char* symbol) {
|
void* resolve(void* handle, const char* symbol) {
|
||||||
|
// Clear any error left by an earlier call so the check below
|
||||||
|
// reflects this resolution alone.
|
||||||
dlerror();
|
dlerror();
|
||||||
|
|
||||||
void* addr = dlsym(handle, symbol);
|
void* addr = dlsym(handle, symbol);
|
||||||
|
|
||||||
if (dlerror() != nullptr) {
|
if (dlerror() != nullptr) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@@ -63,9 +85,27 @@ namespace {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace dpm_core {
|
namespace dpm_core {
|
||||||
|
/**
|
||||||
|
* @brief Runs the load-time validation sequence against a module
|
||||||
|
*
|
||||||
|
* Opens the module and verifies the contract in two steps: every
|
||||||
|
* reserved symbol resolves, then the version and description probes
|
||||||
|
* return well-formed values. A module failing either step is closed
|
||||||
|
* and refused, so a caller never receives a partially valid module.
|
||||||
|
*
|
||||||
|
* The symbols are resolved with RTLD_NOW, so a module carrying an
|
||||||
|
* unresolvable dependency fails at dlopen rather than at the first
|
||||||
|
* call into it. RTLD_LOCAL keeps its symbols out of the global
|
||||||
|
* namespace, which is what prevents one module from reaching another
|
||||||
|
* except through this library.
|
||||||
|
*
|
||||||
|
* Every refusal fills `reason` with a description naming what failed,
|
||||||
|
* which reaches the caller through dpm_last_error or the log.
|
||||||
|
*/
|
||||||
std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
|
std::unique_ptr<dpm_module> validate_and_load(dpm_ctx* ctx,
|
||||||
const std::string& name,
|
const std::string& name,
|
||||||
std::string& reason) {
|
std::string& reason) {
|
||||||
|
// A module's name is its filename without the extension.
|
||||||
std::string so_path = ctx->module_path + name + ".so";
|
std::string so_path = ctx->module_path + name + ".so";
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
@@ -81,13 +121,15 @@ namespace dpm_core {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Step 1: resolve all reserved contract symbols. */
|
// Step 1: every reserved contract symbol resolves.
|
||||||
static const char* required[] = {
|
static const char* required[] = {
|
||||||
"dpm_module_execute",
|
"dpm_module_execute",
|
||||||
"dpm_module_version",
|
"dpm_module_version",
|
||||||
"dpm_module_description",
|
"dpm_module_description",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Collect every missing symbol rather than stopping at the first,
|
||||||
|
// so the refusal names the whole gap in one report.
|
||||||
std::string missing;
|
std::string missing;
|
||||||
for (const char* sym : required) {
|
for (const char* sym : required) {
|
||||||
if (!resolve(handle, sym)) {
|
if (!resolve(handle, sym)) {
|
||||||
@@ -107,7 +149,9 @@ namespace dpm_core {
|
|||||||
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
|
auto version_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_version"));
|
||||||
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
|
auto desc_f = reinterpret_cast<string_fn>(resolve(handle, "dpm_module_description"));
|
||||||
|
|
||||||
/* Step 2: probe the cheap calls. */
|
// Step 2: the probes are called immediately, so a module that
|
||||||
|
// resolves its symbols but answers badly is caught here rather
|
||||||
|
// than partway through an operation.
|
||||||
long parsed[3];
|
long parsed[3];
|
||||||
const char* version = version_f();
|
const char* version = version_f();
|
||||||
if (!version || !parse_version(version, parsed)) {
|
if (!version || !parse_version(version, parsed)) {
|
||||||
@@ -115,6 +159,7 @@ namespace dpm_core {
|
|||||||
dlclose(handle);
|
dlclose(handle);
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* description = desc_f();
|
const char* description = desc_f();
|
||||||
if (!description || !*description) {
|
if (!description || !*description) {
|
||||||
reason = "dpm_module_description() returned nothing";
|
reason = "dpm_module_description() returned nothing";
|
||||||
@@ -122,6 +167,8 @@ namespace dpm_core {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validation passed, so the values read above are recorded and
|
||||||
|
// the handle becomes the module's for the life of the context.
|
||||||
auto mod = std::make_unique<dpm_module>();
|
auto mod = std::make_unique<dpm_module>();
|
||||||
mod->name = name;
|
mod->name = name;
|
||||||
mod->handle = handle;
|
mod->handle = handle;
|
||||||
@@ -133,11 +180,23 @@ namespace dpm_core {
|
|||||||
} // namespace dpm_core
|
} // namespace dpm_core
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
/**
|
||||||
|
* @brief Loads and returns a validated module
|
||||||
|
*
|
||||||
|
* A module is loaded at most once per context: the registry is
|
||||||
|
* consulted first, and a module already in it is returned as the same
|
||||||
|
* handle rather than opened a second time.
|
||||||
|
*
|
||||||
|
* A failure records its reason on the context before returning,
|
||||||
|
* leaving the caller a description of what was wrong with the module
|
||||||
|
* it asked for.
|
||||||
|
*/
|
||||||
dpm_module* dpm_require(dpm_ctx* ctx, const char* name) {
|
dpm_module* dpm_require(dpm_ctx* ctx, const char* name) {
|
||||||
if (!ctx || !name || !*name) {
|
if (!ctx || !name || !*name) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Already loaded in this context: hand back the same handle.
|
||||||
auto it = ctx->modules.find(name);
|
auto it = ctx->modules.find(name);
|
||||||
if (it != ctx->modules.end()) {
|
if (it != ctx->modules.end()) {
|
||||||
return it->second.get();
|
return it->second.get();
|
||||||
@@ -151,11 +210,19 @@ extern "C" {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The registry takes ownership; the returned pointer stays valid
|
||||||
|
// until the context closes.
|
||||||
dpm_module* mod = loaded.get();
|
dpm_module* mod = loaded.get();
|
||||||
ctx->modules[name] = std::move(loaded);
|
ctx->modules[name] = std::move(loaded);
|
||||||
return mod;
|
return mod;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Reports what the library read from a module at load
|
||||||
|
*
|
||||||
|
* The strings point into the module's registry entry, so they remain
|
||||||
|
* valid as long as the context holds the module.
|
||||||
|
*/
|
||||||
int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out) {
|
int dpm_module_info_of(dpm_ctx* ctx, dpm_module* mod, dpm_module_info* out) {
|
||||||
if (!ctx || !mod || !out) {
|
if (!ctx || !mod || !out) {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -167,6 +234,13 @@ extern "C" {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Dispatches a command to a module
|
||||||
|
*
|
||||||
|
* Hands the call to the module's entry point and returns what it
|
||||||
|
* returned, adding nothing to the result. This is the only path into
|
||||||
|
* module code.
|
||||||
|
*/
|
||||||
int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command,
|
int dpm_execute(dpm_ctx* ctx, dpm_module* mod, const char* command,
|
||||||
int argc, char** argv) {
|
int argc, char** argv) {
|
||||||
if (!ctx || !mod || !mod->execute) {
|
if (!ctx || !mod || !mod->execute) {
|
||||||
@@ -175,6 +249,17 @@ extern "C" {
|
|||||||
return mod->execute(ctx, command, argc, argv);
|
return mod->execute(ctx, command, argc, argv);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Enumerates the valid modules in the module path
|
||||||
|
*
|
||||||
|
* Scans the directory and validates every candidate, so a module
|
||||||
|
* reaching the cursor is one that can actually be run. A candidate
|
||||||
|
* that fails validation is logged with its reason and left out,
|
||||||
|
* which keeps a broken module from making the whole listing fail.
|
||||||
|
*
|
||||||
|
* Validated modules enter the context's registry as they are found,
|
||||||
|
* so a later require of the same name reuses this load.
|
||||||
|
*/
|
||||||
dpm_cursor* dpm_list_modules(dpm_ctx* ctx) {
|
dpm_cursor* dpm_list_modules(dpm_ctx* ctx) {
|
||||||
if (!ctx) {
|
if (!ctx) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
@@ -187,8 +272,11 @@ extern "C" {
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A set gives the listing a stable, sorted order and collapses a
|
||||||
|
// name reachable both directly and through a symlink.
|
||||||
std::set<std::string> names;
|
std::set<std::string> names;
|
||||||
for (const auto& entry : fs::directory_iterator(ctx->module_path, ec)) {
|
for (const auto& entry : fs::directory_iterator(ctx->module_path, ec)) {
|
||||||
|
// Stop on a directory read error rather than iterating further.
|
||||||
if (ec) {
|
if (ec) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -215,6 +303,9 @@ extern "C" {
|
|||||||
} else {
|
} else {
|
||||||
std::string reason;
|
std::string reason;
|
||||||
auto loaded = dpm_core::validate_and_load(ctx, name, reason);
|
auto loaded = dpm_core::validate_and_load(ctx, name, reason);
|
||||||
|
|
||||||
|
// A candidate that fails validation is reported and
|
||||||
|
// skipped, so the listing shows what can be run.
|
||||||
if (!loaded) {
|
if (!loaded) {
|
||||||
dpm_log(ctx, DPM_LOG_WARN,
|
dpm_log(ctx, DPM_LOG_WARN,
|
||||||
("module '" + name + "' failed validation: " +
|
("module '" + name + "' failed validation: " +
|
||||||
@@ -235,6 +326,13 @@ extern "C" {
|
|||||||
return cur;
|
return cur;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Advances an enumeration cursor
|
||||||
|
*
|
||||||
|
* Copies the next entry out and moves the cursor past it. Exhaustion
|
||||||
|
* and a bad argument both report the same way, so a caller looping
|
||||||
|
* until nonzero terminates in either case.
|
||||||
|
*/
|
||||||
int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out) {
|
int dpm_cursor_next(dpm_cursor* cur, dpm_module_info* out) {
|
||||||
if (!cur || !out || cur->idx >= cur->infos.size()) {
|
if (!cur || !out || cur->idx >= cur->infos.size()) {
|
||||||
return 1;
|
return 1;
|
||||||
@@ -244,6 +342,13 @@ extern "C" {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Releases an enumeration cursor
|
||||||
|
*
|
||||||
|
* The cursor holds copies of the information it reported, so
|
||||||
|
* releasing it leaves the modules themselves loaded and their
|
||||||
|
* strings valid.
|
||||||
|
*/
|
||||||
void dpm_cursor_free(dpm_cursor* cur) {
|
void dpm_cursor_free(dpm_cursor* cur) {
|
||||||
delete cur;
|
delete cur;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,39 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
/** libdpm-core.so's own version. */
|
/**
|
||||||
|
* @brief libdpm-core.so's own version
|
||||||
|
*
|
||||||
|
* The single place the library's version is written. dpm_core_version()
|
||||||
|
* reports it, and the build reads it for the artifact naming.
|
||||||
|
*/
|
||||||
#define DPM_CORE_VERSION_STR "1.0.0"
|
#define DPM_CORE_VERSION_STR "1.0.0"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns libdpm-core.so's own version
|
||||||
|
*
|
||||||
|
* Answers from a compiled-in constant, so it needs no context and is
|
||||||
|
* callable before one is opened. A module reads this to determine for
|
||||||
|
* itself whether it can work with the library it is running against.
|
||||||
|
*/
|
||||||
extern "C" const char* dpm_core_version(void) {
|
extern "C" const char* dpm_core_version(void) {
|
||||||
return DPM_CORE_VERSION_STR;
|
return DPM_CORE_VERSION_STR;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace dpm_core {
|
namespace dpm_core {
|
||||||
|
/**
|
||||||
|
* @brief Parses a strict X.Y.Z version string
|
||||||
|
*
|
||||||
|
* Accepts exactly three decimal components separated by single dots,
|
||||||
|
* with nothing before, between, or after them. Anything looser — a
|
||||||
|
* leading sign, a missing component, trailing text, a fourth
|
||||||
|
* component — is rejected, so a module's reported version is either
|
||||||
|
* a well-formed triple or refused at load.
|
||||||
|
*
|
||||||
|
* Each component is read with strtol, which stops at the first
|
||||||
|
* character it cannot consume; that stopping point is what the
|
||||||
|
* separator checks below examine.
|
||||||
|
*/
|
||||||
bool parse_version(const char* s, long out[3]) {
|
bool parse_version(const char* s, long out[3]) {
|
||||||
if (!s || !*s) {
|
if (!s || !*s) {
|
||||||
return false;
|
return false;
|
||||||
@@ -42,22 +67,31 @@ namespace dpm_core {
|
|||||||
|
|
||||||
const char* p = s;
|
const char* p = s;
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 3; i++) {
|
||||||
|
// strtol would skip leading whitespace and accept a sign, so
|
||||||
|
// the first character is checked directly to reject both.
|
||||||
if (!std::isdigit(static_cast<unsigned char>(*p))) {
|
if (!std::isdigit(static_cast<unsigned char>(*p))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char* end = nullptr;
|
char* end = nullptr;
|
||||||
out[i] = std::strtol(p, &end, 10);
|
out[i] = std::strtol(p, &end, 10);
|
||||||
|
|
||||||
|
// A component that overflowed into a negative value is not a
|
||||||
|
// version number.
|
||||||
if (out[i] < 0) {
|
if (out[i] < 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i < 2) {
|
if (i < 2) {
|
||||||
|
// The first two components are each followed by a dot,
|
||||||
|
// and parsing resumes just past it.
|
||||||
if (*end != '.') {
|
if (*end != '.') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
p = end + 1;
|
p = end + 1;
|
||||||
} else {
|
} else {
|
||||||
|
// The third component ends the string; trailing text
|
||||||
|
// means this is not an X.Y.Z version.
|
||||||
if (*end != '\0') {
|
if (*end != '\0') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user