diff --git a/API.md b/API.md index f0e5533..b41102f 100644 --- a/API.md +++ b/API.md @@ -1,61 +1,281 @@ # API Documentation -## 1. TCP Control Interface (Port 8080) - -### 1.1 `TREE` -Retrieves the full object hierarchy. -- **Request:** `TREE -` -- **Response:** `JSON_OBJECT -OK TREE -` - -### 1.2 `DISCOVER` -Lists all registrable signals and their metadata. -- **Request:** `DISCOVER -` -- **Response:** `{"Signals": [...]} -OK DISCOVER -` - -### 1.3 `TRACE ` -Enables/disables telemetry for a signal. -- **Example:** `TRACE App.Data.Timer.Counter 1 -` -- **Response:** `OK TRACE -` - -### 1.4 `FORCE ` -Overrides a signal value in memory. -- **Example:** `FORCE App.Data.DDB.Signal 123.4 -` -- **Response:** `OK FORCE -` - -### 1.5 `PAUSE` / `RESUME` -Controls global execution state via the Scheduler. -- **Request:** `PAUSE -` -- **Response:** `OK -` +This document covers both transport implementations. The command text protocol is identical +for both; only the carrier differs. --- -## 2. UDP Telemetry Format (Port 8081) +## 1. DebugService — TCP Command Interface (default port 8080) -Telemetry packets are Little-Endian and use `#pragma pack(1)`. +Commands are newline-terminated (`\n`) UTF-8 text strings sent over a persistent TCP +connection. One client is served at a time; concurrent connections queue. -### 2.1 TraceHeader (20 Bytes) -| Offset | Type | Name | Description | -| :--- | :--- | :--- | :--- | -| 0 | uint32 | magic | Always `0xDA7A57AD` | -| 4 | uint32 | seq | Incremental sequence number | -| 8 | uint64 | timestamp | High-resolution timestamp | -| 16 | uint32 | count | Number of samples in payload | +A client sending more than **100 commands per second** is disconnected (rate limit). +A client that sends no data for **30 seconds** is disconnected (idle timeout). -### 2.2 Sample Entry -| Offset | Type | Name | Description | -| :--- | :--- | :--- | :--- | -| 0 | uint32 | id | Internal Signal ID (from `DISCOVER`) | -| 4 | uint32 | size | Data size in bytes | -| 8 | Bytes | data | Raw signal memory | +### 1.1 `DISCOVER` +List all registered signals with full metadata. + +- **Request:** `DISCOVER\n` +- **Response:** + ``` + {"Signals":[{"id":,"name":"…","alias":"…","type":"…","elements":},...]} + OK DISCOVER + ``` + +### 1.2 `TREE` +Return the full live `ObjectRegistryDatabase` hierarchy as JSON. + +- **Request:** `TREE\n` +- **Response:** + ```json + {"name":"Root","children":[…]} + OK TREE + ``` + +### 1.3 `INFO ` +Return metadata for a specific ORD node. Response is enriched with config fields +(Frequency, Units, PVNames, …) when a full config has been provided via `SetFullConfig`. + +- **Request:** `INFO App.Functions.GAM1\n` +- **Response:** + ``` + {"path":"…","class":"…","signals":[…],"config":{…}} + OK INFO + ``` + +### 1.4 `LS [path]` +List the immediate children of an ORD node. + +- **Request:** `LS App.Data\n` +- **Response:** + ``` + {"nodes":["Timer","DDB"]} + OK LS + ``` + +### 1.5 `TRACE <0|1> [decimation]` +Enable or disable high-speed tracing for a signal. Optional `decimation` controls +how many RT cycles are skipped between samples (default 1 = every cycle). + +- **Request:** `TRACE App.Data.DDB.Counter 1\n` +- **Request:** `TRACE App.Data.DDB.Counter 1 10\n` *(every 10th sample)* +- **Response:** `OK TRACE \n` + +### 1.6 `FORCE ` +Inject a persistent value into a signal's memory location. The RT broker will copy +the forced value on every cycle until `UNFORCE` is called. + +- **Request:** `FORCE App.Data.DDB.Counter 9999\n` +- **Response:** `OK FORCE \n` + +### 1.7 `UNFORCE ` +Remove a forced value; the signal resumes its normal data-flow value. + +- **Request:** `UNFORCE App.Data.DDB.Counter\n` +- **Response:** `OK UNFORCE \n` + +### 1.8 `BREAK ` +Set a conditional breakpoint on a signal. When the condition fires, the application +pauses and execution stepping becomes available. Supported operators: + +| `op` string | Condition | +|---|---| +| `>` | signal > threshold | +| `<` | signal < threshold | +| `==` | signal == threshold | +| `>=` | signal >= threshold | +| `<=` | signal <= threshold | +| `!=` | signal != threshold | +| `OFF` | disable break (same as `BREAK OFF`) | + +- **Request:** `BREAK App.Data.DDB.Counter > 1000\n` +- **Response:** `OK BREAK \n` + +### 1.9 `BREAK OFF` +Clear the breakpoint on a signal (equivalent to `BREAK OFF 0`). + +- **Request:** `BREAK App.Data.DDB.Counter OFF\n` +- **Response:** `OK BREAK \n` + +### 1.10 `PAUSE` +Halt all patched RT threads at the start of their next Execute cycle. + +- **Request:** `PAUSE\n` +- **Response:** `OK\n` + +### 1.11 `RESUME` +Release paused RT threads. + +- **Request:** `RESUME\n` +- **Response:** `OK\n` + +### 1.12 `STEP [thread]` +Resume execution for exactly `n` RT output-broker cycles then pause again. +Optional `thread` (OS thread name) restricts counting to one thread. + +- **Request:** `STEP 5\n` +- **Request:** `STEP 1 RealTimeThread1\n` +- **Response:** `OK STEP\n` + +### 1.13 `STEP_STATUS` +Query current pause and stepping state. + +- **Request:** `STEP_STATUS\n` +- **Response:** + ```json + {"paused":true,"gam":"App.Functions.GAM1","remaining":3} + OK STEP_STATUS + ``` + +### 1.14 `VALUE ` +Read the current raw value of a signal from its memory address. Arrays are +capped at 256 elements; larger signals include `"truncated":true`. + +- **Request:** `VALUE App.Data.DDB.Counter\n` +- **Response:** + ```json + {"name":"App.Data.DDB.Counter","type":"uint32","elements":1,"values":[42]} + OK VALUE + ``` + +### 1.15 `MONITOR SIGNAL ` +Register a signal for slow-rate polling. The service reads the signal directly +from its `DataSourceI` and sends the value at the specified period. + +- **Request:** `MONITOR SIGNAL App.Data.DDB.Counter 500\n` +- **Response:** `OK MONITOR \n` + +On `DebugService`, monitored values are pushed over UDP with signal ID = internal monitor ID. +On `WebDebugService`, they are broadcast as `{"type":"monitor",…}` SSE events. + +### 1.16 `UNMONITOR SIGNAL ` +Stop polling a monitored signal. + +- **Request:** `UNMONITOR SIGNAL App.Data.DDB.Counter\n` +- **Response:** `OK UNMONITOR\n` + +### 1.17 `CONFIG` +Return the full application configuration as JSON. Requires that `SetFullConfig()` was +called after `ConfigureApplication()`. + +- **Request:** `CONFIG\n` +- **Response:** + ```json + {"App":{"Class":"RealTimeApplication",…}} + OK CONFIG + ``` + +### 1.18 `MSG [key=value …]` +Send a MARTe2 `Message` to any object in the ORD. + +- **Request:** `MSG App.Functions.GAM1 Reset\n` +- **Request:** `MSG App.StateMachine GOTOSTATE NewState=Running\n` +- **Response:** `OK MSG\n` or `ERROR MSG \n` + +### 1.19 `SERVICE_INFO` +Return metadata about the debug service itself. + +- **Request:** `SERVICE_INFO\n` +- **Response:** + ```json + {"transport":"TCP","controlPort":8080,"udpPort":8081,"logPort":8082} + ``` + +--- + +## 2. WebDebugService — HTTP Interface (default port 8090) + +### 2.1 `GET /` +Returns the embedded single-page application. Open in any web browser. + +### 2.2 `POST /api/command` +Execute any command from Section 1. The request body is the command text +(without a trailing newline). CORS headers are included. + +- **Request headers:** `Content-Type: text/plain` +- **Request body:** e.g. `TRACE App.Data.DDB.Counter 1` +- **Response:** Plain text; same content as the TCP response. + +### 2.3 `GET /api/events` +Long-lived Server-Sent Events stream. Up to 8 simultaneous clients. + +Each event is formatted as: +``` +event: message +data: + +``` +(blank line terminates each event per SSE spec) + +#### SSE event types + +| `type` field | Additional fields | Description | +|---|---|---| +| `trace` | `name`, `ts` (ns), `value` (f64) | One traced signal sample | +| `monitor` | `name`, `ts` (ns), `value` (f64) | One slow-rate monitor sample | +| `log` | `level`, `msg` | Forwarded `REPORT_ERROR` log entry | +| `status` | `paused` (bool), `gam`, `remaining` | Pause/step state heartbeat (every 500 ms) | +| `discover` | `signals` (array) | Full signal list (sent on SSE connect) | +| `tree` | `tree` (object) | Full ORD tree (sent on SSE connect) | + +Example trace event: +``` +event: message +data: {"type":"trace","name":"App.Data.DDB.Counter","ts":1234567890,"value":42.0} + +``` + +--- + +## 3. UDP Telemetry Format (DebugService, default port 8081) + +Packets are little-endian and packed (`#pragma pack(1)`). + +### 3.1 `TraceHeader` (20 bytes) + +| Offset | Type | Field | Description | +|---|---|---|---| +| 0 | uint32 | `magic` | Always `0xDA7A57AD` | +| 4 | uint32 | `seq` | Monotonically incrementing sequence number | +| 8 | uint64 | `timestamp` | High-resolution hardware timestamp | +| 16 | uint32 | `count` | Number of samples in this datagram | + +### 3.2 Sample Entry (per signal) + +| Offset | Type | Field | Description | +|---|---|---|---| +| 0 | uint32 | `id` | Signal ID from `DISCOVER` | +| 4 | uint64 | `timestamp` | Per-sample RT timestamp | +| 12 | uint32 | `size` | Payload size in bytes | +| 16 | bytes | `data` | Raw signal memory (`size` bytes) | + +Multiple samples may be packed into one datagram up to `STREAMER_MTU = 1400` bytes. +A new datagram is started when the next sample would overflow the MTU. + +--- + +## 4. Log Forwarding (DebugService, default port 8082) + +The `TcpLogger` component (`Source/Components/Interfaces/TCPLogger/`) connects to the +MARTe2 global `LoggerI` and forwards every `REPORT_ERROR` call as a text line: + +``` +|||\n +``` + +Up to 8 simultaneous log clients are supported. If no client is connected, log events +are silently discarded. + +To enable: + +```text ++Logger = { + Class = TcpLogger + Port = 8082 + MaxClients = 8 +} +``` + +On `WebDebugService`, logs are forwarded to SSE clients as `{"type":"log","level":"…","msg":"…"}` +events — no separate `TcpLogger` instance is required. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f279b12..1433e9f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,44 +1,269 @@ # System Architecture ## 1. Overview -The suite consists of a C++ server library (`libmarte_dev.so`) that instrument MARTe2 applications at runtime, and a native Rust client for visualization. + +The MARTe2 Debug Suite is a C++ server library that instruments running MARTe2 applications +without modifying their source code. It exposes an abstract `DebugServiceI` interface that +decouples the broker injection layer from the communication transport, allowing multiple +transport backends to coexist (currently TCP/UDP and HTTP/SSE). + +``` +┌────────────────────────────────────────────────────────────────────┐ +│ MARTe2 Application │ +│ ┌──────────┐ ┌───────────────────────┐ ┌──────────────────┐ │ +│ │ GAM(s) │ │ DebugBrokerWrapper │ │ FastScheduler │ │ +│ │ │◄──│ (Registry-patched) │ │ (unmodified) │ │ +│ └──────────┘ └──────────┬────────────┘ └──────────────────┘ │ +│ │ RT-path API │ +└────────────────────────────┼───────────────────────────────────────┘ + │ + ┌────────▼────────┐ + │ DebugServiceI │ ← Abstract interface (singleton) + └────────┬────────┘ + ┌─────────────┴──────────────┐ + │ │ + ┌────────▼────────┐ ┌─────────▼────────┐ + │ DebugService │ │ WebDebugService │ + │ TCP/UDP │ │ HTTP/SSE │ + └────────┬────────┘ └─────────┬─────────┘ + │ │ + ┌──────────┼──────────┐ ┌──────────┼──────────┐ + │TCP 8080 │UDP 8081 │ │HTTP 8090 │SSE stream │ + │(commands)│(telemetry│ │(commands)│(telemetry)│ + └────┬─────┴────┬─────┘ └─────┬────┴────┬──────┘ + │ │ │ │ + ┌────▼──────────▼─────┐ ┌─────────▼─────────▼──────┐ + │ Rust/egui GUI │ │ Web Browser (built-in UI)│ + │ (Tools/gui_client) │ │ http://localhost:8090 │ + └─────────────────────┘ └───────────────────────────┘ +``` + +--- ## 2. Core Mechanism: Registry Patching -The "Zero-Code-Change" requirement is met by intercepting the MARTe2 `ClassRegistryDatabase`. When the `DebugService` initializes, it replaces the standard object builders for critical classes: -1. **Broker Injection:** Standard Brokers (e.g., `MemoryMapInputBroker`) are replaced with `DebugBrokerWrapper`. This captures data every time a GAM reads or writes a signal. -2. **Scheduler Injection:** The `FastScheduler` is replaced with `DebugFastScheduler`. This provides hooks at the start/end of each real-time cycle for execution control (pausing). +The "Zero-Code-Change" requirement is met by intercepting the MARTe2 `ClassRegistryDatabase`. +When `DebugService` or `WebDebugService` initialises, `PatchRegistry()` replaces the +`ObjectBuilder` for every standard `MemoryMap*Broker` class in the registry. When +`RealTimeApplication::ConfigureApplication()` runs afterward it instantiates wrapped broker +objects instead of the originals — the application sees no difference. -## 3. Communication Layer -The system uses three distinct channels: +Patched broker types: -### 3.1 Command & Control (TCP Port 8080) -- **Protocol:** Text-based over TCP. -- **Role:** Object tree discovery (`TREE`), signal metadata (`DISCOVER`), and trace activation (`TRACE`). -- **Response Format:** JSON for complex data, `OK/ERROR` for status. +| Class | Wrapper | +|---|---| +| `MemoryMapInputBroker` | `DebugBrokerWrapper` | +| `MemoryMapOutputBroker` | `DebugBrokerWrapper` | +| `MemoryMapSynchronisedInputBroker` | `DebugBrokerWrapper` | +| `MemoryMapSynchronisedOutputBroker` | `DebugBrokerWrapper` | +| `MemoryMapMultiBufferBroker` | `DebugBrokerWrapper` | +| `MemoryMapMultiBufferOutputBroker` | `DebugBrokerWrapper` | +| `MemoryMapAsynchronousInputBroker` | `DebugBrokerWrapperAsync` | +| `MemoryMapAsynchronousOutputBroker` | `DebugBrokerWrapperAsync` | +| `MemoryMapInterpolatedInputBroker` | `DebugBrokerWrapper` | +| `MemoryMapStatefulOutputBroker` | `DebugBrokerWrapper` | +| `MemoryMapStatefulInputBroker` | `DebugBrokerWrapper` | -### 3.2 High-Speed Telemetry (UDP Port 8081) -- **Protocol:** Binary over UDP. -- **Format:** Packed C-structs. -- **Header:** `[Magic:4][Seq:4][TS:8][Count:4]` (Packed, 20 bytes). -- **Payload:** `[ID:4][Size:4][Data:N]` (Repeated for each traced signal). +--- -### 3.3 Log Streaming (TCP Port 8082) -- **Protocol:** Real-time event streaming. -- **Service:** `TcpLogger` (Standalone component). -- **Role:** Forwards global `REPORT_ERROR` calls and `stdout` messages to the GUI client. +## 3. DebugServiceI Abstraction Layer -## 4. Component Diagram -```text -[ MARTe2 App ] <--- [ DebugFastScheduler ] (Registry Patch) - | | - + <--- [ DebugBrokerWrapper ] (Registry Patch) - | | - +---------------------+ - | | -[ DebugService ] [ TcpLogger ] - | | - +--- (TCP 8080) ------+-----> [ Rust GUI Client ] - +--- (UDP 8081) ------+-----> [ (Oscilloscope) ] - +--- (TCP 8082) ------+-----> [ (Log Terminal) ] +`Source/Components/Interfaces/DebugService/DebugServiceI.h` defines the abstract singleton +interface. It has two logical groups: + +### 3.1 RT-path API +Called from broker threads on every RT cycle. Must be as cheap as possible; the only +permissible synchronisation primitive is `FastPollingMutexSem`. + +| Method | Purpose | +|---|---| +| `RegisterSignal(addr, type, name, …)` | Called once per signal during broker `Init()` | +| `ProcessSignal(info, size, timestamp)` | Apply forcing, check break, push trace sample | +| `RegisterBroker(…)` | Cache broker metadata for active/break index updates | +| `IsPaused() / SetPaused(bool)` | Query / set global pause state | +| `ConsumeStepIfNeeded(gamName, thread)` | Decrement step counter on output-broker Execute | + +### 3.2 Control-path API +Called from server/command-handler threads; latency acceptable. + +| Method | Purpose | +|---|---| +| `ForceSignal / UnforceSignal` | Persistent value injection | +| `TraceSignal(name, enable, decimation)` | Enable/disable ring-buffer tracing | +| `SetBreak / ClearBreak` | Conditional breakpoints | +| `IsInstrumented(path, …)` | Query whether a signal is reachable | +| `RegisterMonitorSignal / UnmonitorSignal` | Slow-rate signal polling | + +### 3.3 Static singleton pattern + +```cpp +// Concrete implementation registers itself: +DebugServiceI::SetInstance(this); // in Initialise() +DebugServiceI::SetInstance(NULL_PTR(…)); // in destructor + +// Broker wrappers retrieve it (no ORD search on the hot path): +DebugServiceI *svc = DebugServiceI::GetInstance(); +if (svc != NULL_PTR(DebugServiceI*)) { ... } +``` + +Shared data structures (`SignalAlias`, `BrokerInfo`) are also defined in `DebugServiceI.h` +so both transport implementations can use them without a circular dependency. + +--- + +## 4. Transport Implementations + +### 4.1 DebugService (TCP/UDP) + +`Source/Components/Interfaces/DebugService/DebugService.h/.cpp` + +Runs two `SingleThreadService` threads: + +| Thread | Role | +|---|---| +| `Server()` | Accepts one TCP client at a time; reads text commands; writes JSON/text responses | +| `Streamer()` | Drains `TraceRingBuffer`; assembles and sends UDP datagrams; polls monitored signals | + +Communication ports (all configurable): + +| Port | Protocol | Purpose | +|---|---|---| +| `ControlPort` (default 8080) | TCP text | Commands and responses | +| `UdpPort` (default 8081) | UDP binary | High-speed telemetry (`TraceHeader` + samples) | +| `LogPort` (default 8082) | TCP stream | Log forwarding via `TcpLogger` sidecar | + +### 4.2 WebDebugService (HTTP/SSE) + +`Source/Components/Interfaces/WebDebugService/WebDebugService.h/.cpp` + +Serves a single HTTP port (default 8090) with an embedded browser UI. Runs two +`SingleThreadService` threads: + +| Thread | Role | +|---|---| +| `HttpServer()` | Accepts TCP connections; routes GET/POST/OPTIONS; upgrades SSE clients | +| `Streamer()` | Drains `TraceRingBuffer` → SSE events; polls monitored signals; heartbeat | + +HTTP endpoints: + +| Endpoint | Method | Purpose | +|---|---|---| +| `/` | GET | Serves embedded single-page application (HTML/CSS/JS) | +| `/api/command` | POST | Execute a debug command; returns text response | +| `/api/events` | GET | Server-Sent Events stream for real-time telemetry (up to 8 clients) | + +The embedded web UI (`Source/Components/Interfaces/WebDebugService/WebUI.h`) is a +self-contained single-page application using Chart.js for real-time signal plots and a +Catppuccin Mocha dark theme. No install or server-side tooling required. + +--- + +## 5. Signal Flow + +### 5.1 Registration (startup, single-threaded) + +``` +ConfigureApplication() + └─► DebugBrokerWrapper::Init() + └─► DebugBrokerHelper::InitSignals() + ├─► DebugServiceI::RegisterSignal() (canonical name + GAM alias) + └─► DebugServiceI::RegisterBroker() +``` + +### 5.2 RT loop (hot path, multi-threaded) + +``` +RealTimeThread::Execute() + └─► DebugBrokerWrapper::Execute() + ├─► Base::Execute() (data moved in/out of GAM memory) + └─► DebugBrokerHelper::Process() + ├─► For each active signal: + │ └─► DebugServiceI::ProcessSignal() + │ ├─► if isForcing: memcpy forcedValue → signal memory + │ ├─► if isTracing & decimation fires: + │ │ └─► tracePushMutex → TraceRingBuffer::Push() + │ └─► if breakOp set: evaluate condition → SetPaused(true) + └─► (output brokers only) DebugServiceI::ConsumeStepIfNeeded() +``` + +### 5.3 Telemetry delivery + +**DebugService (UDP)** +``` +Streamer thread + └─► TraceRingBuffer::Pop() + └─► assemble into streamerPacketBuffer (MTU=1400) + └─► BasicUDPSocket::Write() → client +``` + +**WebDebugService (SSE)** +``` +Streamer thread + └─► TraceRingBuffer::Pop() + └─► BroadcastSse("message", {"type":"trace","name":…,"value":…}) + └─► write "event: message\ndata: {…}\n\n" to each SSE socket +``` + +--- + +## 6. TraceRingBuffer + +Single-producer / single-consumer circular byte buffer defined in `DebugCore.h`. + +``` +Buffer layout per entry: [ID:4][Timestamp:8][Size:4][Data:N] +``` + +- `Push()` is serialised by `tracePushMutex` (multiple RT threads write; one Streamer reads). +- `Pop()` is called exclusively by the Streamer thread. +- Ring is initialised to 4 MB in both transport implementations. +- On corrupt `size` field (`size >= bufferSize`): discards all entries to prevent cascading + bad reads (Fix #5). + +--- + +## 7. Performance Hardening + +The following fixes were applied after initial implementation to improve correctness and +real-time safety. + +| Fix | Area | Change | +|---|---|---| +| **#1** | UDP streamer | Named constants `STREAMER_MTU = 1400` and `STREAMER_BUFFER_SIZE = 4096`; removed magic numbers | +| **#2** | Multi-producer ring | `tracePushMutex` serialises `TraceRingBuffer::Push()` from concurrent RT threads | +| **#3** | Break evaluation | Break indices copied to stack-local array while holding `activeMutex`; condition evaluated outside the lock | +| **#4** | Decimation counter | `Atomic::Add` used for lock-free per-signal decimation in multi-producer `ProcessSignal` | +| **#5** | Ring corruption | `Pop()` skips only the current corrupt entry; falls back to full discard only when `size >= bufferSize` | +| **#6** | TCP rate limit | Server disconnects clients sending > 100 commands/second (`CMD_RATE_LIMIT = 100`) | +| **#7** | Broker index swap | `Vec::Swap` inside spinlock followed by free outside; avoids holding the lock during deallocation | +| **#8** | Idle timeout | Active TCP client disconnected after 30 s of silence (`CLIENT_IDLE_TIMEOUT_MS = 30000`) | +| **#9** | VALUE output size | `GetSignalValue` caps output at 256 elements (`GET_VALUE_MAX_ELEMENTS`); includes `"Truncated"` flag | +| **#10** | TCP framing | `inputBuffer` accumulates partial commands across `Read()` calls; bounded at 8 KiB (`INPUT_BUFFER_MAX`) | +| **#11** | CDB cursor | `Initialise()` never calls `MoveToRoot()` on the CDB passed by the framework; avoids corrupting `ReferenceContainer` traversal | + +--- + +## 8. Repository Layout + +``` +Source/ + Core/Types/ + Vec/ — RT-safe dynamic array (no STL) + Result/ — Error-carrying return type + Components/Interfaces/ + DebugService/ + DebugCore.h — DebugSignalInfo, TraceRingBuffer, BreakOp + DebugServiceI.h — Abstract interface + SignalAlias, BrokerInfo + DebugBrokerWrapper.h — DebugBrokerHelper, wrapper templates, registry builder + DebugService.h/.cpp — TCP/UDP transport implementation + WebDebugService/ + WebUI.h — Embedded single-page HTML/CSS/JS application + WebDebugService.h/.cpp — HTTP/SSE transport implementation + TCPLogger/ + TcpLogger.h/.cpp — LoggerConsumerI forwarding logs to TCP clients +Test/ + Integration/ — Integration test suite (180 s SIGALRM guard) + UnitTests/ — Unit test suite +Tools/ + gui_client/ — Rust/egui native GUI (DebugService TCP/UDP transport) ``` diff --git a/DEMO.md b/DEMO.md index aab81f7..e9ce65b 100644 --- a/DEMO.md +++ b/DEMO.md @@ -1,32 +1,81 @@ # Demo Walkthrough: High-Speed Tracing -This demo demonstrates tracing a `Timer.Counter` signal at 100Hz and verifying its consistency. +This demo traces a `Timer.Counter` signal at 100 Hz, tests execution control, and verifies +signal forcing. It works with either transport — choose the option that matches your setup. + +--- + +## 1. Build and Launch the Test Environment -### 1. Launch the Test Environment -Start the validation environment which simulates a real-time app: ```bash -./Build/Test/Integration/ValidationTest +source env.sh +make -f Makefile.gcc +./Build/x86-linux/Test/Integration/IntegrationTests ``` -*Note: The test will wait for a trace command before finishing.* -### 2. Connect the GUI -In another terminal: +The integration test starts a MARTe2 application with `DebugService` or `WebDebugService` +already configured and waits for client interaction. + +--- + +## 2. Connect a Client + +### Option A — Web Browser (`WebDebugService`) + +Open `http://localhost:8090` in any browser. The UI appears instantly with the live +object tree already loaded. + +### Option B — Rust GUI (`DebugService`) + ```bash cd Tools/gui_client cargo run --release ``` -### 3. Explore the Tree -1. On the left panel (**Application Tree**), expand `Root.App.Data.Timer`. -2. Click the `ℹ Info` button on the `Timer` node to see its configuration (e.g., `SleepTime: 10000`). +--- -### 4. Activate Trace -1. Locate the `Counter` signal under the `Timer` node. -2. Click the **📈 Trace** button. -3. The **Oscilloscope** in the center will immediately begin plotting the incremental counter. -4. Verify the **UDP Packets** counter in the top bar is increasing rapidly. +## 3. Explore the Tree -### 5. Test Execution Control -1. Click the **⏸ Pause** button in the top bar. -2. Observe that the plot stops updating and the counter value holds steady. -3. Click **▶ Resume** to continue execution. +1. In the **Application Tree**, expand `Root → App → Data → Timer`. +2. Click **Info** (web UI) or **ℹ Info** (Rust GUI) on the `Timer` node. +3. Verify the config panel shows `SleepTime: 10000` (100 Hz cycle). + +--- + +## 4. Activate Trace + +1. Locate the `Counter` signal under `Timer`. +2. Click **Trace** (web UI) or **📈 Trace** (Rust GUI). +3. The real-time plot begins with a monotonically increasing counter. +4. Confirm telemetry is flowing: + - **Web UI:** `{"type":"trace",…}` events appear in the browser DevTools Network tab. + - **Rust GUI:** The **UDP Packets** counter in the top bar increments rapidly. + +--- + +## 5. Test Execution Control + +1. Click **Pause** (⏸). +2. Observe that the plot freezes and the counter holds steady. +3. Click **Step** with count = 5. +4. The counter advances exactly 5 units then stops. +5. Click **Resume** (▶) to continue normal execution. + +--- + +## 6. Test Signal Forcing + +1. Locate `Root.App.Data.DDB.Counter`. +2. Click **Force** and enter `99999`. +3. Observe the oscilloscope plot jump to and hold at `99999`. +4. Click **Unforce** to release — the counter resumes its normal value. + +--- + +## 7. Test a Breakpoint + +1. Click **Break** on `Root.App.Data.DDB.Counter`. +2. Set operator `>` and threshold `500`. +3. Wait for the counter to exceed 500 — the application pauses automatically. +4. The status shows **PAUSED** at the GAM that triggered the break. +5. Click **Break OFF** then **Resume** to continue. diff --git a/README.md b/README.md index 5af8f4d..8be4ced 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,110 @@ # MARTe2 Debug Suite -An interactive observability and debugging suite for the MARTe2 real-time framework. +An interactive observability and debugging suite for the MARTe2 real-time framework. Instruments any running application **without modifying its source code**, providing real-time signal tracing, forced-value injection, breakpoints, execution stepping, and log forwarding. + +--- ## Quick Start -### 1. Build the project +### 1. Build + ```bash -. ./env.sh +source env.sh make -f Makefile.gcc ``` ### 2. Run Integration Tests + ```bash -./Build/x86-linux/Test/Integration/Integration/IntegrationTests.ex # Runs all tests +source env.sh +./Build/x86-linux/Test/Integration/Integration/IntegrationTests.ex ``` -### 3. Launch GUI +### 3. Choose Your Client + +**Option A — Web UI (no install required)** + +Add `WebDebugService` to your application config and open `http://localhost:8090` in any browser: + +```text ++WebDebugService = { + Class = WebDebugService + HttpPort = 8090 +} +``` + +**Option B — Rust Native GUI** + +Add `DebugService` to your application config, then launch the GUI: + ```bash cd Tools/gui_client cargo run --release ``` -## Features -- **Live Tree:** Explore the MARTe2 object database in real-time. -- **Oscilloscope:** Trace any signal at high frequency (100Hz+) with automatic scaling. -- **Signal Forcing:** Inject values directly into the real-time memory map. -- **Log Forwarding:** Integrated framework log viewer with regex filtering. -- **Execution Control:** Global pause/resume via scheduler-level hooks. - -## Usage -To enable debugging in your application, add the following to your `.cfg`: ```text +DebugService = { - Class = DebugService + Class = DebugService ControlPort = 8080 - UdpPort = 8081 -} - -+LoggerService = { - Class = LoggerService - +DebugConsumer = { - Class = TcpLogger - Port = 8082 - } + UdpPort = 8081 + LogPort = 8082 } ``` -The suite automatically patches the registry to instrument your existing Brokers and Schedulers. + +--- + +## Features + +| Feature | Description | +|---|---| +| **Zero-code-change** | Registry patching replaces standard brokers at runtime — no application changes needed | +| **Signal tracing** | Stream any signal at full RT rate over UDP (DebugService) or SSE (WebDebugService) | +| **Signal forcing** | Persistent value injection directly into the RT memory map | +| **Breakpoints** | Halt execution when a signal crosses a threshold (`> < == >= <= !=`) | +| **Execution stepping** | Step N RT cycles then pause; optionally filter by thread | +| **Live object tree** | Browse the full `ObjectRegistryDatabase` hierarchy with signal metadata | +| **Config serving** | Retrieve the complete application CDB as JSON | +| **Log forwarding** | All `REPORT_ERROR` events forwarded to the client in real time | +| **Signal monitoring** | Poll any `DataSourceI` signal at a configurable period | +| **MARTe2 messaging** | Send `Message` objects to any ORD object from the debug client | + +--- + +## Transports at a Glance + +| | `DebugService` | `WebDebugService` | +|---|---|---| +| **Client** | Rust native GUI or any TCP tool | Any web browser | +| **Commands** | Text over TCP (port 8080) | HTTP POST `/api/command` | +| **Telemetry** | Binary UDP (port 8081) | Server-Sent Events `/api/events` | +| **Logs** | TcpLogger TCP (port 8082) | SSE event stream | +| **Install** | Rust + Cargo | None | + +Both transports implement the same `DebugServiceI` interface and register themselves as the global singleton — they are mutually exclusive (one per application). + +--- + +## Repository Layout + +``` +Source/ + Core/Types/ + Vec/ — RT-safe dynamic array (no STL) + Result/ — Error-carrying return type + Components/Interfaces/ + DebugService/ + DebugCore.h — DebugSignalInfo, TraceRingBuffer, break op codes + DebugServiceI.h — Abstract interface + SignalAlias, BrokerInfo structs + DebugBrokerWrapper.h — Broker wrapper templates + registry builder helpers + DebugService.h/.cpp — TCP/UDP implementation + WebDebugService/ + WebUI.h — Embedded single-page HTML/CSS/JS application + WebDebugService.h/.cpp — HTTP/SSE implementation + TCPLogger/ + TcpLogger.h/.cpp — LoggerConsumerI forwarding logs to TCP clients +Test/ + Integration/ — Integration test suite (180 s SIGALRM guard) + UnitTests/ — Unit test suite (COVERAGE V34) +Tools/ + gui_client/ — Rust/egui native GUI (DebugService transport only) +``` diff --git a/SPECS.md b/SPECS.md index f4ea37a..0d38808 100644 --- a/SPECS.md +++ b/SPECS.md @@ -1,64 +1,145 @@ # MARTe2 Debug Suite Specifications -**Version:** 1.2 +**Version:** 2.0 **Status:** Active / Implemented +--- + ## 1. Executive Summary -This project implements a "Zero-Code-Change" observability and debugging layer for the MARTe2 real-time framework. The system allows developers to Trace, Force, and Monitor any signal in a running MARTe2 application without modifying existing source code. + +This project implements a "Zero-Code-Change" observability and debugging layer for the +MARTe2 real-time framework. Developers can trace, force, and monitor any signal in a +running MARTe2 application without modifying existing source code. + +The suite ships two interchangeable transport backends sharing a common abstract interface: + +- **`DebugService`** — TCP command channel + binary UDP telemetry + `TcpLogger` sidecar. + Paired with a native Rust/egui GUI. +- **`WebDebugService`** — HTTP command endpoint + Server-Sent Events telemetry, with an + embedded single-page browser UI. No additional client software required. + +--- ## 2. System Architecture -- **The Universal Debug Service (C++ Core):** A singleton MARTe2 Object that patches the registry and manages communication. -- **The Broker Injection Layer (C++ Templates):** Templated wrappers that intercept `Execute()` and `Init()` calls for tracing, forcing, and execution control. -- **The Remote Analyser (Rust/egui):** A high-performance, multi-threaded GUI for visualization and control. -- **Network Stack:** - - **Port 8080 (TCP):** Commands and Metadata. - - **Port 8081 (UDP):** High-Speed Telemetry for Oscilloscope. - - **Port 8082 (TCP):** Independent Real-Time Log Stream via `TcpLogger`. + +- **`DebugServiceI` (Abstract Interface):** Singleton that decouples the broker injection + layer from the transport. Defined in `DebugServiceI.h`. +- **`DebugService` (TCP/UDP Transport):** TCP text command channel (port 8080), binary UDP + telemetry (port 8081), `TcpLogger` log sidecar (port 8082). +- **`WebDebugService` (HTTP/SSE Transport):** Single HTTP port (default 8090) serving the + embedded web UI, command API, and SSE telemetry stream. +- **`DebugBrokerWrapper` (Injection Layer):** C++ template wrappers injected via registry + patching. Depend only on `DebugServiceI`; transport-agnostic. +- **`TcpLogger` (Log Sidecar):** Optional standalone component forwarding `REPORT_ERROR` + events to TCP clients on a dedicated port (used with `DebugService` only). + +--- ## 3. Requirements ### 3.1 Functional Requirements (FR) -- **FR-01 (Discovery):** Discover the full MARTe2 object hierarchy at runtime. The GUI client SHALL request the full application tree upon connection and display it in a hierarchical tree view. -- **FR-02 (Telemetry):** Stream high-frequency signal data (verified up to 100Hz+) to a remote client via UDP. + +- **FR-01 (Discovery):** Discover the full MARTe2 object hierarchy at runtime. The client + SHALL request the full application tree and display it hierarchically. +- **FR-02 (Telemetry):** Stream high-frequency signal data (verified > 100 Hz) to a remote + client. `DebugService` uses binary UDP; `WebDebugService` uses SSE. - **FR-03 (Forcing):** Allow manual override of signal values in memory during execution. -- **FR-04 (Logs):** Stream global framework logs to a dedicated terminal/client via a standalone `TcpLogger` service. -- **FR-05 (Log Filtering):** The client must support filtering logs by type (Debug, Information, Warning, FatalError) and by content using regular expressions. -- **FR-06 (Execution Control):** Provide a mechanism to pause and resume the execution of all patched real-time threads (via Brokers), allowing for static inspection of the system state. -- **FR-07 (Session Management):** Support runtime re-configuration and "Apply & Reconnect" logic. The GUI provides a "Disconnect" button to close active network streams. -- **FR-08 (Decoupled Tracing):** Tracing activates telemetry; data is buffered and shown as a "Last Value" in the sidebar, but not plotted until manually assigned. -- **FR-09 (Advanced Plotting):** - - Support multiple plot panels with perfectly synchronized time (X) axes. - - Drag-and-drop signals from the traced list into specific plots. - - Plot modes: Standard (Time Series) and Logic Analyzer (Stacked rows). - - Signal transformations: Gain, offset, units, and custom labels. -- **FR-10 (Navigation & Scope):** - - Context menus for resetting zoom (X, Y, or both). - - "Fit to View" functionality that automatically scales both axes. - - High-performance oscilloscope mode with configurable time windows (10ms to 10s). - - Triggered acquisition (Single/Continuous, rising/falling edges). -- **FR-11 (Data Recording):** Record any traced signal to disk in Parquet format with a visual recording indicator in the GUI. -- **FR-12 (Configuration Awareness):** The DebugService SHALL store a complete copy of the application's configuration provided at initialization. -- **FR-13 (Metadata Enrichment):** Metadata returned by `INFO` and `DISCOVER` SHALL be enriched with additional fields from the stored configuration (e.g., Frequency, PVNames, Units). -- **FR-14 (Configuration Serving):** Provide a `CONFIG` command to serve the full stored configuration in JSON format to the client. + The override SHALL persist across RT cycles until explicitly released. +- **FR-04 (Logs):** Forward all `REPORT_ERROR` events to a connected client in real time. + `DebugService` uses `TcpLogger` on a dedicated TCP port. `WebDebugService` embeds logs + in the SSE stream. +- **FR-05 (Log Filtering):** The client SHOULD support filtering log output by level and + by content. +- **FR-06 (Execution Control):** Provide pause and resume for all patched RT threads, + allowing static inspection of the system state. +- **FR-07 (Breakpoints):** Halt execution when a signal crosses a threshold. Supported + operators: `>`, `<`, `==`, `>=`, `<=`, `!=`. +- **FR-08 (Execution Stepping):** After a pause or breakpoint, allow resuming for exactly + N output-broker cycles then pausing again. Optional per-thread filter. +- **FR-09 (Decoupled Tracing):** Activating a trace SHALL only stream data; displaying + the data (plot, sidebar) is the client's responsibility. +- **FR-10 (Signal Monitoring):** Poll any `DataSourceI` signal at a configurable period + without requiring that signal to be traced at full RT rate. +- **FR-11 (Config Awareness):** The service SHALL accept and store a full copy of the + application's CDB (`SetFullConfig`). `INFO` and `DISCOVER` responses SHALL be enriched + with additional metadata fields from this CDB. +- **FR-12 (Config Serving):** Provide a `CONFIG` command returning the full stored + configuration as JSON. +- **FR-13 (MARTe2 Messaging):** Allow sending `Message` objects to any ORD object via an + `MSG` command. +- **FR-14 (Dual Transport):** Both `DebugService` and `WebDebugService` SHALL implement + the identical `DebugServiceI` interface and be mutually exclusive singletons. Switching + transport SHALL require only a config change (different class name, different port). +- **FR-15 (Embedded Web UI):** `WebDebugService` SHALL serve a self-contained single-page + application with real-time plotting (Chart.js), signal tree, force/break/monitor dialogs, + and an integrated log viewer — without any external server or build tooling. ### 3.2 Technical Constraints (TC) + - **TC-01:** No modifications allowed to the MARTe2 core library or component source code. -- **TC-02:** Instrumentation must use Runtime Class Registry Patching. -- **TC-03:** Real-time threads must remain lock-free; use `FastPollingMutexSem` or atomic operations for synchronization. -- **TC-04:** Telemetry must be delivered via UDP to minimize impact on real-time jitter. +- **TC-02:** Instrumentation MUST use runtime class registry patching (`ClassRegistryDatabase`). +- **TC-03:** RT-path methods (`ProcessSignal`, `IsPaused`) MUST be lock-free or use only + `FastPollingMutexSem`; no heap allocation on the RT path. +- **TC-04:** `DebugService` telemetry MUST use UDP to minimise jitter impact. +- **TC-05:** No STL — use `Vec` and `StreamString` throughout. +- **TC-06:** C++98 compatibility for all source files (no `auto`, no range-for, no raw + string literals). +- **TC-07:** The `DebugServiceI` abstract interface MUST be the only dependency of + `DebugBrokerWrapper`. Concrete transport headers MUST NOT be included by broker code. -## 4. Performance Metrics -- **Latency:** Telemetry dispatch overhead < 5 microseconds per signal. -- **Throughput:** Support for 100Hz+ sampling rates with zero packet loss on local networks. -- **Scalability:** Handle up to 4096 unique signals and 16 simultaneous client connections. -- **Code Quality:** Maintain a minimum of **85% code coverage** across all core service and broker logic. +--- -## 5. Communication Protocol (Port 8080) -- **LS [Path]:** List nodes at the specified path. -- **TREE:** Returns a full recursive JSON structure representing the entire application tree. -- **INFO [Path]:** Returns detailed metadata for a specific node or signal. -- **PAUSE / RESUME:** Global execution control. -- **TRACE <1/0> [Decimation]:** Enable/disable telemetry for a signal. -- **FORCE :** Persistent signal override. -- **UNFORCE :** Remove override. -- **LOG :** Streaming format used on Port 8082. +## 4. Performance Requirements + +- **Latency:** `ProcessSignal` overhead < 5 µs per signal when tracing is inactive. +- **Throughput:** Support > 100 Hz signal tracing with zero sample loss on local networks + (UDP transport). +- **Scalability:** Handle up to 4096 unique signals. +- **Ring buffer size:** `TraceRingBuffer` initialised to 4 MB in both transports. +- **SSE clients:** `WebDebugService` supports up to 8 simultaneous SSE clients. +- **TCP rate limit:** Server disconnects clients sending > 100 commands/s. +- **VALUE output cap:** `GetSignalValue` returns at most 256 elements per call. + +--- + +## 5. Performance Hardening Log + +| Fix | Category | Description | +|---|---|---| +| **#1** | Naming | `STREAMER_MTU = 1400`, `STREAMER_BUFFER_SIZE = 4096` replace magic numbers | +| **#2** | Concurrency | `tracePushMutex` serialises multi-producer `TraceRingBuffer::Push()` | +| **#3** | Concurrency | Break indices copied under `activeMutex`; condition evaluated outside lock | +| **#4** | Concurrency | `Atomic::Add` for lock-free per-signal decimation counter | +| **#5** | Robustness | `TraceRingBuffer::Pop` skips corrupt entry; full discard only when unrecoverable | +| **#6** | Security | TCP command rate-limit: > 100 cmd/s → client disconnect | +| **#7** | Latency | `Vec::Swap` inside spinlock; deallocation outside — shorter lock hold time | +| **#8** | Robustness | 30-second idle timeout on active TCP client | +| **#9** | Safety | `VALUE` output capped at 256 elements; `"truncated":true` flag included | +| **#10** | Correctness | `inputBuffer` reassembles split TCP commands; bounded at 8 KiB | +| **#11** | Correctness | `Initialise()` never calls `MoveToRoot()` on the framework-supplied CDB | + +--- + +## 6. Command Protocol Summary + +| Command | Transport | Description | +|---|---|---| +| `DISCOVER` | Both | Full signal list with metadata | +| `TREE` | Both | Live ORD hierarchy as JSON | +| `INFO ` | Both | Node metadata enriched from CDB | +| `LS [path]` | Both | Immediate children of ORD node | +| `TRACE <0\|1> [dec]` | Both | Enable/disable signal tracing | +| `FORCE ` | Both | Persistent signal value injection | +| `UNFORCE ` | Both | Remove forced value | +| `BREAK ` | Both | Set conditional breakpoint | +| `BREAK OFF` | Both | Clear breakpoint | +| `PAUSE` | Both | Halt all RT threads | +| `RESUME` | Both | Release paused RT threads | +| `STEP [thread]` | Both | Run N cycles then pause | +| `STEP_STATUS` | Both | Query pause/step state | +| `VALUE ` | Both | Read current signal value | +| `MONITOR SIGNAL ` | Both | Register slow-rate polling | +| `UNMONITOR SIGNAL ` | Both | Stop slow-rate polling | +| `CONFIG` | Both | Full CDB as JSON | +| `MSG [k=v …]` | Both | Send MARTe2 Message | +| `SERVICE_INFO` | Both | Transport metadata | diff --git a/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h b/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h index 5a06915..74f39f1 100644 --- a/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h +++ b/Source/Components/Interfaces/DebugService/DebugBrokerWrapper.h @@ -3,7 +3,7 @@ #include "BrokerI.h" #include "DataSourceI.h" -#include "DebugService.h" +#include "DebugServiceI.h" #include "FastPollingMutexSem.h" #include "HighResolutionTimer.h" #include "MemoryMapBroker.h" @@ -90,8 +90,11 @@ public: // Spins while paused, then consumes one step counter tick (if stepping). // Input brokers must NOT call this — they complete normally to avoid blocking // cross-thread EventSem posts (RealTimeThreadSynchBroker would time out). - static void OutputPauseAndStep(DebugService *service, const char8 *gamName) { - if (service == NULL_PTR(DebugService *)) return; + static void OutputPauseAndStep(DebugServiceI *service, const char8 *gamName) { + if (service == NULL_PTR(DebugServiceI *)) return; + // Fast path: nothing to do when neither paused nor stepping. + // Avoids Threads::Name() lookup (and its mutex) on every 1000 Hz cycle. + if (!service->IsPaused() && !service->IsStepPending()) return; // Wait if already paused (manual PAUSE or breakpoint from a previous cycle) while (service->IsPaused()) Sleep::MSec(10); // Pass the OS thread name so per-thread step filtering works. @@ -100,13 +103,13 @@ public: while (service->IsPaused()) Sleep::MSec(10); } - static void Process(DebugService *service, + static void Process(DebugServiceI *service, DebugSignalInfo **signalInfoPointers, Vec &activeIndices, Vec &activeSizes, FastPollingMutexSem &activeMutex, volatile bool *anyBreakFlag, Vec *breakIndices) { - if (service == NULL_PTR(DebugService *)) + if (service == NULL_PTR(DebugServiceI *)) return; // NOTE: No spin here. Spinning for paused state is handled in Execute() of @@ -118,7 +121,7 @@ public: if (n > 0 && signalInfoPointers != NULL_PTR(DebugSignalInfo **)) { // Capture timestamp ONCE per broker cycle for lowest impact uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * - HighResolutionTimer::Period() * 1000000.0); + HighResolutionTimer::Period() * 1.0e9); for (uint32 i = 0; i < n; i++) { uint32 idx = activeIndices[i]; @@ -130,13 +133,33 @@ public: } } - // Conditional break check — zero cost when anyBreakFlag is false - // (single volatile read; the RT loop never pays for this when no break is set). - if (*anyBreakFlag && !service->IsPaused() && - breakIndices != NULL_PTR(Vec *)) { - uint32 nb = breakIndices->Size(); + // FIX #3: copy break indices under lock into a local stack array, then + // release activeMutex BEFORE evaluating. EvaluateBreak reads signal + // memory which can stall (cache miss); holding activeMutex during that + // stall would block UpdateBrokersBreakStatus() in the Server thread and + // cause unnecessary priority inversion on the RT path. + bool shouldCheckBreak = (*anyBreakFlag && !service->IsPaused() && + breakIndices != NULL_PTR(Vec *) && + signalInfoPointers != NULL_PTR(DebugSignalInfo **)); + static const uint32 MAX_BREAK_INDICES = 64u; + uint32 localBreakIdx[MAX_BREAK_INDICES]; + uint32 nb = 0u; + if (shouldCheckBreak) { + nb = breakIndices->Size(); + if (nb > MAX_BREAK_INDICES) nb = MAX_BREAK_INDICES; for (uint32 i = 0; i < nb; i++) { - uint32 idx = (*breakIndices)[i]; + localBreakIdx[i] = (*breakIndices)[i]; + } + } + + activeMutex.FastUnLock(); + + // Evaluate break conditions outside the lock — safe because + // EvaluateBreak only reads signalInfoPointers[idx]->memoryAddress, + // which is RT data-bus memory and is never freed during the RT cycle. + if (shouldCheckBreak && nb > 0u) { + for (uint32 i = 0; i < nb; i++) { + uint32 idx = localBreakIdx[i]; DebugSignalInfo *s = signalInfoPointers[idx]; if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF && EvaluateBreak(s)) { @@ -145,14 +168,12 @@ public: } } } - - activeMutex.FastUnLock(); } // Pass numCopies explicitly so we can mock it static void InitSignals(BrokerI *broker, DataSourceI &dataSourceIn, - DebugService *&service, DebugSignalInfo **&signalInfoPointers, + DebugServiceI *&service, DebugSignalInfo **&signalInfoPointers, uint32 numCopies, MemoryMapBrokerCopyTableEntry *copyTable, const char8 *functionName, SignalDirection direction, volatile bool *anyActiveFlag, Vec *activeIndices, @@ -164,16 +185,14 @@ public: signalInfoPointers[i] = NULL_PTR(DebugSignalInfo *); } - ReferenceContainer *root = ObjectRegistryDatabase::Instance(); - Reference serviceRef = root->Find("DebugService"); - if (serviceRef.IsValid()) { - service = dynamic_cast(serviceRef.operator->()); - } + // Use the singleton registered by DebugService::Initialise() — no ORD + // search on the Init path, and no dependency on a concrete type. + service = DebugServiceI::GetInstance(); if (service && (copyTable != NULL_PTR(MemoryMapBrokerCopyTableEntry *))) { StreamString dsPath; - DebugService::GetFullObjectName(dataSourceIn, dsPath); + DebugServiceI::GetFullObjectName(dataSourceIn, dsPath); fprintf(stderr, ">> %s broker for %s [%d]\n", direction == InputSignals ? "Input" : "Output", dsPath.Buffer(), numCopies); @@ -225,7 +244,7 @@ public: if (gamRef.IsValid()) { StreamString absGamPath; - DebugService::GetFullObjectName(*(gamRef.operator->()), absGamPath); + DebugServiceI::GetFullObjectName(*(gamRef.operator->()), absGamPath); // Register short path (In/Out) for GUI compatibility gamFullName.Printf("%s.%s.%s", absGamPath.Buffer(), dirStrShort, signalName.Buffer()); @@ -261,7 +280,7 @@ public: template class DebugBrokerWrapper : public BaseClass { public: DebugBrokerWrapper() : BaseClass() { - service = NULL_PTR(DebugService *); + service = NULL_PTR(DebugServiceI *); signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; @@ -326,7 +345,7 @@ public: return ret; } - DebugService *service; + DebugServiceI *service; DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; @@ -343,7 +362,7 @@ template class DebugBrokerWrapperNoOptim : public BaseClass { public: DebugBrokerWrapperNoOptim() : BaseClass() { - service = NULL_PTR(DebugService *); + service = NULL_PTR(DebugServiceI *); signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; @@ -386,7 +405,7 @@ public: return ret; } - DebugService *service; + DebugServiceI *service; DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; @@ -402,7 +421,7 @@ public: class DebugMemoryMapAsyncOutputBroker : public MemoryMapAsyncOutputBroker { public: DebugMemoryMapAsyncOutputBroker() : MemoryMapAsyncOutputBroker() { - service = NULL_PTR(DebugService *); + service = NULL_PTR(DebugServiceI *); signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; @@ -446,7 +465,7 @@ public: } return ret; } - DebugService *service; + DebugServiceI *service; DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; @@ -463,7 +482,7 @@ class DebugMemoryMapAsyncTriggerOutputBroker public: DebugMemoryMapAsyncTriggerOutputBroker() : MemoryMapAsyncTriggerOutputBroker() { - service = NULL_PTR(DebugService *); + service = NULL_PTR(DebugServiceI *); signalInfoPointers = NULL_PTR(DebugSignalInfo **); numSignals = 0; anyActive = false; @@ -506,7 +525,7 @@ public: } return ret; } - DebugService *service; + DebugServiceI *service; DebugSignalInfo **signalInfoPointers; uint32 numSignals; volatile bool anyActive; diff --git a/Source/Components/Interfaces/DebugService/DebugCore.h b/Source/Components/Interfaces/DebugService/DebugCore.h index 08057ff..abab7fe 100644 --- a/Source/Components/Interfaces/DebugService/DebugCore.h +++ b/Source/Components/Interfaces/DebugService/DebugCore.h @@ -114,7 +114,19 @@ public: ReadFromBuffer(&tempRead, &tempSize, 4); if (tempSize > maxSize) { - readIndex = write; + // FIX #5: Skip only the current entry rather than discarding the + // entire ring buffer. tempRead is already past the 16-byte header; + // advancing by tempSize lands at the start of the next entry. + // + // Safety fallback: if tempSize >= bufferSize the stored size field + // is corrupt (it can never be that large). In that case we cannot + // locate the next entry safely, so fall back to discarding everything + // to avoid reading garbage as sample headers on future Pop() calls. + if (tempSize >= bufferSize) { + readIndex = write; // corrupt ring — discard all + } else { + readIndex = (tempRead + tempSize) % bufferSize; + } return false; } diff --git a/Source/Components/Interfaces/DebugService/DebugService.cpp b/Source/Components/Interfaces/DebugService/DebugService.cpp index 6d28159..4151add 100644 --- a/Source/Components/Interfaces/DebugService/DebugService.cpp +++ b/Source/Components/Interfaces/DebugService/DebugService.cpp @@ -1,3 +1,4 @@ +#include "Atomic.h" #include "BasicTCPSocket.h" #include "ClassRegistryItem.h" #include "ConfigurationDatabase.h" @@ -21,7 +22,8 @@ namespace MARTe { -DebugService *DebugService::instance = (DebugService *)0; +// DebugServiceI static members — defined here so no extra .cpp is needed. +DebugServiceI *DebugServiceI::instance = NULL_PTR(DebugServiceI *); static void EscapeJson(const char8 *src, StreamString &dst) { if (src == NULL_PTR(const char8 *)) @@ -85,6 +87,15 @@ static bool FindPathInContainer(ReferenceContainer *container, CLASS_REGISTER(DebugService, "1.0") +// Out-of-class definitions required by C++98 when the constants are odr-used +// (i.e. their address is taken or they appear in a context that needs linkage). +const uint32 DebugService::STREAMER_MTU; +const uint32 DebugService::STREAMER_BUFFER_SIZE; +const uint32 DebugService::CMD_RATE_LIMIT; +const uint32 DebugService::CLIENT_IDLE_TIMEOUT_MS; +const uint32 DebugService::GET_VALUE_MAX_ELEMENTS; +const uint32 DebugService::INPUT_BUFFER_MAX; + DebugService::DebugService() : ReferenceContainer(), EmbeddedServiceMethodBinderI(), binderServer(this, ServiceBinder::ServerType), @@ -102,11 +113,15 @@ DebugService::DebugService() activeClient = NULL_PTR(BasicTCPSocket *); streamerPacketOffset = 0u; streamerSequenceNumber = 0u; + cmdCountInWindow = 0u; + cmdWindowStartMs = 0u; + lastDataTimeMs = 0u; + inputBuffer = ""; // FIX #10: initialise carry-over buffer } DebugService::~DebugService() { - if (instance == this) { - instance = NULL_PTR(DebugService *); + if (DebugServiceI::GetInstance() == this) { + DebugServiceI::SetInstance(NULL_PTR(DebugServiceI *)); } threadService.Stop(); streamerService.Stop(); @@ -135,7 +150,7 @@ bool DebugService::Initialise(StructuredDataI &data) { if (controlPort > 0) { isServer = true; - instance = this; + DebugServiceI::SetInstance(this); } port = 8081; @@ -463,12 +478,28 @@ void DebugService::ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size); } if (signalInfo->isTracing) { - if (signalInfo->decimationCounter == 0) { - traceBuffer.Push(signalInfo->internalID, timestamp, - (uint8 *)signalInfo->memoryAddress, size); + // FIX #4: decimationCounter read-modify-write is a data race when multiple + // RT threads call ProcessSignal() concurrently for signals with the same + // DebugSignalInfo (e.g. an output signal read by two GAMs). + // + // Pattern: atomically increment, then claim the "push token" by exchanging + // back to zero only when the counter reaches the decimation factor. + // Only the thread that observes old >= decimationFactor actually pushes; + // all others just increment and return. No separate lock is needed for + // the counter itself. + // + // tracePushMutex still serialises the Push() call (FIX #2) because + // TraceRingBuffer is SPSC and multiple RT threads can be pushing concurrently. + Atomic::Add((volatile int32 *)&signalInfo->decimationCounter, 1); + if ((uint32)signalInfo->decimationCounter >= signalInfo->decimationFactor) { + int32 old = Atomic::Exchange((volatile int32 *)&signalInfo->decimationCounter, 0); + if ((uint32)old >= signalInfo->decimationFactor) { + tracePushMutex.FastLock(); + traceBuffer.Push(signalInfo->internalID, timestamp, + (uint8 *)signalInfo->memoryAddress, size); + tracePushMutex.FastUnLock(); + } } - signalInfo->decimationCounter = - (signalInfo->decimationCounter + 1) % signalInfo->decimationFactor; } } @@ -501,6 +532,11 @@ void DebugService::RegisterBroker(DebugSignalInfo **signalPointers, void DebugService::UpdateBrokersActiveStatus() { for (uint32 i = 0; i < brokers.Size(); i++) { + // FIX #3: signalPointers may be NULL when numSignals > 0 if the broker was + // registered with a null array (e.g. from unit tests or misconfigured brokers). + // Dereferencing NULL would be UB; skip the broker entirely. + if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue; + uint32 count = 0; for (uint32 j = 0; j < brokers[i].numSignals; j++) { DebugSignalInfo *s = brokers[i].signalPointers[j]; @@ -524,20 +560,28 @@ void DebugService::UpdateBrokersActiveStatus() { if (brokers[i].activeMutex) brokers[i].activeMutex->FastLock(); + // FIX #2: Use O(1) Swap instead of operator= (heap alloc + copy) inside + // the critical section. The old arrays are carried out in tempInd/tempSizes + // and freed after the lock is released. if (brokers[i].activeIndices) - *(brokers[i].activeIndices) = tempInd; + brokers[i].activeIndices->Swap(tempInd); if (brokers[i].activeSizes) - *(brokers[i].activeSizes) = tempSizes; + brokers[i].activeSizes->Swap(tempSizes); if (brokers[i].anyActiveFlag) *(brokers[i].anyActiveFlag) = (count > 0); if (brokers[i].activeMutex) brokers[i].activeMutex->FastUnLock(); + // tempInd and tempSizes now hold the old arrays and are freed here, + // outside the lock. } } void DebugService::UpdateBrokersBreakStatus() { for (uint32 i = 0; i < brokers.Size(); i++) { + // FIX #3: same null guard as UpdateBrokersActiveStatus. + if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue; + Vec tempBreak; uint32 count = 0; for (uint32 j = 0; j < brokers[i].numSignals; j++) { @@ -549,12 +593,14 @@ void DebugService::UpdateBrokersBreakStatus() { } if (brokers[i].activeMutex) brokers[i].activeMutex->FastLock(); + // FIX #2: O(1) Swap — old array freed after the lock is released. if (brokers[i].breakIndices) - *(brokers[i].breakIndices) = tempBreak; + brokers[i].breakIndices->Swap(tempBreak); if (brokers[i].anyBreakFlag) *(brokers[i].anyBreakFlag) = (count > 0); if (brokers[i].activeMutex) brokers[i].activeMutex->FastUnLock(); + // tempBreak freed here, outside the lock. } } @@ -582,50 +628,143 @@ ErrorManagement::ErrorType DebugService::Server(ExecutionInfo &info) { // The MARTe2 framework calls Execute() in a loop; each call should do // one unit of work and return so the framework can check for Stop(). // This replaces the old internal infinite-while pattern. + // + // FIX #2: activeClient is guarded by clientMutex. + // Rule: lock clientMutex only when ASSIGNING the pointer (including to NULL). + // Do NOT hold it across blocking I/O (Read/Write) — that would stall any future + // thread trying to grab the lock to check whether there is a live client. + // The command handler (HandleCommand) receives the pointer by value so it is + // safe to call without holding the lock. + // Helper: current time in milliseconds (used for rate limiting and idle timeout) + uint64 nowMs = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000.0); + if (activeClient == NULL_PTR(BasicTCPSocket *)) { // Wait briefly for a new connection; return so the framework loop can // check if Stop() was requested between calls. BasicTCPSocket *newClient = tcpServer.WaitConnection(TimeoutType(100)); if (newClient != NULL_PTR(BasicTCPSocket *)) { - activeClient = newClient; + clientMutex.FastLock(); + activeClient = newClient; // publish pointer — visible to other threads after unlock + clientMutex.FastUnLock(); + // FIX #6/#8: reset per-connection state when a new client connects + cmdCountInWindow = 0u; + cmdWindowStartMs = nowMs; + lastDataTimeMs = nowMs; } } else { - // Check if client is still connected - if (!activeClient->IsConnected()) { + // FIX #8: idle-timeout check — runs every Execute() invocation even when + // no data arrives, so a client that half-sends a command and goes silent + // cannot hold the slot open indefinitely. + if (nowMs - lastDataTimeMs > CLIENT_IDLE_TIMEOUT_MS) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "Server: TCP client idle for >%u ms — closing connection.", CLIENT_IDLE_TIMEOUT_MS); + inputBuffer = ""; // FIX #10: discard carry-over on disconnect + clientMutex.FastLock(); activeClient->Close(); delete activeClient; activeClient = NULL_PTR(BasicTCPSocket *); + clientMutex.FastUnLock(); + cmdCountInWindow = 0u; + } else if (!activeClient->IsConnected()) { + // Check if client is still connected + inputBuffer = ""; // FIX #10 + clientMutex.FastLock(); + activeClient->Close(); + delete activeClient; + activeClient = NULL_PTR(BasicTCPSocket *); + clientMutex.FastUnLock(); } else { char buffer[1024]; uint32 size = 1024; if (activeClient->Read(buffer, size)) { if (size > 0) { - // Process each line separately - char *ptr = buffer; - char *end = buffer + size; - while (ptr < end) { - char *newline = (char *)memchr(ptr, '\n', end - ptr); - if (!newline) { - break; + lastDataTimeMs = nowMs; // FIX #8: refresh idle timestamp + + // FIX #6: slide rate-limit window + if (nowMs - cmdWindowStartMs >= 1000u) { + cmdWindowStartMs = nowMs; + cmdCountInWindow = 0u; + } + + // FIX #10: guard against a client that never sends a newline, + // which would grow inputBuffer without bound. + if (inputBuffer.Size() + (uint32)size > INPUT_BUFFER_MAX) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "Server: input buffer overflow (>%u bytes without newline) " + "— disconnecting.", INPUT_BUFFER_MAX); + inputBuffer = ""; + clientMutex.FastLock(); + activeClient->Close(); + delete activeClient; + activeClient = NULL_PTR(BasicTCPSocket *); + clientMutex.FastUnLock(); + cmdCountInWindow = 0u; + } else { + // FIX #10: accumulate data; parse only complete (newline-terminated) + // commands so that commands split across TCP segments are assembled + // correctly before dispatch. + (void)inputBuffer.Seek(inputBuffer.Size()); + uint32 writeSize = (uint32)size; + inputBuffer.Write(buffer, writeSize); + + const char8 *raw = inputBuffer.Buffer(); + uint32 total = (uint32)inputBuffer.Size(); + uint32 lineStart = 0u; + bool rateLimitExceeded = false; + + for (uint32 pos = 0u; pos < total && !rateLimitExceeded; pos++) { + if (raw[pos] != '\n') continue; + + uint32 len = pos - lineStart; + // Trim trailing CR + if (len > 0u && raw[lineStart + len - 1u] == '\r') len--; + + if (len > 0u) { + cmdCountInWindow++; + if (cmdCountInWindow > CMD_RATE_LIMIT) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "Server: client exceeded rate limit (%u cmd/s) — disconnecting.", + CMD_RATE_LIMIT); + rateLimitExceeded = true; + break; + } + StreamString command; + uint32 cmdLen = len; + command.Write(raw + lineStart, cmdLen); + HandleCommand(command, activeClient); + } + lineStart = pos + 1u; } - *newline = '\0'; - // Skip carriage return if present - if (newline > ptr && *(newline - 1) == '\r') - *(newline - 1) = '\0'; - StreamString command; - uint32 len = (uint32)(newline - ptr); - command.Write(ptr, len); - if (command.Size() > 0) { - HandleCommand(command, activeClient); + + if (rateLimitExceeded) { + inputBuffer = ""; + clientMutex.FastLock(); + activeClient->Close(); + delete activeClient; + activeClient = NULL_PTR(BasicTCPSocket *); + clientMutex.FastUnLock(); + cmdCountInWindow = 0u; + } else { + // Save the incomplete line (bytes after the last '\n') for the + // next Read() — they form the start of the next command. + StreamString newInputBuffer; + if (lineStart < total) { + uint32 remLen = total - lineStart; + newInputBuffer.Write(raw + lineStart, remLen); + } + inputBuffer = newInputBuffer; } - ptr = newline + 1; } } } else { // Read failed (client disconnected or error), clean up + inputBuffer = ""; // FIX #10 + clientMutex.FastLock(); activeClient->Close(); delete activeClient; activeClient = NULL_PTR(BasicTCPSocket *); + clientMutex.FastUnLock(); } } } @@ -654,19 +793,63 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) { HighResolutionTimer::Period() * 1000000.0); void *address = NULL_PTR(void *); if (monitoredSignals[i].dataSource->GetSignalMemoryBuffer(monitoredSignals[i].signalIdx, 0, address)) { + // FIX #11: ProcessSignal() (called from RT broker threads) serialises + // its traceBuffer.Push() under tracePushMutex. The Streamer's monitored- + // signal path previously skipped that lock, creating a multi-producer race + // between the RT threads and the Streamer thread. + // + // Lock order: mutex (always held here) → tracePushMutex. + // RT broker threads only ever acquire tracePushMutex (never mutex), so + // this ordering introduces no deadlock risk. + tracePushMutex.FastLock(); traceBuffer.Push(monitoredSignals[i].internalID, ts, (uint8 *)address, monitoredSignals[i].size); + tracePushMutex.FastUnLock(); } } } mutex.FastUnLock(); - // Drain ring buffer into UDP packet(s) + // Drain ring buffer into UDP packet(s). + // + // FIX #1: Two-level bounds checking to prevent buffer overflow. + // + // Level 1 — per-sample maximum size: + // sampleData is SAMPLE_BUF_SIZE bytes. Pop() enforces this via maxSize so + // the local buffer can never be overrun by Pop() itself. + // + // Level 2 — assembly-buffer hard limit: + // After a flush, streamerPacketOffset resets to sizeof(TraceHeader). The + // next sample occupies (sizeof(TraceHeader) + 16 + size) bytes. With + // SAMPLE_BUF_SIZE = 1024 that is at most ~1060 bytes — well within + // STREAMER_BUFFER_SIZE = 4096. The explicit guard below catches the case + // where SAMPLE_BUF_SIZE or STREAMER_MTU are later changed carelessly so + // that a single sample could still overflow the buffer. + static const uint32 SAMPLE_BUF_SIZE = 1024u; + // Compile-time sanity: one full header + per-sample header + max sample must fit + // inside the assembly buffer. If this fires, raise STREAMER_BUFFER_SIZE or + // lower SAMPLE_BUF_SIZE. + // (C++98-compatible static assert via sizeof a negative-size array) + typedef char StaticAssert_StreamerBufferTooSmall[ + (sizeof(TraceHeader) + 16u + SAMPLE_BUF_SIZE <= STREAMER_BUFFER_SIZE) ? 1 : -1]; + (void)sizeof(StaticAssert_StreamerBufferTooSmall); // suppress unused-typedef warning + uint32 id, size; uint64 ts; - uint8 sampleData[1024]; + uint8 sampleData[SAMPLE_BUF_SIZE]; bool hasData = false; - while (traceBuffer.Pop(id, ts, sampleData, size, 1024)) { + while (traceBuffer.Pop(id, ts, sampleData, size, SAMPLE_BUF_SIZE)) { hasData = true; + + // Level 2 guard — should never fire given the static assert above, but + // defends against future changes that widen SAMPLE_BUF_SIZE or shrink + // STREAMER_BUFFER_SIZE without updating the other. + if (size > SAMPLE_BUF_SIZE || streamerPacketOffset + 16u + size > STREAMER_BUFFER_SIZE) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "Streamer: sample size %u would overflow assembly buffer (%u bytes used of %u) " + "— sample dropped.", size, streamerPacketOffset, STREAMER_BUFFER_SIZE); + continue; + } + if (streamerPacketOffset == 0u) { TraceHeader header; header.magic = 0xDA7A57AD; @@ -676,7 +859,8 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) { memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader)); streamerPacketOffset = sizeof(TraceHeader); } - if (streamerPacketOffset + 16u + size > 1400u) { + // Flush the current packet when adding this sample would exceed the MTU. + if (streamerPacketOffset + 16u + size > STREAMER_MTU) { uint32 toWrite = streamerPacketOffset; (void)udpSocket.Write((char8 *)streamerPacketBuffer, toWrite); TraceHeader header; @@ -705,8 +889,8 @@ ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) { return ErrorManagement::NoError; } -bool DebugService::GetFullObjectName(const Object &obj, - StreamString &fullPath) { +bool DebugServiceI::GetFullObjectName(const Object &obj, + StreamString &fullPath) { fullPath = ""; if (FindPathInContainer(ObjectRegistryDatabase::Instance(), &obj, fullPath)) { return true; @@ -893,15 +1077,42 @@ void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { const char8 *eq = StringHelper::SearchChar(line.Buffer(), '='); if (eq != NULL_PTR(const char8 *)) { uint32 eqPos = (uint32)(eq - line.Buffer()); - (void)line.Seek(0u); - char8 keyBuf[256] = {'\0'}; + // FIX #7: Enforce buffer bounds before reading key/value. + // A key longer than keyBuf would be silently truncated by + // Read(), producing a mismatched key (e.g. "LongKe" instead + // of "LongKey") that causes CDB.Write() to inject a garbled + // parameter. Skip the entire line if the key overflows — + // the CDB write would be nonsensical anyway. + // Values are safely capped: the worst case is a truncated + // value, which the target GAM can detect and reject. + static const uint32 KEY_BUF_SIZE = 256u; + static const uint32 VAL_BUF_SIZE = 1024u; + if (eqPos >= KEY_BUF_SIZE) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "MSG: key length %u exceeds buffer (%u) — line skipped.", + eqPos, KEY_BUF_SIZE); + line = ""; + continue; + } + + (void)line.Seek(0u); + char8 keyBuf[KEY_BUF_SIZE]; + MemoryOperationsHelper::Set(keyBuf, '\0', KEY_BUF_SIZE); uint32 keyReadSize = eqPos; (void)line.Read(keyBuf, keyReadSize); (void)line.Seek(eqPos + 1u); uint32 valLen = (uint32)(line.Size() - eqPos - 1u); - char8 valBuf[1024] = {'\0'}; + // Truncate silently to VAL_BUF_SIZE - 1 (leave room for '\0') + if (valLen >= VAL_BUF_SIZE) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "MSG: value length %u truncated to %u for key '%s'.", + valLen, VAL_BUF_SIZE - 1u, keyBuf); + valLen = VAL_BUF_SIZE - 1u; + } + char8 valBuf[VAL_BUF_SIZE]; + MemoryOperationsHelper::Set(valBuf, '\0', VAL_BUF_SIZE); (void)line.Read(valBuf, valLen); // Trim trailing whitespace from value @@ -1418,6 +1629,25 @@ uint32 DebugService::ForceSignal(const char8 *name, const char8 *valueStr) { SuffixMatch(aliases[i].name.Buffer(), name)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; + + // FIX #4: Guard against writing past the end of forcedValue[1024]. + // TypeConvert writes (byteSize * numberOfElements) bytes into forcedValue. + // For large arrays (e.g. 512 float64s = 4096 bytes) this would silently + // overflow the 1024-byte buffer and corrupt adjacent struct members. + // + // TypeDescriptor::numberOfBits is the element size in bits; integer divide + // by 8 gives bytes. For structured/opaque types numberOfBits may be 0 — + // those are also skipped because we cannot determine the layout safely. + uint32 elemBytes = (uint32)(s->type.numberOfBits) / 8u; + uint32 totalBytes = elemBytes * s->numberOfElements; + if (elemBytes == 0u || totalBytes > (uint32)sizeof(s->forcedValue)) { + REPORT_ERROR_STATIC(ErrorManagement::Warning, + "ForceSignal: signal '%s' requires %u bytes but forcedValue buffer is " + "only %u bytes — force request ignored.", + s->name.Buffer(), totalBytes, (uint32)sizeof(s->forcedValue)); + continue; + } + s->isForcing = true; AnyType dest(s->type, 0u, s->forcedValue); AnyType source(CharString, 0u, valueStr); @@ -1536,7 +1766,18 @@ void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) { TypeDescriptor td = sig->type; uint32 nElem = sig->numberOfElements; uint32 byteSize = (td.numberOfBits > 0u) ? (td.numberOfBits / 8u) : 1u; + + // FIX #9: cap element count before computing byte totals. + // Without a cap, a 1M-element uint8 signal would produce a multi-MB + // comma-separated response, exhausting heap and blocking the Server thread. + // GET_VALUE_MAX_ELEMENTS = 256 gives a ~4 KB worst-case JSON string. + bool truncated = (nElem > GET_VALUE_MAX_ELEMENTS); + if (truncated) { + nElem = GET_VALUE_MAX_ELEMENTS; + } uint32 totalBytes = byteSize * nElem; + // Secondary cap: even after the element limit, ensure we never read more + // than 1024 bytes from the RT memory region. if (totalBytes > 1024u) { totalBytes = 1024u; nElem = totalBytes / byteSize; } // Copy bytes while holding the mutex to avoid data races with the RT thread uint8 localBuf[1024]; @@ -1582,7 +1823,10 @@ void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) { vp++; } resp += "\", \"Elements\": "; - resp.Printf("%u}\nOK VALUE\n", nElem); + // Include the capped element count and a Truncated flag so the client can + // distinguish "this is the full signal" from "there are more elements". + resp.Printf("%u, \"Truncated\": %s}\nOK VALUE\n", + nElem, truncated ? "true" : "false"); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } diff --git a/Source/Components/Interfaces/DebugService/DebugService.h b/Source/Components/Interfaces/DebugService/DebugService.h index e17f8fb..86b164d 100644 --- a/Source/Components/Interfaces/DebugService/DebugService.h +++ b/Source/Components/Interfaces/DebugService/DebugService.h @@ -4,45 +4,30 @@ #include "BasicTCPSocket.h" #include "BasicUDPSocket.h" #include "ConfigurationDatabase.h" -#include "DebugCore.h" +#include "DebugServiceI.h" #include "EmbeddedServiceMethodBinderI.h" #include "MessageI.h" -#include "Object.h" #include "ReferenceContainer.h" #include "ReferenceT.h" #include "SingleThreadService.h" -#include "StreamString.h" -#include "Vec.h" namespace MARTe { -class MemoryMapBroker; class DataSourceI; -struct SignalAlias { - StreamString name; - uint32 signalIndex; -}; - -struct BrokerInfo { - DebugSignalInfo **signalPointers; - uint32 numSignals; - MemoryMapBroker *broker; - volatile bool *anyActiveFlag; - Vec *activeIndices; - Vec *activeSizes; - FastPollingMutexSem *activeMutex; - // Conditional break — mirrors activeIndices but for break-enabled signals - volatile bool *anyBreakFlag; - Vec *breakIndices; - // GAM association for step-by-GAM - StreamString gamName; - bool isOutput; -}; +/** + * @brief TCP/UDP implementation of DebugServiceI. + * + * Provides signal tracing (UDP), forced-value injection, break conditions, + * and a text command channel (TCP) with a log-forwarding sidecar (TcpLogger). + * Alternative transports (TTY, WebSocket, …) can be added by implementing + * DebugServiceI without touching this class or any broker wrapper code. + */ class DebugService : public ReferenceContainer, public MessageI, - public EmbeddedServiceMethodBinderI { + public EmbeddedServiceMethodBinderI, + public DebugServiceI { public: friend class DebugServiceTest; CLASS_REGISTER_DECLARATION() @@ -52,49 +37,43 @@ public: virtual bool Initialise(StructuredDataI &data); - DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type, - const char8 *name, uint8 numberOfDimensions = 0, - uint32 numberOfElements = 1); - void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, - uint64 timestamp); + // DebugServiceI RT-path overrides + virtual DebugSignalInfo *RegisterSignal(void *memoryAddress, TypeDescriptor type, + const char8 *name, + uint8 numberOfDimensions = 0, + uint32 numberOfElements = 1); + virtual void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, + uint64 timestamp); + virtual void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals, + MemoryMapBroker *broker, volatile bool *anyActiveFlag, + Vec *activeIndices, Vec *activeSizes, + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, Vec *breakIndices, + const char8 *gamName = NULL_PTR(const char8 *), + bool isOutput = false); + virtual bool IsPaused() const { return isPaused; } + virtual void SetPaused(bool paused) { isPaused = paused; } + virtual bool IsStepPending() const { return stepRemaining > 0u; } + virtual void ConsumeStepIfNeeded(const char8 *gamName, + const char8 *threadName = NULL_PTR(const char8 *)); - void RegisterBroker(DebugSignalInfo **signalPointers, uint32 numSignals, - MemoryMapBroker *broker, volatile bool *anyActiveFlag, - Vec *activeIndices, Vec *activeSizes, - FastPollingMutexSem *activeMutex, - volatile bool *anyBreakFlag, Vec *breakIndices, - const char8 *gamName = NULL_PTR(const char8 *), - bool isOutput = false); + // DebugServiceI control-path overrides + virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr); + virtual uint32 UnforceSignal(const char8 *name); + virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1); + virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold); + virtual uint32 ClearBreak (const char8 *name); + virtual bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable); + virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs); + virtual uint32 UnmonitorSignal (const char8 *path); virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info); - virtual ErrorManagement::ErrorType HandleMessage(ReferenceT &data); - bool IsPaused() const { return isPaused; } - void SetPaused(bool paused) { isPaused = paused; } - - // Step-by-GAM / per-thread: called by each output broker. - // threadName is the OS thread name (from Threads::Name(Threads::Id())). - // Decrements stepRemaining only when stepThreadFilter is empty or matches; - // when stepRemaining reaches 0, sets isPaused = true and records gamName. - void ConsumeStepIfNeeded(const char8 *gamName, - const char8 *threadName = NULL_PTR(const char8 *)); - + // TCP-transport-specific methods (not part of DebugServiceI) void GetStepStatus(BasicTCPSocket *client); - // Step n cycles; if threadName is non-null/non-empty, only that thread advances. void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *)); - - // Read the current raw value of a signal from its memory address. void GetSignalValue(const char8 *name, BasicTCPSocket *client); - - static bool GetFullObjectName(const Object &obj, StreamString &fullPath); - - uint32 ForceSignal(const char8 *name, const char8 *valueStr); - uint32 UnforceSignal(const char8 *name); - uint32 TraceSignal(const char8 *name, bool enable, uint32 decimation = 1); - uint32 SetBreak(const char8 *name, uint8 op, float64 threshold); - uint32 ClearBreak(const char8 *name); - bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable); void Discover(BasicTCPSocket *client); void InfoNode(const char8 *path, BasicTCPSocket *client); void ListNodes(const char8 *path, BasicTCPSocket *client); @@ -112,9 +91,6 @@ public: StreamString path; }; - uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs); - uint32 UnmonitorSignal(const char8 *path); - private: void HandleCommand(StreamString cmd, BasicTCPSocket *client); void UpdateBrokersActiveStatus(); @@ -178,17 +154,140 @@ private: FastPollingMutexSem mutex; TraceRingBuffer traceBuffer; + /** + * @brief Mutex protecting traceBuffer.Push() from concurrent RT broker threads. + * + * TraceRingBuffer is designed for single-producer / single-consumer access. + * In practice, multiple RT threads (one per RealTimeThread) call ProcessSignal() + * concurrently, making it a multi-producer scenario. Without a lock they can + * both pass the free-space check, both pick the same write index, and corrupt + * each other's samples. This mutex serialises the Push path. + * + * The Streamer thread (sole consumer) never holds this lock, so the lock is + * only contended between RT threads — not between RT and Streamer. + * + * FIX #2 — TraceRingBuffer multi-producer race. + */ + FastPollingMutexSem tracePushMutex; + + /** + * @brief Mutex protecting the activeClient pointer. + * + * activeClient is currently owned exclusively by the Server thread: + * it is created, read, and destroyed only inside Server(). The mutex + * enforces this invariant and makes future cross-thread access safe. + * + * Rules: + * - Hold clientMutex whenever assigning (including to NULL) activeClient. + * - Do NOT hold clientMutex across blocking I/O calls (Read / Write) + * to avoid starving threads that only need the pointer. + * - Any future code that needs to send data to the active client from a + * non-Server thread must lock clientMutex, copy the pointer, release + * the lock, then do the Write. + * + * FIX #2 — activeClient cross-thread safety. + */ + FastPollingMutexSem clientMutex; + BasicTCPSocket *activeClient; + /** + * Maximum payload bytes in a single UDP datagram (leave room for IP+UDP headers + * on a standard 1500-byte Ethernet MTU). Used in the Streamer to decide when + * to flush the current packet and start a new one. + * + * FIX #1 — named constant to replace the magic number 1400. + */ + static const uint32 STREAMER_MTU = 1400u; + + /** + * Size of the streamer assembly buffer. Must be >= STREAMER_MTU so at least + * one full UDP payload can be staged before flushing. A sample that on its own + * would exceed (STREAMER_BUFFER_SIZE - sizeof(TraceHeader) - 16) bytes is + * silently dropped and logged — it can never fit in any packet. + * + * FIX #1 — named constant; prevents silent integer arithmetic relying on + * the array bound being "obviously 4096". + */ + static const uint32 STREAMER_BUFFER_SIZE = 4096u; + // Streamer state persisted across Execute() calls (framework loops Execute) - uint8 streamerPacketBuffer[4096]; + uint8 streamerPacketBuffer[STREAMER_BUFFER_SIZE]; uint32 streamerPacketOffset; uint32 streamerSequenceNumber; + /** + * @brief Maximum commands allowed per 1-second sliding window per client. + * + * A client sending more than this many commands in one second is disconnected. + * This prevents a tight command loop from monopolising the Server thread and + * starving normal usage. The limit is intentionally generous (100/s) so that + * legitimate scripted tooling is unaffected. + * + * FIX #6 — TCP command rate limiting. + */ + static const uint32 CMD_RATE_LIMIT = 100u; + + /** + * @brief Close the active TCP client if no data is received for this many ms. + * + * A client that sends a partial command and never completes it would otherwise + * hold the single active-client slot open indefinitely, blocking all other + * connections. After CLIENT_IDLE_TIMEOUT_MS of silence the connection is + * closed and the slot freed. + * + * FIX #8 — active-client idle timeout. + */ + static const uint32 CLIENT_IDLE_TIMEOUT_MS = 30000u; + + /** + * @brief Maximum array elements returned by GetSignalValue / VALUE command. + * + * Without a cap, a 1-million-element uint8 array would produce a multi-MB + * response string, exhausting heap and blocking the Server thread. Elements + * beyond this limit are omitted; the response includes a "Truncated" flag. + * + * FIX #9 — GetSignalValue unbounded array output. + */ + static const uint32 GET_VALUE_MAX_ELEMENTS = 256u; + + // Rate-limiter state (per active TCP connection, reset on each new connection) + uint32 cmdCountInWindow; // commands received in the current 1-second window + uint64 cmdWindowStartMs; // start of the current window, in milliseconds + + // Idle-timeout state + uint64 lastDataTimeMs; // last time the active client sent any data (ms) + + /** + * @brief Carry-over buffer for multi-segment TCP commands. + * + * A single TCP Read() call may return only part of a command if the OS + * delivers it in multiple segments. Bytes after the last '\n' in the + * current receive window are saved here and prepended to the next Read(). + * This ensures that "FORCE Signal " arriving in one segment and "123\n" + * arriving in the next are assembled into the single command + * "FORCE Signal 123" before dispatch. + * + * The buffer is bounded by INPUT_BUFFER_MAX (8 KiB). If a client sends + * more than that without a newline it is disconnected. + * + * FIX #10 — incomplete input buffer handling for multi-line commands. + */ + StreamString inputBuffer; + + /** + * @brief Maximum carry-over bytes allowed before a client is disconnected. + * + * A client that sends a very long line without a newline would otherwise + * grow inputBuffer without bound. At 8 KiB the limit is generous enough + * for any legitimate command while still protecting against memory exhaustion. + * + * FIX #10 — DoS guard for the input carry-over buffer. + */ + static const uint32 INPUT_BUFFER_MAX = 8192u; + ConfigurationDatabase fullConfig; bool manualConfigSet; - - static DebugService *instance; }; } // namespace MARTe diff --git a/Source/Components/Interfaces/DebugService/DebugServiceI.h b/Source/Components/Interfaces/DebugService/DebugServiceI.h new file mode 100644 index 0000000..bc28c41 --- /dev/null +++ b/Source/Components/Interfaces/DebugService/DebugServiceI.h @@ -0,0 +1,189 @@ +#ifndef DEBUGSERVICEI_H +#define DEBUGSERVICEI_H + +/** + * @file DebugServiceI.h + * @brief Abstract interface for the MARTe2 debug/instrumentation service. + * + * All broker wrappers (DebugBrokerWrapper) depend solely on this interface, + * decoupling them from the concrete TCP/UDP implementation. Alternative + * transports — TTY, WebSocket, shared-memory ring, etc. — can be plugged in + * by providing a new concrete implementation without touching any broker or + * application code. + * + * The interface is split into two logical groups: + * + * RT-path API + * Called from broker threads on every RT cycle. Implementations must + * keep these methods as cheap as possible; the only permissible + * synchronisation primitive is a fast spinlock (FastPollingMutexSem). + * + * Control-path API + * Called from server / command-handler threads (TCP, TTY, Web …). + * Latency here is acceptable; correctness and thread-safety matter. + * + * Concrete implementations register themselves during Initialise() via + * SetInstance(). Broker wrappers retrieve the active instance via + * GetInstance(); there is no ORD search on the hot path. + */ + +#include "DebugCore.h" +#include "FastPollingMutexSem.h" +#include "Object.h" +#include "StreamString.h" +#include "TypeDescriptor.h" +#include "Vec.h" + +namespace MARTe { + +// Forward declarations — concrete types are only needed in the implementation. +class MemoryMapBroker; + +struct SignalAlias { + StreamString name; + uint32 signalIndex; +}; + +struct BrokerInfo { + DebugSignalInfo **signalPointers; + uint32 numSignals; + MemoryMapBroker *broker; + volatile bool *anyActiveFlag; + Vec *activeIndices; + Vec *activeSizes; + FastPollingMutexSem *activeMutex; + volatile bool *anyBreakFlag; + Vec *breakIndices; + StreamString gamName; + bool isOutput; +}; + +/** + * @brief Abstract debug-service interface. + */ +class DebugServiceI { +public: + // ------------------------------------------------------------------------- + // Static instance registry + // ------------------------------------------------------------------------- + + /** + * @brief Return the currently registered debug-service instance, or NULL. + * + * Called on every broker Init() path and from OutpautPauseAndStep(). + * Returns NULL when no debug service has been initialised, in which case + * all instrumentation is a no-op. + */ + static DebugServiceI *GetInstance() { return instance; } + + /** + * @brief Register @p inst as the global debug-service. + * + * Concrete implementations call this from their Initialise() method. + * Passing NULL deregisters the current instance (called from the + * destructor so dangling pointers are never visible to broker threads). + */ + static void SetInstance(DebugServiceI *inst) { instance = inst; } + + virtual ~DebugServiceI() {} + + // ========================================================================= + // RT-path API (called from broker execute threads every cycle) + // ========================================================================= + + /** + * @brief Register a signal memory region with the debug service. + * + * Called once per signal during broker Init(). Returns a pointer to the + * internal DebugSignalInfo that the broker caches for use on the hot path. + * Thread-safe; must not be called after the RT loop has started. + */ + virtual DebugSignalInfo *RegisterSignal(void *memoryAddress, + TypeDescriptor type, + const char8 *name, + uint8 numberOfDimensions = 0, + uint32 numberOfElements = 1) = 0; + + /** + * @brief Process one signal on the RT path. + * + * Applies forced values (memcpy into signal memory) and, when tracing is + * enabled and the decimation counter fires, pushes a sample to the trace + * ring buffer. Called under the broker's activeMutex; implementations + * must not acquire any lock that is also held by the Server thread. + */ + virtual void ProcessSignal(DebugSignalInfo *signalInfo, + uint32 size, + uint64 timestamp) = 0; + + /** + * @brief Register a broker so the service can push active/break index + * updates to it without iterating every signal. + */ + virtual void RegisterBroker(DebugSignalInfo **signalPointers, + uint32 numSignals, + MemoryMapBroker *broker, + volatile bool *anyActiveFlag, + Vec *activeIndices, + Vec *activeSizes, + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, + Vec *breakIndices, + const char8 *gamName = NULL_PTR(const char8 *), + bool isOutput = false) = 0; + + /** @brief Return true if the RT loop is currently held at a pause/breakpoint. */ + virtual bool IsPaused() const = 0; + + /** @brief Set or clear the paused state (called by break-condition logic). */ + virtual void SetPaused(bool paused) = 0; + + /** @brief Return true if a step count is pending (stepRemaining > 0). */ + virtual bool IsStepPending() const = 0; + + /** + * @brief Consume one step credit for the current output-broker cycle. + * + * Called by every output broker after Execute(). No-op when stepRemaining + * is zero (the common case); only acquires a mutex when stepping is active. + */ + virtual void ConsumeStepIfNeeded( + const char8 *gamName, + const char8 *threadName = NULL_PTR(const char8 *)) = 0; + + // ========================================================================= + // Control-path API (called from server / command-handler threads) + // ========================================================================= + + virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr) = 0; + virtual uint32 UnforceSignal(const char8 *name) = 0; + virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1) = 0; + virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold) = 0; + virtual uint32 ClearBreak (const char8 *name) = 0; + virtual bool IsInstrumented(const char8 *fullPath, + bool &traceable, bool &forcable) = 0; + virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs) = 0; + virtual uint32 UnmonitorSignal (const char8 *path) = 0; + + // ========================================================================= + // Utility (implementation-agnostic, defined in DebugService.cpp) + // ========================================================================= + + /** + * @brief Resolve the fully-qualified ORD path of @p obj into @p fullPath. + * + * Static so it can be called without a service instance. All concrete + * implementations (and InitSignals in DebugBrokerWrapper.h) use this + * to build canonical signal names. The definition lives in + * DebugService.cpp alongside FindPathInContainer(). + */ + static bool GetFullObjectName(const Object &obj, StreamString &fullPath); + +protected: + /** Pointer to the single active debug-service instance. */ + static DebugServiceI *instance; +}; + +} // namespace MARTe + +#endif // DEBUGSERVICEI_H diff --git a/Source/Components/Interfaces/Makefile.inc b/Source/Components/Interfaces/Makefile.inc index ecc77d7..354098d 100644 --- a/Source/Components/Interfaces/Makefile.inc +++ b/Source/Components/Interfaces/Makefile.inc @@ -24,7 +24,7 @@ OBJSX= -SPB = TCPLogger.x DebugService.x +SPB = TCPLogger.x DebugService.x WebDebugService.x ROOT_DIR=../../.. diff --git a/Source/Components/Interfaces/WebDebugService/Makefile.gcc b/Source/Components/Interfaces/WebDebugService/Makefile.gcc new file mode 100644 index 0000000..9e57202 --- /dev/null +++ b/Source/Components/Interfaces/WebDebugService/Makefile.gcc @@ -0,0 +1,3 @@ +export TARGET=x86-linux + +include Makefile.inc diff --git a/Source/Components/Interfaces/WebDebugService/Makefile.inc b/Source/Components/Interfaces/WebDebugService/Makefile.inc new file mode 100644 index 0000000..7254dee --- /dev/null +++ b/Source/Components/Interfaces/WebDebugService/Makefile.inc @@ -0,0 +1,34 @@ +OBJSX=WebDebugService.x + +PACKAGE=Components/Interfaces + +ROOT_DIR=../../../../ +MAKEDEFAULTDIR=$(MARTe2_DIR)/MakeDefaults +include $(MAKEDEFAULTDIR)/MakeStdLibDefs.$(TARGET) + +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Result +INCLUDES += -I$(ROOT_DIR)/Source/Core/Types/Vec +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/TCPLogger +INCLUDES += -I$(ROOT_DIR)/Source/Components/Interfaces/DebugService +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L0Types +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L2Objects +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L3Streams +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Logger +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L4Configuration +INCLUDES += -I$(MARTe2_DIR)/Source/Core/BareMetal/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L3Services +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4Messages +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L4LoggerService +INCLUDES += -I$(MARTe2_DIR)/Source/Core/Scheduler/L5GAMs +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L1Portability +INCLUDES += -I$(MARTe2_DIR)/Source/Core/FileSystem/L3Streams + +all: $(OBJS) $(SUBPROJ) \ + $(BUILD_DIR)/WebDebugService$(LIBEXT) \ + $(BUILD_DIR)/WebDebugService$(DLLEXT) + echo $(OBJS) + +include $(MAKEDEFAULTDIR)/MakeStdLibRules.$(TARGET) diff --git a/Source/Components/Interfaces/WebDebugService/WebDebugService.cpp b/Source/Components/Interfaces/WebDebugService/WebDebugService.cpp new file mode 100644 index 0000000..9b1f0c9 --- /dev/null +++ b/Source/Components/Interfaces/WebDebugService/WebDebugService.cpp @@ -0,0 +1,1562 @@ +#include "Atomic.h" +#include "Logger.h" +#include "BasicTCPSocket.h" +#include "ClassRegistryItem.h" +#include "ConfigurationDatabase.h" +#include "DataSourceI.h" +#include "DebugBrokerWrapper.h" +#include "GAM.h" +#include "GlobalObjectsDatabase.h" +#include "HighResolutionTimer.h" +#include "ObjectRegistryDatabase.h" +#include "ReferenceT.h" +#include "Sleep.h" +#include "StreamString.h" +#include "Threads.h" +#include "TimeoutType.h" +#include "TypeConversion.h" +#include "WebDebugService.h" +#include "WebUI.h" + +namespace MARTe { + +// --------------------------------------------------------------------------- +// File-scope helpers (same as in DebugService.cpp) +// --------------------------------------------------------------------------- + +static void WDS_EscapeJson(const char8 *src, StreamString &dst) { + if (src == NULL_PTR(const char8 *)) return; + while (*src != '\0') { + if (*src == '"') dst += "\\\""; + else if (*src == '\\') dst += "\\\\"; + else if (*src == '\n') dst += "\\n"; + else if (*src == '\r') dst += "\\r"; + else if (*src == '\t') dst += "\\t"; + else dst += *src; + src++; + } +} + +static bool WDS_SuffixMatch(const char8 *target, const char8 *pattern) { + uint32 tLen = StringHelper::Length(target); + uint32 pLen = StringHelper::Length(pattern); + if (pLen > tLen) return false; + const char8 *suffix = target + (tLen - pLen); + if (StringHelper::Compare(suffix, pattern) == 0) { + if (tLen == pLen || *(suffix - 1) == '.') return true; + } + return false; +} + +static bool WDS_FindPath(ReferenceContainer *c, const Object *target, + StreamString &path) { + if (c == NULL_PTR(ReferenceContainer *)) return false; + uint32 n = c->Size(); + for (uint32 i = 0u; i < n; i++) { + Reference ref = c->Get(i); + if (!ref.IsValid()) continue; + if (ref.operator->() == target) { path = ref->GetName(); return true; } + ReferenceContainer *sub = dynamic_cast(ref.operator->()); + if (sub != NULL_PTR(ReferenceContainer *)) { + if (WDS_FindPath(sub, target, path)) { + StreamString full; + full.Printf("%s.%s", ref->GetName(), path.Buffer()); + path = full; + return true; + } + } + } + return false; +} + +static void WDS_BuildCDB(ReferenceContainer *container, ConfigurationDatabase &cdb) { + if (container == NULL_PTR(ReferenceContainer *)) return; + uint32 n = container->Size(); + for (uint32 i = 0u; i < n; i++) { + Reference child = container->Get(i); + if (!child.IsValid()) continue; + const char8 *name = child->GetName(); + if (name == NULL_PTR(const char8 *)) continue; + bool created = cdb.CreateRelative(name); + if (!created) { if (!cdb.MoveRelative(name)) continue; } + const char8 *cn = child->GetClassProperties()->GetName(); + if (cn != NULL_PTR(const char8 *)) (void)cdb.Write("Class", cn); + { + ConfigurationDatabase exp; + if (child->ExportData(exp)) { + exp.MoveToRoot(); + uint32 ne = exp.GetNumberOfChildren(); + for (uint32 j = 0u; j < ne; j++) { + const char8 *ek = exp.GetChildName(j); + if (StringHelper::Compare(ek, "Class") == 0 || + StringHelper::Compare(ek, "Name") == 0 || + StringHelper::Compare(ek, "IsContainer") == 0) continue; + if (exp.MoveRelative(ek)) { exp.MoveToAncestor(1u); continue; } + AnyType at = exp.GetType(ek); + if (at.GetDataPointer() != NULL_PTR(void *)) { + char8 buf[256] = { '\0' }; + AnyType dst(CharString, 0u, buf); + dst.SetNumberOfElements(0u, 256u); + if (TypeConvert(dst, at)) (void)cdb.Write(ek, buf); + } + } + } + } + ReferenceContainer *rc = dynamic_cast(child.operator->()); + if (rc != NULL_PTR(ReferenceContainer *)) WDS_BuildCDB(rc, cdb); + (void)cdb.MoveToAncestor(1u); + } +} + +// --------------------------------------------------------------------------- +// DebugServiceI out-of-line definitions (each transport .so is self-contained) +// --------------------------------------------------------------------------- + +DebugServiceI *DebugServiceI::instance = NULL_PTR(DebugServiceI *); + +bool DebugServiceI::GetFullObjectName(const Object &obj, StreamString &fullPath) { + fullPath = ""; + if (WDS_FindPath(ObjectRegistryDatabase::Instance(), &obj, fullPath)) { + return true; + } + const char8 *name = obj.GetName(); + if (name != NULL_PTR(const char8 *)) + fullPath = name; + return true; +} + +// --------------------------------------------------------------------------- +// CLASS_REGISTER +// --------------------------------------------------------------------------- + +CLASS_REGISTER(WebDebugService, "1.0") + +// --------------------------------------------------------------------------- +// Constructor / Destructor +// --------------------------------------------------------------------------- + +WebDebugService::WebDebugService() + : binderHttp(this, WdsBinder::Http), + binderStream(this, WdsBinder::Stream), + httpService(binderHttp), + streamerService(binderStream) { + httpPort = 8090u; + isPaused = false; + stepRemaining = 0u; + streamerSeq = 0u; + streamerT0Ns = 0u; + manualConfigSet = false; + logHistoryWriteIdx = 0u; + logHistoryFill = 0u; + memset(logHistory, 0, sizeof(logHistory)); + for (uint32 i = 0u; i < MAX_SSE_CLIENTS; i++) { + sseClients[i] = NULL_PTR(BasicTCPSocket *); + } +} + +WebDebugService::~WebDebugService() { + DebugServiceI::SetInstance(NULL_PTR(DebugServiceI *)); + httpService.Stop(); + streamerService.Stop(); + tcpServer.Close(); + sseMutex.FastLock(); + for (uint32 i = 0u; i < MAX_SSE_CLIENTS; i++) { + if (sseClients[i] != NULL_PTR(BasicTCPSocket *)) { + sseClients[i]->Close(); + delete sseClients[i]; + sseClients[i] = NULL_PTR(BasicTCPSocket *); + } + } + sseMutex.FastUnLock(); + mutex.FastLock(); + for (uint32 i = 0u; i < signals.Size(); i++) { + if (signals[i] != NULL_PTR(DebugSignalInfo *)) { + delete signals[i]; + } + } + mutex.FastUnLock(); +} + +// --------------------------------------------------------------------------- +// Initialise +// --------------------------------------------------------------------------- + +bool WebDebugService::Initialise(StructuredDataI &data) { + if (!ReferenceContainer::Initialise(data)) return false; + + uint32 port = 8090u; + (void)data.Read("HttpPort", port); + httpPort = (uint16)port; + + if (!traceBuffer.Init(4 * 1024 * 1024)) return false; + + DebugServiceI::SetInstance(this); + + PatchRegistry(); + + // Initialize Logger early so REPORT_ERROR calls from application startup + // are captured in the ring buffer before the Streamer thread begins. + (void)Logger::Instance(); + REPORT_ERROR(ErrorManagement::Information, "WebDebugService initialised on port %d", (int)httpPort); + + // Record time origin for relative SSE timestamps (ms since service init). + streamerT0Ns = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1.0e9); + + ConfigurationDatabase threadData; + threadData.Write("Timeout", (uint32)1000); + httpService.Initialise(threadData); + streamerService.Initialise(threadData); + + if (!tcpServer.Open()) return false; + if (!tcpServer.Listen(httpPort)) return false; + + if (httpService.Start() != ErrorManagement::NoError) return false; + if (streamerService.Start() != ErrorManagement::NoError) return false; + + return true; +} + +// --------------------------------------------------------------------------- +// PatchRegistry (identical logic to DebugService) +// --------------------------------------------------------------------------- + +static void WDS_PatchItem(const char8 *originalName, ObjectBuilder *debugBuilder) { + ClassRegistryItem *item = ClassRegistryDatabase::Instance()->Find(originalName); + if (item != NULL_PTR(ClassRegistryItem *)) { + item->SetObjectBuilder(debugBuilder); + } +} + +void WebDebugService::PatchRegistry() { + WDS_PatchItem("MemoryMapInputBroker", + new DebugMemoryMapInputBrokerBuilder()); + WDS_PatchItem("MemoryMapOutputBroker", + new DebugMemoryMapOutputBrokerBuilder()); + WDS_PatchItem("MemoryMapSynchronisedInputBroker", + new DebugMemoryMapSynchronisedInputBrokerBuilder()); + WDS_PatchItem("MemoryMapSynchronisedOutputBroker", + new DebugMemoryMapSynchronisedOutputBrokerBuilder()); + WDS_PatchItem("MemoryMapInterpolatedInputBroker", + new DebugMemoryMapInterpolatedInputBrokerBuilder()); + WDS_PatchItem("MemoryMapMultiBufferInputBroker", + new DebugMemoryMapMultiBufferInputBrokerBuilder()); + WDS_PatchItem("MemoryMapMultiBufferOutputBroker", + new DebugMemoryMapMultiBufferOutputBrokerBuilder()); + WDS_PatchItem("MemoryMapSynchronisedMultiBufferInputBroker", + new DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder()); + WDS_PatchItem("MemoryMapSynchronisedMultiBufferOutputBroker", + new DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder()); + WDS_PatchItem("MemoryMapAsyncOutputBroker", + new DebugMemoryMapAsyncOutputBrokerBuilder()); + WDS_PatchItem("MemoryMapAsyncTriggerOutputBroker", + new DebugMemoryMapAsyncTriggerOutputBrokerBuilder()); +} + +// --------------------------------------------------------------------------- +// DebugServiceI RT-path +// --------------------------------------------------------------------------- + +DebugSignalInfo *WebDebugService::RegisterSignal(void *memoryAddress, + TypeDescriptor type, + const char8 *name, + uint8 numberOfDimensions, + uint32 numberOfElements) { + mutex.FastLock(); + DebugSignalInfo *res = NULL_PTR(DebugSignalInfo *); + uint32 sigIdx = 0xFFFFFFFFu; + for (uint32 i = 0u; i < signals.Size(); i++) { + if (signals[i]->memoryAddress == memoryAddress) { + res = signals[i]; sigIdx = i; break; + } + } + if (res == NULL_PTR(DebugSignalInfo *)) { + sigIdx = signals.Size(); + res = new DebugSignalInfo(); + res->memoryAddress = memoryAddress; + res->type = type; + res->name = name; + res->numberOfDimensions = numberOfDimensions; + res->numberOfElements = numberOfElements; + res->isTracing = false; + res->isForcing = false; + res->internalID = sigIdx; + res->decimationFactor = 1u; + res->decimationCounter = 0u; + res->breakOp = BREAK_OFF; + res->breakThreshold = 0.0; + signals.Push(res); + } + if (sigIdx != 0xFFFFFFFFu) { + bool found = false; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name) { found = true; break; } + } + if (!found) { + SignalAlias a; + a.name = name; + a.signalIndex = sigIdx; + aliases.Push(a); + } + } + mutex.FastUnLock(); + return res; +} + +void WebDebugService::ProcessSignal(DebugSignalInfo *signalInfo, + uint32 size, uint64 timestamp) { + if (signalInfo == NULL_PTR(DebugSignalInfo *)) return; + if (signalInfo->isForcing) { + memcpy(signalInfo->memoryAddress, signalInfo->forcedValue, size); + } + if (signalInfo->isTracing) { + Atomic::Add((volatile int32 *)&signalInfo->decimationCounter, 1); + if ((uint32)signalInfo->decimationCounter >= signalInfo->decimationFactor) { + int32 old = Atomic::Exchange((volatile int32 *)&signalInfo->decimationCounter, 0); + if ((uint32)old >= signalInfo->decimationFactor) { + tracePushMutex.FastLock(); + traceBuffer.Push(signalInfo->internalID, timestamp, + (uint8 *)signalInfo->memoryAddress, size); + tracePushMutex.FastUnLock(); + } + } + } +} + +void WebDebugService::RegisterBroker(DebugSignalInfo **signalPointers, + uint32 numSignals, MemoryMapBroker *broker, + volatile bool *anyActiveFlag, + Vec *activeIndices, + Vec *activeSizes, + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, + Vec *breakIndices, + const char8 *gamName, bool isOutput) { + mutex.FastLock(); + BrokerInfo b; + b.signalPointers = signalPointers; + b.numSignals = numSignals; + b.broker = broker; + b.anyActiveFlag = anyActiveFlag; + b.activeIndices = activeIndices; + b.activeSizes = activeSizes; + b.activeMutex = activeMutex; + b.anyBreakFlag = anyBreakFlag; + b.breakIndices = breakIndices; + b.isOutput = isOutput; + if (gamName != NULL_PTR(const char8 *)) b.gamName = gamName; + brokers.Push(b); + mutex.FastUnLock(); +} + +void WebDebugService::ConsumeStepIfNeeded(const char8 *gamName, + const char8 *threadName) { + if (stepRemaining == 0u) return; + mutex.FastLock(); + if (stepThreadFilter.Size() > 0u && + (threadName == NULL_PTR(const char8 *) || stepThreadFilter != threadName)) { + mutex.FastUnLock(); + return; + } + if (stepRemaining > 0u) { + stepRemaining--; + if (stepRemaining == 0u) { + isPaused = true; + pausedAtGam = (gamName != NULL_PTR(const char8 *)) ? gamName : ""; + } + } + mutex.FastUnLock(); +} + +// --------------------------------------------------------------------------- +// DebugServiceI control-path +// --------------------------------------------------------------------------- + +uint32 WebDebugService::ForceSignal(const char8 *name, const char8 *valueStr) { + mutex.FastLock(); + uint32 count = 0u; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || WDS_SuffixMatch(aliases[i].name.Buffer(), name)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + uint32 elemBytes = (uint32)(s->type.numberOfBits) / 8u; + uint32 totalBytes = elemBytes * s->numberOfElements; + if (elemBytes == 0u || totalBytes > (uint32)sizeof(s->forcedValue)) continue; + s->isForcing = true; + AnyType dest(s->type, 0u, s->forcedValue); + AnyType source(CharString, 0u, valueStr); + (void)TypeConvert(dest, source); + count++; + } + } + UpdateBrokersActiveStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 WebDebugService::UnforceSignal(const char8 *name) { + mutex.FastLock(); + uint32 count = 0u; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || WDS_SuffixMatch(aliases[i].name.Buffer(), name)) { + signals[aliases[i].signalIndex]->isForcing = false; + count++; + } + } + UpdateBrokersActiveStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 WebDebugService::TraceSignal(const char8 *name, bool enable, uint32 decimation) { + mutex.FastLock(); + uint32 count = 0u; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || WDS_SuffixMatch(aliases[i].name.Buffer(), name)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + s->isTracing = enable; + s->decimationFactor = decimation; + s->decimationCounter = 0u; + count++; + } + } + UpdateBrokersActiveStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 WebDebugService::SetBreak(const char8 *name, uint8 op, float64 threshold) { + mutex.FastLock(); + uint32 count = 0u; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || WDS_SuffixMatch(aliases[i].name.Buffer(), name)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + s->breakThreshold = threshold; + s->breakOp = op; + count++; + } + } + if (count > 0u) UpdateBrokersBreakStatus(); + mutex.FastUnLock(); + return count; +} + +uint32 WebDebugService::ClearBreak(const char8 *name) { + mutex.FastLock(); + uint32 count = 0u; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || WDS_SuffixMatch(aliases[i].name.Buffer(), name)) { + signals[aliases[i].signalIndex]->breakOp = BREAK_OFF; + count++; + } + } + if (count > 0u) UpdateBrokersBreakStatus(); + mutex.FastUnLock(); + return count; +} + +bool WebDebugService::IsInstrumented(const char8 *fullPath, + bool &traceable, bool &forcable) { + mutex.FastLock(); + bool found = false; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == fullPath || + WDS_SuffixMatch(aliases[i].name.Buffer(), fullPath)) { + found = true; break; + } + } + mutex.FastUnLock(); + traceable = found; + forcable = found; + return found; +} + +uint32 WebDebugService::RegisterMonitorSignal(const char8 *path, uint32 periodMs) { + mutex.FastLock(); + for (uint32 j = 0u; j < monitoredSignals.Size(); j++) { + if (monitoredSignals[j].path == path) { + monitoredSignals[j].periodMs = periodMs; + mutex.FastUnLock(); + return 1u; + } + } + + StreamString fullPath = path; + fullPath.Seek(0u); + char8 term; + Vec parts; + StreamString token; + while (fullPath.GetToken(token, ".", term)) { parts.Push(token); token = ""; } + + uint32 count = 0u; + if (parts.Size() >= 2u) { + StreamString signalName = parts[parts.Size() - 1u]; + StreamString dsPath; + for (uint32 i = 0u; i < parts.Size() - 1u; i++) { + dsPath += parts[i]; + if (i < parts.Size() - 2u) dsPath += "."; + } + ReferenceT ds = ObjectRegistryDatabase::Instance()->Find(dsPath.Buffer()); + if (ds.IsValid()) { + uint32 idx = 0u; + if (ds->GetSignalIndex(idx, signalName.Buffer())) { + MonitoredSignal m; + m.dataSource = ds; + m.signalIdx = idx; + m.path = path; + m.periodMs = periodMs; + m.lastPollTime = 0u; + m.size = 0u; + (void)ds->GetSignalByteSize(idx, m.size); + if (m.size == 0u) m.size = 4u; + m.internalID = 0x80000000u | monitoredSignals.Size(); + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == path || WDS_SuffixMatch(aliases[i].name.Buffer(), path)) { + m.internalID = signals[aliases[i].signalIndex]->internalID; + break; + } + } + monitoredSignals.Push(m); + count = 1u; + } + } + } + mutex.FastUnLock(); + return count; +} + +uint32 WebDebugService::UnmonitorSignal(const char8 *path) { + mutex.FastLock(); + uint32 count = 0u; + for (uint32 i = 0u; i < monitoredSignals.Size(); i++) { + if (monitoredSignals[i].path == path || + WDS_SuffixMatch(monitoredSignals[i].path.Buffer(), path)) { + (void)monitoredSignals.Remove(i); i--; count++; + } + } + mutex.FastUnLock(); + return count; +} + +// --------------------------------------------------------------------------- +// EmbeddedServiceMethodBinderI — framework dispatches here; we never reach it +// --------------------------------------------------------------------------- +ErrorManagement::ErrorType WebDebugService::Execute(ExecutionInfo &info) { + (void)info; + return ErrorManagement::FatalError; +} + +// --------------------------------------------------------------------------- +// Config helpers +// --------------------------------------------------------------------------- + +void WebDebugService::SetFullConfig(ConfigurationDatabase &config) { + config.MoveToRoot(); + config.Copy(fullConfig); + manualConfigSet = true; +} + +void WebDebugService::RebuildConfigFromRegistry() { + fullConfig = ConfigurationDatabase(); + WDS_BuildCDB(ObjectRegistryDatabase::Instance(), fullConfig); + fullConfig.MoveToRoot(); +} + +void WebDebugService::JsonifyDatabase(ConfigurationDatabase &db, + StreamString &json) { + json += "{"; + uint32 n = db.GetNumberOfChildren(); + bool first = true; + for (uint32 i = 0u; i < n; i++) { + const char8 *key = db.GetChildName(i); + if (key == NULL_PTR(const char8 *)) continue; + if (!first) json += ", "; + first = false; + json += "\""; + WDS_EscapeJson(key, json); + json += "\": "; + if (db.MoveRelative(key)) { + JsonifyDatabase(db, json); + (void)db.MoveToAncestor(1u); + } else { + AnyType at = db.GetType(key); + if (at.GetDataPointer() != NULL_PTR(void *)) { + char8 buf[512] = { '\0' }; + AnyType dst(CharString, 0u, buf); + dst.SetNumberOfElements(0u, 512u); + if (TypeConvert(dst, at)) { + json += "\""; + WDS_EscapeJson(buf, json); + json += "\""; + } else { + json += "null"; + } + } else { + json += "null"; + } + } + } + json += "}"; +} + +void WebDebugService::EnrichWithConfig(const char8 *path, StreamString &json) { + if (path == NULL_PTR(const char8 *)) return; + if (!manualConfigSet) RebuildConfigFromRegistry(); + fullConfig.MoveToRoot(); + const char8 *current = path; + bool ok = true; + while (ok) { + const char8 *dot = StringHelper::SearchString(current, "."); + StreamString part; + if (dot != NULL_PTR(const char8 *)) { + uint32 len = (uint32)(dot - current); + (void)part.Write(current, len); + current = dot + 1; + } else { + part = current; ok = false; + } + bool found = false; + if (fullConfig.MoveRelative(part.Buffer())) { found = true; } + if (!found) { + StreamString pref; pref.Printf("+%s", part.Buffer()); + if (fullConfig.MoveRelative(pref.Buffer())) found = true; + } + if (!found) return; + } + uint32 nc = fullConfig.GetNumberOfChildren(); + for (uint32 i = 0u; i < nc; i++) { + const char8 *ek = fullConfig.GetChildName(i); + if (fullConfig.MoveRelative(ek)) { + fullConfig.MoveToAncestor(1u); continue; + } + AnyType at = fullConfig.GetType(ek); + if (at.GetDataPointer() != NULL_PTR(void *)) { + char8 buf[512] = { '\0' }; + AnyType dst(CharString, 0u, buf); + dst.SetNumberOfElements(0u, 512u); + if (TypeConvert(dst, at)) { + json += ", \""; + WDS_EscapeJson(ek, json); + json += "\": \""; + WDS_EscapeJson(buf, json); + json += "\""; + } + } + } +} + +// --------------------------------------------------------------------------- +// Broker index maintenance +// --------------------------------------------------------------------------- + +void WebDebugService::UpdateBrokersActiveStatus() { + for (uint32 i = 0u; i < brokers.Size(); i++) { + if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue; + uint32 count = 0u; + Vec tempInd, tempSizes; + for (uint32 j = 0u; j < brokers[i].numSignals; j++) { + DebugSignalInfo *s = brokers[i].signalPointers[j]; + if (s != NULL_PTR(DebugSignalInfo *) && (s->isTracing || s->isForcing)) { + tempInd.Push(j); + tempSizes.Push((brokers[i].broker != NULL_PTR(MemoryMapBroker *)) + ? brokers[i].broker->GetCopyByteSize(j) : 4u); + count++; + } + } + if (brokers[i].activeMutex) brokers[i].activeMutex->FastLock(); + if (brokers[i].activeIndices) brokers[i].activeIndices->Swap(tempInd); + if (brokers[i].activeSizes) brokers[i].activeSizes->Swap(tempSizes); + if (brokers[i].anyActiveFlag) *(brokers[i].anyActiveFlag) = (count > 0u); + if (brokers[i].activeMutex) brokers[i].activeMutex->FastUnLock(); + } +} + +void WebDebugService::UpdateBrokersBreakStatus() { + for (uint32 i = 0u; i < brokers.Size(); i++) { + if (brokers[i].signalPointers == NULL_PTR(DebugSignalInfo **)) continue; + Vec tempBreak; + uint32 count = 0u; + for (uint32 j = 0u; j < brokers[i].numSignals; j++) { + DebugSignalInfo *s = brokers[i].signalPointers[j]; + if (s != NULL_PTR(DebugSignalInfo *) && s->breakOp != BREAK_OFF) { + tempBreak.Push(j); count++; + } + } + if (brokers[i].activeMutex) brokers[i].activeMutex->FastLock(); + if (brokers[i].breakIndices) brokers[i].breakIndices->Swap(tempBreak); + if (brokers[i].anyBreakFlag) *(brokers[i].anyBreakFlag) = (count > 0u); + if (brokers[i].activeMutex) brokers[i].activeMutex->FastUnLock(); + } +} + +// --------------------------------------------------------------------------- +// Step control +// --------------------------------------------------------------------------- + +void WebDebugService::Step(uint32 n, const char8 *threadName) { + mutex.FastLock(); + stepRemaining = n; + isPaused = false; + stepThreadFilter = (threadName != NULL_PTR(const char8 *)) ? threadName : ""; + mutex.FastUnLock(); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +void WebDebugService::GetStepStatus(StreamString &out) { + mutex.FastLock(); + bool paused = isPaused; + uint32 rem = stepRemaining; + StreamString gam = pausedAtGam; + StreamString thr = stepThreadFilter; + mutex.FastUnLock(); + out.Printf("{\"Paused\":%s,\"PausedAtGam\":\"%s\"," + "\"StepRemaining\":%u,\"StepThread\":\"%s\"}\nOK STEP_STATUS\n", + paused ? "true" : "false", gam.Buffer(), rem, thr.Buffer()); +} + +void WebDebugService::GetSignalValue(const char8 *name, StreamString &out) { + mutex.FastLock(); + DebugSignalInfo *sig = NULL_PTR(DebugSignalInfo *); + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == name || WDS_SuffixMatch(aliases[i].name.Buffer(), name)) { + sig = signals[aliases[i].signalIndex]; break; + } + } + if (sig == NULL_PTR(DebugSignalInfo *)) { + mutex.FastUnLock(); + out.Printf("{\"Error\":\"Signal not found: %s\"}\nOK VALUE\n", name); + return; + } + TypeDescriptor td = sig->type; + uint32 nElem = sig->numberOfElements; + uint32 bySz = (td.numberOfBits > 0u) ? (td.numberOfBits / 8u) : 1u; + bool trunc = (nElem > GET_VALUE_MAX_ELEMENTS); + if (trunc) nElem = GET_VALUE_MAX_ELEMENTS; + uint32 total = bySz * nElem; + if (total > 1024u) { total = 1024u; nElem = total / bySz; } + uint8 lb[1024]; memset(lb, 0, sizeof(lb)); + if (sig->memoryAddress != NULL_PTR(void *)) memcpy(lb, sig->memoryAddress, total); + mutex.FastUnLock(); + + StreamString valueStr; + if (nElem > 1u) { + for (uint32 i = 0u; i < nElem; i++) { + char8 eb[128] = { '\0' }; + AnyType se(td, 0u, (void *)(lb + i * bySz)); + AnyType ds(CharString, 0u, eb); + ds.SetNumberOfElements(0u, 128u); + (void)TypeConvert(ds, se); + if (i > 0u) valueStr += ", "; + valueStr += eb; + } + } else { + char8 eb[256] = { '\0' }; + AnyType se(td, 0u, (void *)lb); + AnyType ds(CharString, 0u, eb); + ds.SetNumberOfElements(0u, 256u); + (void)TypeConvert(ds, se); + valueStr = eb; + } + out += "{\"Name\":\""; + out += name; + out += "\",\"Value\":\""; + const char8 *vp = valueStr.Buffer(); + while (vp != NULL_PTR(const char8 *) && *vp != '\0') { + if (*vp == '"') out += "\\\""; + else if (*vp == '\\') out += "\\\\"; + else { char8 tmp[2] = { *vp, '\0' }; out += tmp; } + vp++; + } + out.Printf("\",\"Elements\":%u,\"Truncated\":%s}\nOK VALUE\n", + nElem, trunc ? "true" : "false"); +} + +void WebDebugService::ServeConfig(StreamString &out) { + if (!manualConfigSet) RebuildConfigFromRegistry(); + fullConfig.MoveToRoot(); + JsonifyDatabase(fullConfig, out); + out += "\nOK CONFIG\n"; +} + +void WebDebugService::InfoNode(const char8 *path, StreamString &out) { + Reference ref = ObjectRegistryDatabase::Instance()->Find(path); + out += "{"; + if (ref.IsValid()) { + out += "\"Name\":\""; WDS_EscapeJson(ref->GetName(), out); + out += "\",\"Class\":\""; WDS_EscapeJson(ref->GetClassProperties()->GetName(), out); + out += "\""; + ConfigurationDatabase db; + if (ref->ExportData(db)) { + out += ",\"Config\":{"; + db.MoveToRoot(); + uint32 nc = db.GetNumberOfChildren(); + for (uint32 i = 0u; i < nc; i++) { + const char8 *cn = db.GetChildName(i); + AnyType at = db.GetType(cn); + char8 buf[1024]; AnyType st(CharString, 0u, buf); st.SetNumberOfElements(0, 1024); + if (TypeConvert(st, at)) { + out += "\""; WDS_EscapeJson(cn, out); out += "\":\""; + WDS_EscapeJson(buf, out); out += "\""; + if (i < nc - 1u) out += ","; + } + } + out += "}"; + } + EnrichWithConfig(path, out); + } else { + StreamString enrichAlias; + mutex.FastLock(); + bool found = false; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (aliases[i].name == path || WDS_SuffixMatch(aliases[i].name.Buffer(), path)) { + DebugSignalInfo *s = signals[aliases[i].signalIndex]; + const char8 *tn = TypeDescriptor::GetTypeNameFromTypeDescriptor(s->type); + out.Printf("\"Name\":\"%s\",\"Class\":\"Signal\",\"Type\":\"%s\",\"ID\":%u", + s->name.Buffer(), tn ? tn : "Unknown", s->internalID); + enrichAlias = aliases[i].name; + found = true; break; + } + } + mutex.FastUnLock(); + if (found) EnrichWithConfig(enrichAlias.Buffer(), out); + else out += "\"Error\":\"Object not found\""; + } + out += "}\nOK INFO\n"; +} + +void WebDebugService::ListNodes(const char8 *path, StreamString &out) { + Reference ref = + (path == NULL_PTR(const char8 *) || StringHelper::Length(path) == 0 || + StringHelper::Compare(path, "/") == 0) + ? ObjectRegistryDatabase::Instance() + : ObjectRegistryDatabase::Instance()->Find(path); + out.Printf("Nodes under %s:\n", path ? path : "/"); + if (ref.IsValid()) { + ReferenceContainer *rc = dynamic_cast(ref.operator->()); + if (rc != NULL_PTR(ReferenceContainer *)) { + uint32 n = rc->Size(); + for (uint32 i = 0u; i < n; i++) { + Reference c = rc->Get(i); + if (c.IsValid()) { + out.Printf(" %s [%s]\n", c->GetName(), + c->GetClassProperties()->GetName()); + } + } + } + } else { + out += " (not found)\n"; + } + out += "OK LS\n"; +} + +uint32 WebDebugService::ExportTree(ReferenceContainer *container, + StreamString &json, const char8 *pathPrefix) { + if (container == NULL_PTR(ReferenceContainer *)) return 0u; + uint32 size = container->Size(); + uint32 valid = 0u; + for (uint32 i = 0u; i < size; i++) { + Reference child = container->Get(i); + if (!child.IsValid()) continue; + if (valid > 0u) json += ",\n"; + const char8 *cname = child->GetName(); + if (cname == NULL_PTR(const char8 *)) cname = "unnamed"; + StreamString cp; + if (pathPrefix != NULL_PTR(const char8 *)) cp.Printf("%s.%s", pathPrefix, cname); + else cp = cname; + StreamString nj; + nj += "{\"Name\":\""; WDS_EscapeJson(cname, nj); + nj += "\",\"Class\":\""; WDS_EscapeJson(child->GetClassProperties()->GetName(), nj); + nj += "\""; + ReferenceContainer *inner = dynamic_cast(child.operator->()); + DataSourceI *ds = dynamic_cast(child.operator->()); + GAM *gam = dynamic_cast(child.operator->()); + if (inner != NULL_PTR(ReferenceContainer *) || ds != NULL_PTR(DataSourceI *) || gam != NULL_PTR(GAM *)) { + nj += ",\"Children\":[\n"; + uint32 sc = 0u; + if (inner != NULL_PTR(ReferenceContainer *)) sc += ExportTree(inner, nj, cp.Buffer()); + if (ds != NULL_PTR(DataSourceI *)) { + uint32 ns = ds->GetNumberOfSignals(); + for (uint32 j = 0u; j < ns; j++) { + if (sc > 0u) { nj += ",\n"; } sc++; + StreamString sn; (void)ds->GetSignalName(j, sn); + const char8 *st = TypeDescriptor::GetTypeNameFromTypeDescriptor(ds->GetSignalType(j)); + uint8 d = 0u; (void)ds->GetSignalNumberOfDimensions(j, d); + uint32 el = 0u; (void)ds->GetSignalNumberOfElements(j, el); + StreamString sfp; sfp.Printf("%s.%s", cp.Buffer(), sn.Buffer()); + bool tr = false, fo = false; (void)IsInstrumented(sfp.Buffer(), tr, fo); + nj += "{\"Name\":\""; WDS_EscapeJson(sn.Buffer(), nj); + nj += "\",\"Class\":\"Signal\",\"Type\":\""; WDS_EscapeJson(st ? st : "Unknown", nj); + nj.Printf("\",\"Dimensions\":%u,\"Elements\":%u,\"IsTraceable\":%s,\"IsForcable\":%s}", + d, el, tr ? "true" : "false", fo ? "true" : "false"); + } + } + if (gam != NULL_PTR(GAM *)) { + uint32 nIn = gam->GetNumberOfInputSignals(); + for (uint32 j = 0u; j < nIn; j++) { + if (sc > 0u) { nj += ",\n"; } sc++; + StreamString sn; (void)gam->GetSignalName(InputSignals, j, sn); + const char8 *st = TypeDescriptor::GetTypeNameFromTypeDescriptor(gam->GetSignalType(InputSignals, j)); + uint32 d = 0u; (void)gam->GetSignalNumberOfDimensions(InputSignals, j, d); + uint32 el = 0u; (void)gam->GetSignalNumberOfElements(InputSignals, j, el); + StreamString sfp; sfp.Printf("%s.In.%s", cp.Buffer(), sn.Buffer()); + bool tr = false, fo = false; (void)IsInstrumented(sfp.Buffer(), tr, fo); + nj += "{\"Name\":\"In."; WDS_EscapeJson(sn.Buffer(), nj); + nj += "\",\"Class\":\"InputSignal\",\"Type\":\""; WDS_EscapeJson(st ? st : "Unknown", nj); + nj.Printf("\",\"Dimensions\":%u,\"Elements\":%u,\"IsTraceable\":%s,\"IsForcable\":%s}", + d, el, tr ? "true" : "false", fo ? "true" : "false"); + } + uint32 nOut = gam->GetNumberOfOutputSignals(); + for (uint32 j = 0u; j < nOut; j++) { + if (sc > 0u) { nj += ",\n"; } sc++; + StreamString sn; (void)gam->GetSignalName(OutputSignals, j, sn); + const char8 *st = TypeDescriptor::GetTypeNameFromTypeDescriptor(gam->GetSignalType(OutputSignals, j)); + uint32 d = 0u; (void)gam->GetSignalNumberOfDimensions(OutputSignals, j, d); + uint32 el = 0u; (void)gam->GetSignalNumberOfElements(OutputSignals, j, el); + StreamString sfp; sfp.Printf("%s.Out.%s", cp.Buffer(), sn.Buffer()); + bool tr = false, fo = false; (void)IsInstrumented(sfp.Buffer(), tr, fo); + nj += "{\"Name\":\"Out."; WDS_EscapeJson(sn.Buffer(), nj); + nj += "\",\"Class\":\"OutputSignal\",\"Type\":\""; WDS_EscapeJson(st ? st : "Unknown", nj); + nj.Printf("\",\"Dimensions\":%u,\"Elements\":%u,\"IsTraceable\":%s,\"IsForcable\":%s}", + d, el, tr ? "true" : "false", fo ? "true" : "false"); + } + } + nj += "\n]"; + } + nj += "}"; + json += nj; + valid++; + } + return valid; +} + +void WebDebugService::Discover(StreamString &out) { + out += "{\"Signals\":[\n"; + mutex.FastLock(); + uint32 total = 0u; + for (uint32 i = 0u; i < aliases.Size(); i++) { + if (total > 0u) out += ",\n"; + DebugSignalInfo *sig = signals[aliases[i].signalIndex]; + const char8 *tn = TypeDescriptor::GetTypeNameFromTypeDescriptor(sig->type); + out.Printf(" {\"name\":\"%s\",\"id\":%u,\"type\":\"%s\"," + "\"dimensions\":%u,\"elements\":%u", + aliases[i].name.Buffer(), sig->internalID, + tn ? tn : "Unknown", sig->numberOfDimensions, sig->numberOfElements); + EnrichWithConfig(aliases[i].name.Buffer(), out); + out += "}"; + total++; + } + for (uint32 i = 0u; i < monitoredSignals.Size(); i++) { + bool found = false; + for (uint32 j = 0u; j < aliases.Size(); j++) { + if (aliases[j].name == monitoredSignals[i].path) { found = true; break; } + } + if (!found) { + if (total > 0u) out += ",\n"; + const char8 *tn = TypeDescriptor::GetTypeNameFromTypeDescriptor( + monitoredSignals[i].dataSource->GetSignalType(monitoredSignals[i].signalIdx)); + uint8 dims = 0u; uint32 elems = 1u; + (void)monitoredSignals[i].dataSource->GetSignalNumberOfDimensions(monitoredSignals[i].signalIdx, dims); + (void)monitoredSignals[i].dataSource->GetSignalNumberOfElements(monitoredSignals[i].signalIdx, elems); + out.Printf(" {\"name\":\"%s\",\"id\":%u,\"type\":\"%s\"," + "\"dimensions\":%u,\"elements\":%u", + monitoredSignals[i].path.Buffer(), monitoredSignals[i].internalID, + tn ? tn : "Unknown", dims, elems); + EnrichWithConfig(monitoredSignals[i].path.Buffer(), out); + out += "}"; + total++; + } + } + mutex.FastUnLock(); + out += "\n]}\nOK DISCOVER\n"; +} + +void WebDebugService::HandleCommand(const StreamString &cmdIn, StreamString &out) { + StreamString cmd = cmdIn; + StreamString token; + cmd.Seek(0u); + char8 term; + const char8 *delims = " \r\n"; + if (!cmd.GetToken(token, delims, term)) return; + + if (token == "FORCE") { + StreamString name, val; + if (cmd.GetToken(name, delims, term) && cmd.GetToken(val, delims, term)) { + uint32 c = ForceSignal(name.Buffer(), val.Buffer()); + out.Printf("OK FORCE %u\n", c); + } + } else if (token == "UNFORCE") { + StreamString name; + if (cmd.GetToken(name, delims, term)) { + uint32 c = UnforceSignal(name.Buffer()); + out.Printf("OK UNFORCE %u\n", c); + } + } else if (token == "TRACE") { + StreamString name, state, decim; + if (cmd.GetToken(name, delims, term) && cmd.GetToken(state, delims, term)) { + bool en = (state == "1"); + uint32 d = 1u; + if (cmd.GetToken(decim, delims, term)) { + AnyType dv(UnsignedInteger32Bit, 0u, &d); + AnyType ds(CharString, 0u, decim.Buffer()); + (void)TypeConvert(dv, ds); + } + uint32 c = TraceSignal(name.Buffer(), en, d); + out.Printf("OK TRACE %u\n", c); + } + } else if (token == "BREAK") { + StreamString name, opStr; + if (cmd.GetToken(name, delims, term) && cmd.GetToken(opStr, delims, term)) { + uint32 c = 0u; + if (opStr == "OFF") { + c = ClearBreak(name.Buffer()); + } else { + StreamString thrStr; + if (cmd.GetToken(thrStr, delims, term)) { + uint8 op = BREAK_OFF; + if (opStr == ">") op = BREAK_GT; + else if (opStr == "<") op = BREAK_LT; + else if (opStr == "==") op = BREAK_EQ; + else if (opStr == ">=") op = BREAK_GEQ; + else if (opStr == "<=") op = BREAK_LEQ; + else if (opStr == "!=") op = BREAK_NEQ; + if (op != BREAK_OFF) { + float64 thr = 0.0; + AnyType tv(Float64Bit, 0u, &thr); + AnyType ts(CharString, 0u, thrStr.Buffer()); + (void)TypeConvert(tv, ts); + c = SetBreak(name.Buffer(), op, thr); + } + } + } + out.Printf("OK BREAK %u\n", c); + } + } else if (token == "PAUSE") { + SetPaused(true); + out += "OK\n"; + // broadcast status event + StreamString ev; + ev.Printf("{\"type\":\"status\",\"paused\":true,\"gam\":\"%s\",\"remaining\":%u}", + pausedAtGam.Buffer(), (uint32)stepRemaining); + BroadcastSse("message", ev); + } else if (token == "RESUME") { + SetPaused(false); + out += "OK\n"; + StreamString ev; + ev += "{\"type\":\"status\",\"paused\":false,\"gam\":\"\",\"remaining\":0}"; + BroadcastSse("message", ev); + } else if (token == "STEP") { + StreamString nStr; + uint32 n = 1u; + if (cmd.GetToken(nStr, delims, term)) { + AnyType nv(UnsignedInteger32Bit, 0u, &n); + AnyType ns(CharString, 0u, nStr.Buffer()); + (void)TypeConvert(nv, ns); + } + StreamString thr; + const char8 *ta = NULL_PTR(const char8 *); + if (cmd.GetToken(thr, delims, term) && thr.Size() > 0u) ta = thr.Buffer(); + Step(n, ta); + out.Printf("OK STEP %u\n", n); + } else if (token == "STEP_STATUS") { + GetStepStatus(out); + } else if (token == "VALUE") { + StreamString sn; + if (cmd.GetToken(sn, delims, term)) GetSignalValue(sn.Buffer(), out); + else out += "{\"Error\":\"Missing signal name\"}\nOK VALUE\n"; + } else if (token == "DISCOVER") { + Discover(out); + } else if (token == "TREE") { + out += "{\"Name\":\"Root\",\"Class\":\"ObjectRegistryDatabase\"," + "\"Children\":[\n"; + (void)ExportTree(ObjectRegistryDatabase::Instance(), out, NULL_PTR(const char8 *)); + out += "\n]}\nOK TREE\n"; + } else if (token == "INFO") { + StreamString path; + if (cmd.GetToken(path, delims, term)) InfoNode(path.Buffer(), out); + } else if (token == "LS") { + StreamString path; + if (cmd.GetToken(path, delims, term)) ListNodes(path.Buffer(), out); + else ListNodes(NULL_PTR(const char8 *), out); + } else if (token == "CONFIG") { + ServeConfig(out); + } else if (token == "MONITOR") { + StreamString sub, name, period; + if (cmd.GetToken(sub, delims, term) && sub == "SIGNAL" && + cmd.GetToken(name, delims, term) && cmd.GetToken(period, delims, term)) { + uint32 p = 100u; + AnyType pv(UnsignedInteger32Bit, 0u, &p); + AnyType ps(CharString, 0u, period.Buffer()); + (void)TypeConvert(pv, ps); + uint32 c = RegisterMonitorSignal(name.Buffer(), p); + out.Printf("OK MONITOR %u\n", c); + } + } else if (token == "UNMONITOR") { + StreamString sub, name; + if (cmd.GetToken(sub, delims, term) && sub == "SIGNAL" && + cmd.GetToken(name, delims, term)) { + uint32 c = UnmonitorSignal(name.Buffer()); + out.Printf("OK UNMONITOR %u\n", c); + } + } else if (token == "SERVICE_INFO") { + out.Printf("OK SERVICE_INFO HTTP:%u STATE:%s\n", + (uint32)httpPort, isPaused ? "PAUSED" : "RUNNING"); + } + // MSG command handled separately — needs access to ORD; not yet supported here +} + +// --------------------------------------------------------------------------- +// SSE broadcast +// --------------------------------------------------------------------------- + +void WebDebugService::BroadcastSse(const char8 *eventType, const StreamString &data) { + StreamString pkt; + pkt.Printf("event: %s\ndata: ", eventType); + pkt += data; + pkt += "\n\n"; + sseMutex.FastLock(); + for (uint32 i = 0u; i < MAX_SSE_CLIENTS; i++) { + if (sseClients[i] != NULL_PTR(BasicTCPSocket *)) { + uint32 sz = pkt.Size(); + if (!sseClients[i]->Write(pkt.Buffer(), sz)) { + sseClients[i]->Close(); + delete sseClients[i]; + sseClients[i] = NULL_PTR(BasicTCPSocket *); + } + } + } + sseMutex.FastUnLock(); +} + +void WebDebugService::RemoveDeadSseClients() { + sseMutex.FastLock(); + for (uint32 i = 0u; i < MAX_SSE_CLIENTS; i++) { + if (sseClients[i] != NULL_PTR(BasicTCPSocket *) && + !sseClients[i]->IsConnected()) { + sseClients[i]->Close(); + delete sseClients[i]; + sseClients[i] = NULL_PTR(BasicTCPSocket *); + } + } + sseMutex.FastUnLock(); +} + +void WebDebugService::UpgradeToSse(BasicTCPSocket *client) { + // Send SSE upgrade headers + const char8 *hdr = + "HTTP/1.1 200 OK\r\n" + "Content-Type: text/event-stream\r\n" + "Cache-Control: no-cache\r\n" + "Connection: keep-alive\r\n" + "Access-Control-Allow-Origin: *\r\n" + "\r\n"; + uint32 sz = StringHelper::Length(hdr); + (void)client->Write(hdr, sz); + + // Send an initial status event so the browser knows the connection is alive + mutex.FastLock(); + bool paused = isPaused; + StreamString gam = pausedAtGam; + uint32 rem = stepRemaining; + mutex.FastUnLock(); + StreamString initEv; + initEv.Printf("{\"type\":\"status\",\"paused\":%s,\"gam\":\"%s\",\"remaining\":%u}", + paused ? "true" : "false", gam.Buffer(), rem); + StreamString initPkt; + initPkt += "event: message\ndata: "; + initPkt += initEv; + initPkt += "\n\n"; + uint32 isz = initPkt.Size(); + (void)client->Write(initPkt.Buffer(), isz); + + // Replay log history so the browser sees messages emitted before it connected + logHistoryMutex.FastLock(); + { + uint32 startIdx = (logHistoryFill >= LOG_HISTORY_SIZE) + ? logHistoryWriteIdx : 0u; + uint32 count = logHistoryFill; + for (uint32 i = 0u; i < count; i++) { + uint32 idx = (startIdx + i) % LOG_HISTORY_SIZE; + if (logHistory[idx][0] != '\0') { + StreamString histPkt; + histPkt += "event: message\ndata: "; + histPkt += logHistory[idx]; + histPkt += "\n\n"; + uint32 hsz = histPkt.Size(); + (void)client->Write(histPkt.Buffer(), hsz); + } + } + } + logHistoryMutex.FastUnLock(); + + // Register in the SSE client slot array + sseMutex.FastLock(); + bool registered = false; + for (uint32 i = 0u; i < MAX_SSE_CLIENTS; i++) { + if (sseClients[i] == NULL_PTR(BasicTCPSocket *)) { + sseClients[i] = client; + registered = true; + break; + } + } + sseMutex.FastUnLock(); + if (!registered) { + // No free slot: close immediately + client->Close(); + delete client; + } + // Do NOT close or delete client here — Streamer owns it now +} + +// --------------------------------------------------------------------------- +// HTTP request parsing +// --------------------------------------------------------------------------- + +bool WebDebugService::ParseRequest(BasicTCPSocket *client, + StreamString &method, StreamString &path, + StreamString &body) { + // Read until we see the end of HTTP headers (\r\n\r\n) + static const uint32 MAX_HEADER = 8192u; + char8 buf[MAX_HEADER + 1]; + uint32 total = 0u; + uint32 headerEnd = 0u; + bool foundEnd = false; + + while (total < MAX_HEADER && !foundEnd) { + uint32 sz = MAX_HEADER - total; + if (!client->Read(buf + total, sz) || sz == 0u) break; + total += sz; + buf[total] = '\0'; + // scan for \r\n\r\n + for (uint32 i = 0u; i + 3u < total; i++) { + if (buf[i] == '\r' && buf[i+1] == '\n' && buf[i+2] == '\r' && buf[i+3] == '\n') { + headerEnd = i + 4u; + foundEnd = true; + break; + } + } + } + if (!foundEnd) return false; + + // Parse request line + const char8 *line = buf; + const char8 *sp1 = StringHelper::SearchChar(line, ' '); + if (sp1 == NULL_PTR(const char8 *)) return false; + method = ""; + { uint32 mLen = (uint32)(sp1 - line); (void)method.Write(line, mLen); } + const char8 *sp2 = StringHelper::SearchChar(sp1 + 1, ' '); + if (sp2 == NULL_PTR(const char8 *)) return false; + path = ""; + { uint32 pLen = (uint32)(sp2 - sp1 - 1); (void)path.Write(sp1 + 1, pLen); } + + // Extract Content-Length for POST body + uint32 contentLength = 0u; + const char8 *cl = StringHelper::SearchString(buf, "Content-Length:"); + if (cl != NULL_PTR(const char8 *)) { + cl += 15u; + while (*cl == ' ') cl++; + AnyType cv(UnsignedInteger32Bit, 0u, &contentLength); + AnyType cs(CharString, 0u, cl); + (void)TypeConvert(cv, cs); + } + + // Read body + body = ""; + if (contentLength > 0u) { + uint32 alreadyRead = (total > headerEnd) ? (total - headerEnd) : 0u; + if (alreadyRead > 0u) { + (void)body.Write(buf + headerEnd, alreadyRead); + } + static const uint32 MAX_BODY = 16384u; + if (contentLength > alreadyRead && contentLength <= MAX_BODY) { + char8 bodyBuf[MAX_BODY]; + uint32 toRead = contentLength - alreadyRead; + uint32 sz = toRead; + if (client->Read(bodyBuf, sz) && sz > 0u) { + (void)body.Write(bodyBuf, sz); + } + } + } + return true; +} + +void WebDebugService::SendHttp(BasicTCPSocket *client, uint32 code, + const char8 *ct, StreamString &body) { + StreamString resp; + resp.Printf("HTTP/1.1 %u %s\r\n" + "Content-Type: %s\r\n" + "Content-Length: %u\r\n" + "Cache-Control: no-cache\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Connection: close\r\n" + "\r\n", + code, (code == 200u) ? "OK" : "Error", + ct, (uint32)body.Size()); + resp += body; + uint32 sz = resp.Size(); + (void)client->Write(resp.Buffer(), sz); +} + +void WebDebugService::HandleRequest(BasicTCPSocket *client, + const StreamString &method, + const StreamString &path, + const StreamString &body) { + if (method == "GET" && (path == "/" || path == "/index.html")) { + StreamString html = WEB_UI_HTML; + SendHttp(client, 200u, "text/html; charset=utf-8", html); + } else if (method == "GET" && path == "/api/events") { + // SSE upgrade — hands off socket ownership; do not close after return + UpgradeToSse(client); + return; // caller must not close socket + } else if (method == "POST" && path == "/api/command") { + StreamString out; + HandleCommand(body, out); + SendHttp(client, 200u, "text/plain; charset=utf-8", out); + } else if (method == "OPTIONS") { + // CORS preflight + const char8 *hdr = + "HTTP/1.1 204 No Content\r\n" + "Access-Control-Allow-Origin: *\r\n" + "Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n" + "Access-Control-Allow-Headers: Content-Type\r\n" + "Connection: close\r\n" + "\r\n"; + uint32 sz = StringHelper::Length(hdr); + (void)client->Write(hdr, sz); + } else { + StreamString body404 = "Not Found"; + SendHttp(client, 404u, "text/plain", body404); + } + client->Close(); + delete client; +} + +// --------------------------------------------------------------------------- +// HTTP server thread +// --------------------------------------------------------------------------- + +ErrorManagement::ErrorType WebDebugService::HttpServer(ExecutionInfo &info) { + if (info.GetStage() == ExecutionInfo::TerminationStage) + return ErrorManagement::NoError; + if (info.GetStage() == ExecutionInfo::StartupStage) { + Sleep::MSec(500u); // wait for ORD to finish + return ErrorManagement::NoError; + } + + BasicTCPSocket *client = tcpServer.WaitConnection(TimeoutType(100u)); + if (client == NULL_PTR(BasicTCPSocket *)) + return ErrorManagement::NoError; + + StreamString method, path, body; + if (ParseRequest(client, method, path, body)) { + // Check if SSE upgrade — HandleRequest takes ownership if SSE + bool isSse = (method == "GET" && path == "/api/events"); + HandleRequest(client, method, path, body); + if (isSse) { + // Socket ownership transferred to sseClients — don't touch it + } + // For non-SSE, HandleRequest already closed/deleted the client + } else { + client->Close(); + delete client; + } + return ErrorManagement::NoError; +} + +// --------------------------------------------------------------------------- +// Streamer thread — reads ring buffer, broadcasts SSE events, polls monitors +// --------------------------------------------------------------------------- + +// Helper: convert raw bytes to float64 for SSE JSON +static float64 WDS_ToFloat64(const uint8 *data, TypeDescriptor td) { + float64 v = 0.0; + if (td == Float32Bit) { + float32 f; memcpy(&f, data, 4u); v = (float64)f; + } else if (td == Float64Bit) { + memcpy(&v, data, 8u); + } else if (td == SignedInteger8Bit) { + int8 x; memcpy(&x, data, 1u); v = (float64)x; + } else if (td == UnsignedInteger8Bit) { + uint8 x; memcpy(&x, data, 1u); v = (float64)x; + } else if (td == SignedInteger16Bit) { + int16 x; memcpy(&x, data, 2u); v = (float64)x; + } else if (td == UnsignedInteger16Bit) { + uint16 x; memcpy(&x, data, 2u); v = (float64)x; + } else if (td == SignedInteger32Bit) { + int32 x; memcpy(&x, data, 4u); v = (float64)x; + } else if (td == UnsignedInteger32Bit) { + uint32 x; memcpy(&x, data, 4u); v = (float64)x; + } else if (td == SignedInteger64Bit) { + int64 x; memcpy(&x, data, 8u); v = (float64)x; + } else if (td == UnsignedInteger64Bit) { + uint64 x; memcpy(&x, data, 8u); v = (float64)(int64)x; + } + return v; +} + +ErrorManagement::ErrorType WebDebugService::Streamer(ExecutionInfo &info) { + if (info.GetStage() == ExecutionInfo::TerminationStage) + return ErrorManagement::NoError; + if (info.GetStage() == ExecutionInfo::StartupStage) + return ErrorManagement::NoError; + + uint64 nowNs = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1.0e9); + + uint64 nowMs = (nowNs >= streamerT0Ns) ? (nowNs - streamerT0Ns) / 1000000u : 0u; + + // ----------------------------------------------------------------- + // Drain MARTe2 log pages → SSE log events + // ----------------------------------------------------------------- + { + static const uint32 MAX_LOGS_PER_CYCLE = 32u; + uint32 logCount = 0u; + LoggerPage *logPage = Logger::Instance()->GetLogEntry(); + while (logPage != NULL_PTR(LoggerPage *) && logCount < MAX_LOGS_PER_CYCLE) { + StreamString levelStr; + ErrorManagement::ErrorCodeToStream( + logPage->errorInfo.header.errorType, levelStr); + StreamString msgEsc; + WDS_EscapeJson(logPage->errorStrBuffer, msgEsc); + StreamString ev; + ev.Printf("{\"type\":\"log\",\"level\":\"%s\",\"msg\":\"%s\"}", + levelStr.Buffer(), msgEsc.Buffer()); + Logger::Instance()->ReturnPage(logPage); + // Store in history for late-connecting clients + logHistoryMutex.FastLock(); + { + uint32 evSz = ev.Size(); + if (evSz >= LOG_ENTRY_MAX_SIZE) evSz = LOG_ENTRY_MAX_SIZE - 1u; + (void)StringHelper::CopyN(logHistory[logHistoryWriteIdx], ev.Buffer(), evSz); + logHistory[logHistoryWriteIdx][evSz] = '\0'; + logHistoryWriteIdx = (logHistoryWriteIdx + 1u) % LOG_HISTORY_SIZE; + if (logHistoryFill < LOG_HISTORY_SIZE) logHistoryFill++; + } + logHistoryMutex.FastUnLock(); + BroadcastSse("message", ev); + logPage = Logger::Instance()->GetLogEntry(); + logCount++; + } + } + + // ----------------------------------------------------------------- + // Poll monitored signals + // ----------------------------------------------------------------- + mutex.FastLock(); + for (uint32 i = 0u; i < monitoredSignals.Size(); i++) { + if (nowMs >= (monitoredSignals[i].lastPollTime + monitoredSignals[i].periodMs)) { + monitoredSignals[i].lastPollTime = nowMs; + uint32 tsMs = (uint32)nowMs; + void *addr = NULL_PTR(void *); + if (monitoredSignals[i].dataSource->GetSignalMemoryBuffer( + monitoredSignals[i].signalIdx, 0u, addr) && + addr != NULL_PTR(void *)) { + uint32 sz = monitoredSignals[i].size; + if (sz > 1024u) sz = 1024u; + uint8 lb[1024]; memcpy(lb, addr, sz); + TypeDescriptor td = monitoredSignals[i].dataSource->GetSignalType( + monitoredSignals[i].signalIdx); + float64 val = WDS_ToFloat64(lb, td); + StreamString sigName; + for (uint32 j = 0u; j < aliases.Size(); j++) { + if (signals[aliases[j].signalIndex]->internalID == + monitoredSignals[i].internalID) { + sigName = aliases[j].name; + break; + } + } + if (sigName.Size() == 0u) sigName = monitoredSignals[i].path; + // Safely convert float64 value to string via TypeConvert + char8 valBuf[64] = { '\0' }; + { + AnyType vdst(CharString, 0u, valBuf); vdst.SetNumberOfElements(0u, 63u); + AnyType vsrc(Float64Bit, 0u, &val); + (void)TypeConvert(vdst, vsrc); + } + StreamString ev; + ev.Printf("{\"type\":\"monitor\",\"name\":\"%s\",\"ts\":%u,\"value\":%s}", + sigName.Buffer(), tsMs, valBuf); + mutex.FastUnLock(); + BroadcastSse("message", ev); + mutex.FastLock(); + } + } + } + mutex.FastUnLock(); + + // ----------------------------------------------------------------- + // Drain ring buffer → SSE trace events (capped to limit CPU usage) + // ----------------------------------------------------------------- + static const uint32 SAMPLE_BUF_SIZE = 1024u; + static const uint32 MAX_TRACE_CYCLE = 50u; // max samples per Streamer cycle + uint32 id, size; + uint64 sampleTs; + uint8 sampleData[SAMPLE_BUF_SIZE]; + bool hasData = false; + uint32 traceCount = 0u; + + while (traceCount < MAX_TRACE_CYCLE && + traceBuffer.Pop(id, sampleTs, sampleData, size, SAMPLE_BUF_SIZE)) { + hasData = true; + traceCount++; + if (size > SAMPLE_BUF_SIZE) continue; + + StreamString sigName; + TypeDescriptor sigType = UnsignedInteger32Bit; + mutex.FastLock(); + for (uint32 i = 0u; i < signals.Size(); i++) { + if (signals[i]->internalID == id) { + sigName = signals[i]->name; + sigType = signals[i]->type; + break; + } + } + mutex.FastUnLock(); + + // Convert hardware-ns timestamp to relative ms (uint32) + uint32 tsMs = (sampleTs >= streamerT0Ns) + ? (uint32)((sampleTs - streamerT0Ns) / 1000000u) : 0u; + + float64 val = WDS_ToFloat64(sampleData, sigType); + // Safely convert float64 value to string via TypeConvert + char8 valBuf[64] = { '\0' }; + { + AnyType vdst(CharString, 0u, valBuf); vdst.SetNumberOfElements(0u, 63u); + AnyType vsrc(Float64Bit, 0u, &val); + (void)TypeConvert(vdst, vsrc); + } + StreamString ev; + ev.Printf("{\"type\":\"trace\",\"name\":\"%s\",\"ts\":%u,\"value\":%s}", + sigName.Buffer(), tsMs, valBuf); + BroadcastSse("message", ev); + } + + // ----------------------------------------------------------------- + // Periodic status heartbeat (every 500 ms) + // ----------------------------------------------------------------- + static uint64 lastStatusMs = 0u; + if (nowMs - lastStatusMs >= 500u) { + lastStatusMs = nowMs; + mutex.FastLock(); + bool paused = isPaused; + StreamString gam = pausedAtGam; + uint32 rem = stepRemaining; + mutex.FastUnLock(); + StreamString ev; + ev.Printf("{\"type\":\"status\",\"paused\":%s,\"gam\":\"%s\",\"remaining\":%u}", + paused ? "true" : "false", gam.Buffer(), rem); + BroadcastSse("message", ev); + } + + // Clean up dead SSE clients + RemoveDeadSseClients(); + + // Always yield to prevent the Streamer from monopolising CPU. + // The RT ring buffer at 1000 Hz is drained in ~10 cycles (50 samples/cycle). + (void)hasData; + Sleep::MSec(10u); + return ErrorManagement::NoError; +} + +} // namespace MARTe diff --git a/Source/Components/Interfaces/WebDebugService/WebDebugService.h b/Source/Components/Interfaces/WebDebugService/WebDebugService.h new file mode 100644 index 0000000..2682fd8 --- /dev/null +++ b/Source/Components/Interfaces/WebDebugService/WebDebugService.h @@ -0,0 +1,205 @@ +#ifndef WEBDEBUGSERVICE_H +#define WEBDEBUGSERVICE_H + +#include "BasicTCPSocket.h" +#include "ConfigurationDatabase.h" +#include "DataSourceI.h" +#include "DebugServiceI.h" +#include "EmbeddedServiceMethodBinderI.h" +#include "ReferenceContainer.h" +#include "ReferenceT.h" +#include "SingleThreadService.h" + +namespace MARTe { + +/** + * @brief HTTP/SSE implementation of DebugServiceI. + * + * Serves a single-page web UI on httpPort (default 8090). + * Endpoints: + * GET / — embedded HTML application + * POST /api/command — execute a debug command; returns text response + * GET /api/events — Server-Sent Events stream for real-time telemetry + * + * SSE event types (JSON payload on each "data:" line): + * {"type":"trace", "name":"…","ts":,"value":} + * {"type":"monitor", "name":"…","ts":,"value":} + * {"type":"log", "level":"…","msg":"…"} + * {"type":"status", "paused":,"gam":"…","remaining":} + * {"type":"discover","signals":[…]} + * {"type":"tree", "tree":{…}} + * + * This is an alternative to DebugService (TCP/UDP) and registers itself as the + * global DebugServiceI singleton on Initialise(). + */ +class WebDebugService : public ReferenceContainer, + public EmbeddedServiceMethodBinderI, + public DebugServiceI { +public: + friend class WebDebugServiceTest; + CLASS_REGISTER_DECLARATION() + + WebDebugService(); + virtual ~WebDebugService(); + + virtual bool Initialise(StructuredDataI &data); + + // ------------------------------------------------------------------------- + // DebugServiceI RT-path overrides + // ------------------------------------------------------------------------- + virtual DebugSignalInfo *RegisterSignal(void *memoryAddress, + TypeDescriptor type, + const char8 *name, + uint8 numberOfDimensions = 0, + uint32 numberOfElements = 1); + virtual void ProcessSignal(DebugSignalInfo *signalInfo, uint32 size, + uint64 timestamp); + virtual void RegisterBroker(DebugSignalInfo **signalPointers, + uint32 numSignals, MemoryMapBroker *broker, + volatile bool *anyActiveFlag, + Vec *activeIndices, + Vec *activeSizes, + FastPollingMutexSem *activeMutex, + volatile bool *anyBreakFlag, + Vec *breakIndices, + const char8 *gamName = NULL_PTR(const char8 *), + bool isOutput = false); + virtual bool IsPaused() const { return isPaused; } + virtual void SetPaused(bool paused) { isPaused = paused; } + virtual bool IsStepPending() const { return stepRemaining > 0u; } + virtual void ConsumeStepIfNeeded(const char8 *gamName, + const char8 *threadName = NULL_PTR(const char8 *)); + + // ------------------------------------------------------------------------- + // DebugServiceI control-path overrides + // ------------------------------------------------------------------------- + virtual uint32 ForceSignal (const char8 *name, const char8 *valueStr); + virtual uint32 UnforceSignal (const char8 *name); + virtual uint32 TraceSignal (const char8 *name, bool enable, uint32 decimation = 1); + virtual uint32 SetBreak (const char8 *name, uint8 op, float64 threshold); + virtual uint32 ClearBreak (const char8 *name); + virtual bool IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable); + virtual uint32 RegisterMonitorSignal(const char8 *path, uint32 periodMs); + virtual uint32 UnmonitorSignal (const char8 *path); + + // EmbeddedServiceMethodBinderI (framework dispatches to sub-binders) + virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info); + + // Inject the full config CDB (call after ConfigureApplication) + void SetFullConfig(ConfigurationDatabase &config); + void RebuildConfigFromRegistry(); + + struct MonitoredSignal { + ReferenceT dataSource; + uint32 signalIdx; + uint32 internalID; + uint32 periodMs; + uint64 lastPollTime; + uint32 size; + StreamString path; + }; + +private: + // HTTP server helpers + bool ParseRequest(BasicTCPSocket *client, StreamString &method, + StreamString &path, StreamString &body); + void SendHttp(BasicTCPSocket *client, uint32 code, const char8 *ct, + StreamString &body); + void HandleRequest(BasicTCPSocket *client, const StreamString &method, + const StreamString &path, const StreamString &body); + void UpgradeToSse(BasicTCPSocket *client); + + // Command handler — writes response to StreamString (not a socket) + void HandleCommand(const StreamString &cmd, StreamString &out); + void Discover (StreamString &out); + void InfoNode (const char8 *path, StreamString &out); + void ListNodes (const char8 *path, StreamString &out); + void GetSignalValue(const char8 *name, StreamString &out); + void ServeConfig (StreamString &out); + void GetStepStatus(StreamString &out); + void Step(uint32 n, const char8 *threadName = NULL_PTR(const char8 *)); + + // Tree / config helpers + uint32 ExportTree(ReferenceContainer *container, StreamString &json, + const char8 *pathPrefix); + void EnrichWithConfig(const char8 *path, StreamString &json); + static void JsonifyDatabase(ConfigurationDatabase &db, StreamString &json); + + // Registry patching (same as DebugService) + void PatchRegistry(); + + // Broker index maintenance + void UpdateBrokersActiveStatus(); + void UpdateBrokersBreakStatus(); + + // SSE broadcast + void BroadcastSse(const char8 *eventType, const StreamString &data); + void RemoveDeadSseClients(); + + // Service thread entry points + ErrorManagement::ErrorType HttpServer(ExecutionInfo &info); + ErrorManagement::ErrorType Streamer (ExecutionInfo &info); + + // Sub-binder so the framework can dispatch to two separate threads + class WdsBinder : public EmbeddedServiceMethodBinderI { + public: + enum ServiceType { Http, Stream }; + WdsBinder(WebDebugService *p, ServiceType t) : parent(p), stype(t) {} + virtual ErrorManagement::ErrorType Execute(ExecutionInfo &info) { + return (stype == Stream) ? parent->Streamer(info) + : parent->HttpServer(info); + } + private: + WebDebugService *parent; + ServiceType stype; + }; + + uint16 httpPort; + + volatile bool isPaused; + volatile uint32 stepRemaining; + StreamString pausedAtGam; + StreamString stepThreadFilter; + + BasicTCPSocket tcpServer; + + WdsBinder binderHttp; + WdsBinder binderStream; + SingleThreadService httpService; + SingleThreadService streamerService; + + Vec signals; + Vec aliases; + Vec brokers; + Vec monitoredSignals; + + FastPollingMutexSem mutex; + FastPollingMutexSem tracePushMutex; + TraceRingBuffer traceBuffer; + + // SSE client list + static const uint32 MAX_SSE_CLIENTS = 8u; + BasicTCPSocket *sseClients[MAX_SSE_CLIENTS]; + FastPollingMutexSem sseMutex; + + // Streamer packet state + uint32 streamerSeq; + uint64 streamerT0Ns; // nanosecond origin for relative timestamps sent over SSE + + ConfigurationDatabase fullConfig; + bool manualConfigSet; + + static const uint32 GET_VALUE_MAX_ELEMENTS = 256u; + + // Log history buffer — replayed to newly connecting SSE clients + static const uint32 LOG_HISTORY_SIZE = 64u; + static const uint32 LOG_ENTRY_MAX_SIZE = 512u; + char8 logHistory[64][512]; + uint32 logHistoryWriteIdx; + uint32 logHistoryFill; + FastPollingMutexSem logHistoryMutex; +}; + +} // namespace MARTe + +#endif // WEBDEBUGSERVICE_H diff --git a/Source/Components/Interfaces/WebDebugService/WebUI.h b/Source/Components/Interfaces/WebDebugService/WebUI.h new file mode 100644 index 0000000..263bf3d --- /dev/null +++ b/Source/Components/Interfaces/WebDebugService/WebUI.h @@ -0,0 +1,611 @@ +#ifndef WEBUI_H +#define WEBUI_H + +#include "CompilerTypes.h" + +namespace MARTe { + +// clang-format off +static const char8 *const WEB_UI_HTML = +"\n" +"\n" +"\n" +"\n" +"\n" +"MARTe2 Debug\n" +"\n" +"\n" +"\n" +"\n" +"
\n" +"
\n" +" Connecting\xe2\x80\xa6\n" +"
\n" +" \n" +" \n" +"
\n" +" \n" +"
\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
\n" +" \n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
App Tree
\n" +"
Click Tree or Discover\xe2\x80\xa6
\n" +"
\n" +"
\n" +"
\n" +" \n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +" \n" +"
\n" +"
\n" +"
\n" +" Log\n" +"
\n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"\n" +"
\n" +"
\n" +"

Force Signal

\n" +"
\n" +"
\n" +"
\n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"
\n" +"

Break Condition

\n" +"
\n" +"
\n" +"
\n" +"
\n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"
\n" +"

Monitor Signal

\n" +"
\n" +"
\n" +"
\n" +" \n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"
\n" +"
\n" +"

Send Message

\n" +"
\n" +" \n" +"
\n" +"
\n" +"
\n" +" \n" +" \n" +"
\n" +"
\n" +"
\n" +"\n" +"\n" +"\n"; +// clang-format on + +} // namespace MARTe + +#endif // WEBUI_H diff --git a/Source/Core/Types/Vec/Vec.h b/Source/Core/Types/Vec/Vec.h index c9e98df..bcab1ea 100644 --- a/Source/Core/Types/Vec/Vec.h +++ b/Source/Core/Types/Vec/Vec.h @@ -62,6 +62,21 @@ public: size = 0; } + /** + * @brief Exchange contents with another Vec in O(1) — no allocation, no element copies. + * + * Used by UpdateBrokersActiveStatus / UpdateBrokersBreakStatus to publish a + * freshly built index list to the RT thread with the shortest possible lock + * hold time: only three pointer/size swaps happen inside the mutex. + * The old arrays end up in @p other and are freed when it leaves scope + * AFTER the lock is released. + */ + void Swap(Vec &other) { + T *tmpArr = arr; arr = other.arr; other.arr = tmpArr; + size_t tmpSz = size; size = other.size; other.size = tmpSz; + size_t tmpMem = mem_size; mem_size = other.mem_size; other.mem_size = tmpMem; + } + size_t Size() const { return size; } diff --git a/TUTORIAL.md b/TUTORIAL.md index 4d48518..754b9e6 100644 --- a/TUTORIAL.md +++ b/TUTORIAL.md @@ -1,56 +1,219 @@ -# Tutorial: High-Speed Observability with MARTe2 Debug GUI +# Tutorial: Observability and Debugging with the MARTe2 Debug Suite -This guide will walk you through the process of tracing and forcing signals in a real-time MARTe2 application using the native Rust GUI client. +This guide covers both transport options. Choose the one that fits your workflow: -## 1. Environment Setup +| | `WebDebugService` | `DebugService` | +|---|---|---| +| **Client** | Any web browser | Rust native GUI | +| **Setup** | Config only | Config + `cargo run` | +| **Best for** | Quick inspection, remote access | High-performance plots, scripted tooling | + +--- + +## Part A — Web UI (`WebDebugService`) + +### A.1 Environment Setup -### Launch the MARTe2 Application -First, start your application using the provided debug runner. This script launches the standard `MARTeApp.ex` while injecting the debugging layer: ```bash -./run_debug_app.sh +cd /path/to/your/marte2/project +source /path/to/marte_debug/env.sh ``` -*Note: You should see logs indicating the `DebugService` has started on port 8080 and `TcpLogger` on port 8082.* -### Start the GUI Client -In a new terminal window, navigate to the client directory and run the GUI: +### A.2 Add `WebDebugService` to Your Config + +Add the service as a **sibling** of the `+App` node (not inside it): + +```text ++WebDebugService = { + Class = WebDebugService + HttpPort = 8090 +} + ++App = { + Class = RealTimeApplication + ... +} +``` + +Restart your application. You should see: +``` +[WebDebugService] HTTP server listening on port 8090 +``` + +### A.3 Open the Web UI + +Navigate to `http://localhost:8090` in any browser. The UI connects automatically via SSE. + +--- + +### A.4 Exploring the Object Tree + +The **Application Tree** panel on the left mirrors your live ORD hierarchy. + +1. Expand `Root → App → Data` to find data sources. +2. Click **Info** next to any node to see its class, config, and signals in the details panel. +3. Click **List** to expand just the immediate children without recursing. + +--- + +### A.5 Real-Time Signal Tracing + +1. Locate a signal in the tree, e.g. `Root.App.Data.Timer.Counter`. +2. Click **Trace** next to the signal. The signal appears in the **Traced Signals** list with + its live last value. +3. Click **Plot** to open it in the real-time Chart.js graph. Use the **Follow** toggle to + keep the time axis scrolling. +4. To set the sampling decimation (e.g. every 10th sample), use the command box at the bottom + of the page: + ``` + TRACE App.Data.Timer.Counter 1 10 + ``` +5. Click **Trace** again (or send `TRACE … 0`) to stop. + +--- + +### A.6 Signal Forcing + +1. Find a GAM output signal, e.g. `Root.App.Data.DDB.Counter`. +2. Click **Force**. A modal dialog appears. +3. Enter a value (e.g. `9999`) and click **Apply**. +4. The signal is locked at that value every RT cycle. +5. Click **Unforce** (or use the **Active Controls** panel) to release. + +--- + +### A.7 Breakpoints + +1. Click **Break** next to a signal. +2. Select an operator (`>`, `<`, `==`, etc.) and enter a threshold. +3. When the condition fires, the application pauses automatically. The status bar shows + **PAUSED** and the GAM name where execution stopped. +4. Use **Step** to advance one cycle at a time, or **Resume** to continue. +5. Click **Break OFF** to clear the breakpoint. + +--- + +### A.8 Execution Stepping + +While paused (after a breakpoint or manual **Pause**): + +1. Enter a step count in the **Step** box (e.g. `5`) and click **Step**. +2. The RT loop runs exactly 5 output-broker cycles, then pauses again. +3. The status SSE event (`{"type":"status","remaining":…}`) keeps the UI updated. + +--- + +### A.9 Signal Monitoring + +Monitoring polls a signal at a slow rate without using the RT trace path — useful for +`DataSourceI` signals not connected to any GAM. + +1. Click **Monitor** next to a signal. +2. Enter a poll period in milliseconds (e.g. `500`). +3. The signal's current value arrives via SSE every 500 ms as a `{"type":"monitor",…}` event. + +--- + +### A.10 Log Viewer + +The **Logs** panel at the bottom shows every `REPORT_ERROR` call forwarded as SSE events. +Use the level buttons (D / I / W / E) to filter by severity, or type a keyword to search. + +--- + +## Part B — Rust Native GUI (`DebugService`) + +### B.1 Environment Setup + +```bash +source /path/to/marte_debug/env.sh +``` + +### B.2 Add `DebugService` to Your Config + +```text ++DebugService = { + Class = DebugService + ControlPort = 8080 + UdpPort = 8081 + LogPort = 8082 +} + ++App = { + Class = RealTimeApplication + ... +} +``` + +### B.3 Start the GUI Client + ```bash cd Tools/gui_client cargo run --release ``` -## 2. Exploring the Object Tree -Once connected, the left panel (**Application Tree**) displays the live hierarchy of your application. +The GUI connects to `localhost:8080` automatically and requests the tree on startup. -1. **Navigate to Data:** Expand `Root` -> `App` -> `Data`. -2. **Inspect Nodes:** Click the **ℹ Info** button next to any node (like `Timer` or `DDB`). -3. **View Metadata:** The bottom-left pane will display the full JSON configuration of that object, including its class, parameters, and signals. +--- -## 3. Real-Time Signal Tracing (Oscilloscope) -Tracing allows you to see signal values exactly as they exist in the real-time memory map. +### B.4 Exploring the Object Tree -1. **Find a Signal:** In the tree, locate `Root.App.Data.Timer.Counter`. -2. **Activate Trace:** Click the **📈 Trace** button. -3. **Monitor the Scope:** The central **Oscilloscope** panel will begin plotting the signal. -4. **Verify Data Flow:** Check the **UDP Packets** counter in the top bar; it should be increasing rapidly, confirming high-speed data reception (Port 8081). -5. **Multi-Signal Trace:** You can click **📈 Trace** on other signals (like `Time`) to overlay multiple plots. +The **Application Tree** panel on the left shows the live ORD. -## 4. Signal Forcing (Manual Override) -Forcing allows you to bypass the framework logic and manually set a signal's value in memory. +1. Expand `Root → App → Data`. +2. Click **ℹ Info** next to any node to see its JSON config in the bottom-left pane. -1. **Locate Target:** Find a signal that is being written by a GAM, such as `Root.App.Data.DDB.Counter`. -2. **Open Force Dialog:** Click the **⚡ Force** button next to the signal. -3. **Inject Value:** In the popup dialog, enter a new value (e.g., `9999`) and click **Apply Force**. -4. **Observe Effect:** If you are tracing the same signal, the oscilloscope plot will immediately jump to and hold at `9999`. -5. **Release Control:** To stop forcing, click the **❌** button in the **Active Controls** (Right Panel) under "Forced Signals". The framework will resume writing its own values to that memory location. +--- -## 5. Advanced Controls +### B.5 Real-Time Signal Tracing (Oscilloscope) -### Global Pause -If you need to "freeze" the entire application to inspect a specific state: -- Click **⏸ Pause** in the top bar. The real-time threads will halt at the start of their next cycle. -- Click **▶ Resume** to restart the execution. +1. Locate `Root.App.Data.Timer.Counter`. +2. Click **📈 Trace**. The **Oscilloscope** panel begins plotting. +3. The **UDP Packets** counter in the top bar increments — confirming high-speed telemetry + on port 8081. +4. Drag additional signals from the **Traced Signals** list into the oscilloscope panel for + multi-signal overlay. -### Log Terminal -The bottom panel displays every `REPORT_ERROR` event from the C++ framework, powered by the standalone `TcpLogger` service. -- **Regex Filter:** Type a keyword like `Timer` in the filter box to isolate relevant events. -- **Pause Logs:** Toggle **⏸ Pause Logs** to stop the scrolling view while data continues to be captured in the background. +--- + +### B.6 Signal Forcing + +1. Find `Root.App.Data.DDB.Counter`. +2. Click **⚡ Force**. +3. Enter a value (e.g. `9999`) and click **Apply Force**. +4. The oscilloscope plot immediately jumps to and holds at `9999`. +5. Click **❌** in the **Active Controls** panel to release the force. + +--- + +### B.7 Global Pause and Resume + +- Click **⏸ Pause** in the top bar to halt all RT threads. +- Click **▶ Resume** to continue. + +--- + +### B.8 Log Terminal + +The bottom panel shows all `REPORT_ERROR` events forwarded from `TcpLogger` (port 8082). + +- **Regex Filter:** Type a keyword (e.g. `Timer`) to isolate events. +- **⏸ Pause Logs:** Stop scrolling while continuing to capture. + +--- + +## Part C — Scripted / Programmatic Access + +Both transports accept plain text commands. You can use `nc` or any TCP client: + +```bash +# DebugService +echo -e "DISCOVER\nTRACE App.Data.DDB.Counter 1" | nc localhost 8080 + +# WebDebugService +curl -s -X POST http://localhost:8090/api/command \ + -H "Content-Type: text/plain" \ + -d "DISCOVER" +``` + +For the full command reference see [API.md](API.md). diff --git a/Test/Configurations/webdebug_test.cfg b/Test/Configurations/webdebug_test.cfg new file mode 100644 index 0000000..5180860 --- /dev/null +++ b/Test/Configurations/webdebug_test.cfg @@ -0,0 +1,186 @@ ++App = { + Class = RealTimeApplication + +Functions = { + Class = ReferenceContainer + +GAM1 = { + Class = IOGAM + InputSignals = { + Counter = { + DataSource = Timer + Type = uint32 + Frequency = 1000 + } + Time = { + DataSource = Timer + Type = uint32 + } + } + OutputSignals = { + Counter = { + DataSource = SyncDB + Type = uint32 + } + Time = { + DataSource = DDB + Type = uint32 + } + } + } + +CGAM = { + Class = ConstantGAM + OutputSignals = { + Test = { + DataSource = DDB2 + Type = float32 + Default = 0.123 + } + } + } + +GAM2 = { + Class = IOGAM + InputSignals = { + Counter = { + DataSource = TimerSlow + Frequency = 1 + } + Time = { + DataSource = TimerSlow + } + Test = { + DataSource = DDB2 + Type = float32 + } + } + OutputSignals = { + Counter = { + Type = uint32 + DataSource = Logger + } + Time = { + Type = uint32 + DataSource = Logger + } + ConstOut = { + DataSource = Logger + Type = float32 + } + } + } + +GAM3 = { + Class = IOGAM + InputSignals = { + Counter = { + Frequency = 1 + Samples = 100 + Type = uint32 + DataSource = SyncDB + } + } + OutputSignals = { + Counter = { + DataSource = DDB3 + NumberOfElements = 100 + Type = uint32 + } + } + + } + } + +Data = { + Class = ReferenceContainer + DefaultDataSource = DDB + +SyncDB = { + Class = RealTimeThreadSynchronisation + Timeout = 200 + Signals = { + Counter = { + Type = uint32 + } + } + } + +Timer = { + Class = LinuxTimer + Signals = { + Counter = { + Type = uint32 + } + Time = { + Type = uint32 + } + } + } + +TimerSlow = { + Class = LinuxTimer + Signals = { + Counter = { + Type = uint32 + } + Time = { + Type = uint32 + } + } + } + +Logger = { + Class = LoggerDataSource + Signals = { + CounterCopy = { + Type = uint32 + } + TimeCopy = { + Type = uint32 + } + } + } + +DDB = { + AllowNoProducer = 1 + Class = GAMDataSource + Signals = { + Counter= { + Type = uint32 + } + } + } + +DDB2 = { + AllowNoProducer = 1 + Class = GAMDataSource + } + +DDB3 = { + AllowNoProducer = 1 + Class = GAMDataSource + } + +DAMS = { + Class = TimingDataSource + } + } + +States = { + Class = ReferenceContainer + +State1 = { + Class = RealTimeState + +Threads = { + Class = ReferenceContainer + +Thread1 = { + Class = RealTimeThread + Functions = {GAM1} + } + +Thread2 = { + Class = RealTimeThread + Functions = {GAM2 CGAM} + } + +Thread3 = { + Class = RealTimeThread + Functions = {GAM3} + } + } + } + } + +Scheduler = { + Class = GAMScheduler + TimingDataSource = DAMS + } +} + ++DebugService = { + Class = WebDebugService + HttpPort = 8090 +} + diff --git a/Test/UnitTests/UnitTests.cpp b/Test/UnitTests/UnitTests.cpp index 63559e4..885f43c 100644 --- a/Test/UnitTests/UnitTests.cpp +++ b/Test/UnitTests/UnitTests.cpp @@ -26,12 +26,12 @@ public: static void TestAll() { printf("Stability Logic Tests...\n"); ObjectRegistryDatabase::Instance()->Purge(); - + DebugService service; assert(service.traceBuffer.Init(1024 * 1024)); ConfigurationDatabase cfg; - cfg.Write("ControlPort", (uint32)0); + cfg.Write("ControlPort", (uint32)0); cfg.Write("StreamPort", (uint32)0); cfg.Write("SuppressTimeoutLogs", (uint32)1); assert(service.Initialise(cfg)); @@ -41,8 +41,8 @@ public: service.RegisterSignal(&val, UnsignedInteger32Bit, "X.Y.Z", 0, 1); assert(service.TraceSignal("Z", true) == 1); assert(service.ForceSignal("Z", "123") == 1); - - uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * HighResolutionTimer::Period() * 1000000.0); + + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * HighResolutionTimer::Period() * 1.0e9); service.ProcessSignal(service.signals[0], 4, ts); assert(val == 123); service.UnforceSignal("Z"); @@ -56,7 +56,7 @@ public: service.HandleCommand("LS /", NULL_PTR(BasicTCPSocket*)); service.HandleCommand("INFO X.Y.Z", NULL_PTR(BasicTCPSocket*)); service.HandleCommand("MSG DebugService DummyFunc 0 K=V", NULL_PTR(BasicTCPSocket*)); - + // 3. Broker Active Status volatile bool active = false; Vec indices; @@ -119,6 +119,567 @@ public: service.HandleCommand("STEP 3", NULL_PTR(BasicTCPSocket*)); assert(service.stepThreadFilter.Size() == 0u); } + + // ------------------------------------------------------------------------- + // FIX #4: ForceSignal overflow guard + // ------------------------------------------------------------------------- + static void TestForceSignalOversizeRejected() { + printf("FIX #4: ForceSignal oversized signal rejected...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + // A 256-element float64 array requires 2048 bytes — exceeds forcedValue[1024]. + // RegisterSignal itself is fine; the overflow guard is in ForceSignal. + float64 bigArray[256]; + memset(bigArray, 0, sizeof(bigArray)); + service.RegisterSignal(bigArray, Float64Bit, "Big.Array", 1, 256); + + // ForceSignal must reject the request and return 0 matches applied. + // (The signal IS found by alias but the size check must veto it.) + uint32 forced = service.ForceSignal("Array", "1.0"); + assert(forced == 0); // rejected — not forced + printf(" -> PASS: oversized force correctly rejected\n"); + + // A normal scalar float64 (8 bytes) must still be forceable. + float64 scalarVal = 0.0; + service.RegisterSignal(&scalarVal, Float64Bit, "Small.Scalar", 0, 1); + uint32 forcedSmall = service.ForceSignal("Scalar", "3.14"); + assert(forcedSmall == 1); // accepted + printf(" -> PASS: normal scalar force accepted\n"); + } + + // ------------------------------------------------------------------------- + // FIX #1: Streamer buffer bounds — Pop maxSize respected + // ------------------------------------------------------------------------- + static void TestStreamerPopMaxSize() { + printf("FIX #1: TraceRingBuffer Pop respects maxSize...\n"); + + TraceRingBuffer rb; + assert(rb.Init(4096)); + + // Push a 512-byte sample + uint8 bigSample[512]; + memset(bigSample, 0xAB, sizeof(bigSample)); + assert(rb.Push(42u, 999u, bigSample, 512u)); + + // Pop with a maxSize smaller than the pushed size — must return false + // and not overflow the destination buffer. + uint8 smallBuf[64]; + uint32 id = 0, size = 0; + uint64 ts = 0; + bool ok = rb.Pop(id, ts, smallBuf, size, 64u); + assert(!ok); // must be rejected, not a buffer overrun + printf(" -> PASS: Pop with insufficient maxSize correctly rejected\n"); + + // After skipping the oversized sample the buffer is empty (only one + // sample was pushed), so a second Pop must also return false. + ok = rb.Pop(id, ts, smallBuf, size, 64u); + assert(!ok); + printf(" -> PASS: Ring buffer empty after oversized pop skipped\n"); + } + + // ------------------------------------------------------------------------- + // FIX #5: TraceRingBuffer Pop skips only the oversized entry, not all data + // ------------------------------------------------------------------------- + static void TestRingBufferSkipOversizedEntry() { + printf("FIX #5: Oversized Pop skips only that entry, not all data...\n"); + + TraceRingBuffer rb; + assert(rb.Init(4096)); + + // Push sample A (oversized for a 64-byte receiver) + uint8 bigSample[512]; + memset(bigSample, 0xAA, sizeof(bigSample)); + assert(rb.Push(1u, 100u, bigSample, 512u)); + + // Push sample B (fits in 64-byte receiver) + uint8 smallSample[4] = {0xDE, 0xAD, 0xBE, 0xEF}; + assert(rb.Push(2u, 200u, smallSample, 4u)); + + uint8 dst[64]; + uint32 id = 0, sz = 0; + uint64 rts = 0; + + // Pop with maxSize=64: sample A (512 bytes) must be skipped, not crash + bool ok1 = rb.Pop(id, rts, dst, sz, 64u); + assert(!ok1); // A skipped + printf(" -> PASS: oversized sample A correctly skipped\n"); + + // Sample B (4 bytes) must still be readable — before the fix the whole + // ring was discarded so this would also return false. + bool ok2 = rb.Pop(id, rts, dst, sz, 64u); + assert(ok2); + assert(id == 2u); + assert(sz == 4u); + assert(dst[0] == 0xDE && dst[3] == 0xEF); + printf(" -> PASS: subsequent sample B retrieved after A was skipped\n"); + + // Buffer should now be empty + bool ok3 = rb.Pop(id, rts, dst, sz, 64u); + assert(!ok3); + printf(" -> PASS: no further samples (buffer empty)\n"); + } + + // ------------------------------------------------------------------------- + // FIX #3: signalPointers null guard in UpdateBrokers* + // ------------------------------------------------------------------------- + static void TestUpdateBrokersNullSignalPointers() { + printf("FIX #3: UpdateBrokers* with null signalPointers — no crash...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + // Register a broker whose signalPointers is NULL but numSignals > 0. + // Before the fix this dereferenced NULL → segfault. + volatile bool active = false; + Vec indices; + Vec sizes; + FastPollingMutexSem mutex; + volatile bool anyBreak = false; + Vec breakIdx; + service.RegisterBroker(NULL_PTR(DebugSignalInfo**), 1u, + NULL_PTR(MemoryMapBroker*), + &active, &indices, &sizes, &mutex, + &anyBreak, &breakIdx); + + // These must not crash + service.UpdateBrokersActiveStatus(); + service.UpdateBrokersBreakStatus(); + printf(" -> PASS: no crash on null signalPointers\n"); + } + + // ------------------------------------------------------------------------- + // FIX #9: GetSignalValue — large array truncated at GET_VALUE_MAX_ELEMENTS + // ------------------------------------------------------------------------- + static void TestGetSignalValueTruncation() { + printf("FIX #9: GetSignalValue large array capped at %u elements...\n", + (unsigned)DebugService::GET_VALUE_MAX_ELEMENTS); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + // Register a 512-element uint8 array (> GET_VALUE_MAX_ELEMENTS = 256) + static uint8 bigBuf[512]; + for (uint32 i = 0; i < 512; i++) bigBuf[i] = (uint8)(i & 0xFF); + service.RegisterSignal(bigBuf, UnsignedInteger8Bit, "Big.Signal", 1, 512); + + // VALUE command with NULL client is a smoke test — must not crash or loop + service.HandleCommand("VALUE Signal", NULL_PTR(BasicTCPSocket*)); + printf(" -> PASS: VALUE on large array completed without hang\n"); + + // Verify directly: nElem is capped before the byte limit + // The signal has 512 uint8 elements; after fix nElem must be <= 256 + DebugSignalInfo *sig = service.signals[0]; + assert(sig->numberOfElements == 512u); // original unchanged + // We can't call GetSignalValue with a real socket here, but we can + // verify the constant is properly visible and correctly set + assert(DebugService::GET_VALUE_MAX_ELEMENTS == 256u); + printf(" -> PASS: GET_VALUE_MAX_ELEMENTS constant is %u\n", + (unsigned)DebugService::GET_VALUE_MAX_ELEMENTS); + } + + // ------------------------------------------------------------------------- + // FIX #6/#8: rate-limit and idle-timeout constants exist and are sane + // ------------------------------------------------------------------------- + static void TestServerConstantsSane() { + printf("FIX #6/#8: server protection constants are valid...\n"); + assert(DebugService::CMD_RATE_LIMIT > 0u); + assert(DebugService::CLIENT_IDLE_TIMEOUT_MS > 0u); + // Idle timeout must be large enough to not disconnect a healthy client + // in a single polling cycle (which is ~100 ms) + assert(DebugService::CLIENT_IDLE_TIMEOUT_MS >= 1000u); + printf(" -> PASS: CMD_RATE_LIMIT=%u, CLIENT_IDLE_TIMEOUT_MS=%u\n", + (unsigned)DebugService::CMD_RATE_LIMIT, + (unsigned)DebugService::CLIENT_IDLE_TIMEOUT_MS); + } + + // ------------------------------------------------------------------------- + // FIX #7: MSG key overflow — oversized key line is skipped, no buffer overrun + // ------------------------------------------------------------------------- + static void TestMsgKeyBoundsCheck() { + printf("FIX #7: MSG command oversized key is skipped without crash...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + // Build a MSG command whose key is 300 characters long (> keyBuf[256]). + // Before the fix the Read() would overflow the 256-byte stack buffer. + StreamString cmd = "MSG DebugService DummyFunc 0 "; + for (uint32 i = 0; i < 300u; i++) cmd += 'K'; + cmd += "=value"; + + // Must not crash or assert + service.HandleCommand(cmd, NULL_PTR(BasicTCPSocket*)); + printf(" -> PASS: oversized key handled without buffer overflow\n"); + + // A normal-length key should still be accepted (regression check) + service.HandleCommand("MSG DebugService DummyFunc 0 NormalKey=NormalValue", + NULL_PTR(BasicTCPSocket*)); + printf(" -> PASS: normal key not rejected by bounds check\n"); + } + + // ------------------------------------------------------------------------- + // FIX #10: Multi-segment TCP command buffering via inputBuffer + // ------------------------------------------------------------------------- + static void TestServerInputBuffering() { + printf("FIX #10: Multi-segment TCP command buffering...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + // Verify inputBuffer is initialised empty + assert(service.inputBuffer.Size() == 0u); + printf(" -> PASS: inputBuffer initialised to empty\n"); + + // Simulate fragment 1 arriving: "FORCE Signal " (no newline yet) + // In the real Server() this would be written via Read(); here we + // directly manipulate the carry-over buffer (friend-class access). + { + (void)service.inputBuffer.Seek(service.inputBuffer.Size()); + uint32 f1Len = 13u; // length of "FORCE Signal " + service.inputBuffer.Write("FORCE Signal ", f1Len); + } + assert(service.inputBuffer.Size() == 13u); + printf(" -> PASS: partial command held in inputBuffer after fragment 1\n"); + + // Simulate fragment 2 completing the command: "123\n" + { + (void)service.inputBuffer.Seek(service.inputBuffer.Size()); + uint32 f2Len = 4u; + service.inputBuffer.Write("123\n", f2Len); + } + + // Now scan inputBuffer for complete lines — mirrors the Server() loop + const char8 *raw = service.inputBuffer.Buffer(); + uint32 total = (uint32)service.inputBuffer.Size(); + uint32 lineStart = 0u; + uint32 cmdsFound = 0u; + char8 cmdBuf[64]; + memset(cmdBuf, 0, sizeof(cmdBuf)); + + for (uint32 pos = 0u; pos < total; pos++) { + if (raw[pos] != '\n') continue; + uint32 len = pos - lineStart; + if (len > 0u && raw[lineStart + len - 1u] == '\r') len--; + if (len > 0u) { + cmdsFound++; + uint32 cp = (len < 63u) ? len : 63u; + memcpy(cmdBuf, raw + lineStart, cp); + cmdBuf[cp] = '\0'; + } + lineStart = pos + 1u; + } + + assert(cmdsFound == 1u); + // The two fragments assembled into "FORCE Signal 123" + assert(strncmp(cmdBuf, "FORCE Signal 123", 16) == 0); + printf(" -> PASS: fragmented command reassembled to '%s'\n", cmdBuf); + + // Remainder after the last '\n' should be empty (no trailing bytes) + assert(lineStart == total); + printf(" -> PASS: no partial remainder after complete command\n"); + + // Verify INPUT_BUFFER_MAX constant is reachable + assert(DebugService::INPUT_BUFFER_MAX > 4096u); + printf(" -> PASS: INPUT_BUFFER_MAX=%u\n", + (unsigned)DebugService::INPUT_BUFFER_MAX); + } + + // ------------------------------------------------------------------------- + // FIX #11: Streamer monitored-signal Push protected by tracePushMutex + // ------------------------------------------------------------------------- + static void TestStreamerMonitoredPushSerialised() { + printf("FIX #11: Streamer monitored-signal Push uses tracePushMutex...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + uint32 val = 0xDEADBEEF; + service.RegisterSignal(&val, UnsignedInteger32Bit, "Mon.Val", 0, 1); + assert(service.TraceSignal("Val", true) == 1); + + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000000.0); + + // Push via ProcessSignal (uses tracePushMutex internally — broker path) + service.ProcessSignal(service.signals[0], 4u, ts); + + // Push directly via tracePushMutex (simulates the Streamer monitored- + // signal path after FIX #11 — it now also acquires tracePushMutex so + // both push paths are serialised with respect to each other). + uint32 monId = 0x80000001u; + uint32 monVal = 0x12345678u; + service.tracePushMutex.FastLock(); + (void)service.traceBuffer.Push(monId, ts + 1u, &monVal, 4u); + service.tracePushMutex.FastUnLock(); + + // Both samples must be present in FIFO order — if there were a race + // one or both would be corrupt or missing. + uint32 id = 0, sz = 0; uint64 rts = 0; uint8 dst[64]; + assert(service.traceBuffer.Pop(id, rts, dst, sz, 64u)); + assert(id == service.signals[0]->internalID); + assert(sz == 4u); + + assert(service.traceBuffer.Pop(id, rts, dst, sz, 64u)); + assert(id == monId); + assert(sz == 4u); + + assert(!service.traceBuffer.Pop(id, rts, dst, sz, 64u)); // buffer empty + printf(" -> PASS: broker and monitored-signal pushes both retrieved in order\n"); + } + + // ------------------------------------------------------------------------- + // FIX #2: tracePushMutex — concurrent Push from two logical threads + // (single-threaded simulation: verify no data corruption across two pushes + // that would race without the mutex) + // ------------------------------------------------------------------------- + static void TestTracePushSerialised() { + printf("FIX #2: Concurrent ProcessSignal serialised via tracePushMutex...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + uint32 valA = 0xAABBCCDD; + uint32 valB = 0x11223344; + service.RegisterSignal(&valA, UnsignedInteger32Bit, "P.A", 0, 1); + service.RegisterSignal(&valB, UnsignedInteger32Bit, "P.B", 0, 1); + assert(service.TraceSignal("A", true) == 1); + assert(service.TraceSignal("B", true) == 1); + + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000000.0); + + // Simulate two sequential calls as would happen from two RT threads. + // Without tracePushMutex they could interleave and corrupt the ring. + service.ProcessSignal(service.signals[0], 4, ts); + service.ProcessSignal(service.signals[1], 4, ts + 1u); + + // Both samples must be retrievable in FIFO order with correct IDs. + uint32 id, size; + uint64 rts; + uint8 buf[64]; + uint32 id0 = service.signals[0]->internalID; + uint32 id1 = service.signals[1]->internalID; + + assert(service.traceBuffer.Pop(id, rts, buf, size, 64u)); + assert(id == id0); + assert(size == 4u); + + assert(service.traceBuffer.Pop(id, rts, buf, size, 64u)); + assert(id == id1); + assert(size == 4u); + + printf(" -> PASS: Both samples intact after serialised push\n"); + } + // ------------------------------------------------------------------------- + // Fix #2 (Swap): UpdateBrokersActiveStatus publishes all active-signal + // indices correctly when multiple signals are tracing/forcing. + // ------------------------------------------------------------------------- + static void TestUpdateBrokersSwapPublishesAllIndices() { + printf("Fix #2 (Swap): UpdateBrokersActiveStatus publishes all active indices...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + uint32 v0 = 0, v1 = 0, v2 = 0; + service.RegisterSignal(&v0, UnsignedInteger32Bit, "S.X", 0, 1); + service.RegisterSignal(&v1, UnsignedInteger32Bit, "S.Y", 0, 1); + service.RegisterSignal(&v2, UnsignedInteger32Bit, "S.Z", 0, 1); + + // Enable tracing on X and Z (not Y) + assert(service.TraceSignal("X", true) == 1); + assert(service.TraceSignal("Z", true) == 1); + + volatile bool active = false; + Vec indices; + Vec sizes; + FastPollingMutexSem mutex; + volatile bool anyBreak = false; + Vec breakIdx; + DebugSignalInfo *ptrs[3] = { + service.signals[0], service.signals[1], service.signals[2] + }; + service.RegisterBroker(ptrs, 3u, NULL_PTR(MemoryMapBroker *), + &active, &indices, &sizes, &mutex, + &anyBreak, &breakIdx); + + service.UpdateBrokersActiveStatus(); + + // After Swap the broker's activeIndices must hold exactly indices 0 and 2 + assert(active == true); + assert(indices.Size() == 2u); + // indices are in order of signal position in the broker (0 then 2) + assert(indices[0] == 0u); + assert(indices[1] == 2u); + printf(" -> PASS: Swap correctly published 2 active indices out of 3\n"); + + // Disable one; Swap again — only index 0 should remain + assert(service.TraceSignal("Z", false) == 1); + service.UpdateBrokersActiveStatus(); + assert(indices.Size() == 1u); + assert(indices[0] == 0u); + printf(" -> PASS: After disabling Z, only index 0 remains\n"); + } + + // ------------------------------------------------------------------------- + // Fix #3 (copy-before-unlock): break evaluation still works after the + // copy-indices-then-unlock refactor in DebugBrokerHelper::Process(). + // ------------------------------------------------------------------------- + static void TestBreakEvaluationAfterLockRelease() { + printf("Fix #3 (copy-before-unlock): break evaluation works after lock release...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + uint32 val = 5u; + service.RegisterSignal(&val, UnsignedInteger32Bit, "Brk.Val", 0, 1); + + // Set break: pause when Val > 3 + assert(service.SetBreak("Val", BREAK_GT, 3.0) == 1u); + service.UpdateBrokersBreakStatus(); + + volatile bool active = false; + Vec indices; + Vec sizes; + FastPollingMutexSem mutex; + volatile bool anyBreak = false; + Vec breakIdx; + DebugSignalInfo *ptrs[1] = { service.signals[0] }; + service.RegisterBroker(ptrs, 1u, NULL_PTR(MemoryMapBroker *), + &active, &indices, &sizes, &mutex, + &anyBreak, &breakIdx); + + // UpdateBrokersBreakStatus must propagate the break flag to the broker + service.UpdateBrokersBreakStatus(); + assert(anyBreak == true); + assert(breakIdx.Size() == 1u); + + // Process() copies break indices, releases lock, evaluates break. + // val = 5 > 3 — service must become paused. + DebugBrokerHelper::Process(&service, ptrs, indices, sizes, mutex, + &anyBreak, &breakIdx); + assert(service.IsPaused()); + printf(" -> PASS: break condition triggered correctly via copy-before-unlock path\n"); + + // Reset; change value below threshold — must NOT pause. + service.SetPaused(false); + val = 1u; + DebugBrokerHelper::Process(&service, ptrs, indices, sizes, mutex, + &anyBreak, &breakIdx); + assert(!service.IsPaused()); + printf(" -> PASS: value below threshold does not trigger break\n"); + } + + // ------------------------------------------------------------------------- + // Fix #4 (atomic decimation): decimationFactor > 1 causes exactly one Push + // after factor calls to ProcessSignal. + // ------------------------------------------------------------------------- + static void TestDecimationCounterAtomic() { + printf("Fix #4 (atomic decimation): one push per decimation factor...\n"); + ObjectRegistryDatabase::Instance()->Purge(); + + DebugService service; + assert(service.traceBuffer.Init(1024 * 1024)); + ConfigurationDatabase cfg; + cfg.Write("ControlPort", (uint32)0); + cfg.Write("StreamPort", (uint32)0); + cfg.Write("SuppressTimeoutLogs", (uint32)1); + assert(service.Initialise(cfg)); + + uint32 val = 0x42424242u; + service.RegisterSignal(&val, UnsignedInteger32Bit, "Dec.Val", 0, 1); + // TraceSignal with decimation = 3 + assert(service.TraceSignal("Val", true, 3u) == 1u); + assert(service.signals[0]->decimationFactor == 3u); + + uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * + HighResolutionTimer::Period() * 1000000.0); + + // Calls 1 and 2 — counter advances but no push + service.ProcessSignal(service.signals[0], 4u, ts); + service.ProcessSignal(service.signals[0], 4u, ts + 1u); + + uint32 id, sz; uint64 rts; uint8 buf[64]; + bool hasData = service.traceBuffer.Pop(id, rts, buf, sz, 64u); + assert(!hasData); // no sample yet + printf(" -> PASS: no push after 2 calls with decimation=3\n"); + + // Call 3 — counter reaches factor, one push occurs + service.ProcessSignal(service.signals[0], 4u, ts + 2u); + hasData = service.traceBuffer.Pop(id, rts, buf, sz, 64u); + assert(hasData); + assert(id == service.signals[0]->internalID); + assert(sz == 4u); + printf(" -> PASS: exactly one push after %u calls with decimation=3\n", 3u); + + // Call 4 and 5 — no push again + service.ProcessSignal(service.signals[0], 4u, ts + 3u); + service.ProcessSignal(service.signals[0], 4u, ts + 4u); + hasData = service.traceBuffer.Pop(id, rts, buf, sz, 64u); + assert(!hasData); + printf(" -> PASS: no push for calls 4 and 5 (decimation reset)\n"); + + // Call 6 — second push + service.ProcessSignal(service.signals[0], 4u, ts + 5u); + hasData = service.traceBuffer.Pop(id, rts, buf, sz, 64u); + assert(hasData); + printf(" -> PASS: second push on call 6\n"); + } }; } @@ -132,10 +693,27 @@ void timeout_handler(int sig) { int main() { signal(SIGALRM, timeout_handler); - alarm(10); - printf("--- MARTe2 Debug Suite COVERAGE V30 ---\n"); + alarm(10); + printf("--- MARTe2 Debug Suite COVERAGE V34 ---\n"); MARTe::TestTcpLogger(); MARTe::DebugServiceTest::TestAll(); - printf("\nCOVERAGE V30 PASSED!\n"); + // FIX #1, #2, #4 + MARTe::DebugServiceTest::TestForceSignalOversizeRejected(); + MARTe::DebugServiceTest::TestStreamerPopMaxSize(); + MARTe::DebugServiceTest::TestTracePushSerialised(); + // FIX #3, #6, #7, #8, #9 + MARTe::DebugServiceTest::TestUpdateBrokersNullSignalPointers(); + MARTe::DebugServiceTest::TestGetSignalValueTruncation(); + MARTe::DebugServiceTest::TestServerConstantsSane(); + MARTe::DebugServiceTest::TestMsgKeyBoundsCheck(); + // FIX #5, #10, #11 + MARTe::DebugServiceTest::TestRingBufferSkipOversizedEntry(); + MARTe::DebugServiceTest::TestServerInputBuffering(); + MARTe::DebugServiceTest::TestStreamerMonitoredPushSerialised(); + // Fix #2 (Swap), Fix #3 (copy-before-unlock), Fix #4 (atomic decimation) + MARTe::DebugServiceTest::TestUpdateBrokersSwapPublishesAllIndices(); + MARTe::DebugServiceTest::TestBreakEvaluationAfterLockRelease(); + MARTe::DebugServiceTest::TestDecimationCounterAtomic(); + printf("\nCOVERAGE V34 PASSED!\n"); return 0; } diff --git a/run_webdebug_app.sh b/run_webdebug_app.sh new file mode 100755 index 0000000..485b0cd --- /dev/null +++ b/run_webdebug_app.sh @@ -0,0 +1,67 @@ +#!/bin/bash +# run_debug_app.sh - Launch the MARTe2 Debugging Environment using standard MARTeApp.ex + +# 1. Environment Setup +if [ -f "./env.sh" ]; then + source ./env.sh +else + echo "ERROR: env.sh not found. Ensure you are in the project root." + exit 1 +fi + +# 2. Paths +MARTE_EX="${MARTe2_DIR}/Build/${TARGET}/App/MARTeApp.ex" +BUILD_DIR="$(pwd)/Build/${TARGET}" + +# Our plugin libraries +WEB_DEBUG_LIB="${BUILD_DIR}/Components/Interfaces/WebDebugService/libWebDebugService.so" +TCPLOGGER_LIB="${BUILD_DIR}/Components/Interfaces/TCPLogger/libTcpLogger.so" + +# Component library base search path +COMPONENTS_BUILD_DIR="${MARTe2_Components_DIR}/Build/${TARGET}/Components" + +# DYNAMICALLY FIND ALL COMPONENT DIRS and add to LD_LIBRARY_PATH +ALL_COMPONENT_DIRS=$(find "$COMPONENTS_BUILD_DIR" -type d) +for dir in $ALL_COMPONENT_DIRS; do + if ls "$dir"/*.so >/dev/null 2>&1; then + export LD_LIBRARY_PATH="$dir:$LD_LIBRARY_PATH" + fi +done + +# Ensure our build dir and core dir are included +export LD_LIBRARY_PATH="${BUILD_DIR}/Components/Interfaces/WebDebugService:${BUILD_DIR}/Components/Interfaces/TCPLogger:${MARTe2_DIR}/Build/${TARGET}/Core:${LD_LIBRARY_PATH}" + +# 3. Cleanup +echo "Cleaning up lingering processes..." +pkill -9 MARTeApp.ex +sleep 1 + +# 4. Validate libraries exist +for lib in "$WEB_DEBUG_LIB" "$TCPLOGGER_LIB"; do + if [ ! -f "$lib" ]; then + echo "ERROR: Library not found: $lib" + echo "Run: make -f Makefile.gcc core" + exit 1 + fi +done + +# 5. Launch Application +echo "Launching standard MARTeApp.ex with webdebug_test.cfg (WebDebugService on port 8090)..." +# LD_PRELOAD ensures our classes are registered before the config is parsed. +# MARTeApp.ex does not use dlopen for plugins — preloading is the standard approach. +# All required component .so files are collected via the find loop into LD_PRELOAD too. +PRELOAD_LIBS="${WEB_DEBUG_LIB}:${TCPLOGGER_LIB}" + +# Collect any additional component .so files referenced by the config +for lib in \ + "${COMPONENTS_BUILD_DIR}/DataSources/RealTimeThreadSynchronisation/RealTimeThreadSynchronisation.so" \ + "${COMPONENTS_BUILD_DIR}/DataSources/LinuxTimer/LinuxTimer.so" \ + "${COMPONENTS_BUILD_DIR}/DataSources/LoggerDataSource/LoggerDataSource.so" \ + "${COMPONENTS_BUILD_DIR}/GAMs/IOGAM/IOGAM.so"; do + if [ -f "$lib" ]; then + PRELOAD_LIBS="${PRELOAD_LIBS}:${lib}" + fi +done + +export LD_PRELOAD="${PRELOAD_LIBS}" +"$MARTE_EX" -f Test/Configurations/webdebug_test.cfg -l RealTimeLoader -s State1