full module runtime test

This commit is contained in:
Iuri Matias
2026-04-17 09:40:36 -04:00
parent e96b05b657
commit 5d0b1de701
32 changed files with 1455 additions and 342 deletions
+101 -51
View File
@@ -14,27 +14,33 @@ logos-liblogos/
│ └── project.md # This document
├── src/
│ ├── CMakeLists.txt # Source build configuration
│ ├── logos_core/ # Core library implementation
│ ├── logos_core/ # Core library implementation (Qt-free)
│ │ ├── logos_core.h # C API header (public)
│ │ ├── logos_core.cpp # C API implementation
│ │ ├── plugin_manager.h/cpp # Facade: orchestrates registry, launcher, resolver
│ │ ├── plugin_manager.h/cpp # Facade: orchestrates registry, runtime, resolver
│ │ ├── plugin_registry.h/cpp # In-memory registry of discovered/loaded modules
│ │ ├── plugin_launcher.h/cpp # Spawns and manages logos_host subprocesses
│ │ ├── dependency_resolver.h/cpp # Topological sort with circular dependency detection
│ │ └── process_manager.h/cpp # Boost.Process-based subprocess management
│ └── logos_host/ # Module subprocess host
── logos_host.cpp # Host entry point
├── command_line_parser.h/cpp # CLI argument parsing (--name, --path)
├── plugin_initializer.h/cpp # Plugin loading and token setup
└── qt/ # Qt-specific host implementations
├── qt_app.h/cpp # Qt application setup for host
└── qt_token_receiver.h/cpp # Auth token reception via local socket
│ │ ├── module_runtime.h # Abstract ModuleRuntime interface + ModuleDescriptor
│ │ ├── runtime_registry.h/cpp # Registry for ModuleRuntime implementations
│ │ └── dependency_resolver.h/cpp # Topological sort with circular dependency detection
│ └── runtimes/ # Runtime implementations (extensible)
── qt_subprocess/ # Qt-based subprocess runtime (current default)
├── qt_subprocess_runtime.h/cpp # ModuleRuntime impl: spawns logos_host processes
├── subprocess_manager.h/cpp # Boost.Process-based subprocess management
└── host/ # logos_host executable sources
├── main.cpp # Host entry point
├── command_line_parser.h/cpp # CLI argument parsing (--name, --path)
│ ├── plugin_initializer.h/cpp # Plugin loading and token setup
│ └── qt/ # Qt-specific host implementations
│ ├── qt_app.h/cpp # Qt application setup for host
│ └── qt_token_receiver.h/cpp # Auth token reception via local socket
├── tests/ # Google Test suite
│ ├── CMakeLists.txt # Test build configuration
│ ├── test_app_lifecycle.cpp # C API lifecycle tests (init, exec, cleanup, processEvents)
│ ├── test_app_lifecycle.cpp # C API lifecycle tests (init, exec, cleanup)
│ ├── test_plugin_manager.cpp # PluginManager + PluginRegistry tests
│ ├── test_process_manager.cpp # ProcessManager lifecycle and subprocess tests
│ ├── test_dependency_resolver.cpp # DependencyResolver tests
│ ├── test_subprocess_manager.cpp # SubprocessManager lifecycle and subprocess tests
│ ├── test_runtime_registry.cpp # RuntimeRegistry: registration, selection, fan-out
│ ├── test_module_runtime_abstraction.cpp # PluginManager ↔ ModuleRuntime routing tests
│ ├── test_dependency_resolver.cpp # DependencyResolver tests
│ ├── test_process_stats.cpp # ProcessStats tests (external process-stats lib)
│ ├── test_token_exchange.cpp # Token exchange via Unix domain socket tests
│ └── qt_test_adapter.h # Qt test utilities/adapter header
@@ -83,7 +89,7 @@ logos-liblogos/
**Files:** `src/logos_core/plugin_manager.h`, `src/logos_core/plugin_manager.cpp`
**Purpose:** Thin facade that orchestrates `PluginRegistry`, `PluginLauncher`, and `DependencyResolver`. Provides the C++-level API for module lifecycle management. Each module runs in a separate `logos_host` process for isolation.
**Purpose:** Thin facade that orchestrates `PluginRegistry`, `RuntimeRegistry`, and `DependencyResolver`. Provides the C++-level API for module lifecycle management. How a module is loaded (subprocess, in-process, WASM, etc.) is determined by the registered `ModuleRuntime` implementation — `PluginManager` only decides *what* to load and *when*.
**Thread safety:** `loadPlugin`, `loadPluginWithDependencies`, and `unloadPlugin` are serialised by a static `loadMutex()` (one load/unload at a time). `discoverInstalledModules` delegates to `PluginRegistry` which has its own reader-writer lock.
@@ -92,28 +98,29 @@ logos-liblogos/
| Method | Description |
|--------|-------------|
| `registry() → PluginRegistry&` | Access the shared plugin registry |
| `runtimes() → RuntimeRegistry&` | Access the shared runtime registry (for installing custom runtimes) |
| `setPluginsDir(path)` | Set the primary plugin directory (clears existing) |
| `addPluginsDir(path)` | Add an additional plugin directory |
| `setPersistenceBasePath(path)` | Set base directory for module instance persistence |
| `discoverInstalledModules()` | Scan all plugin directories and register discovered modules |
| `processPlugin(path) → std::string` | Extract metadata from a module file, register as known |
| `processPluginCStr(path) → char*` | C-string variant of processPlugin |
| `loadPlugin(name) → bool` | Load a module (spawns `logos_host` process, sends auth token) |
| `loadPlugin(name) → bool` | Load a module via the selected `ModuleRuntime`, sends auth token |
| `loadPluginWithDependencies(name) → bool` | Resolve dependency tree, load in topological order |
| `initializeCapabilityModule() → bool` | Load the built-in capability module if available |
| `unloadPlugin(name) → bool` | Terminate module process and update registry |
| `unloadPlugin(name) → bool` | Terminate module via its runtime and update registry |
| `unloadPluginWithDependents(name) → bool` | Cascade unload: terminate the named module together with every currently loaded module that transitively depends on it, leaves-first |
| `terminateAll()` | Terminate all running module processes |
| `terminateAll()` | Terminate all running modules across all runtimes |
| `clear()` | Clear registry and reset all state |
| `resolveDependencies(modules) → std::vector<std::string>` | Topological sort with circular dependency detection |
| `getDependencies(name, recursive) → std::vector<std::string>` | Declared dependencies of `name` among known modules; walks the forward graph transitively when `recursive=true`. Cycle- and diamond-safe BFS |
| `getDependents(name, recursive) → std::vector<std::string>` | Declared dependents of `name` among known modules; walks the reverse graph transitively when `recursive=true`. Reads from the in-process registry, no disk query |
| `getDependencies(name, recursive) → std::vector<std::string>` | Declared dependencies of `name` among known modules |
| `getDependents(name, recursive) → std::vector<std::string>` | Declared dependents of `name` among known modules |
| `getDependenciesCStr(name, recursive) → char**` | C-string variant backing `logos_core_get_module_dependencies` |
| `getDependentsCStr(name, recursive) → char**` | C-string variant backing `logos_core_get_module_dependents` |
| `getLoadedPluginsCStr() → char**` | Return loaded module names as null-terminated C string array |
| `getKnownPluginsCStr() → char**` | Return known module names as null-terminated C string array |
| `isPluginLoaded(name) → bool` | Check if a module is currently loaded |
| `getPluginProcessIds() → std::unordered_map<std::string, int64_t>` | Return module name → process ID mappings |
| `getPluginProcessIds() → std::unordered_map<std::string, int64_t>` | Return module name → process ID mappings (aggregated from all runtimes) |
### PluginRegistry
@@ -122,7 +129,7 @@ logos-liblogos/
**Purpose:** In-memory registry of discovered and loaded modules. Single source of truth for the dependency graph: stores plugin paths, forward dependencies, and the derived reverse edges (dependents). All public methods are thread-safe: mutating methods acquire a `std::unique_lock` on an internal `std::shared_mutex`; read-only methods acquire a `std::shared_lock`, allowing concurrent reads.
**Data:**
- `PluginInfo` struct — holds `path`, `dependencies` (`std::vector<std::string>`), `dependents` (`std::vector<std::string>`, reverse-edge cache), `loaded` flag
- `PluginInfo` struct — holds `path`, `dependencies`, `dependents` (reverse-edge cache), `loaded` flag, `std::shared_ptr<LogosCore::ModuleRuntime> runtime` (runtime that loaded this module, nullptr for externally-marked), `LogosCore::LoadedModuleHandle handle` (runtime-private state)
- `std::unordered_map<std::string, PluginInfo> m_plugins` — plugin database keyed by name
- `std::vector<std::string> m_pluginsDirs` — configured plugin directories
- `std::shared_mutex m_mutex` — reader-writer lock protecting all fields
@@ -146,57 +153,82 @@ logos-liblogos/
| `pluginDependents(name, recursive) → std::vector<std::string>` | Reverse-edge lookup. `recursive=false` returns direct dependents from `PluginInfo`; `recursive=true` walks the reverse graph breadth-first (cycle/diamond safe) |
| `knownPluginNames() → std::vector<std::string>` | All discovered module names |
| `isLoaded(name) → bool` | Plugin is currently running |
| `markLoaded(name)` / `markUnloaded(name)` | Update load state |
| `markLoaded(name)` / `markUnloaded(name)` | Update load state (markLoaded without runtime is for test/external use) |
| `markLoaded(name, runtime, handle)` | Mark as loaded and record which runtime is responsible |
| `runtimeFor(name) → shared_ptr<ModuleRuntime>` | Return the runtime that loaded this module (or nullptr) |
| `loadedPluginNames() → std::vector<std::string>` | Currently running module names |
| `clearLoaded()` | Clear all loaded state |
| `clear()` | Reset entire registry |
### PluginLauncher
### ModuleRuntime (Abstract Interface)
**Files:** `src/logos_core/plugin_launcher.h`, `src/logos_core/plugin_launcher.cpp`
**File:** `src/logos_core/module_runtime.h`
**Purpose:** Spawn and manage module subprocesses. Delegates to the process manager (Boost.Process v2 / Boost.Asio) for subprocess operations.
**Purpose:** Qt-free abstract interface for module loading strategies. Decouples `PluginManager` from any specific module format, isolation mechanism, or transport layer. Each implementation decides *how* a module is loaded, isolated, and communicated with.
**API (namespace `PluginLauncher`):**
**Data structures:**
- `ModuleDescriptor` — describes a module to load: `name`, `path`, `format` (`"qt-plugin"`, `"wasm"`, etc.), `dependencies`, `instancePersistencePath`, `pluginsDirs`, `rawMetadata` (JSON), `runtimeConfig` (JSON, optional, e.g. `{"id":"docker","image":"..."}`)
- `LoadedModuleHandle` — returned by `load()`: `name`, `pid` (-1 for non-process runtimes), `endpoint` (transport-specific URI), `opaque` (`std::any` for runtime-private state)
**Interface (`LogosCore::ModuleRuntime`):**
| Method | Description |
|--------|-------------|
| `launch(name, path, dirs, instancePersistencePath, onTerminated) → bool` | Spawn `logos_host` process for a module |
| `sendToken(name, token) → bool` | Send auth token to module process via stdin |
| `terminate(name)` | Kill a specific module process |
| `terminateAll()` | Kill all module processes |
| `hasProcess(name) → bool` | Check if a process exists for this module |
| `getAllProcessIds() → std::unordered_map<std::string, int64_t>` | Map module names to process IDs |
| `id() → std::string` | Unique runtime identifier (e.g. `"qt-subprocess"`, `"inproc"`, `"extism"`) |
| `canHandle(desc) → bool` | Return true if this runtime can load the described module |
| `load(desc, onTerminated, out) → bool` | Load the module; populate `out`, call `onTerminated` when it exits |
| `sendToken(name, token) → bool` | Deliver the auth token to the loaded module |
| `terminate(name)` | Terminate a single module by name |
| `terminateAll()` | Terminate all modules managed by this runtime |
| `hasModule(name) → bool` | True if this runtime has an active entry for the module |
| `pid(name) → optional<int64_t>` | PID of the module (default: nullopt for non-process runtimes) |
| `getAllPids() → unordered_map` | All (name → pid) entries (default: empty) |
### DependencyResolver
### RuntimeRegistry
**Files:** `src/logos_core/dependency_resolver.h`, `src/logos_core/dependency_resolver.cpp`
**Files:** `src/logos_core/runtime_registry.h`, `src/logos_core/runtime_registry.cpp`
**Purpose:** Compute topological sort of module dependencies using Kahn's algorithm. Detects circular dependencies and missing modules.
**Purpose:** Holds all registered `ModuleRuntime` instances and selects the right one for a given `ModuleDescriptor`. Thread-safe (protected by an internal mutex).
**API (namespace `DependencyResolver`):**
**Selection order:**
1. If `desc.runtimeConfig["id"]` is set, return the matching runtime by id (no fallback).
2. Otherwise, return the first registered runtime whose `canHandle(desc)` returns true.
3. Returns `nullptr` if no runtime matches.
**API (class `LogosCore::RuntimeRegistry`):**
| Method | Description |
|--------|-------------|
| `resolve(requested, isKnown, getDependencies) → std::vector<std::string>` | Returns modules in load order (dependencies first) |
| `registerRuntime(runtime)` | Add a runtime; consulted in registration order for `canHandle` |
| `select(desc) → shared_ptr<ModuleRuntime>` | Pick the right runtime for a descriptor |
| `terminateAll()` | Fan-out `terminateAll()` to every registered runtime |
| `getAllPids() → unordered_map` | Aggregate `getAllPids()` across all runtimes |
| `clearForTests()` | Remove all runtimes (testing hook for installing fakes) |
Takes callback functions (`IsKnownFn`, `GetDependenciesFn`) so it has no coupling to the registry implementation.
### QtSubprocessRuntime
### Process Manager
**Files:** `src/runtimes/qt_subprocess/qt_subprocess_runtime.h`, `src/runtimes/qt_subprocess/qt_subprocess_runtime.cpp`
**Files:** `src/logos_core/process_manager.h`, `src/logos_core/process_manager.cpp`
**Purpose:** Concrete `ModuleRuntime` implementation for the existing Qt-subprocess strategy. Spawns one `logos_host` process per module, delivers the auth token via a Unix-domain socket, and communicates via Qt Remote Objects. Registered as the default runtime.
**Purpose:** Manages module subprocesses using Boost.Process v2 and Boost.Asio. Replaces the former Qt-based `QProcess` implementation.
**id:** `"qt-subprocess"`
- Uses `boost::process::v2::process` for subprocess spawning and `boost::asio::io_context` for async I/O
- Background `io_context` thread with work guard for non-blocking async read and wait callbacks
**canHandle:** Accepts `format == "qt-plugin"` or empty format.
### SubprocessManager
**Files:** `src/runtimes/qt_subprocess/subprocess_manager.h`, `src/runtimes/qt_subprocess/subprocess_manager.cpp`
**Purpose:** Manages module subprocesses using Boost.Process v2 and Boost.Asio. Used internally by `QtSubprocessRuntime`.
- Background `io_context` thread with work guard for non-blocking async I/O
- Async read loop for stdout/stderr with line buffering
- Synchronous kill with graceful SIGTERM → SIGKILL escalation (5s timeout)
- Unix domain socket for token delivery (matches previous `QLocalSocket` behavior)
- A `std::mutex` (`s_processesMutex`) protects the `s_processes` map against concurrent access
- Shared pointer-based lifetime management for safe async callback handling
- Unix domain socket for token delivery
- Thread-safe `s_processesMutex` protects the process map
**API (namespace `QtProcessManager`):**
**API (namespace `SubprocessManager`):**
| Method | Description |
|--------|-------------|
@@ -210,6 +242,24 @@ Takes callback functions (`IsKnownFn`, `GetDependenciesFn`) so it has no couplin
| `registerProcess(name)` | Register a placeholder process entry |
| `clearAll()` | Clear all process entries |
### LogosHost
**Files:** `src/runtimes/qt_subprocess/host/main.cpp`, `src/runtimes/qt_subprocess/host/command_line_parser.h/cpp`, `src/runtimes/qt_subprocess/host/plugin_initializer.h/cpp`, `src/runtimes/qt_subprocess/host/qt/qt_app.h/cpp`, `src/runtimes/qt_subprocess/host/qt/qt_token_receiver.h/cpp`
**Purpose:** Lightweight subprocess that loads a single module. Part of the `qt_subprocess` runtime implementation. Parses `--name`, `--path`, and optional `--instance-persistence-path` arguments, loads the plugin, authenticates via token from the core, registers the module with the remote object registry, and runs the Qt event loop.
**Files:** `src/logos_core/dependency_resolver.h`, `src/logos_core/dependency_resolver.cpp`
**Purpose:** Compute topological sort of module dependencies using Kahn's algorithm. Detects circular dependencies and missing modules.
**API (namespace `DependencyResolver`):**
| Method | Description |
|--------|-------------|
| `resolve(requested, isKnown, getDependencies) → std::vector<std::string>` | Returns modules in load order (dependencies first) |
Takes callback functions (`IsKnownFn`, `GetDependenciesFn`) so it has no coupling to the registry implementation.
### ProcessStats (external dependency)
**Source:** [process-stats](https://github.com/logos-co/process-stats) library (linked as a static dependency)
@@ -225,9 +275,9 @@ Takes callback functions (`IsKnownFn`, `GetDependenciesFn`) so it has no couplin
### LogosHost
**Files:** `src/logos_host/logos_host.cpp`, `src/logos_host/command_line_parser.h/cpp`, `src/logos_host/plugin_initializer.h/cpp`, `src/logos_host/qt/qt_app.h/cpp`, `src/logos_host/qt/qt_token_receiver.h/cpp`
**Files:** `src/runtimes/qt_subprocess/host/main.cpp` (and sibling files under `src/runtimes/qt_subprocess/host/`)
**Purpose:** Lightweight subprocess that loads a single module. Parses `--name`, `--path`, and optional `--instance-persistence-path` arguments, loads the plugin, authenticates via token from the core, registers the module with the remote object registry, and runs the Qt event loop.
**Purpose:** Lightweight subprocess that loads a single module. Part of the `qt_subprocess` runtime. Parses `--name`, `--path`, and optional `--instance-persistence-path` arguments, loads the plugin, authenticates via token from the core, registers the module with the remote object registry, and runs the Qt event loop. Lives under `src/runtimes/qt_subprocess/host/` so it is co-located with its runtime implementation.
## C API
+12 -13
View File
@@ -43,7 +43,7 @@ At a high level, the Logos Core consists of:
### Process Architecture
Each module runs in its own process for isolation:
Each module runs in its own process for isolation (using the default `qt-subprocess` runtime):
```
┌─────────────────────────────────────────────────┐
@@ -64,10 +64,10 @@ Each module runs in its own process for isolation:
└─────────────────────────────────────────────────┘
```
- The core spawns a `logos_host` process per module
- The core spawns a `logos_host` process per module via the `QtSubprocessRuntime`
- Communication happens via the Logos API (currently uses Qt Remote Objects over local sockets)
- Faulty or untrusted modules cannot crash the core or other modules
- Modules can be written in different languages as long as they implement the RPC protocol
- The `ModuleRuntime` abstraction allows swapping the loading strategy per-module: in-process, WASM (Extism), Docker, gRPC, etc.
### Token-Based Authentication
@@ -109,14 +109,12 @@ Every module ships a `metadata.json` referenced by Qt's `Q_PLUGIN_METADATA` macr
1. Core locates the plugin file for the requested module name
2. Core resolves dependencies and loads them first (topological sort with circular dependency detection)
3. If a persistence base path is configured, core resolves an instance ID and persistence directory for the module (reusing an existing instance or creating a new one)
4. Core spawns a `logos_host` process with the plugin path and instance persistence path
5. Core generates a UUID authentication token
6. Core sends the token to the host process via local socket
7. Host process loads the plugin and calls `initLogos(LogosAPI*)`
8. The `LogosAPI` instance exposes `modulePath`, `instanceId`, and `instancePersistencePath` as properties
9. Host process registers the plugin with the remote object registry
10. Core waits for registration and records the module as loaded
3. If a persistence base path is configured, core resolves an instance ID and persistence directory for the module
4. Core builds a `ModuleDescriptor` and selects the appropriate `ModuleRuntime` via `RuntimeRegistry`
5. The selected runtime's `load()` spawns (or otherwise starts) the module and returns a `LoadedModuleHandle`
6. Core generates a UUID authentication token and calls `runtime->sendToken()`
7. For the default `qt-subprocess` runtime: `logos_host` process loads the plugin, calls `initLogos(LogosAPI*)`, and registers with the remote object registry
8. Core records the module as loaded, storing the runtime and handle in `PluginRegistry`
#### Unloading
@@ -235,5 +233,6 @@ The SDK abstracts away registry lookup, token management, and async invocation.
## Future Work
- **Signature support** — Signing and verifying module packages
- **Cross-language modules** — Modules in languages other than C++
- **Move away from Qt** — Logos API will move away from Qt. Process management has been migrated from Qt (`QProcess`) to Boost.Process v2, and Qt container/utility types (`QString`, `QStringList`, `QHash`, `QDir`, `QFile`, `QUuid`) have been replaced with standard C++ and Boost equivalents (`std::string`, `std::vector`, `std::unordered_map`, `std::filesystem`, `boost::uuids`). Remaining Qt dependencies (event loop, plugin loading, remote objects) should be similarly abstracted from liblogos's perspective.
- **Cross-language modules** — Modules in languages other than C++ (enabled by `ModuleRuntime` abstraction)
- **Alternative runtimes** — In-process loading, Extism/WASM modules, Docker container isolation, gRPC transport (all implementable as new `ModuleRuntime` subclasses)
- **Move away from Qt** — Logos API will move away from Qt. `logos_core`'s internal code (`PluginManager`, `RuntimeRegistry`, `ModuleRuntime`) is already Qt-free. The `qt_subprocess` runtime and `logos_host` encapsulate the remaining Qt dependencies (event loop, plugin loading, Qt Remote Objects). New runtimes can avoid Qt entirely.
+190
View File
@@ -0,0 +1,190 @@
# Plan: Pluggable Module Runtimes in `logos-liblogos`
This document describes how to abstract module loading and transport so `logos_core` orchestrates **modules** without hard-coding Qt plugins, subprocess + `logos_host`, or Qt Remote Objects over Unix sockets.
## 1. What is coupled today
The core currently hard-wires three concerns together:
| Concern | Where | Coupling |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Discovery / metadata** | `PluginRegistry` (`plugin_registry.cpp`) | Uses `ModuleLib::LogosModule::getModuleName` / `getModuleDependencies`, which open the binary with `QPluginLoader` (Qt plugin format is baked in). |
| **Isolation strategy** | `PluginLauncher::launch``QtProcessManager::startProcess` | Always spawns a `logos_host` subprocess via Boost.Process. |
| **Transport** | `logos_host` (token receiver, `plugin_initializer` + `LogosAPIProvider`) + token delivery over Unix domain socket | Registry + Qt Remote Objects is implicit everywhere. |
| **Module ABI** | `logos_host` calls `module.as<PluginInterface>()` and expects a `QObject` with `Q_INVOKABLE` methods | Only Qt/C++ plugins work. |
`PluginManager::loadPluginInternal` performs a single linear recipe: generate token → `PluginLauncher::launch``sendToken``markLoaded` → notify capability module. That recipe should become polymorphic.
## 2. Proposed architecture: `ModuleRuntime` abstraction
Introduce a single extension point — a `**ModuleRuntime`** — and reduce `PluginManager` to a registry of runtimes plus orchestrator. Analogy: containerd → runc / kata / gvisor: the **core** decides **what** to load and **when**; the **runtime** decides **how**.
### 2.1 The interface
```cpp
// src/logos_core/module_runtime.h (conceptual)
namespace LogosCore {
struct ModuleDescriptor {
std::string name;
std::string path; // file or directory; meaning depends on runtime
std::string format; // "qt-plugin" | "extism-wasm" | "native-lib" | ...
std::vector<std::string> dependencies;
std::string instancePersistencePath;
std::vector<std::string> pluginsDirs;
nlohmann::json rawMetadata; // from manifest.json / embedded metadata
nlohmann::json runtimeConfig; // free-form, e.g. docker image, grpc endpoint
};
struct LoadedModuleHandle {
std::string name;
int64_t pid = -1; // -1 when not a process (in-proc, wasm, remote)
std::string endpoint; // transport-specific: unix:///..., grpc://host:port, inproc://<id>
std::any opaque; // runtime-private state
};
class ModuleRuntime {
public:
virtual ~ModuleRuntime() = default;
// Identity + capabilities
virtual std::string id() const = 0; // "qt-subprocess", "extism", "inproc", "docker"
virtual bool canHandle(const ModuleDescriptor&) const = 0;
// Lifecycle (must be idempotent per name)
virtual bool load(const ModuleDescriptor&,
std::function<void(const std::string&)> onTerminated,
LoadedModuleHandle& out) = 0;
virtual bool sendToken(const std::string& name, const std::string& token) = 0;
virtual void terminate(const std::string& name) = 0;
virtual void terminateAll() = 0;
// Optional introspection
virtual std::optional<int64_t> pid(const std::string& name) const { return std::nullopt; }
virtual std::string endpoint(const std::string& name) const { return {}; }
};
} // namespace LogosCore
```
### 2.2 The registry / selector
```cpp
// src/logos_core/runtime_registry.h (conceptual)
class RuntimeRegistry {
public:
void registerRuntime(std::shared_ptr<ModuleRuntime>);
// Selection priority: explicit override in manifest > format match > first canHandle
std::shared_ptr<ModuleRuntime> select(const ModuleDescriptor&) const;
// Load discovery of dynamic runtime plugins (e.g. liblogos-runtime-extism.so)
void loadRuntimePlugin(const std::string& soPath);
};
```
Runtimes can be implemented as shared libraries with a single C entry point:
```cpp
extern "C" LOGOS_RUNTIME_EXPORT
LogosCore::ModuleRuntime* logos_runtime_create(const LogosCore::RuntimeHostServices*);
```
`RuntimeHostServices` gives a runtime back-references it may need (e.g. `LogosAPI` for the `core` identity, `TokenManager`, logging, registry access — everything todays `notifyCapabilityModule` uses).
### 2.3 `PluginManager` becomes transport-agnostic
`loadPluginInternal` collapses to:
1. Build `ModuleDescriptor` from registry (manifest + embedded metadata merged).
2. `auto runtime = runtimes.select(desc);` — fail if none.
3. `LoadedModuleHandle h;``runtime->load(desc, onTerminated, h)`.
4. Generate token; `runtime->sendToken(name, token)` — on failure, `runtime->terminate(name)` and return false.
5. `registry.markLoaded(name, h)` — registry stores handle + owning runtime.
6. `TokenManager::instance().saveToken(name, token);`
7. `notifyCapabilityModule(name, token);`
`PluginRegistry` keeps both the runtime reference and `LoadedModuleHandle` per loaded module so `unloadPlugin`, `getPluginProcessIds`, and stats routing know who owns what.
### 2.4 Mapping requested variants to runtimes
| Variant | Runtime impl | Notes |
| ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| **Status quo**`logos_host` subprocess + QtRO over Unix socket | `QtSubprocessRuntime` (wraps todays `PluginLauncher` + `QtProcessManager`) | No behavioral change; default for `format: "qt-plugin"`. |
| **In-process** | `InProcRuntime``dlopen` + instantiate `PluginInterface` in the host, register with local `LogosAPIProvider` using **local transport** (SDK already has `implementations/qt_local/local_transport.`*). | `pid = getpid()`, `endpoint = "inproc://<module>"`. Token delivery can short-circuit via `TokenManager` inside `sendToken`. |
| **WASM (Extism)** | `ExtismRuntime` — loads `.wasm`, drives via host functions; shim exposes exports so the rest of the core sees a consistent invocation surface (bridge implementing whatever abstract `LogosObject`-style API you standardize on). | No QObject. Metadata from sidecar `manifest.json`, not only `QPluginLoader`. |
| **Different transport (gRPC)** | `GrpcSubprocessRuntime` — spawns a host binary (could be `logos_host` built with gRPC transport, or language-specific host), reads `endpoint` from stdout or config, registers with SDK transport. | SDK already has `LogosTransport` (`qt_remote`, `qt_local`, `mock`); add `grpc` and wire from runtime. |
| **Docker** | `DockerRuntime``docker run` (or libdocker) with image from `runtimeConfig`, mount token socket or expose port; container ID as logical “pid” or map via `docker inspect` for stats. | Same `ModuleRuntime` interface; different `load` implementation. |
### 2.5 How a module picks its runtime
Two mechanisms, layered:
1. **Per-module override** in `manifest.json` (authoritative example):
```json
{
"name": "my_module",
"runtime": {
"id": "docker",
"config": { "image": "ghcr.io/me/my_module:1.2.3", "transport": "grpc" }
}
}
```
2. **Format sniff** when `runtime` is absent: file extension (`.wasm` → extism, `.so`/`.dylib` with Qt plugin metadata → qt-subprocess), then first runtime whose `canHandle` returns true.
A **core-wide policy** (env var or `logos_core_set_default_runtime`) can force e.g. “everything in-process” for tests or “everything in docker” in production — expressed as an ordered list of runtime IDs consulted before `canHandle`.
### 2.6 Where todays code moves
| Today | Tomorrow |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `plugin_launcher.{h,cpp}` | Logic moves into `runtimes/qt_subprocess/qt_subprocess_runtime.{h,cpp}` (or equivalent path). |
| `process_manager.{h,cpp}` (`QtProcessManager` namespace) | Rename to something like `SubprocessManager`; keep as shared utility for any runtime that spawns OS processes (qt-subprocess, docker, grpc subprocess). |
| `logos_host/*` | Stays; owned conceptually by the qt-subprocess runtime. Other runtimes ship their own host binary (or none). |
| `plugin_manager.cpp` — `notifyCapabilityModule`, orchestration | Stays in `PluginManager`; no runtime-specific branching beyond `select` + virtual calls. |
| `PluginRegistry` + `ModuleLib::LogosModule::getModuleName` | Pluggable manifest reader and/or Qt metadata as **fallback**; primary source can become `manifest.json` fields including `format` / `runtime`. |
### 2.7 Capability module / token flow
`notifyCapabilityModule` can stay largely as-is: it already uses `LogosAPI` / `LogosAPIClient`, which sit on the SDKs transport abstraction. As long as each runtime registers the module with the same provider model the capability module expects, inter-module auth keeps working. **The SDK already abstracted transport; `logos-liblogos` is the main place still assuming “subprocess + QtRO”.**
## 3. Suggested rollout (green at every step)
1. **Introduce `ModuleRuntime` + `RuntimeRegistry`**, ship `QtSubprocessRuntime` as a thin wrapper around todays `PluginLauncher` / process manager. Wire `PluginManager` through it only. **No behavior change.**
2. **Teach `PluginRegistry` to read `manifest.json` as primary** (if applicable to your package layout) and keep Qt-plugin metadata as fallback; add `runtime` / `format` fields (ignored until non-default runtimes exist).
3. **Add `InProcRuntime`** behind manifest opt-in or env override — validates second runtime + local transport; good for tests and trusted built-ins.
4. **Rename `QtProcessManager` → `SubprocessManager`** (or similar) and document as shared subprocess utility.
5. **Add `ExtismRuntime`** as optional shared library (`liblogos-runtime-extism.so`) loaded via `RuntimeRegistry::loadRuntimePlugin` — keeps wasm/extism deps out of core.
6. **Add `GrpcSubprocessRuntime` + gRPC `LogosTransport` in logos-cpp-sdk** — proves transport swap end-to-end.
7. **Add `DockerRuntime`** once subprocess + optional SO loading paths are stable.
## 4. Risks and design notes
- **Token exchange is runtime-specific** (Unix socket today in `qt_token_receiver` / `QtProcessManager::sendToken`). Each runtime owns end-to-end token delivery; avoid forcing one mechanism on all.
- **Stats** (`ProcessStats::getModuleStats`) assumes PIDs. Widen to module handles: `(pid-or-0, endpoint, runtime_id)` so UI/ops can show in-process, container ID, etc.
- **Event loop.** Qt subprocess runtime relies on Qt in `logos_host`. In-proc needs a Qt event loop in the host process where applicable. The runtime contract should state which runtimes require a loop and whether the host must pump it.
- `**logos_core_exec()` is currently a no-op** in this repo — acceptable; runtimes that need a loop either use the embedding apps loop or an internal thread. Avoid leaking “must pump events” into the C API unless necessary.
- **Keep `ModuleRuntime` headers Qt-free** so optional runtimes do not pull Qt into the abstraction layer.
- **Discovery namespace:** multiple formats can collide on `name`. Treat manifest `name` as authoritative; on-disk artifact is opaque to the selected runtime.
## 5. Minimal first commit (concrete starting point)
Zero behavior change, three conceptual additions:
1. `src/logos_core/module_runtime.h` — interface + `ModuleDescriptor` / `LoadedModuleHandle`.
2. `src/logos_core/runtime_registry.{h,cpp}` — register runtimes, `select(descriptor)`.
3. `src/logos_core/runtimes/qt_subprocess_runtime.{h,cpp}` — move current `PluginLauncher` logic here; `id()` returns `"qt-subprocess"`; `canHandle` true for `format == "qt-plugin"` or empty format (default).
Then change `plugin_manager.cpp` to call `runtime->load` / `sendToken` / `terminate` instead of `PluginLauncher::`* directly. Downstream (`logos_host`, `PluginRegistry`, C API, tests) unchanged. Every further runtime is additive.
## 6. Goals recap
- `**logos_core` abstracts** discovery, load order, tokens, capability notification, unload — not **how** a module is hosted.
- **Extensions** (WASM, in-proc, gRPC, Docker) ship as runtimes (in-tree or `.so`), selected by manifest and/or policy.
- **Reuse** existing SDK transport split (`qt_remote`, `qt_local`, mock) when adding gRPC or other transports instead of forking the whole stack.
+18 -14
View File
@@ -96,23 +96,26 @@ set(LOGOS_CORE_SOURCES
logos_core/dependency_resolver.h
logos_core/plugin_manager.cpp
logos_core/plugin_manager.h
logos_core/plugin_launcher.cpp
logos_core/plugin_launcher.h
logos_core/process_manager.cpp
logos_core/process_manager.h
logos_core/module_runtime.h
logos_core/runtime_registry.cpp
logos_core/runtime_registry.h
runtimes/qt_subprocess/qt_subprocess_runtime.cpp
runtimes/qt_subprocess/qt_subprocess_runtime.h
runtimes/qt_subprocess/subprocess_manager.cpp
runtimes/qt_subprocess/subprocess_manager.h
)
# Define the logos host sources
set(LOGOS_HOST_SOURCES
logos_host/logos_host.cpp
logos_host/command_line_parser.cpp
logos_host/command_line_parser.h
logos_host/plugin_initializer.cpp
logos_host/plugin_initializer.h
logos_host/qt/qt_app.cpp
logos_host/qt/qt_app.h
logos_host/qt/qt_token_receiver.cpp
logos_host/qt/qt_token_receiver.h
runtimes/qt_subprocess/host/main.cpp
runtimes/qt_subprocess/host/command_line_parser.cpp
runtimes/qt_subprocess/host/command_line_parser.h
runtimes/qt_subprocess/host/plugin_initializer.cpp
runtimes/qt_subprocess/host/plugin_initializer.h
runtimes/qt_subprocess/host/qt/qt_app.cpp
runtimes/qt_subprocess/host/qt/qt_app.h
runtimes/qt_subprocess/host/qt/qt_token_receiver.cpp
runtimes/qt_subprocess/host/qt/qt_token_receiver.h
)
# Create the logos core library
@@ -170,6 +173,7 @@ target_link_libraries(logos_core PUBLIC
target_include_directories(logos_core PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/logos_core
${CMAKE_CURRENT_SOURCE_DIR}/runtimes
${Qt${QT_VERSION_MAJOR}_INCLUDE_DIRS}
)
@@ -234,7 +238,7 @@ target_link_libraries(logos_host PRIVATE
# Include directories for the logos host application
target_include_directories(logos_host PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/logos_host
${CMAKE_CURRENT_SOURCE_DIR}/runtimes/qt_subprocess/host
${Qt${QT_VERSION_MAJOR}_INCLUDE_DIRS}
)
+1 -1
View File
@@ -84,7 +84,7 @@ char* logos_core_process_plugin(const char* plugin_path) {
char* logos_core_get_token(const char* key) {
if (!key) { fprintf(stderr, "logos_core_get_token: key must not be null\n"); std::abort(); }
std::string token = TokenManager::instance().getToken(std::string(key));
std::string token = TokenManager::instance().getToken(key).toStdString();
if (token.empty()) return nullptr;
char* result = new char[token.size() + 1];
+78
View File
@@ -0,0 +1,78 @@
#ifndef MODULE_RUNTIME_H
#define MODULE_RUNTIME_H
#include <any>
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <nlohmann/json.hpp>
// Qt-free abstract interface for module loading strategies.
// An implementation decides *how* a module is loaded, isolated, and communicated with.
// The core (PluginManager) decides *what* to load and *when*.
namespace LogosCore {
// Describes a module that the core wants to load. Passed to ModuleRuntime::load().
struct ModuleDescriptor {
std::string name;
std::string path; // path to the module binary/bundle/wasm/etc.
std::string format; // "qt-plugin", "wasm", "" (empty = default)
std::vector<std::string> dependencies;
std::string instancePersistencePath; // empty if not configured
std::vector<std::string> pluginsDirs; // directories siblings are looked up in
nlohmann::json rawMetadata; // metadata parsed from manifest.json
nlohmann::json runtimeConfig; // optional: {"id":"docker","image":"..."}, etc.
};
// A handle to a successfully loaded module. Stored in PluginRegistry (PluginInfo).
struct LoadedModuleHandle {
std::string name;
int64_t pid = -1; // -1 when not process-based (in-proc, wasm, remote, etc.)
std::string endpoint; // transport-specific URI, e.g. "qtro+unix://my_module"
std::any opaque; // runtime-private state (optional)
};
// Abstract base: one instance per runtime kind, shared across all modules it manages.
// All implementations must be Qt-free at the interface level.
class ModuleRuntime {
public:
virtual ~ModuleRuntime() = default;
// Unique identifier for this runtime (e.g. "qt-subprocess", "inproc", "extism").
virtual std::string id() const = 0;
// Return true if this runtime knows how to load the described module.
virtual bool canHandle(const ModuleDescriptor& desc) const = 0;
// Load the module. On success, populate `out` and return true.
// `onTerminated` may be called from a background thread when the module exits.
virtual bool load(const ModuleDescriptor& desc,
std::function<void(const std::string& name)> onTerminated,
LoadedModuleHandle& out) = 0;
// Deliver the auth token to the named module. Called immediately after a successful load().
virtual bool sendToken(const std::string& name, const std::string& token) = 0;
// Terminate a single module by name.
virtual void terminate(const std::string& name) = 0;
// Terminate all modules managed by this runtime.
virtual void terminateAll() = 0;
// Return true if this runtime currently has an active entry for the named module.
virtual bool hasModule(const std::string& name) const = 0;
// Return the PID of the named module, or nullopt if not process-based.
virtual std::optional<int64_t> pid(const std::string& /*name*/) const { return std::nullopt; }
// Return all (name -> pid) mappings. PIDs are -1 for non-process runtimes.
virtual std::unordered_map<std::string, int64_t> getAllPids() const { return {}; }
};
} // namespace LogosCore
#endif // MODULE_RUNTIME_H
-123
View File
@@ -1,123 +0,0 @@
#include "plugin_launcher.h"
#include "process_manager.h"
#include <spdlog/spdlog.h>
#include <filesystem>
#include <cstdlib>
#include <boost/dll/runtime_symbol_info.hpp>
namespace fs = std::filesystem;
namespace {
std::string resolveLogosHostPath(const std::vector<std::string>& pluginsDirs) {
std::string logosHostPath;
const char* envPath = std::getenv("LOGOS_HOST_PATH");
if (envPath) {
logosHostPath = envPath;
}
if (logosHostPath.empty()) {
auto appDir = boost::dll::program_location().parent_path();
auto normalized = (appDir / "logos_host").lexically_normal();
logosHostPath = normalized.string();
}
if (!fs::exists(logosHostPath)) {
if (!pluginsDirs.empty()) {
auto candidatePath = fs::absolute(
fs::path(pluginsDirs.front()) / ".." / "bin" / "logos_host"
).lexically_normal();
if (fs::exists(candidatePath)) {
logosHostPath = candidatePath.string();
}
}
}
if (!fs::exists(logosHostPath)) {
spdlog::critical("logos_host not found at: {} - set LOGOS_HOST_PATH or place it next to the executable",
logosHostPath);
return {};
}
return logosHostPath;
}
}
namespace PluginLauncher {
bool launch(const std::string& name, const std::string& pluginPath,
const std::vector<std::string>& pluginsDirs,
const std::string& instancePersistencePath,
OnTerminatedFn onTerminated) {
std::string logosHostPath = resolveLogosHostPath(pluginsDirs);
if (logosHostPath.empty())
return false;
std::vector<std::string> arguments = {
"--name", name,
"--path", pluginPath
};
if (!instancePersistencePath.empty()) {
arguments.push_back("--instance-persistence-path");
arguments.push_back(instancePersistencePath);
}
QtProcessManager::ProcessCallbacks callbacks;
callbacks.onFinished = [onTerminated](const std::string& pName, int exitCode, bool crashed) {
(void)exitCode;
if (crashed) {
spdlog::critical("Plugin process crashed: {}", pName);
exit(1);
}
if (onTerminated)
onTerminated(pName);
};
callbacks.onError = [](const std::string& pName, bool crashed) {
if (crashed) {
spdlog::critical("Plugin process crashed: {}", pName);
exit(1);
}
};
callbacks.onOutput = [](const std::string& pName, const std::string& line, bool isStderr) {
if (isStderr) {
spdlog::critical("[{}] {}", pName, line);
} else if (line.find("Warning:") != std::string::npos ||
line.find("WARNING:") != std::string::npos) {
spdlog::warn("[{}] {}", pName, line);
} else if (line.find("Critical:") != std::string::npos ||
line.find("FAILED:") != std::string::npos ||
line.find("ERROR:") != std::string::npos) {
spdlog::critical("[{}] {}", pName, line);
}
};
return QtProcessManager::startProcess(name, logosHostPath, arguments, callbacks);
}
bool sendToken(const std::string& name, const std::string& token) {
return QtProcessManager::sendToken(name, token);
}
void terminate(const std::string& name) {
QtProcessManager::terminateProcess(name);
}
void terminateAll() {
QtProcessManager::terminateAll();
}
bool hasProcess(const std::string& name) {
return QtProcessManager::hasProcess(name);
}
std::unordered_map<std::string, int64_t> getAllProcessIds() {
return QtProcessManager::getAllProcessIds();
}
}
-24
View File
@@ -1,24 +0,0 @@
#ifndef PLUGIN_LAUNCHER_H
#define PLUGIN_LAUNCHER_H
#include <string>
#include <vector>
#include <unordered_map>
#include <functional>
#include <cstdint>
namespace PluginLauncher {
using OnTerminatedFn = std::function<void(const std::string& name)>;
bool launch(const std::string& name, const std::string& pluginPath,
const std::vector<std::string>& pluginsDirs,
const std::string& instancePersistencePath,
OnTerminatedFn onTerminated);
bool sendToken(const std::string& name, const std::string& token);
void terminate(const std::string& name);
void terminateAll();
bool hasProcess(const std::string& name);
std::unordered_map<std::string, int64_t> getAllProcessIds();
}
#endif // PLUGIN_LAUNCHER_H
+66 -26
View File
@@ -1,8 +1,11 @@
#include "plugin_manager.h"
#include "plugin_registry.h"
#include "dependency_resolver.h"
#include "plugin_launcher.h"
#include "runtime_registry.h"
#include "runtimes/qt_subprocess/qt_subprocess_runtime.h"
#include "runtimes/qt_subprocess/subprocess_manager.h"
#include <spdlog/spdlog.h>
#include <QString>
#include <mutex>
#include <cassert>
#include <cstring>
@@ -31,6 +34,17 @@ namespace {
return path;
}
// Lazily initialised RuntimeRegistry. On first call, registers the default
// QtSubprocessRuntime. clearForTests() can replace it for unit tests.
LogosCore::RuntimeRegistry& runtimeRegistry() {
static LogosCore::RuntimeRegistry reg;
static std::once_flag initFlag;
std::call_once(initFlag, []() {
reg.registerRuntime(std::make_shared<LogosCore::QtSubprocessRuntime>());
});
return reg;
}
char** toNullTerminatedArray(const std::vector<std::string>& list) {
int count = static_cast<int>(list.size());
if (count == 0) {
@@ -53,14 +67,14 @@ namespace {
return;
TokenManager& tokenManager = TokenManager::instance();
std::string capabilityModuleToken = tokenManager.getToken(std::string("capability_module"));
std::string capabilityModuleToken = tokenManager.getToken("capability_module").toStdString();
static LogosAPI* s_coreApi = nullptr;
if (!s_coreApi)
s_coreApi = new LogosAPI(std::string("core"));
s_coreApi = new LogosAPI("core");
LogosAPIClient* client = s_coreApi->getClient(std::string("capability_module"));
if (!client->informModuleToken(capabilityModuleToken, name, token)) {
LogosAPIClient* client = s_coreApi->getClient("capability_module");
if (!client->informModuleToken(capabilityModuleToken.c_str(), name.c_str(), token.c_str())) {
spdlog::warn("Failed to register token with capability module for: {}", name);
}
}
@@ -80,31 +94,45 @@ namespace {
std::string pluginPath = registryInstance().pluginPath(name);
// Resolve instance persistence path if a base path has been configured
std::string instancePersistencePath;
// Build a descriptor for the runtime to inspect.
LogosCore::ModuleDescriptor desc;
desc.name = name;
desc.path = pluginPath;
desc.format = "qt-plugin";
desc.dependencies = registryInstance().pluginDependencies(name);
desc.pluginsDirs = registryInstance().pluginsDirs();
if (!persistenceBasePath().empty()) {
auto info = ModuleLib::InstancePersistence::resolveInstance(
persistenceBasePath(),
name);
instancePersistencePath = info.persistencePath;
QString::fromStdString(persistenceBasePath()),
QString::fromStdString(name));
desc.instancePersistencePath = info.persistencePath.toStdString();
}
auto rt = runtimeRegistry().select(desc);
if (!rt) {
spdlog::warn("No runtime available to load plugin: {}", name);
return false;
}
auto onTerminated = [](const std::string& n) {
registryInstance().markUnloaded(n);
};
if (!PluginLauncher::launch(name, pluginPath, registryInstance().pluginsDirs(),
instancePersistencePath, onTerminated))
LogosCore::LoadedModuleHandle handle;
if (!rt->load(desc, onTerminated, handle))
return false;
std::string authToken = boost::uuids::to_string(boost::uuids::random_generator()());
if (!PluginLauncher::sendToken(name, authToken))
if (!rt->sendToken(name, authToken)) {
rt->terminate(name);
return false;
}
registryInstance().markLoaded(name);
registryInstance().markLoaded(name, rt, std::move(handle));
TokenManager::instance().saveToken(name, authToken);
TokenManager::instance().saveToken(name.c_str(), authToken.c_str());
notifyCapabilityModule(name, authToken);
@@ -122,12 +150,23 @@ namespace {
return false;
}
if (!PluginLauncher::hasProcess(name)) {
spdlog::warn("No process found for plugin: {}", name);
return false;
auto rt = registryInstance().runtimeFor(name);
if (rt) {
if (!rt->hasModule(name)) {
spdlog::warn("No module entry found for plugin: {}", name);
return false;
}
rt->terminate(name);
} else {
// Fallback: module was loaded via markLoaded(name) directly
// (test scenarios or external setup). Use SubprocessManager directly.
if (!SubprocessManager::hasProcess(name)) {
spdlog::warn("No process found for plugin: {}", name);
return false;
}
SubprocessManager::terminateProcess(name);
}
PluginLauncher::terminate(name);
registryInstance().markUnloaded(name);
spdlog::info("Plugin unloaded: {}", name);
@@ -141,6 +180,10 @@ namespace PluginManager {
return registryInstance();
}
LogosCore::RuntimeRegistry& runtimes() {
return runtimeRegistry();
}
void setPluginsDir(const char* plugins_dir) {
assert(plugins_dir != nullptr);
registryInstance().setPluginsDir(std::string(plugins_dir));
@@ -282,6 +325,7 @@ namespace PluginManager {
if (teardownSetMembers.count(*it) && teardownOrderMembers.insert(*it).second)
teardownOrder.push_back(*it);
}
// Safety net: any members not seen by the resolver (shouldn't happen,
// but don't silently skip them) go to the end.
for (const std::string& n : teardownSet) {
@@ -303,13 +347,13 @@ namespace PluginManager {
void terminateAll() {
std::lock_guard lock(loadMutex());
PluginLauncher::terminateAll();
runtimeRegistry().terminateAll();
registryInstance().clearLoaded();
}
void clear() {
std::lock_guard lock(loadMutex());
PluginLauncher::terminateAll();
runtimeRegistry().terminateAll();
registryInstance().clear();
}
@@ -330,7 +374,7 @@ namespace PluginManager {
}
std::unordered_map<std::string, int64_t> getPluginProcessIds() {
return PluginLauncher::getAllProcessIds();
return runtimeRegistry().getAllPids();
}
std::vector<std::string> resolveDependencies(const std::vector<std::string>& requestedModules) {
@@ -343,10 +387,6 @@ namespace PluginManager {
std::vector<std::string> getDependencies(const std::string& name, bool recursive) {
// Filter to known modules to honour the documented contract.
// PluginInfo::dependencies holds whatever the manifest declares,
// including names that aren't installed. The reverse-edge accessor
// doesn't need this treatment because recomputeDependentsLocked only
// writes known names into PluginInfo::dependents by construction.
std::vector<std::string> deps = registryInstance().pluginDependencies(name, recursive);
std::vector<std::string> knownDeps;
knownDeps.reserve(deps.size());
+4
View File
@@ -5,12 +5,16 @@
#include <vector>
#include <unordered_map>
#include <cstdint>
#include "runtime_registry.h"
class PluginRegistry;
namespace PluginManager {
PluginRegistry& registry();
// Access the runtime registry (e.g. for tests that need to install a FakeRuntime).
LogosCore::RuntimeRegistry& runtimes();
void setPluginsDir(const char* plugins_dir);
void addPluginsDir(const char* plugins_dir);
void setPersistenceBasePath(const char* path);
+99 -17
View File
@@ -2,12 +2,87 @@
#include <spdlog/spdlog.h>
#include <cassert>
#include <deque>
#include <functional>
#include <mutex>
#include <shared_mutex>
#include <algorithm>
#include <unordered_set>
#include <module_lib/module_lib.h>
#include <package_manager_lib.h>
#if __has_include(<nlohmann/json.hpp>)
#include <nlohmann/json.hpp>
#endif
// ---------------------------------------------------------------------------
// Compatibility shim for two API generations of PackageManagerLib:
//
// Old (logos-package-manager ≤ some version):
// std::string getInstalledModules() — returns a JSON array string
//
// New (logos-package-manager with InstalledPackage struct):
// std::vector<InstalledPackage> getInstalledModules()
//
// We use a template + overload strategy so neither branch needs to name the
// type that the *other* API version doesn't define. The compiler instantiates
// only the overload that matches the actual return type; the other overload is
// parsed but never instantiated, so undefined types in its body don't matter.
// ---------------------------------------------------------------------------
// Overload for the old JSON-string API.
static std::unordered_set<std::string>
extractScannedNames(const std::string& jsonModules,
std::function<std::string(const std::string&)> processPlugin)
{
std::unordered_set<std::string> scannedNames;
// Avoid pulling in nlohmann/json here — do a minimal manual parse that
// is robust enough for the well-structured JSON produced by the library.
// Each module object has at least {"name":..., "mainFilePath":...}.
try {
// Use nlohmann/json if available (it is — logos_core links it).
#if __has_include(<nlohmann/json.hpp>)
auto arr = nlohmann::json::parse(jsonModules, nullptr, /*exceptions=*/false);
if (arr.is_array()) {
for (const auto& obj : arr) {
std::string mainPath;
if (obj.contains("mainFilePath") && obj["mainFilePath"].is_string())
mainPath = obj["mainFilePath"].get<std::string>();
if (mainPath.empty()) continue;
std::string pluginName = processPlugin(mainPath);
if (pluginName.empty()) {
spdlog::warn("Failed to process plugin: {}", mainPath);
continue;
}
scannedNames.insert(pluginName);
}
}
#endif
} catch (...) {
spdlog::warn("Failed to parse installed modules JSON");
}
return scannedNames;
}
// Overload for the new InstalledPackage vector API.
// `T` is deduced as `InstalledPackage`; if that type is not defined,
// this template is simply never instantiated.
template<typename T>
static std::unordered_set<std::string>
extractScannedNames(const std::vector<T>& modules,
std::function<std::string(const std::string&)> processPlugin)
{
std::unordered_set<std::string> scannedNames;
for (const auto& mod : modules) {
if (mod.name.empty() || mod.mainFilePath.empty())
continue;
std::string pluginName = processPlugin(mod.mainFilePath);
if (pluginName.empty()) {
spdlog::warn("Failed to process plugin: {}", mod.mainFilePath);
continue;
}
scannedNames.insert(pluginName);
}
return scannedNames;
}
static PackageManagerLib& packageManagerInstance() {
static PackageManagerLib instance;
@@ -43,7 +118,10 @@ void PluginRegistry::discoverInstalledModules() {
}
}
std::vector<InstalledPackage> modules = pm.getInstalledModules();
auto modules = pm.getInstalledModules();
std::function<std::string(const std::string&)> processPlugin =
[this](const std::string& path) { return processPluginInternal(path); };
// Collect names seen in this scan. Used after the upsert loop to prune
// entries for plugins whose files disappeared (typical path: the user
@@ -51,22 +129,8 @@ void PluginRegistry::discoverInstalledModules() {
// the stale PluginInfo would stay in m_plugins forever and
// knownPluginNames()/`logos_core_get_known_plugins` would keep returning
// it, so the UI would never see the uninstall land.
std::unordered_set<std::string> scannedNames;
for (const InstalledPackage& mod : modules) {
if (mod.name.empty() || mod.mainFilePath.empty())
continue;
std::string pluginName = processPluginInternal(mod.mainFilePath);
if (pluginName.empty()) {
// Skip entries with no extractable metadata — a bare
// `scannedNames.insert` would record an empty string and the
// prune loop would mis-identify still-present plugins as gone.
spdlog::warn("Failed to process plugin: {}", mod.mainFilePath);
continue;
}
scannedNames.insert(pluginName);
}
std::unordered_set<std::string> scannedNames =
extractScannedNames(modules, processPlugin);
// Prune entries that aren't on disk anymore. Preserve currently-loaded
// plugins even if their backing files are gone — the module is still
@@ -271,6 +335,24 @@ void PluginRegistry::markLoaded(const std::string& name) {
m_plugins[name].loaded = true;
}
void PluginRegistry::markLoaded(const std::string& name,
std::shared_ptr<LogosCore::ModuleRuntime> runtime,
LogosCore::LoadedModuleHandle handle) {
std::unique_lock lock(m_mutex);
auto& info = m_plugins[name];
info.loaded = true;
info.runtime = std::move(runtime);
info.handle = std::move(handle);
}
std::shared_ptr<LogosCore::ModuleRuntime>
PluginRegistry::runtimeFor(const std::string& name) const {
std::shared_lock lock(m_mutex);
auto it = m_plugins.find(name);
if (it == m_plugins.end()) return nullptr;
return it->second.runtime;
}
void PluginRegistry::markUnloaded(const std::string& name) {
std::unique_lock lock(m_mutex);
auto it = m_plugins.find(name);
+21
View File
@@ -1,10 +1,13 @@
#ifndef PLUGIN_REGISTRY_H
#define PLUGIN_REGISTRY_H
#include <memory>
#include <string>
#include <unordered_set>
#include <vector>
#include <unordered_map>
#include <shared_mutex>
#include "module_runtime.h"
struct PluginInfo {
std::string path;
@@ -15,6 +18,10 @@ struct PluginInfo {
// directly. Use PluginRegistry::pluginDependents() for transitive walks.
std::vector<std::string> dependents;
bool loaded = false;
// Owning runtime, set when a module is loaded via loadPluginInternal().
// Null when loaded directly via markLoaded(name) (test/external scenarios).
std::shared_ptr<LogosCore::ModuleRuntime> runtime;
LogosCore::LoadedModuleHandle handle;
};
class PluginRegistry {
@@ -46,11 +53,25 @@ public:
void registerDependencies(const std::string& name, const std::vector<std::string>& dependencies);
bool isLoaded(const std::string& name) const;
// Simple mark-as-loaded (no runtime association). Used by tests and
// external callers that set up state directly without going through loadPlugin.
void markLoaded(const std::string& name);
// Full mark-as-loaded that stores the owning runtime and handle for later
// use by unloadPlugin(). Should be called from loadPluginInternal().
void markLoaded(const std::string& name,
std::shared_ptr<LogosCore::ModuleRuntime> runtime,
LogosCore::LoadedModuleHandle handle);
void markUnloaded(const std::string& name);
std::vector<std::string> loadedPluginNames() const;
void clearLoaded();
// Returns the runtime that owns the named module, or nullptr if it was
// loaded without a runtime association (e.g. via markLoaded(name) only).
std::shared_ptr<LogosCore::ModuleRuntime> runtimeFor(const std::string& name) const;
void clear();
private:
+58
View File
@@ -0,0 +1,58 @@
#include "runtime_registry.h"
namespace LogosCore {
void RuntimeRegistry::registerRuntime(std::shared_ptr<ModuleRuntime> runtime)
{
std::lock_guard lock(m_mutex);
m_runtimes.push_back(std::move(runtime));
}
std::shared_ptr<ModuleRuntime> RuntimeRegistry::select(const ModuleDescriptor& desc) const
{
std::lock_guard lock(m_mutex);
// Explicit id override: caller pinned a specific runtime id.
if (desc.runtimeConfig.contains("id")) {
std::string requestedId = desc.runtimeConfig.at("id").get<std::string>();
for (const auto& rt : m_runtimes) {
if (rt->id() == requestedId)
return rt;
}
// Unknown explicit id — don't fall through to canHandle.
return nullptr;
}
// Format/capability-based: first runtime that accepts this descriptor.
for (const auto& rt : m_runtimes) {
if (rt->canHandle(desc))
return rt;
}
return nullptr;
}
void RuntimeRegistry::terminateAll()
{
std::lock_guard lock(m_mutex);
for (const auto& rt : m_runtimes)
rt->terminateAll();
}
std::unordered_map<std::string, int64_t> RuntimeRegistry::getAllPids() const
{
std::lock_guard lock(m_mutex);
std::unordered_map<std::string, int64_t> result;
for (const auto& rt : m_runtimes) {
auto pids = rt->getAllPids();
result.insert(pids.begin(), pids.end());
}
return result;
}
void RuntimeRegistry::clearForTests()
{
std::lock_guard lock(m_mutex);
m_runtimes.clear();
}
} // namespace LogosCore
+46
View File
@@ -0,0 +1,46 @@
#ifndef RUNTIME_REGISTRY_H
#define RUNTIME_REGISTRY_H
#include "module_runtime.h"
#include <memory>
#include <mutex>
#include <unordered_map>
#include <vector>
namespace LogosCore {
// Holds all registered ModuleRuntime instances and selects one for a given descriptor.
// Thread-safe (all operations protected by an internal mutex).
class RuntimeRegistry {
public:
// Register a runtime. Runtimes are consulted in registration order when
// no explicit runtimeConfig["id"] is present.
void registerRuntime(std::shared_ptr<ModuleRuntime> runtime);
// Select a runtime for the descriptor.
// Selection order:
// 1. If desc.runtimeConfig["id"] is set, return the matching runtime by id.
// Returns nullptr if the id is unknown (does not fall through to canHandle).
// 2. Otherwise, return the first registered runtime whose canHandle(desc) is true.
// 3. Returns nullptr if no runtime matches.
std::shared_ptr<ModuleRuntime> select(const ModuleDescriptor& desc) const;
// Fan-out terminateAll() to every registered runtime.
void terminateAll();
// Aggregate getAllPids() across all runtimes. Later-registered runtimes win on
// name collision (should not happen in practice).
std::unordered_map<std::string, int64_t> getAllPids() const;
// Testing hook: remove all runtimes so a test can install a FakeRuntime
// without triggering any real Qt subprocess side effects.
void clearForTests();
private:
mutable std::mutex m_mutex;
std::vector<std::shared_ptr<ModuleRuntime>> m_runtimes;
};
} // namespace LogosCore
#endif // RUNTIME_REGISTRY_H
@@ -1,6 +1,8 @@
#include "plugin_initializer.h"
#include "qt/qt_token_receiver.h"
#include <QObject>
#include <QString>
#include <QVariant>
#include <spdlog/spdlog.h>
#include <filesystem>
#include "interface.h"
@@ -15,12 +17,11 @@ using namespace ModuleLib;
LogosModule loadPlugin(const std::string& pluginPath, const std::string& expectedName)
{
// Load the plugin using module_lib for abstraction
std::string errorString;
LogosModule module = LogosModule::loadFromPath(pluginPath, &errorString);
QString errorStringQ;
LogosModule module = LogosModule::loadFromPath(QString::fromStdString(pluginPath), &errorStringQ);
if (!module.isValid()) {
spdlog::critical("Failed to load plugin: {}", errorString);
spdlog::critical("Failed to load plugin: {}", errorStringQ.toStdString());
return LogosModule();
}
@@ -43,20 +44,21 @@ LogosAPI* initializeLogosAPI(const std::string& pluginName, QObject* plugin,
const std::string& pluginPath,
const std::string& instancePersistencePath)
{
LogosAPI* logos_api = new LogosAPI(pluginName, plugin);
LogosAPI* logos_api = new LogosAPI(QString::fromStdString(pluginName), plugin);
logos_api->setProperty("modulePath",
fs::absolute(fs::path(pluginPath)).parent_path().string());
QVariant(QString::fromStdString(fs::absolute(fs::path(pluginPath)).parent_path().string())));
if (!instancePersistencePath.empty()) {
logos_api->setProperty("instancePersistencePath", instancePersistencePath);
logos_api->setProperty("instancePersistencePath",
QVariant(QString::fromStdString(instancePersistencePath)));
logos_api->setProperty("instanceId",
fs::path(instancePersistencePath).filename().string());
QVariant(QString::fromStdString(fs::path(instancePersistencePath).filename().string())));
}
bool success = logos_api->getProvider()->registerObject(basePlugin->name(), plugin);
if (success) {
logos_api->getTokenManager()->saveToken(std::string("core"), authToken);
logos_api->getTokenManager()->saveToken(std::string("capability_module"), authToken);
logos_api->getTokenManager()->saveToken("core", authToken.c_str());
logos_api->getTokenManager()->saveToken("capability_module", authToken.c_str());
} else {
spdlog::critical("Failed to register plugin for remote access: {}", basePlugin->name().toStdString());
delete plugin;
@@ -0,0 +1,145 @@
#include "qt_subprocess_runtime.h"
#include "subprocess_manager.h"
#include <spdlog/spdlog.h>
#include <filesystem>
#include <cstdlib>
#include <boost/dll/runtime_symbol_info.hpp>
namespace fs = std::filesystem;
namespace {
std::string resolveLogosHostPath(const std::vector<std::string>& pluginsDirs) {
std::string logosHostPath;
const char* envPath = std::getenv("LOGOS_HOST_PATH");
if (envPath)
logosHostPath = envPath;
if (logosHostPath.empty()) {
auto appDir = boost::dll::program_location().parent_path();
auto normalized = (appDir / "logos_host").lexically_normal();
logosHostPath = normalized.string();
}
if (!fs::exists(logosHostPath)) {
if (!pluginsDirs.empty()) {
auto candidatePath = fs::absolute(
fs::path(pluginsDirs.front()) / ".." / "bin" / "logos_host"
).lexically_normal();
if (fs::exists(candidatePath))
logosHostPath = candidatePath.string();
}
}
if (!fs::exists(logosHostPath)) {
spdlog::critical(
"logos_host not found at: {} - set LOGOS_HOST_PATH or place it next to the executable",
logosHostPath);
return {};
}
return logosHostPath;
}
} // anonymous namespace
namespace LogosCore {
bool QtSubprocessRuntime::canHandle(const ModuleDescriptor& desc) const
{
return desc.format == "qt-plugin" || desc.format.empty();
}
bool QtSubprocessRuntime::load(const ModuleDescriptor& desc,
std::function<void(const std::string&)> onTerminated,
LoadedModuleHandle& out)
{
std::string logosHostPath = resolveLogosHostPath(desc.pluginsDirs);
if (logosHostPath.empty())
return false;
std::vector<std::string> arguments = {
"--name", desc.name,
"--path", desc.path
};
if (!desc.instancePersistencePath.empty()) {
arguments.push_back("--instance-persistence-path");
arguments.push_back(desc.instancePersistencePath);
}
SubprocessManager::ProcessCallbacks callbacks;
callbacks.onFinished = [onTerminated](const std::string& pName, int exitCode, bool crashed) {
(void)exitCode;
if (crashed) {
spdlog::critical("Plugin process crashed: {}", pName);
exit(1);
}
if (onTerminated)
onTerminated(pName);
};
callbacks.onError = [](const std::string& pName, bool crashed) {
if (crashed) {
spdlog::critical("Plugin process crashed: {}", pName);
exit(1);
}
};
callbacks.onOutput = [](const std::string& pName, const std::string& line, bool isStderr) {
if (isStderr) {
spdlog::critical("[{}] {}", pName, line);
} else if (line.find("Warning:") != std::string::npos ||
line.find("WARNING:") != std::string::npos) {
spdlog::warn("[{}] {}", pName, line);
} else if (line.find("Critical:") != std::string::npos ||
line.find("FAILED:") != std::string::npos ||
line.find("ERROR:") != std::string::npos) {
spdlog::critical("[{}] {}", pName, line);
}
};
if (!SubprocessManager::startProcess(desc.name, logosHostPath, arguments, callbacks))
return false;
out.name = desc.name;
out.pid = SubprocessManager::getProcessId(desc.name);
out.endpoint = "qtro+unix://" + desc.name;
return true;
}
bool QtSubprocessRuntime::sendToken(const std::string& name, const std::string& token)
{
return SubprocessManager::sendToken(name, token);
}
void QtSubprocessRuntime::terminate(const std::string& name)
{
SubprocessManager::terminateProcess(name);
}
void QtSubprocessRuntime::terminateAll()
{
SubprocessManager::terminateAll();
}
bool QtSubprocessRuntime::hasModule(const std::string& name) const
{
return SubprocessManager::hasProcess(name);
}
std::optional<int64_t> QtSubprocessRuntime::pid(const std::string& name) const
{
int64_t p = SubprocessManager::getProcessId(name);
if (p < 0) return std::nullopt;
return p;
}
std::unordered_map<std::string, int64_t> QtSubprocessRuntime::getAllPids() const
{
return SubprocessManager::getAllProcessIds();
}
} // namespace LogosCore
@@ -0,0 +1,33 @@
#ifndef QT_SUBPROCESS_RUNTIME_H
#define QT_SUBPROCESS_RUNTIME_H
#include "../../logos_core/module_runtime.h"
namespace LogosCore {
// ModuleRuntime implementation for the "qt-subprocess" strategy:
// - spawns a logos_host child process per module
// - delivers an auth token via a Unix-domain socket (logos_token_<name>)
// - module communicates back via Qt Remote Objects over a local socket
class QtSubprocessRuntime : public ModuleRuntime {
public:
std::string id() const override { return "qt-subprocess"; }
// Handles qt-plugin format (or unspecified format, which defaults to qt-plugin).
bool canHandle(const ModuleDescriptor& desc) const override;
bool load(const ModuleDescriptor& desc,
std::function<void(const std::string&)> onTerminated,
LoadedModuleHandle& out) override;
bool sendToken(const std::string& name, const std::string& token) override;
void terminate(const std::string& name) override;
void terminateAll() override;
bool hasModule(const std::string& name) const override;
std::optional<int64_t> pid(const std::string& name) const override;
std::unordered_map<std::string, int64_t> getAllPids() const override;
};
} // namespace LogosCore
#endif // QT_SUBPROCESS_RUNTIME_H
@@ -1,4 +1,4 @@
#include "process_manager.h"
#include "subprocess_manager.h"
#include <boost/asio/connect_pipe.hpp>
#include <boost/asio/executor_work_guard.hpp>
@@ -63,19 +63,19 @@ IoRuntime& ioRuntime() {
// ---------------------------------------------------------------------------
struct ProcessEntry {
bp2::process process;
asio::readable_pipe pipe;
QtProcessManager::ProcessCallbacks callbacks;
std::string name;
std::array<char, 4096> read_buf{};
std::string line_buf;
bp2::process process;
asio::readable_pipe pipe;
SubprocessManager::ProcessCallbacks callbacks;
std::string name;
std::array<char, 4096> read_buf{};
std::string line_buf;
// Set by async_wait callback; used by syncKill to avoid double-waitpid.
std::atomic<bool> exited{false};
std::atomic<bool> exited{false};
// Set by terminateProcess/terminateAll to suppress onFinished callback.
std::atomic<bool> cancelled{false};
std::atomic<bool> cancelled{false};
ProcessEntry(bp2::process proc, asio::readable_pipe rp,
const std::string& n, const QtProcessManager::ProcessCallbacks& cb)
const std::string& n, const SubprocessManager::ProcessCallbacks& cb)
: process(std::move(proc))
, pipe(std::move(rp))
, name(n)
@@ -210,11 +210,11 @@ void syncKill(std::shared_ptr<ProcessEntry> entry) {
};
if (!wait(std::chrono::seconds(5))) {
fprintf(stderr, "[QtProcessManager] Process did not terminate gracefully, killing: %s\n",
fprintf(stderr, "[SubprocessManager] Process did not terminate gracefully, killing: %s\n",
entry->name.c_str());
entry->process.terminate(ec); // SIGKILL
if (!wait(std::chrono::seconds(2))) {
fprintf(stderr, "[QtProcessManager] Process did not respond to SIGKILL: %s\n",
fprintf(stderr, "[SubprocessManager] Process did not respond to SIGKILL: %s\n",
entry->name.c_str());
}
}
@@ -223,10 +223,10 @@ void syncKill(std::shared_ptr<ProcessEntry> entry) {
} // anonymous namespace
// ===========================================================================
// QtProcessManager public API
// SubprocessManager public API
// ===========================================================================
namespace QtProcessManager {
namespace SubprocessManager {
bool startProcess(const std::string& name, const std::string& executable,
const std::vector<std::string>& arguments,
@@ -241,7 +241,7 @@ bool startProcess(const std::string& name, const std::string& executable,
asio::writable_pipe wpipe(rt.ctx);
asio::connect_pipe(rpipe, wpipe, ec);
if (ec) {
fprintf(stderr, "[QtProcessManager] Failed to create pipe for %s: %s\n",
fprintf(stderr, "[SubprocessManager] Failed to create pipe for %s: %s\n",
name.c_str(), ec.message().c_str());
return false;
}
@@ -251,14 +251,13 @@ bool startProcess(const std::string& name, const std::string& executable,
pstdio.out = wpipe;
pstdio.err = wpipe;
// Use the launcher directly with ec as second arg (non-throwing variant)
bp2::process proc = bp2::default_process_launcher()(rt.ctx, ec, executable, arguments, pstdio);
// Close write end in parent once child has inherited it
wpipe.close();
if (ec) {
fprintf(stderr, "[QtProcessManager] Failed to start process for %s: %s\n",
fprintf(stderr, "[SubprocessManager] Failed to start process for %s: %s\n",
name.c_str(), ec.message().c_str());
return false;
}
@@ -289,7 +288,7 @@ bool sendToken(const std::string& name, const std::string& token)
for (int attempt = 0; attempt < 10; ++attempt) {
sock = ::socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0) {
fprintf(stderr, "[QtProcessManager] socket() failed: %s\n", strerror(errno));
fprintf(stderr, "[SubprocessManager] socket() failed: %s\n", strerror(errno));
break;
}
@@ -306,10 +305,10 @@ bool sendToken(const std::string& name, const std::string& token)
}
if (sock < 0) {
fprintf(stderr, "[QtProcessManager] Failed to connect to token socket for: %s\n",
fprintf(stderr, "[SubprocessManager] Failed to connect to token socket for: %s\n",
name.c_str());
// Remove and kill associated process (matching Qt behaviour)
// Remove and kill associated process
std::shared_ptr<ProcessEntry> entry;
{
std::lock_guard<std::mutex> lock(s_processesMutex);
@@ -404,4 +403,4 @@ void registerProcess(const std::string& name)
s_processes[name] = nullptr;
}
} // namespace QtProcessManager
} // namespace SubprocessManager
@@ -1,5 +1,5 @@
#ifndef PROCESS_MANAGER_H
#define PROCESS_MANAGER_H
#ifndef SUBPROCESS_MANAGER_H
#define SUBPROCESS_MANAGER_H
#include <string>
#include <vector>
@@ -7,7 +7,9 @@
#include <functional>
#include <cstdint>
namespace QtProcessManager {
// Subprocess management for the qt_subprocess runtime.
// Renamed from QtProcessManager — uses Boost.Process + Boost.Asio (no Qt).
namespace SubprocessManager {
struct ProcessCallbacks {
std::function<void(const std::string& name, int exitCode, bool crashed)> onFinished;
@@ -27,4 +29,4 @@ namespace QtProcessManager {
void registerProcess(const std::string& name);
}
#endif // PROCESS_MANAGER_H
#endif // SUBPROCESS_MANAGER_H
+8 -5
View File
@@ -12,12 +12,14 @@ add_executable(logos_core_tests
test_plugin_manager.cpp
test_process_stats.cpp
test_dependency_resolver.cpp
test_process_manager.cpp
test_subprocess_manager.cpp
test_token_exchange.cpp
test_runtime_registry.cpp
test_module_runtime_abstraction.cpp
# qt_test_adapter.h calls QtTokenReceiver::receiveAuthToken; compile the
# implementation unit directly into the test binary (it lives in logos_host,
# which is an executable, not a linkable library).
${CMAKE_SOURCE_DIR}/src/logos_host/qt/qt_token_receiver.cpp
# implementation unit directly into the test binary (it lives in the runtime
# host, which is an executable, not a linkable library).
${CMAKE_SOURCE_DIR}/src/runtimes/qt_subprocess/host/qt/qt_token_receiver.cpp
)
target_link_libraries(logos_core_tests PRIVATE
@@ -31,7 +33,8 @@ target_link_libraries(logos_core_tests PRIVATE
target_include_directories(logos_core_tests PRIVATE
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/logos_core
${CMAKE_SOURCE_DIR}/src/logos_host
${CMAKE_SOURCE_DIR}/src/runtimes
${CMAKE_SOURCE_DIR}/src/runtimes/qt_subprocess/host
)
# Propagate portable build flag to tests so manifest variant keys match
+13 -13
View File
@@ -12,7 +12,7 @@
#include "plugin_manager.h"
#include "plugin_registry.h"
#include "process_manager.h"
#include "qt_subprocess/subprocess_manager.h"
#include "qt/qt_token_receiver.h"
#include <QLocalServer>
@@ -135,14 +135,14 @@ inline void logos_core_clear()
}
// ---------------------------------------------------------------------------
// Process management — QtProcessManager already uses std::string in its API;
// Process management — SubprocessManager uses std::string throughout;
// these pass-throughs keep the test call sites Qt-free.
// ---------------------------------------------------------------------------
inline void logos_core_register_process(const char* name)
{
if (!name) return;
QtProcessManager::registerProcess(std::string(name));
SubprocessManager::registerProcess(std::string(name));
}
inline int logos_core_start_process(const char* name,
@@ -154,40 +154,40 @@ inline int logos_core_start_process(const char* name,
if (args)
for (int i = 0; args[i] != nullptr; ++i)
arguments.push_back(args[i]);
QtProcessManager::ProcessCallbacks noopCallbacks;
return QtProcessManager::startProcess(std::string(name),
std::string(executable),
arguments,
noopCallbacks) ? 1 : 0;
SubprocessManager::ProcessCallbacks noopCallbacks;
return SubprocessManager::startProcess(std::string(name),
std::string(executable),
arguments,
noopCallbacks) ? 1 : 0;
}
inline int logos_core_send_token(const char* name, const char* token)
{
if (!name || !token) return 0;
return QtProcessManager::sendToken(std::string(name), std::string(token)) ? 1 : 0;
return SubprocessManager::sendToken(std::string(name), std::string(token)) ? 1 : 0;
}
inline int logos_core_has_process(const char* name)
{
if (!name) return 0;
return QtProcessManager::hasProcess(std::string(name)) ? 1 : 0;
return SubprocessManager::hasProcess(std::string(name)) ? 1 : 0;
}
inline int64_t logos_core_get_process_id(const char* name)
{
if (!name) return -1;
return QtProcessManager::getProcessId(std::string(name));
return SubprocessManager::getProcessId(std::string(name));
}
inline void logos_core_terminate_process(const char* name)
{
if (!name) return;
QtProcessManager::terminateProcess(std::string(name));
SubprocessManager::terminateProcess(std::string(name));
}
inline void logos_core_clear_processes()
{
QtProcessManager::clearAll();
SubprocessManager::clearAll();
}
// ---------------------------------------------------------------------------
+266
View File
@@ -0,0 +1,266 @@
// =============================================================================
// Tests for the ModuleRuntime abstraction seam.
//
// Installs a FakeRuntime into PluginManager's RuntimeRegistry and drives the
// full PluginManager load/unload/terminateAll path. Proves that:
// - load(), sendToken(), terminate(), terminateAll() are routed through the
// runtime abstraction (not directly to a subprocess or Qt mechanism).
// - Dependency-ordered loads call load() in the correct (topo) order.
// - Error paths (load returns false) prevent sendToken from being called.
// No child processes are spawned; no Qt Remote Objects are used.
// =============================================================================
#include <gtest/gtest.h>
#include "logos_core.h"
#include "qt_test_adapter.h"
#include "plugin_manager.h"
#include "plugin_registry.h"
#include "runtime_registry.h"
#include "module_runtime.h"
#include "qt_subprocess/qt_subprocess_runtime.h"
#include <string>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <memory>
using namespace LogosCore;
// ---------------------------------------------------------------------------
// FakeRuntime: records all calls; configurable per-module load result.
// Placed in an anonymous namespace to avoid ODR conflicts with the FakeRuntime
// stub in test_runtime_registry.cpp (same binary, different definition).
// ---------------------------------------------------------------------------
namespace {
struct FakeRuntime : public ModuleRuntime {
std::string id() const override { return "fake"; }
bool canHandle(const ModuleDescriptor&) const override { return true; }
bool load(const ModuleDescriptor& desc,
std::function<void(const std::string&)>,
LoadedModuleHandle& out) override {
loadCalls.push_back(desc.name);
if (failOn.count(desc.name)) return false;
out.name = desc.name;
out.pid = 1234;
out.endpoint = "fake://" + desc.name;
activeModules.insert(desc.name);
return true;
}
bool sendToken(const std::string& name, const std::string& token) override {
sendTokenCalls.push_back({name, token});
return true;
}
void terminate(const std::string& name) override {
terminateCalls.push_back(name);
activeModules.erase(name);
}
void terminateAll() override {
terminateAllCount++;
activeModules.clear();
}
bool hasModule(const std::string& name) const override {
return activeModules.count(name) > 0;
}
// Call records
std::vector<std::string> loadCalls;
std::vector<std::pair<std::string,std::string>> sendTokenCalls;
std::vector<std::string> terminateCalls;
int terminateAllCount = 0;
// Modules to fail on load
std::unordered_set<std::string> failOn;
// Modules currently "running"
std::unordered_set<std::string> activeModules;
};
} // anonymous namespace
// ---------------------------------------------------------------------------
// Test fixture: installs FakeRuntime, cleans up registry after each test.
// ---------------------------------------------------------------------------
class ModuleRuntimeAbstractionTest : public ::testing::Test {
protected:
std::shared_ptr<FakeRuntime> fake;
void SetUp() override {
logos_core_terminate_all();
logos_core_clear();
SubprocessManager::clearAll();
fake = std::make_shared<FakeRuntime>();
PluginManager::runtimes().clearForTests();
PluginManager::runtimes().registerRuntime(fake);
}
void TearDown() override {
logos_core_terminate_all();
logos_core_clear();
SubprocessManager::clearAll();
// Restore default runtime so other test suites aren't affected.
PluginManager::runtimes().clearForTests();
PluginManager::runtimes().registerRuntime(
std::make_shared<LogosCore::QtSubprocessRuntime>());
}
void registerPlugin(const std::string& name,
const std::vector<std::string>& deps = {}) {
std::string path = "/fake/" + name + "_plugin.so";
logos_core_register_plugin(name.c_str(), path.c_str());
std::vector<const char*> depPtrs;
for (const auto& d : deps) depPtrs.push_back(d.c_str());
logos_core_register_plugin_dependencies(
name.c_str(),
depPtrs.empty() ? nullptr : depPtrs.data(),
static_cast<int>(depPtrs.size()));
}
};
// =============================================================================
// Basic load/unload routing
// =============================================================================
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_CallsFakeRuntimeLoad) {
registerPlugin("foo");
int result = logos_core_load_plugin("foo");
ASSERT_EQ(result, 1);
ASSERT_EQ(fake->loadCalls.size(), 1u);
EXPECT_EQ(fake->loadCalls[0], "foo");
}
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_CallsSendTokenAfterLoad) {
registerPlugin("foo");
logos_core_load_plugin("foo");
ASSERT_EQ(fake->sendTokenCalls.size(), 1u);
EXPECT_EQ(fake->sendTokenCalls[0].first, "foo");
EXPECT_FALSE(fake->sendTokenCalls[0].second.empty());
}
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_MarksModuleAsLoaded) {
registerPlugin("foo");
logos_core_load_plugin("foo");
EXPECT_EQ(logos_core_is_plugin_loaded("foo"), 1);
}
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_StoresRuntimeInRegistry) {
registerPlugin("foo");
logos_core_load_plugin("foo");
auto rt = PluginManager::registry().runtimeFor("foo");
EXPECT_EQ(rt.get(), fake.get());
}
TEST_F(ModuleRuntimeAbstractionTest, UnloadPlugin_CallsFakeRuntimeTerminate) {
registerPlugin("foo");
logos_core_load_plugin("foo");
int result = logos_core_unload_plugin("foo");
ASSERT_EQ(result, 1);
ASSERT_EQ(fake->terminateCalls.size(), 1u);
EXPECT_EQ(fake->terminateCalls[0], "foo");
}
TEST_F(ModuleRuntimeAbstractionTest, UnloadPlugin_MarksModuleAsUnloaded) {
registerPlugin("foo");
logos_core_load_plugin("foo");
logos_core_unload_plugin("foo");
EXPECT_EQ(logos_core_is_plugin_loaded("foo"), 0);
}
// =============================================================================
// Dependency-ordered loads
// =============================================================================
TEST_F(ModuleRuntimeAbstractionTest, LoadWithDeps_LoadsInTopologicalOrder) {
// Chain: c depends on b, b depends on a.
// Expected load order: a, b, c.
registerPlugin("a");
registerPlugin("b", {"a"});
registerPlugin("c", {"b"});
int result = logos_core_load_plugin_with_dependencies("c");
ASSERT_EQ(result, 1);
ASSERT_EQ(fake->loadCalls.size(), 3u);
EXPECT_EQ(fake->loadCalls[0], "a");
EXPECT_EQ(fake->loadCalls[1], "b");
EXPECT_EQ(fake->loadCalls[2], "c");
}
TEST_F(ModuleRuntimeAbstractionTest, LoadWithDeps_SkipsAlreadyLoadedModules) {
registerPlugin("a");
registerPlugin("b", {"a"});
logos_core_load_plugin("a");
fake->loadCalls.clear();
logos_core_load_plugin_with_dependencies("b");
ASSERT_EQ(fake->loadCalls.size(), 1u);
EXPECT_EQ(fake->loadCalls[0], "b");
}
// =============================================================================
// terminateAll routing
// =============================================================================
TEST_F(ModuleRuntimeAbstractionTest, TerminateAll_CallsFakeTerminateAll) {
registerPlugin("foo");
logos_core_load_plugin("foo");
logos_core_terminate_all();
EXPECT_EQ(fake->terminateAllCount, 1);
EXPECT_EQ(logos_core_is_plugin_loaded("foo"), 0);
}
// =============================================================================
// Error paths
// =============================================================================
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_ReturnsFalseWhenRuntimeLoadFails) {
registerPlugin("bad");
fake->failOn.insert("bad");
int result = logos_core_load_plugin("bad");
EXPECT_EQ(result, 0);
}
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_DoesNotCallSendTokenOnLoadFailure) {
registerPlugin("bad");
fake->failOn.insert("bad");
logos_core_load_plugin("bad");
EXPECT_TRUE(fake->sendTokenCalls.empty());
}
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_DoesNotMarkAsLoadedOnFailure) {
registerPlugin("bad");
fake->failOn.insert("bad");
logos_core_load_plugin("bad");
EXPECT_EQ(logos_core_is_plugin_loaded("bad"), 0);
}
TEST_F(ModuleRuntimeAbstractionTest, LoadPlugin_ReturnsFalseForUnknownPlugin) {
int result = logos_core_load_plugin("not_registered");
EXPECT_EQ(result, 0);
EXPECT_TRUE(fake->loadCalls.empty());
}
+238
View File
@@ -0,0 +1,238 @@
// =============================================================================
// Tests for RuntimeRegistry: registration, selection, fan-out operations.
//
// These tests run entirely in-process with FakeRuntime stubs — no Qt event
// loop, no subprocess, no file I/O.
// =============================================================================
#include <gtest/gtest.h>
#include "runtime_registry.h"
#include <string>
#include <unordered_map>
#include <vector>
using namespace LogosCore;
// ---------------------------------------------------------------------------
// A minimal in-process stub runtime used by every test below.
// Anonymous namespace prevents ODR conflicts with other FakeRuntime definitions
// in the same binary (e.g. test_module_runtime_abstraction.cpp).
// ---------------------------------------------------------------------------
namespace {
struct FakeRuntime : public ModuleRuntime {
explicit FakeRuntime(std::string myId, std::string handledFormat = "")
: m_id(std::move(myId)), m_handledFormat(std::move(handledFormat)) {}
std::string id() const override { return m_id; }
bool canHandle(const ModuleDescriptor& desc) const override {
if (m_handledFormat.empty()) return true; // accepts anything
return desc.format == m_handledFormat;
}
bool load(const ModuleDescriptor& desc,
std::function<void(const std::string&)>,
LoadedModuleHandle& out) override {
out.name = desc.name;
out.pid = 42;
loadCalls.push_back(desc.name);
return loadShouldSucceed;
}
bool sendToken(const std::string& name, const std::string& token) override {
sendTokenCalls.push_back({name, token});
return true;
}
void terminate(const std::string& name) override {
terminateCalls.push_back(name);
}
void terminateAll() override { terminateAllCount++; }
bool hasModule(const std::string& name) const override {
(void)name; return false;
}
std::unordered_map<std::string, int64_t> getAllPids() const override {
return fakePids;
}
// Controllable behaviour
bool loadShouldSucceed = true;
// Call records
std::vector<std::string> loadCalls;
std::vector<std::pair<std::string, std::string>> sendTokenCalls;
std::vector<std::string> terminateCalls;
int terminateAllCount = 0;
// Configurable pids for getAllPids()
std::unordered_map<std::string, int64_t> fakePids;
private:
std::string m_id;
std::string m_handledFormat;
};
} // anonymous namespace
// =============================================================================
// Empty registry
// =============================================================================
TEST(RuntimeRegistryTest, SelectReturnsNullWhenEmpty) {
RuntimeRegistry reg;
ModuleDescriptor desc;
desc.name = "foo";
desc.format = "qt-plugin";
EXPECT_EQ(reg.select(desc), nullptr);
}
TEST(RuntimeRegistryTest, TerminateAllOnEmptyRegistryDoesNotCrash) {
RuntimeRegistry reg;
EXPECT_NO_THROW(reg.terminateAll());
}
TEST(RuntimeRegistryTest, GetAllPidsOnEmptyRegistryReturnsEmptyMap) {
RuntimeRegistry reg;
EXPECT_TRUE(reg.getAllPids().empty());
}
// =============================================================================
// canHandle dispatch
// =============================================================================
TEST(RuntimeRegistryTest, SelectPicksFirstCanHandleRuntime) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a", "qt-plugin");
auto rtB = std::make_shared<FakeRuntime>("b", "qt-plugin");
reg.registerRuntime(rtA);
reg.registerRuntime(rtB);
ModuleDescriptor desc;
desc.format = "qt-plugin";
auto selected = reg.select(desc);
ASSERT_NE(selected, nullptr);
EXPECT_EQ(selected->id(), "a");
}
TEST(RuntimeRegistryTest, SelectSkipsRuntimeThatCannotHandle) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a", "wasm");
auto rtB = std::make_shared<FakeRuntime>("b", "qt-plugin");
reg.registerRuntime(rtA);
reg.registerRuntime(rtB);
ModuleDescriptor desc;
desc.format = "qt-plugin";
auto selected = reg.select(desc);
ASSERT_NE(selected, nullptr);
EXPECT_EQ(selected->id(), "b");
}
TEST(RuntimeRegistryTest, SelectReturnsNullIfNoRuntimeHandlesFormat) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a", "wasm");
reg.registerRuntime(rtA);
ModuleDescriptor desc;
desc.format = "qt-plugin";
EXPECT_EQ(reg.select(desc), nullptr);
}
// =============================================================================
// Explicit runtimeConfig["id"] override
// =============================================================================
TEST(RuntimeRegistryTest, SelectUsesExplicitIdOverride) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a", "qt-plugin");
auto rtB = std::make_shared<FakeRuntime>("b", "qt-plugin");
reg.registerRuntime(rtA);
reg.registerRuntime(rtB);
ModuleDescriptor desc;
desc.format = "qt-plugin";
desc.runtimeConfig["id"] = "b"; // explicitly request the second one
auto selected = reg.select(desc);
ASSERT_NE(selected, nullptr);
EXPECT_EQ(selected->id(), "b");
}
TEST(RuntimeRegistryTest, SelectReturnsNullForUnknownExplicitId) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a", "qt-plugin");
reg.registerRuntime(rtA);
ModuleDescriptor desc;
desc.runtimeConfig["id"] = "nonexistent-id";
EXPECT_EQ(reg.select(desc), nullptr);
}
TEST(RuntimeRegistryTest, ExplicitIdDoesNotFallThroughToCanHandle) {
// Even if "other" is the only runtime that canHandle, requesting "missing"
// explicitly must return nullptr rather than silently routing to "other".
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("other"); // accepts anything
reg.registerRuntime(rtA);
ModuleDescriptor desc;
desc.runtimeConfig["id"] = "missing";
EXPECT_EQ(reg.select(desc), nullptr);
}
// =============================================================================
// terminateAll fan-out
// =============================================================================
TEST(RuntimeRegistryTest, TerminateAllCallsEveryRuntime) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a");
auto rtB = std::make_shared<FakeRuntime>("b");
reg.registerRuntime(rtA);
reg.registerRuntime(rtB);
reg.terminateAll();
EXPECT_EQ(rtA->terminateAllCount, 1);
EXPECT_EQ(rtB->terminateAllCount, 1);
}
// =============================================================================
// getAllPids aggregation
// =============================================================================
TEST(RuntimeRegistryTest, GetAllPidsAggregatesAcrossRuntimes) {
RuntimeRegistry reg;
auto rtA = std::make_shared<FakeRuntime>("a");
auto rtB = std::make_shared<FakeRuntime>("b");
rtA->fakePids["mod1"] = 100;
rtA->fakePids["mod2"] = 200;
rtB->fakePids["mod3"] = 300;
reg.registerRuntime(rtA);
reg.registerRuntime(rtB);
auto pids = reg.getAllPids();
EXPECT_EQ(pids.size(), 3u);
EXPECT_EQ(pids.at("mod1"), 100);
EXPECT_EQ(pids.at("mod2"), 200);
EXPECT_EQ(pids.at("mod3"), 300);
}
// =============================================================================
// clearForTests
// =============================================================================
TEST(RuntimeRegistryTest, ClearForTests_RemovesAllRuntimes) {
RuntimeRegistry reg;
reg.registerRuntime(std::make_shared<FakeRuntime>("a"));
reg.registerRuntime(std::make_shared<FakeRuntime>("b"));
reg.clearForTests();
ModuleDescriptor desc;
EXPECT_EQ(reg.select(desc), nullptr);
EXPECT_NO_THROW(reg.terminateAll());
}
@@ -1,7 +1,7 @@
// =============================================================================
// Tests for the process manager lifecycle, exposed via logos_core.h.
// Tests for SubprocessManager lifecycle, exposed via qt_test_adapter.h.
//
// The process manager maintains a registry of named child processes and
// The subprocess manager maintains a registry of named child processes and
// provides token-IPC (sendToken / receive) between the core and plugin
// host processes. These tests verify:
// - register / hasProcess / clearAll lifecycle
@@ -26,7 +26,7 @@ static void clearProcessState() {
logos_core_clear_processes();
}
class ProcessManagerTest : public ::testing::Test {
class SubprocessManagerTest : public ::testing::Test {
protected:
void SetUp() override { clearProcessState(); }
void TearDown() override { clearProcessState(); }
@@ -36,16 +36,16 @@ protected:
// register / hasProcess / clear lifecycle
// ---------------------------------------------------------------------------
TEST_F(ProcessManagerTest, RegisterProcess_HasProcessReturnsTrue) {
TEST_F(SubprocessManagerTest, RegisterProcess_HasProcessReturnsTrue) {
logos_core_register_process("my_plugin");
EXPECT_EQ(logos_core_has_process("my_plugin"), 1);
}
TEST_F(ProcessManagerTest, HasProcess_ReturnsFalseForUnregistered) {
TEST_F(SubprocessManagerTest, HasProcess_ReturnsFalseForUnregistered) {
EXPECT_EQ(logos_core_has_process("nope"), 0);
}
TEST_F(ProcessManagerTest, ClearAll_RemovesAllEntries) {
TEST_F(SubprocessManagerTest, ClearAll_RemovesAllEntries) {
logos_core_register_process("p1");
logos_core_register_process("p2");
logos_core_register_process("p3");
@@ -57,7 +57,7 @@ TEST_F(ProcessManagerTest, ClearAll_RemovesAllEntries) {
EXPECT_EQ(logos_core_has_process("p3"), 0);
}
TEST_F(ProcessManagerTest, RegisterProcess_IsIdempotent) {
TEST_F(SubprocessManagerTest, RegisterProcess_IsIdempotent) {
logos_core_register_process("dup");
logos_core_register_process("dup");
logos_core_register_process("dup");
@@ -68,7 +68,7 @@ TEST_F(ProcessManagerTest, RegisterProcess_IsIdempotent) {
EXPECT_EQ(logos_core_has_process("dup"), 0);
}
TEST_F(ProcessManagerTest, NullName_DoesNotCrash) {
TEST_F(SubprocessManagerTest, NullName_DoesNotCrash) {
logos_core_register_process(nullptr);
EXPECT_EQ(logos_core_has_process(nullptr), 0);
}
@@ -77,14 +77,14 @@ TEST_F(ProcessManagerTest, NullName_DoesNotCrash) {
// get_process_id: placeholder entry returns -1
// ---------------------------------------------------------------------------
TEST_F(ProcessManagerTest, GetProcessId_ReturnsNegativeOneForPlaceholder) {
TEST_F(SubprocessManagerTest, GetProcessId_ReturnsNegativeOneForPlaceholder) {
logos_core_register_process("placeholder");
// Placeholder has no real process — should return -1.
int64_t pid = logos_core_get_process_id("placeholder");
EXPECT_EQ(pid, -1);
}
TEST_F(ProcessManagerTest, GetProcessId_ReturnsNegativeOneForUnknown) {
TEST_F(SubprocessManagerTest, GetProcessId_ReturnsNegativeOneForUnknown) {
int64_t pid = logos_core_get_process_id("unknown");
EXPECT_EQ(pid, -1);
}
@@ -100,7 +100,7 @@ static const char* sleepBinary() {
return nullptr;
}
TEST_F(ProcessManagerTest, StartProcess_ReturnsOneOnSuccess) {
TEST_F(SubprocessManagerTest, StartProcess_ReturnsOneOnSuccess) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";
@@ -109,7 +109,7 @@ TEST_F(ProcessManagerTest, StartProcess_ReturnsOneOnSuccess) {
EXPECT_EQ(ok, 1);
}
TEST_F(ProcessManagerTest, StartProcess_HasProcessReturnsTrueAfterStart) {
TEST_F(SubprocessManagerTest, StartProcess_HasProcessReturnsTrueAfterStart) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";
@@ -118,7 +118,7 @@ TEST_F(ProcessManagerTest, StartProcess_HasProcessReturnsTrueAfterStart) {
EXPECT_EQ(logos_core_has_process("sleep_has"), 1);
}
TEST_F(ProcessManagerTest, StartProcess_GetProcessIdReturnsValidPid) {
TEST_F(SubprocessManagerTest, StartProcess_GetProcessIdReturnsValidPid) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";
@@ -129,7 +129,7 @@ TEST_F(ProcessManagerTest, StartProcess_GetProcessIdReturnsValidPid) {
EXPECT_GT(pid, 0) << "started process must have a positive PID";
}
TEST_F(ProcessManagerTest, StartProcess_ReturnsFalseForNonexistentExecutable) {
TEST_F(SubprocessManagerTest, StartProcess_ReturnsFalseForNonexistentExecutable) {
const char* args[] = {nullptr};
int ok = logos_core_start_process("bad_exec",
"/nonexistent/binary_that_does_not_exist",
@@ -137,7 +137,7 @@ TEST_F(ProcessManagerTest, StartProcess_ReturnsFalseForNonexistentExecutable) {
EXPECT_EQ(ok, 0);
}
TEST_F(ProcessManagerTest, TerminateProcess_RemovesEntry) {
TEST_F(SubprocessManagerTest, TerminateProcess_RemovesEntry) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";
@@ -150,13 +150,13 @@ TEST_F(ProcessManagerTest, TerminateProcess_RemovesEntry) {
EXPECT_EQ(logos_core_has_process("sleep_term"), 0);
}
TEST_F(ProcessManagerTest, TerminateProcess_NoopForUnknownName) {
TEST_F(SubprocessManagerTest, TerminateProcess_NoopForUnknownName) {
// Should not crash when terminating a name that was never registered.
logos_core_terminate_process("i_do_not_exist");
SUCCEED();
}
TEST_F(ProcessManagerTest, TerminateProcess_NoopForNullName) {
TEST_F(SubprocessManagerTest, TerminateProcess_NoopForNullName) {
logos_core_terminate_process(nullptr);
SUCCEED();
}
@@ -165,7 +165,7 @@ TEST_F(ProcessManagerTest, TerminateProcess_NoopForNullName) {
// Multiple distinct processes coexist without colliding
// ---------------------------------------------------------------------------
TEST_F(ProcessManagerTest, MultipleProcesses_DistinctPids) {
TEST_F(SubprocessManagerTest, MultipleProcesses_DistinctPids) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";
@@ -181,7 +181,7 @@ TEST_F(ProcessManagerTest, MultipleProcesses_DistinctPids) {
EXPECT_NE(pidA, pidB) << "two separate processes must have different PIDs";
}
TEST_F(ProcessManagerTest, MultipleProcesses_TerminateOneKeepsOther) {
TEST_F(SubprocessManagerTest, MultipleProcesses_TerminateOneKeepsOther) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";
@@ -199,7 +199,7 @@ TEST_F(ProcessManagerTest, MultipleProcesses_TerminateOneKeepsOther) {
// terminateAll removes all running processes
// ---------------------------------------------------------------------------
TEST_F(ProcessManagerTest, TerminateAll_RemovesAllRunningProcesses) {
TEST_F(SubprocessManagerTest, TerminateAll_RemovesAllRunningProcesses) {
const char* sleep = sleepBinary();
if (!sleep) GTEST_SKIP() << "sleep binary not found";