interface added and web ui created

This commit is contained in:
Martino Ferrari
2026-05-07 10:49:24 +02:00
parent 340b024ae4
commit 774808a054
21 changed files with 4986 additions and 362 deletions
@@ -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<uint32> &activeIndices, Vec<uint32> &activeSizes,
FastPollingMutexSem &activeMutex,
volatile bool *anyBreakFlag,
Vec<uint32> *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> *)) {
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<uint32> *) &&
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<uint32> *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<DebugService *>(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 <typename BaseClass> 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 <typename BaseClass>
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;
@@ -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;
}
@@ -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<uint32> 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);
}
@@ -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<uint32> *activeIndices;
Vec<uint32> *activeSizes;
FastPollingMutexSem *activeMutex;
// Conditional break — mirrors activeIndices but for break-enabled signals
volatile bool *anyBreakFlag;
Vec<uint32> *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<uint32> *activeIndices, Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag, Vec<uint32> *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<uint32> *activeIndices, Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag, Vec<uint32> *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<Message> &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
@@ -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<uint32> *activeIndices;
Vec<uint32> *activeSizes;
FastPollingMutexSem *activeMutex;
volatile bool *anyBreakFlag;
Vec<uint32> *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<uint32> *activeIndices,
Vec<uint32> *activeSizes,
FastPollingMutexSem *activeMutex,
volatile bool *anyBreakFlag,
Vec<uint32> *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