mirror of
https://github.com/logos-co/logos-liblogos.git
synced 2026-08-27 12:51:10 +00:00
feat: add logos_core_get_modules_info() for generic module introspection (#159)
* feat: add logos_core_get_modules_info() for generic module introspection New C API returning a JSON array describing every known module — one object each with name, path, loaded flag, direct dependencies, direct dependents, and the full embedded metadata (parsed from the plugin's declarative metadata.json; null when unreadable). ModuleInfo now caches the raw metadata JSON at discovery (via ModuleLib::LogosModule::getRawMetadataJson, no plugin instantiation), ModuleRegistry::allModulesInfo() assembles the array under the registry lock, and ModuleManager exposes it as getModulesInfoJson()/CStr. Methods/events are intentionally excluded: they require instantiating the plugin in-process, which would defeat the subprocess-isolation model for a bulk "all known modules" query. Tests: ModuleManagerTest.GetModulesInfo_* (shape, empty) and RealModuleRegistryTest.GetModulesInfo_PopulatesEmbeddedMetadata (real plugin metadata via TEST_PLUGIN). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: bump logos-module to merged getRawMetadataJson (#21) Re-pin logos-module a3e288a → 2ec64c4 (master, includes #21) so the modules-info API builds against the merged getRawMetadataJson without an override. Full test suite green on this lock (181/181). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat: record module load timestamp (loaded_at) in modules-info Add ModuleInfo::loadedAt — a unix-seconds timestamp stamped by markLoaded (both overloads) and cleared to 0 by markUnloaded, so loaded_at is 0 ⟺ not loaded (reload re-stamps it). Surfaced as "loaded_at" in logos_core_get_modules_info(), letting callers derive a module's uptime as now - loaded_at (valid only while loaded). Tests: GetModulesInfo_ReturnsRichEntryPerModule now asserts loaded_at is 0 for an unloaded module and > 0 for a loaded one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
87ae7ce5db
commit
819faac420
Generated
+3
-3
@@ -1417,11 +1417,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781311603,
|
||||
"narHash": "sha256-Ym3i52f5MWuZy8Qas3+zfoxz+mxQUsyW+Qol1no6kDs=",
|
||||
"lastModified": 1782766905,
|
||||
"narHash": "sha256-oYE7sMr7PS+kDoX//Cx90HmRiRHBnHONx6r4Ekkc7DI=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-module",
|
||||
"rev": "a3e288a71d6f79445db598b0ffcca2ee435596b9",
|
||||
"rev": "2ec64c4a65f8966b5137cdba3a19a6498ce17b6d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -67,6 +67,10 @@ char** logos_core_get_module_dependents(const char* module_name, bool recursive)
|
||||
return ModuleManager::getDependentsCStr(module_name, recursive);
|
||||
}
|
||||
|
||||
char* logos_core_get_modules_info() {
|
||||
return ModuleManager::getModulesInfoCStr();
|
||||
}
|
||||
|
||||
char* logos_core_process_module(const char* module_path) {
|
||||
if (!module_path) { logos::logger("core").critical("logos_core_process_module: module_path must not be null"); std::abort(); }
|
||||
return ModuleManager::processModuleCStr(module_path);
|
||||
|
||||
@@ -70,6 +70,20 @@ LOGOS_CORE_EXPORT char** logos_core_get_module_dependencies(const char* module_n
|
||||
// Returns a null-terminated array of module names that must be freed by the caller.
|
||||
LOGOS_CORE_EXPORT char** logos_core_get_module_dependents(const char* module_name, bool recursive);
|
||||
|
||||
// Get information about all known modules as a JSON string.
|
||||
// Returns a JSON array; each element is an object with:
|
||||
// "name" module name
|
||||
// "path" path to the module binary
|
||||
// "loaded" bool — whether the module is currently loaded
|
||||
// "loaded_at" unix-seconds timestamp of the current load, 0 when not
|
||||
// loaded (callers compute uptime as now - loaded_at)
|
||||
// "dependencies" array of direct dependency names
|
||||
// "dependents" array of direct dependent names
|
||||
// "metadata" the module's full embedded metadata object (name, version,
|
||||
// type, description, dependencies, …), or null if unreadable
|
||||
// The returned string must be freed by the caller.
|
||||
LOGOS_CORE_EXPORT char* logos_core_get_modules_info();
|
||||
|
||||
// Process a module file and add it to known modules
|
||||
// Returns the module name if successful, NULL if failed
|
||||
LOGOS_CORE_EXPORT char* logos_core_process_module(const char* module_path);
|
||||
|
||||
@@ -671,6 +671,17 @@ namespace ModuleManager {
|
||||
getDependents(std::string(name), recursive));
|
||||
}
|
||||
|
||||
std::string getModulesInfoJson() {
|
||||
return registryInstance().allModulesInfo().dump();
|
||||
}
|
||||
|
||||
char* getModulesInfoCStr() {
|
||||
std::string json = getModulesInfoJson();
|
||||
char* result = new char[json.size() + 1];
|
||||
strcpy(result, json.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> computeDerivedAllowedCallers(const std::string& target) {
|
||||
std::lock_guard lock(loadMutex());
|
||||
return computeDerivedAllowedCallersLocked(target);
|
||||
|
||||
@@ -96,6 +96,13 @@ namespace ModuleManager {
|
||||
// array, matching the C API contract used by the other getters.
|
||||
char** getDependenciesCStr(const char* name, bool recursive);
|
||||
char** getDependentsCStr(const char* name, bool recursive);
|
||||
|
||||
// JSON (string) describing every known module: name, path, loaded flag,
|
||||
// direct dependencies, direct dependents, and full embedded metadata.
|
||||
// See ModuleRegistry::allModulesInfo for the shape.
|
||||
std::string getModulesInfoJson();
|
||||
// char* variant. Caller owns the returned string. Never null.
|
||||
char* getModulesInfoCStr();
|
||||
}
|
||||
|
||||
#endif // MODULE_MANAGER_H
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "module_registry.h"
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <cassert>
|
||||
#include <ctime>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <shared_mutex>
|
||||
@@ -165,6 +166,7 @@ std::string ModuleRegistry::processModuleInternal(const std::string& modulePath,
|
||||
// (and any other state that lives on ModuleInfo).
|
||||
ModuleInfo& info = m_modules[name];
|
||||
info.path = modulePath;
|
||||
info.metadataJson = ModuleLib::LogosModule::getRawMetadataJson(modulePath);
|
||||
info.dependencies.clear();
|
||||
for (const auto& d : ModuleLib::LogosModule::getModuleDependencies(modulePath)) {
|
||||
info.dependencies.push_back(d);
|
||||
@@ -184,6 +186,33 @@ std::string ModuleRegistry::modulePath(const std::string& name) const {
|
||||
return it != m_modules.end() ? it->second.path : std::string{};
|
||||
}
|
||||
|
||||
nlohmann::json ModuleRegistry::allModulesInfo() const {
|
||||
std::shared_lock lock(m_mutex);
|
||||
nlohmann::json modules = nlohmann::json::array();
|
||||
for (const auto& [name, info] : m_modules) {
|
||||
nlohmann::json entry;
|
||||
entry["name"] = name;
|
||||
entry["path"] = info.path;
|
||||
entry["loaded"] = info.loaded;
|
||||
// Unix-seconds timestamp of the current load (0 when not loaded).
|
||||
// Callers compute uptime as now - loaded_at while loaded.
|
||||
entry["loaded_at"] = info.loadedAt;
|
||||
entry["dependencies"] = info.dependencies;
|
||||
entry["dependents"] = info.dependents;
|
||||
// Parse the cached metadata JSON back into structured form. Tolerate a
|
||||
// missing/garbled blob by reporting null rather than aborting the call.
|
||||
if (info.metadataJson.empty()) {
|
||||
entry["metadata"] = nlohmann::json(nullptr);
|
||||
} else {
|
||||
nlohmann::json meta = nlohmann::json::parse(
|
||||
info.metadataJson, nullptr, /*allow_exceptions=*/false);
|
||||
entry["metadata"] = meta.is_discarded() ? nlohmann::json(nullptr) : meta;
|
||||
}
|
||||
modules.push_back(std::move(entry));
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
std::vector<std::string> ModuleRegistry::moduleDependencies(const std::string& name,
|
||||
bool recursive) const {
|
||||
std::shared_lock lock(m_mutex);
|
||||
@@ -322,9 +351,18 @@ bool ModuleRegistry::isLoaded(const std::string& name) const {
|
||||
return it != m_modules.end() && it->second.loaded;
|
||||
}
|
||||
|
||||
// Current wall-clock time in unix seconds. Stamped on load so callers can
|
||||
// derive a module's uptime; a free function so both markLoaded overloads
|
||||
// agree on the source.
|
||||
static int64_t nowUnixSeconds() {
|
||||
return static_cast<int64_t>(std::time(nullptr));
|
||||
}
|
||||
|
||||
void ModuleRegistry::markLoaded(const std::string& name) {
|
||||
std::unique_lock lock(m_mutex);
|
||||
m_modules[name].loaded = true;
|
||||
auto& info = m_modules[name];
|
||||
info.loaded = true;
|
||||
info.loadedAt = nowUnixSeconds();
|
||||
}
|
||||
|
||||
void ModuleRegistry::markLoaded(const std::string& name,
|
||||
@@ -333,6 +371,7 @@ void ModuleRegistry::markLoaded(const std::string& name,
|
||||
std::unique_lock lock(m_mutex);
|
||||
auto& info = m_modules[name];
|
||||
info.loaded = true;
|
||||
info.loadedAt = nowUnixSeconds();
|
||||
info.loader = std::move(loader);
|
||||
info.handle = std::move(handle);
|
||||
}
|
||||
@@ -348,8 +387,10 @@ ModuleRegistry::loaderFor(const std::string& name) const {
|
||||
void ModuleRegistry::markUnloaded(const std::string& name) {
|
||||
std::unique_lock lock(m_mutex);
|
||||
auto it = m_modules.find(name);
|
||||
if (it != m_modules.end())
|
||||
if (it != m_modules.end()) {
|
||||
it->second.loaded = false;
|
||||
it->second.loadedAt = 0;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ModuleRegistry::loadedModuleNames() const {
|
||||
|
||||
@@ -23,6 +23,10 @@ bool isValidModuleName(const std::string& name);
|
||||
|
||||
struct ModuleInfo {
|
||||
std::string path;
|
||||
// The module's full embedded metadata as a compact JSON string, read once
|
||||
// at discovery time via ModuleLib::LogosModule (no plugin instantiation).
|
||||
// Empty when the plugin exposes no readable metadata.
|
||||
std::string metadataJson;
|
||||
std::vector<std::string> dependencies;
|
||||
// Direct reverse edges — names of modules whose `dependencies` list
|
||||
// includes this module. Kept in sync with `dependencies` across every
|
||||
@@ -30,6 +34,10 @@ struct ModuleInfo {
|
||||
// directly. Use ModuleRegistry::moduleDependents() for transitive walks.
|
||||
std::vector<std::string> dependents;
|
||||
bool loaded = false;
|
||||
// Unix timestamp (seconds) of the most recent load, set by markLoaded and
|
||||
// cleared to 0 by markUnloaded. 0 ⟺ not currently loaded. Callers derive a
|
||||
// module's uptime from it (now - loadedAt), valid only while loaded.
|
||||
int64_t loadedAt = 0;
|
||||
// Null when loaded directly via markLoaded(name) (test/external scenarios).
|
||||
std::shared_ptr<LogosCore::ModuleLoader> loader;
|
||||
LogosCore::LoadedModuleHandle handle;
|
||||
@@ -46,6 +54,12 @@ public:
|
||||
|
||||
bool isKnown(const std::string& name) const;
|
||||
std::string modulePath(const std::string& name) const;
|
||||
// A JSON array describing every known module: one object per module with
|
||||
// its name, path, loaded flag, load timestamp (loaded_at, unix seconds; 0
|
||||
// when not loaded), direct dependencies, direct dependents, and full
|
||||
// embedded metadata (parsed from the cached metadata JSON; null when
|
||||
// unreadable). This is the data backing logos_core_get_modules_info.
|
||||
nlohmann::json allModulesInfo() const;
|
||||
// Forward-edge accessor. `recursive=false` returns the direct
|
||||
// dependencies stored on ModuleInfo. `recursive=true` walks the forward
|
||||
// graph breadth-first and returns every transitive dependency. Unknown
|
||||
|
||||
@@ -146,6 +146,66 @@ TEST_F(ModuleManagerTest, GetKnownModules_ReturnsCorrectHash) {
|
||||
freeStringArray(result);
|
||||
}
|
||||
|
||||
// logos_core_get_modules_info returns one rich JSON entry per known module:
|
||||
// name, path, loaded flag, direct dependencies, direct dependents, and the
|
||||
// embedded metadata. (Registered fake modules have no plugin file, so their
|
||||
// metadata is null — the real-plugin metadata is covered separately.)
|
||||
TEST_F(ModuleManagerTest, GetModulesInfo_ReturnsRichEntryPerModule) {
|
||||
logos_core_register_module("module_a", "/path/to/module_a.dylib");
|
||||
logos_core_register_module("module_b", "/path/to/module_b.dylib");
|
||||
const char* depsA[] = {"module_b"};
|
||||
logos_core_register_module_dependencies("module_a", depsA, 1);
|
||||
logos_core_mark_module_loaded("module_b");
|
||||
|
||||
char* json = logos_core_get_modules_info();
|
||||
ASSERT_NE(json, nullptr);
|
||||
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
|
||||
free(json);
|
||||
|
||||
ASSERT_TRUE(info.is_array());
|
||||
ASSERT_EQ(info.size(), 2u);
|
||||
|
||||
auto find = [&](const std::string& n) -> nlohmann::json {
|
||||
for (const auto& e : info)
|
||||
if (e.value("name", std::string{}) == n) return e;
|
||||
return nlohmann::json();
|
||||
};
|
||||
|
||||
nlohmann::json a = find("module_a");
|
||||
ASSERT_FALSE(a.is_null());
|
||||
EXPECT_EQ(a.value("path", std::string{}), "/path/to/module_a.dylib");
|
||||
EXPECT_FALSE(a.value("loaded", true));
|
||||
// Not loaded ⇒ loaded_at is 0.
|
||||
EXPECT_EQ(a.value("loaded_at", int64_t{-1}), 0);
|
||||
ASSERT_TRUE(a["dependencies"].is_array());
|
||||
ASSERT_EQ(a["dependencies"].size(), 1u);
|
||||
EXPECT_EQ(a["dependencies"][0].get<std::string>(), "module_b");
|
||||
EXPECT_TRUE(a["dependents"].is_array());
|
||||
EXPECT_TRUE(a["dependents"].empty());
|
||||
// metadata key is always present; null for a registered (un-processed) module.
|
||||
ASSERT_TRUE(a.contains("metadata"));
|
||||
EXPECT_TRUE(a["metadata"].is_null());
|
||||
|
||||
nlohmann::json b = find("module_b");
|
||||
ASSERT_FALSE(b.is_null());
|
||||
EXPECT_TRUE(b.value("loaded", false));
|
||||
// Loaded ⇒ loaded_at is a real timestamp (stamped at markLoaded).
|
||||
EXPECT_GT(b.value("loaded_at", int64_t{0}), 0);
|
||||
// module_a depends on module_b ⇒ module_b lists module_a as a dependent.
|
||||
ASSERT_TRUE(b["dependents"].is_array());
|
||||
ASSERT_EQ(b["dependents"].size(), 1u);
|
||||
EXPECT_EQ(b["dependents"][0].get<std::string>(), "module_a");
|
||||
}
|
||||
|
||||
TEST_F(ModuleManagerTest, GetModulesInfo_EmptyWhenNoModules) {
|
||||
char* json = logos_core_get_modules_info();
|
||||
ASSERT_NE(json, nullptr);
|
||||
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
|
||||
free(json);
|
||||
ASSERT_TRUE(info.is_array());
|
||||
EXPECT_TRUE(info.empty());
|
||||
}
|
||||
|
||||
TEST_F(ModuleManagerTest, IsModuleLoaded_ReturnsFalseForUnloaded) {
|
||||
EXPECT_EQ(logos_core_is_module_loaded("nonexistent_module"), 0);
|
||||
}
|
||||
@@ -626,6 +686,33 @@ TEST_F(RealModuleRegistryTest, ProcessModule_RegistersRealModule) {
|
||||
delete[] name;
|
||||
}
|
||||
|
||||
// For a real plugin, get_modules_info must carry the embedded metadata parsed
|
||||
// straight from the binary (via ModuleLib::LogosModule) — name + version.
|
||||
TEST_F(RealModuleRegistryTest, GetModulesInfo_PopulatesEmbeddedMetadata) {
|
||||
char* name = logos_core_process_module(modulePath.c_str());
|
||||
ASSERT_NE(name, nullptr) << "process_module failed for " << modulePath;
|
||||
std::string moduleName(name);
|
||||
delete[] name;
|
||||
|
||||
char* json = logos_core_get_modules_info();
|
||||
ASSERT_NE(json, nullptr);
|
||||
nlohmann::json info = nlohmann::json::parse(json, nullptr, /*allow_exceptions=*/false);
|
||||
free(json);
|
||||
|
||||
ASSERT_TRUE(info.is_array());
|
||||
nlohmann::json entry;
|
||||
for (const auto& e : info)
|
||||
if (e.value("name", std::string{}) == moduleName) { entry = e; break; }
|
||||
ASSERT_FALSE(entry.is_null()) << "processed module absent from modules-info";
|
||||
|
||||
EXPECT_FALSE(entry.value("path", std::string{}).empty());
|
||||
ASSERT_TRUE(entry["metadata"].is_object())
|
||||
<< "real plugin must yield a non-null metadata object";
|
||||
EXPECT_EQ(entry["metadata"].value("name", std::string{}), moduleName);
|
||||
EXPECT_FALSE(entry["metadata"].value("version", std::string{}).empty())
|
||||
<< "built test modules declare a version in metadata.json";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Security regression: privileged-name impersonation during discovery (F-022).
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user