Modules can report why they failed

A module had one channel back to its caller: the int returned from
dpm_module_execute, handed through by dpm_execute. Any detail behind
that number could only reach a log, so a caller wanting the reason had
to read output rather than ask for it.

dpm_set_last_error joins the exported API. A module records its reason
on the context handed to its entry point, which belongs to the caller,
and the caller reads it back with dpm_get_last_error.

The two accessors on the context now say what they do to it:
dpm_module_path becomes dpm_get_resolved_module_path, naming the value
it reports rather than the setting it came from, and dpm_last_error
becomes dpm_get_last_error, pairing with the setter.

Path normalization moves to sanitizers.cpp, which holds the conversions
that put a value written by a person into the single form the library
stores it in.
This commit is contained in:
2026-08-19 00:49:42 -04:00
parent c71913ded9
commit 50d71ca55f
14 changed files with 162 additions and 58 deletions

View File

@@ -54,7 +54,7 @@ The root override is what makes chroot builds, image assembly, and sysroot manag
dpm_module* mod = dpm_require(ctx, "mymodule");
```
libdpm-core.so validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent or invalid — `dpm_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle.
libdpm-core.so validates the module completely at load; a handle is returned only for a fully valid module. NULL means the module is absent or invalid — `dpm_get_last_error`(ctx) carries the precise reason. Modules load at most once per context; repeated calls return the same handle.
`dpm_module_info_of` reports what the library saw in the loaded module:
@@ -94,12 +94,13 @@ The cursor covers every valid module in the module path; invalid candidates are
- `dpm_module_info_of`(ctx, mod, out)** — the name, version, and description read from a loaded module.
- `dpm_config_get`(ctx, module, section, key)** — a value from a module's configuration namespace, or NULL if unset.
- `dpm_log`(ctx, level, message)** — writes to the context's configured log targets; levels are `DPM_LOG_FATAL` through `DPM_LOG_DEBUG`.
- `dpm_module_path`(ctx)** — the resolved module directory.
- `dpm_last_error`(ctx)** — a human-readable description of the most recent failure on the context, or NULL.
- `dpm_get_resolved_module_path`(ctx)** — the resolved module directory.
- `dpm_set_last_error`(ctx, msg) — records a failure reason on the context; what a module calls to explain a nonzero return.
- `dpm_get_last_error`(ctx)** — a human-readable description of the most recent failure on the context, or NULL.
## Ownership and Errors
Strings returned by the library are owned by the context (or by the module that produced them) and remain valid until `dpm_close`; callers never free them. Functions returning int use 0 for success. Functions returning pointers use NULL for failure, with detail available from `dpm_last_error`.
Strings returned by the library are owned by the context (or by the module that produced them) and remain valid until `dpm_close`; callers never free them. Functions returning int use 0 for success. Functions returning pointers use NULL for failure, with detail available from `dpm_get_last_error`.
## Complete Example
@@ -116,7 +117,7 @@ int main(void) {
dpm_module* mod = dpm_require(ctx, "info");
if (!mod) {
fprintf(stderr, "%s\n", dpm_last_error(ctx));
fprintf(stderr, "%s\n", dpm_get_last_error(ctx));
dpm_close(ctx);
return 1;
}

View File

@@ -92,10 +92,13 @@ Returns the configured value for `key` in `section` of the named module's config
`void dpm_log(dpm_ctx* ctx, int level, const char* message)`
Writes `message` at `level` (FATAL=0, ERROR=1, WARN=2, INFO=3, DEBUG=4) to the context's configured log targets (console and/or file). Messages above the configured level are dropped. NULL message is a no-op.
`const char* dpm_module_path(dpm_ctx* ctx)`
`const char* dpm_get_resolved_module_path(dpm_ctx* ctx)`
Returns the resolved module directory path for this context.
`const char* dpm_last_error(dpm_ctx* ctx)`
`void dpm_set_last_error(dpm_ctx* ctx, const char* msg)`
Records `msg` as the most recent failure on this context; the string is copied. A module explains a nonzero return by recording the reason here, using the context handed to its entry point — the caller's own — so that the caller reads back what the module wrote. The context holds one reason at a time.
`const char* dpm_get_last_error(dpm_ctx* ctx)`
Returns a human-readable description of the most recent failure recorded on this context, or NULL if none. Overwritten by the next failing call on the same context.
## Module Contract
@@ -103,7 +106,7 @@ Returns a human-readable description of the most recent failure recorded on this
A module is one .so in the module directory. It includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following reserved symbols as extern "C". Returned strings are static or module-owned, non-NULL, and valid for the lifetime of the loaded module; the library and consumers never free them.
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
The module's command entry point, and the only entry through which it performs work. `ctx` is the host context that dispatched the call — the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
The module's command entry point, and the only entry through which it performs work. `ctx` is the host context that dispatched the call — the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_get_resolved_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name (equal to argv[0] when argc > 0); argc/argv are the remaining CLI-style arguments. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
`const char* dpm_module_version(void)`
Returns the module's own version as an X.Y.Z string. Must be constant for the life of the module. This is the value the library reports to consumers, and the value they judge compatibility against.

View File

@@ -7,7 +7,7 @@ A DPM module is one shared object in the module directory. libdpm-core.so loads
A module includes `<dpm/core.h>`, links `-ldpm-core`, and exports the following symbols as extern "C". All returned strings must be non-NULL, static or module-owned, and valid for the lifetime of the loaded module; callers never free them.
`int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)`
The command entry point, and the only entry through which the module performs work. `ctx` is the host context that dispatched the call; the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name, equal to argv[0] when argc > 0. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. It must be callable immediately after load with no other setup.
The command entry point, and the only entry through which the module performs work. `ctx` is the host context that dispatched the call; the module reaches every service (`dpm_log`, `dpm_config_get`, `dpm_get_resolved_module_path`, ...) through it, and reaches peer modules through it as well. `command` is the subcommand name, equal to argv[0] when argc > 0. NULL or empty `command` must behave as the module's help command. Returns 0 on success, nonzero on failure. Where a nonzero return needs explaining, record the reason with `dpm_set_last_error` immediately before returning, and the caller reads it back with `dpm_get_last_error`. It must be callable immediately after load with no other setup.
`const char* dpm_module_version(void)`
The module's own version as an X.Y.Z string. libdpm-core.so reports this value to consumers, and each consumer decides for itself whether the version suits it.
@@ -48,7 +48,7 @@ int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)
{
dpm_module* peer = dpm_require(ctx, "othermodule");
if (!peer) {
dpm_log(ctx, DPM_LOG_ERROR, dpm_last_error(ctx));
dpm_log(ctx, DPM_LOG_ERROR, dpm_get_last_error(ctx));
return 1;
}

View File

@@ -51,7 +51,7 @@ The target root override is what makes chroot builds, image assembly, and sysroo
The context owns everything it hands out. Every string a caller receives stays valid until `dpm_close`, and callers never free anything.
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_last_error` carrying the precise reason. Modules load at most once per context, and repeated calls return the same handle.
**Acquiring a module.** `dpm_require` resolves a module by name, validates it completely, and returns a handle — or returns NULL, with `dpm_get_last_error` carrying the precise reason. Modules load at most once per context, and repeated calls return the same handle.
**Reading what the library saw.** `dpm_module_info_of` fills in a module's name, version, and description. Those values are reported, and no conclusion is drawn from them.
@@ -59,7 +59,7 @@ The context owns everything it hands out. Every string a caller receives stays v
**Enumerating.** `dpm_list_modules` yields a cursor over every valid module, which is what backs the listing the `dpm` binary prints.
**Services.** A module reaches the library through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_module_path` to learn where modules live, `dpm_core_version` to learn the library's version. A module needs no file handling and no logging machinery of its own.
**Services.** A module reaches the library through the context that dispatched the call: `dpm_log` to write a message, `dpm_config_get` to read a value from its own configuration namespace, `dpm_get_resolved_module_path` to learn where modules live, `dpm_core_version` to learn the library's version, `dpm_set_last_error` to record why it failed. A module needs no file handling and no logging machinery of its own.
## How a Module Reaches Another Module
@@ -70,7 +70,7 @@ int dpm_module_execute(dpm_ctx* ctx, const char* command, int argc, char** argv)
{
dpm_module* peer = dpm_require(ctx, "othermodule");
if (!peer) {
dpm_log(ctx, DPM_LOG_ERROR, dpm_last_error(ctx));
dpm_log(ctx, DPM_LOG_ERROR, dpm_get_last_error(ctx));
return 1;
}
return dpm_execute(ctx, peer, "somecommand", argc, argv);