mirror of
https://github.com/logos-co/logos-liblogos.git
synced 2026-08-27 12:51:10 +00:00
full module runtime test
This commit is contained in:
+101
-51
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 today’s `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 today’s `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 today’s 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 SDK’s 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 today’s `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 app’s 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
@@ -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}
|
||||
)
|
||||
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+12
-10
@@ -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
|
||||
+20
-21
@@ -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
|
||||
@@ -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
@@ -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();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user