#include "Atomic.h" #include "BasicTCPSocket.h" #include "ClassRegistryItem.h" #include "ConfigurationDatabase.h" #include "DataSourceI.h" #include "DebugBrokerWrapper.h" #include "DebugService.h" #include "GAM.h" #include "GlobalObjectsDatabase.h" #include "HighResolutionTimer.h" #include "LoggerService.h" #include "Message.h" #include "ObjectBuilder.h" #include "ObjectRegistryDatabase.h" #include "Threads.h" #include "Sleep.h" #include "StreamString.h" #include "TimeoutType.h" #include "TcpLogger.h" #include "TypeConversion.h" #include "ReferenceT.h" namespace MARTe { // 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 *)) 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 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 FindPathInContainer(ReferenceContainer *container, const Object *target, StreamString &path) { if (container == NULL_PTR(ReferenceContainer *)) return false; uint32 n = container->Size(); for (uint32 i = 0u; i < n; i++) { Reference ref = container->Get(i); if (ref.IsValid()) { if (ref.operator->() == target) { path = ref->GetName(); return true; } ReferenceContainer *sub = dynamic_cast(ref.operator->()); if (sub != NULL_PTR(ReferenceContainer *)) { if (FindPathInContainer(sub, target, path)) { StreamString full; full.Printf("%s.%s", ref->GetName(), path.Buffer()); path = full; return true; } } } } return false; } 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), binderStreamer(this, ServiceBinder::StreamerType), threadService(binderServer), streamerService(binderStreamer) { controlPort = 0; streamPort = 8081; logPort = 8082; streamIP = "127.0.0.1"; isServer = false; suppressTimeoutLogs = true; isPaused = false; stepRemaining = 0u; manualConfigSet = false; activeClient = NULL_PTR(BasicTCPSocket *); streamerPacketOffset = 0u; streamerSequenceNumber = 0u; cmdCountInWindow = 0u; cmdWindowStartMs = 0u; lastDataTimeMs = 0u; inputBuffer = ""; // FIX #10: initialise carry-over buffer } DebugService::~DebugService() { if (DebugServiceI::GetInstance() == this) { DebugServiceI::SetInstance(NULL_PTR(DebugServiceI *)); } threadService.Stop(); streamerService.Stop(); tcpServer.Close(); udpSocket.Close(); if (activeClient != NULL_PTR(BasicTCPSocket *)) { activeClient->Close(); delete activeClient; } for (uint32 i = 0; i < signals.Size(); i++) { delete signals[i]; } } bool DebugService::Initialise(StructuredDataI &data) { if (!ReferenceContainer::Initialise(data)) return false; uint32 port = 0; if (data.Read("ControlPort", port)) { controlPort = (uint16)port; } else { (void)data.Read("TcpPort", port); controlPort = (uint16)port; } if (controlPort > 0) { isServer = true; DebugServiceI::SetInstance(this); } port = 8081; if (data.Read("StreamPort", port)) { streamPort = (uint16)port; } else { (void)data.Read("UdpPort", port); streamPort = (uint16)port; } port = 8082; if (data.Read("LogPort", port)) { logPort = (uint16)port; } else { (void)data.Read("TcpLogPort", port); logPort = (uint16)port; } StreamString tempIP; if (data.Read("StreamIP", tempIP)) { streamIP = tempIP; } else { streamIP = "127.0.0.1"; } uint32 suppress = 1; if (data.Read("SuppressTimeoutLogs", suppress)) { suppressTimeoutLogs = (suppress == 1); } // Do NOT call MoveToRoot() on the shared CDB here — that corrupts the // ReferenceContainer::Initialise() traversal cursor for sibling objects. // Just capture the local subtree; full config is rebuilt lazily from the // live ObjectRegistryDatabase when ServeConfig/EnrichWithConfig is called. (void)data.Copy(fullConfig); if (isServer) { if (!traceBuffer.Init(8 * 1024 * 1024)) return false; PatchRegistry(); ConfigurationDatabase threadData; threadData.Write("Timeout", (uint32)1000); threadService.Initialise(threadData); streamerService.Initialise(threadData); if (!tcpServer.Open()) return false; if (!tcpServer.Listen(controlPort)) return false; if (!udpSocket.Open()) return false; if (threadService.Start() != ErrorManagement::NoError) return false; if (streamerService.Start() != ErrorManagement::NoError) return false; } return true; } void DebugService::InjectTcpLoggerIfNeeded() { if (logPort == 0u) return; // Check if the ORD already contains a LoggerService with at least one TcpLogger. // If so, leave it untouched. Reference existing = ObjectRegistryDatabase::Instance()->Find("LoggerService"); if (existing.IsValid()) { ReferenceContainer *rc = dynamic_cast(existing.operator->()); if (rc != NULL_PTR(ReferenceContainer *)) { for (uint32 i = 0u; i < rc->Size(); i++) { ReferenceT child = rc->Get(i); if (child.IsValid()) { printf("[DebugService] Found existing TcpLogger in LoggerService — skipping injection.\n"); return; } } } } // Build a CDB that mirrors the config-file declaration: // LoggerService node (current position) // Class = LoggerService // CPUs = 1 // DebugConsumer (child) // Class = TcpLogger // Port = ConfigurationDatabase lsCdb; (void)lsCdb.Write("Class", "LoggerService"); uint32 cpus = 1u; (void)lsCdb.Write("CPUs", cpus); // ReferenceContainer::Initialise only instantiates children whose names // start with '+' (matching the StandardParser convention). if (lsCdb.CreateRelative("+DebugConsumer")) { (void)lsCdb.Write("Class", "TcpLogger"); uint32 p = static_cast(logPort); (void)lsCdb.Write("Port", p); (void)lsCdb.MoveToAncestor(1u); } (void)lsCdb.MoveToRoot(); ReferenceT ls( "LoggerService", GlobalObjectsDatabase::Instance()->GetStandardHeap()); if (!ls.IsValid()) { printf("[DebugService] Failed to create LoggerService object.\n"); return; } ls->SetName("LoggerService"); if (!ls->Initialise(lsCdb)) { printf("[DebugService] LoggerService::Initialise() failed.\n"); return; } // Insert into the ORD so it is findable by name and cleaned up on shutdown. if (!ObjectRegistryDatabase::Instance()->Insert(ls)) { printf("[DebugService] Failed to insert LoggerService into ORD.\n"); // Keep a local reference anyway so the logger thread stays alive. } // The ORD now holds a reference to ls; the LoggerService stays alive until // ObjectRegistryDatabase::Purge(). No need to store a local reference. printf("[DebugService] Auto-injected LoggerService + TcpLogger on port %u.\n", logPort); } void DebugService::SetFullConfig(ConfigurationDatabase &config) { config.MoveToRoot(); config.Copy(fullConfig); manualConfigSet = true; } static void BuildCDBFromContainer(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 *className = child->GetClassProperties()->GetName(); if (className != NULL_PTR(const char8 *)) (void)cdb.Write("Class", className); // Export scalar parameters via a SEPARATE CDB so the live cursor is // never touched by ExportData. Then copy only top-level LEAF values // (i.e. scalars where MoveRelative fails) and skip sub-nodes. // // This avoids two ExportData pitfalls: // 1. ReferenceContainer::ExportData writes numeric-indexed child nodes // (+0, +1, ...) — those are sub-nodes and get filtered out. // 2. Some DataSource ExportData implementations follow internal // references and write sibling objects as children — also sub-nodes, // also filtered out. // // Scalar parameters (ControlPort, UdpPort, CPUs, Port, ...) pass through // because they are leaf values, not sub-nodes. { ConfigurationDatabase exportCdb; if (child->ExportData(exportCdb)) { exportCdb.MoveToRoot(); uint32 nExport = exportCdb.GetNumberOfChildren(); for (uint32 j = 0u; j < nExport; j++) { const char8 *ek = exportCdb.GetChildName(j); if (StringHelper::Compare(ek, "Class") == 0 || StringHelper::Compare(ek, "Name") == 0 || StringHelper::Compare(ek, "IsContainer") == 0) continue; // Sub-node check: MoveRelative succeeds only for nodes, not scalars if (exportCdb.MoveRelative(ek)) { exportCdb.MoveToAncestor(1u); continue; // skip sub-nodes entirely } // Leaf scalar — convert to string and write into the main CDB AnyType at = exportCdb.GetType(ek); if (at.GetDataPointer() != NULL_PTR(void *)) { char8 buf[1024]; AnyType st(CharString, 0u, buf); st.SetNumberOfElements(0, 1024); if (TypeConvert(st, at)) { (void)cdb.Write(ek, buf); } } } } } ReferenceContainer *sub = dynamic_cast(child.operator->()); if (sub != NULL_PTR(ReferenceContainer *)) BuildCDBFromContainer(sub, cdb); (void)cdb.MoveToAncestor(1u); } } void DebugService::RebuildConfigFromRegistry() { fullConfig = ConfigurationDatabase(); BuildCDBFromContainer(ObjectRegistryDatabase::Instance(), fullConfig); // ExportData on ReferenceContainer subclasses (including DebugService itself) // only writes Name/IsContainer/indexed children — it never re-emits the // config-file parameters that were read in Initialise(). Write them back // explicitly from the member variables that Initialise() stored. const char8 *myName = GetName(); if (myName != NULL_PTR(const char8 *)) { if (fullConfig.MoveRelative(myName)) { (void)fullConfig.Write("ControlPort", static_cast(controlPort)); (void)fullConfig.Write("UdpPort", static_cast(streamPort)); (void)fullConfig.Write("LogPort", static_cast(logPort)); if (streamIP.Size() > 0u) (void)fullConfig.Write("StreamIP", streamIP.Buffer()); (void)fullConfig.MoveToAncestor(1u); } } } static void PatchItemInternal(const char8 *originalName, ObjectBuilder *debugBuilder) { ClassRegistryItem *item = ClassRegistryDatabase::Instance()->Find(originalName); if (item != NULL_PTR(ClassRegistryItem *)) { item->SetObjectBuilder(debugBuilder); } } void DebugService::PatchRegistry() { DebugMemoryMapInputBrokerBuilder *b1 = new DebugMemoryMapInputBrokerBuilder(); PatchItemInternal("MemoryMapInputBroker", b1); DebugMemoryMapOutputBrokerBuilder *b2 = new DebugMemoryMapOutputBrokerBuilder(); PatchItemInternal("MemoryMapOutputBroker", b2); DebugMemoryMapSynchronisedInputBrokerBuilder *b3 = new DebugMemoryMapSynchronisedInputBrokerBuilder(); PatchItemInternal("MemoryMapSynchronisedInputBroker", b3); DebugMemoryMapSynchronisedOutputBrokerBuilder *b4 = new DebugMemoryMapSynchronisedOutputBrokerBuilder(); PatchItemInternal("MemoryMapSynchronisedOutputBroker", b4); DebugMemoryMapInterpolatedInputBrokerBuilder *b5 = new DebugMemoryMapInterpolatedInputBrokerBuilder(); PatchItemInternal("MemoryMapInterpolatedInputBroker", b5); DebugMemoryMapMultiBufferInputBrokerBuilder *b6 = new DebugMemoryMapMultiBufferInputBrokerBuilder(); PatchItemInternal("MemoryMapMultiBufferInputBroker", b6); DebugMemoryMapMultiBufferOutputBrokerBuilder *b7 = new DebugMemoryMapMultiBufferOutputBrokerBuilder(); PatchItemInternal("MemoryMapMultiBufferOutputBroker", b7); DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder *b8 = new DebugMemoryMapSynchronisedMultiBufferInputBrokerBuilder(); PatchItemInternal("MemoryMapSynchronisedMultiBufferInputBroker", b8); DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder *b9 = new DebugMemoryMapSynchronisedMultiBufferOutputBrokerBuilder(); PatchItemInternal("MemoryMapSynchronisedMultiBufferOutputBroker", b9); DebugMemoryMapAsyncOutputBrokerBuilder *b10 = new DebugMemoryMapAsyncOutputBrokerBuilder(); PatchItemInternal("MemoryMapAsyncOutputBroker", b10); DebugMemoryMapAsyncTriggerOutputBrokerBuilder *b11 = new DebugMemoryMapAsyncTriggerOutputBrokerBuilder(); PatchItemInternal("MemoryMapAsyncTriggerOutputBroker", b11); } DebugSignalInfo *DebugService::RegisterSignal(void *memoryAddress, TypeDescriptor type, const char8 *name, uint8 numberOfDimensions, uint32 numberOfElements) { fprintf(stderr, " RegisterSignal[%p]: %s\n", (void*)this, name); mutex.FastLock(); DebugSignalInfo *res = NULL_PTR(DebugSignalInfo *); uint32 sigIdx = 0xFFFFFFFF; for (uint32 i = 0; 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 = 1; res->decimationCounter = 0; res->breakOp = BREAK_OFF; res->breakThreshold = 0.0; signals.Push(res); } if (sigIdx != 0xFFFFFFFF) { bool foundAlias = false; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == name) { foundAlias = true; break; } } if (!foundAlias) { SignalAlias a; a.name = name; a.signalIndex = sigIdx; aliases.Push(a); } } mutex.FastUnLock(); return res; } void DebugService::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) { // 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(); } } } } void DebugService::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 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]; if (s != NULL_PTR(DebugSignalInfo *) && (s->isTracing || s->isForcing)) { count++; } } Vec tempInd; Vec tempSizes; for (uint32 j = 0; 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) : 4); } } 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->Swap(tempInd); if (brokers[i].activeSizes) 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++) { 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(); // FIX #2: O(1) Swap — old array freed after the lock is released. if (brokers[i].breakIndices) 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. } } ErrorManagement::ErrorType DebugService::Execute(ExecutionInfo &info) { return ErrorManagement::FatalError; } ErrorManagement::ErrorType DebugService::HandleMessage(ReferenceT &data) { printf(" DebugService received custom message: Function=%s\n", (const char8*)data->GetFunction()); return ErrorManagement::NoError; } ErrorManagement::ErrorType DebugService::Server(ExecutionInfo &info) { if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError; if (info.GetStage() == ExecutionInfo::StartupStage) { serverThreadId = Threads::Id(); // Wait for ObjectRegistryDatabase::Initialise() to finish processing all // sibling objects (LoggerService etc. come after DebugService in the config). // 500 ms is well above any realistic initialisation time. Sleep::MSec(500u); InjectTcpLoggerIfNeeded(); return ErrorManagement::NoError; } // 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 *)) { 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 { // 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) { 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; } 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; } } } } else { // Read failed (client disconnected or error), clean up inputBuffer = ""; // FIX #10 clientMutex.FastLock(); activeClient->Close(); delete activeClient; activeClient = NULL_PTR(BasicTCPSocket *); clientMutex.FastUnLock(); } } } return ErrorManagement::NoError; } ErrorManagement::ErrorType DebugService::Streamer(ExecutionInfo &info) { if (info.GetStage() == ExecutionInfo::TerminationStage) return ErrorManagement::NoError; if (info.GetStage() == ExecutionInfo::StartupStage) { streamerThreadId = Threads::Id(); return ErrorManagement::NoError; } // Set UDP destination (idempotent, called each Execute() invocation) InternetHost dest(streamPort, streamIP.Buffer()); (void)udpSocket.SetDestination(dest); // Poll monitored signals uint64 currentTimeMs = (uint64)((float64)HighResolutionTimer::Counter() * HighResolutionTimer::Period() * 1000.0); mutex.FastLock(); for (uint32 i = 0; i < monitoredSignals.Size(); i++) { if (currentTimeMs >= (monitoredSignals[i].lastPollTime + monitoredSignals[i].periodMs)) { monitoredSignals[i].lastPollTime = currentTimeMs; uint64 ts = (uint64)((float64)HighResolutionTimer::Counter() * 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). // // 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[SAMPLE_BUF_SIZE]; bool hasData = false; 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; header.seq = streamerSequenceNumber++; header.timestamp = HighResolutionTimer::Counter(); header.count = 0; memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader)); streamerPacketOffset = sizeof(TraceHeader); } // 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; header.magic = 0xDA7A57AD; header.seq = streamerSequenceNumber++; header.timestamp = HighResolutionTimer::Counter(); header.count = 0; memcpy(streamerPacketBuffer, &header, sizeof(TraceHeader)); streamerPacketOffset = sizeof(TraceHeader); } memcpy(&streamerPacketBuffer[streamerPacketOffset], &id, 4); memcpy(&streamerPacketBuffer[streamerPacketOffset + 4], &ts, 8); memcpy(&streamerPacketBuffer[streamerPacketOffset + 12], &size, 4); memcpy(&streamerPacketBuffer[streamerPacketOffset + 16], sampleData, size); streamerPacketOffset += (16u + size); ((TraceHeader *)streamerPacketBuffer)->count++; } if (streamerPacketOffset > 0u) { uint32 toWrite = streamerPacketOffset; (void)udpSocket.Write((char8 *)streamerPacketBuffer, toWrite); streamerPacketOffset = 0u; } if (!hasData) { Sleep::MSec(1); } return ErrorManagement::NoError; } bool DebugServiceI::GetFullObjectName(const Object &obj, StreamString &fullPath) { fullPath = ""; if (FindPathInContainer(ObjectRegistryDatabase::Instance(), &obj, fullPath)) { return true; } const char8 *name = obj.GetName(); if (name != NULL_PTR(const char8 *)) fullPath = name; return true; } void DebugService::HandleCommand(StreamString cmd, BasicTCPSocket *client) { StreamString token; cmd.Seek(0); char8 term; const char8 *delims = " \r\n"; if (cmd.GetToken(token, delims, term)) { if (token == "FORCE") { StreamString name, val; if (cmd.GetToken(name, delims, term) && cmd.GetToken(val, delims, term)) { uint32 count = ForceSignal(name.Buffer(), val.Buffer()); if (client) { StreamString resp; resp.Printf("OK FORCE %u\n", count); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } else if (token == "UNFORCE") { StreamString name; if (cmd.GetToken(name, delims, term)) { uint32 count = UnforceSignal(name.Buffer()); if (client) { StreamString resp; resp.Printf("OK UNFORCE %u\n", count); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } else if (token == "TRACE") { StreamString name, state, decim; if (cmd.GetToken(name, delims, term) && cmd.GetToken(state, delims, term)) { bool enable = (state == "1"); uint32 d = 1; if (cmd.GetToken(decim, delims, term)) { AnyType decimVal(UnsignedInteger32Bit, 0u, &d); AnyType decimStr(CharString, 0u, decim.Buffer()); (void)TypeConvert(decimVal, decimStr); } uint32 count = TraceSignal(name.Buffer(), enable, d); if (client) { StreamString resp; resp.Printf("OK TRACE %u\n", count); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } else if (token == "STEP") { StreamString nStr; uint32 n = 1u; if (cmd.GetToken(nStr, delims, term)) { AnyType nVal(UnsignedInteger32Bit, 0u, &n); AnyType nS(CharString, 0u, nStr.Buffer()); (void)TypeConvert(nVal, nS); } // Optional thread name: STEP [] StreamString threadStr; const char8 *threadArg = NULL_PTR(const char8 *); if (cmd.GetToken(threadStr, delims, term) && threadStr.Size() > 0u) { threadArg = threadStr.Buffer(); } Step(n, threadArg); if (client) { StreamString resp; resp.Printf("OK STEP %u\n", n); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } else if (token == "STEP_STATUS") { GetStepStatus(client); } else if (token == "VALUE") { StreamString sigName; if (cmd.GetToken(sigName, delims, term)) { GetSignalValue(sigName.Buffer(), client); } else if (client) { const char8 *errResp = "{\"Error\": \"Missing signal name\"}\nOK VALUE\n"; uint32 s = StringHelper::Length(errResp); (void)client->Write(errResp, s); } } else if (token == "BREAK") { // BREAK — set break condition // BREAK OFF — clear break condition StreamString name, opStr; if (cmd.GetToken(name, delims, term) && cmd.GetToken(opStr, delims, term)) { uint32 count = 0; if (opStr == "OFF") { count = ClearBreak(name.Buffer()); } else { StreamString threshStr; if (cmd.GetToken(threshStr, 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 threshold = 0.0; AnyType thrVal(Float64Bit, 0u, &threshold); AnyType thrStr(CharString, 0u, threshStr.Buffer()); (void)TypeConvert(thrVal, thrStr); count = SetBreak(name.Buffer(), op, threshold); } } } if (client) { StreamString resp; resp.Printf("OK BREAK %u\n", count); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } else if (token == "DISCOVER") Discover(client); else if (token == "MSG") { StreamString dest, func, waitStr; if (cmd.GetToken(dest, delims, term) && cmd.GetToken(func, delims, term) && cmd.GetToken(waitStr, delims, term)) { bool wait = (waitStr == "1"); const char8 *pStart = cmd.Buffer() + cmd.Position(); StreamString rawPayload = pStart; // Decode escaped newlines (\n) StreamString payload; rawPayload.Seek(0u); char8 c; while (rawPayload.Size() > rawPayload.Position()) { uint32 readS = 1; if (rawPayload.Read(&c, readS)) { if (c == '\\') { char8 next; if (rawPayload.Read(&next, readS)) { if (next == 'n') { payload += '\n'; } else { payload += c; payload += next; } } else { payload += c; } } else { payload += c; } } } ReferenceT msg( "Message", GlobalObjectsDatabase::Instance()->GetStandardHeap()); ConfigurationDatabase msgConfig; msgConfig.Write("Destination", dest.Buffer()); msgConfig.Write("Function", func.Buffer()); if (wait) { msgConfig.Write("Mode", "ExpectsReply"); } // Parse payload key=value lines into a ConfigurationDatabase. // ConstantGAM::SetOutput (and similar handlers) expect a // ReferenceT inserted into the Message's // reference container — NOT a sub-node of the message config. ReferenceT paramCdb( "ConfigurationDatabase", GlobalObjectsDatabase::Instance()->GetStandardHeap()); if (payload.Size() > 0u && paramCdb.IsValid()) { payload.Seek(0u); StreamString line; while (payload.GetToken(line, "\n", term)) { if (line.Size() > 0u) { const char8 *eq = StringHelper::SearchChar(line.Buffer(), '='); if (eq != NULL_PTR(const char8 *)) { uint32 eqPos = (uint32)(eq - line.Buffer()); // 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); // 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 for (int32 ti = (int32)valLen - 1; ti >= 0; ti--) { if (valBuf[ti] == ' ' || valBuf[ti] == '\r' || valBuf[ti] == '\t') valBuf[ti] = '\0'; else break; } StreamString key = keyBuf; key = key.Buffer(); // trim happens via assignment // Trim leading whitespace from key const char8 *kp = keyBuf; while (*kp == ' ' || *kp == '\t') kp++; if (*kp != '\0') { (void)paramCdb->Write(kp, valBuf); } } } line = ""; } } ErrorManagement::ErrorType err = ErrorManagement::ParametersError; if (msg->Initialise(msgConfig)) { if (paramCdb.IsValid() && payload.Size() > 0u) { // Insert the CDB as a ReferenceT parameter (void)msg->Insert(paramCdb); } // Find destination object in the global database Reference destObj = ObjectRegistryDatabase::Instance()->Find(dest.Buffer()); if (destObj.IsValid()) { Object* sender = this; StreamString myPath; if (!GetFullObjectName(*this, myPath)) { sender = NULL_PTR(Object*); } if (wait) { err = MessageI::WaitForReply(msg, TTInfiniteWait); } else { (void)MessageI::SendMessage(msg, sender); // Fire-and-forget: destination found, message sent. // Whether the recipient had a matching filter is not // reported back to the caller — return OK. err = ErrorManagement::NoError; } } else { printf(" MSG: Destination object %s not found in ORD\n", dest.Buffer()); } if (err != ErrorManagement::NoError) { printf(" MSG: MessageI dispatch failed.\n"); } } else { printf(" MSG: Message initialization failed\n"); } if (client) { if (err == ErrorManagement::NoError) { uint32 okSize = 7; (void)client->Write("OK MSG\n", okSize); } else { uint32 errSize = 10; (void)client->Write("ERROR MSG\n", errSize); } } } } else if (token == "SERVICE_INFO") { if (client) { StreamString resp; resp.Printf("OK SERVICE_INFO TCP_CTRL:%u UDP_STREAM:%u TCP_LOG:%u STATE:%s\n", controlPort, streamPort, logPort, isPaused ? "PAUSED" : "RUNNING"); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } else if (token == "MONITOR") { StreamString subToken; if (cmd.GetToken(subToken, delims, term) && subToken == "SIGNAL") { StreamString name, period; if (cmd.GetToken(name, delims, term) && cmd.GetToken(period, delims, term)) { uint32 p = 100; AnyType pVal(UnsignedInteger32Bit, 0u, &p); AnyType pStr(CharString, 0u, period.Buffer()); (void)TypeConvert(pVal, pStr); uint32 count = RegisterMonitorSignal(name.Buffer(), p); if (client) { StreamString resp; resp.Printf("OK MONITOR %u\n", count); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } } else if (token == "UNMONITOR") { StreamString subToken; if (cmd.GetToken(subToken, delims, term) && subToken == "SIGNAL") { StreamString name; if (cmd.GetToken(name, delims, term)) { uint32 count = UnmonitorSignal(name.Buffer()); if (client) { StreamString resp; resp.Printf("OK UNMONITOR %u\n", count); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } } } } else if (token == "CONFIG") ServeConfig(client); else if (token == "PAUSE") { SetPaused(true); if (client) { uint32 s = 3; (void)client->Write("OK\n", s); } } else if (token == "RESUME") { SetPaused(false); if (client) { uint32 s = 3; (void)client->Write("OK\n", s); } } else if (token == "TREE") { StreamString json; json = "{\"Name\": \"Root\", \"Class\": \"ObjectRegistryDatabase\", " "\"Children\": [\n"; (void)ExportTree(ObjectRegistryDatabase::Instance(), json, NULL_PTR(const char8 *)); json += "\n]}\nOK TREE\n"; uint32 s = json.Size(); if (client) (void)client->Write(json.Buffer(), s); } else if (token == "INFO") { StreamString path; if (cmd.GetToken(path, delims, term)) InfoNode(path.Buffer(), client); } else if (token == "LS") { StreamString path; if (cmd.GetToken(path, delims, term)) ListNodes(path.Buffer(), client); else ListNodes(NULL_PTR(const char8 *), client); } } } void DebugService::EnrichWithConfig(const char8 *path, StreamString &json) { if (path == NULL_PTR(const char8 *)) return; if (!manualConfigSet) { RebuildConfigFromRegistry(); } fullConfig.MoveToRoot(); fprintf(stderr, "[EnrichWithConfig] path=%s\n", path); const char8 *current = path; bool ok = true; while (ok) { const char8 *nextDot = StringHelper::SearchString(current, "."); StreamString part; if (nextDot != NULL_PTR(const char8 *)) { uint32 len = (uint32)(nextDot - current); (void)part.Write(current, len); current = nextDot + 1; } else { part = current; ok = false; } // Normalise short direction names to both forms so we search consistently. // Paths use "In"/"Out" (alias convention); CDBs may store either form. bool found = false; // 1. Try exact match (bare name - rebuilt-from-registry CDBs use this) if (fullConfig.MoveRelative(part.Buffer())) { fprintf(stderr, "[EnrichWithConfig] nav exact '%s' OK\n", part.Buffer()); found = true; } // 2. Try +name (raw config-file CDBs prefix nodes with '+') if (!found) { StreamString prefixed; prefixed.Printf("+%s", part.Buffer()); if (fullConfig.MoveRelative(prefixed.Buffer())) { fprintf(stderr, "[EnrichWithConfig] nav prefixed '%s' OK\n", prefixed.Buffer()); found = true; } } // 3. Expand short direction aliases: In -> InputSignals / Out -> OutputSignals if (!found) { if (part == "In") { if (fullConfig.MoveRelative("InputSignals")) { fprintf(stderr, "[EnrichWithConfig] nav 'In'->InputSignals OK\n"); found = true; } else if (fullConfig.MoveRelative("+InputSignals")) { fprintf(stderr, "[EnrichWithConfig] nav 'In'->+InputSignals OK\n"); found = true; } } else if (part == "Out") { if (fullConfig.MoveRelative("OutputSignals")) { found = true; } else if (fullConfig.MoveRelative("+OutputSignals")) { found = true; } } } if (!found) { fprintf(stderr, "[EnrichWithConfig] FAILED at part '%s'\n", part.Buffer()); fullConfig.MoveToRoot(); return; } } ConfigurationDatabase db; fullConfig.Copy(db); fullConfig.MoveToRoot(); db.MoveToRoot(); uint32 n = db.GetNumberOfChildren(); for (uint32 i = 0u; i < n; i++) { const char8 *name = db.GetChildName(i); AnyType at = db.GetType(name); if (!at.GetTypeDescriptor().isStructuredData) { json += ", \""; EscapeJson(name, json); json += "\": \""; char8 buf[1024]; AnyType st(CharString, 0u, buf); st.SetNumberOfElements(0, 1024); if (TypeConvert(st, at)) { EscapeJson(buf, json); } json += "\""; } } } void DebugService::JsonifyDatabase(ConfigurationDatabase &db, StreamString &json) { json += "{"; uint32 n = db.GetNumberOfChildren(); for (uint32 i = 0u; i < n; i++) { const char8 *name = db.GetChildName(i); json += "\""; EscapeJson(name, json); json += "\": "; if (db.MoveRelative(name)) { ConfigurationDatabase child; db.Copy(child); JsonifyDatabase(child, json); db.MoveToAncestor(1u); } else { AnyType at = db.GetType(name); char8 buf[1024]; AnyType st(CharString, 0u, buf); st.SetNumberOfElements(0, 1024); if (TypeConvert(st, at)) { json += "\""; EscapeJson(buf, json); json += "\""; } else { json += "null"; } } if (i < n - 1) json += ", "; } json += "}"; } void DebugService::ServeConfig(BasicTCPSocket *client) { if (client == NULL_PTR(BasicTCPSocket *)) return; // If no manual config was injected (test case), rebuild from the live registry. // In production, Initialise() only captures the DebugService subtree (to avoid // corrupting the shared CDB cursor), so we always need to rebuild here. if (!manualConfigSet) { RebuildConfigFromRegistry(); } StreamString json; fullConfig.MoveToRoot(); JsonifyDatabase(fullConfig, json); json += "\nOK CONFIG\n"; uint32 s = json.Size(); (void)client->Write(json.Buffer(), s); } void DebugService::InfoNode(const char8 *path, BasicTCPSocket *client) { if (!client) return; Reference ref = ObjectRegistryDatabase::Instance()->Find(path); StreamString json = "{"; if (ref.IsValid()) { json += "\"Name\": \""; EscapeJson(ref->GetName(), json); json += "\", \"Class\": \""; EscapeJson(ref->GetClassProperties()->GetName(), json); json += "\""; ConfigurationDatabase db; if (ref->ExportData(db)) { json += ", \"Config\": {"; db.MoveToRoot(); uint32 nChildren = db.GetNumberOfChildren(); for (uint32 i = 0; i < nChildren; i++) { const char8 *cname = db.GetChildName(i); AnyType at = db.GetType(cname); char8 valBuf[1024]; AnyType strType(CharString, 0u, valBuf); strType.SetNumberOfElements(0, 1024); if (TypeConvert(strType, at)) { json += "\""; EscapeJson(cname, json); json += "\": \""; EscapeJson(valBuf, json); json += "\""; if (i < nChildren - 1) json += ", "; } } json += "}"; } EnrichWithConfig(path, json); } else { StreamString enrichAlias; mutex.FastLock(); bool found = false; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == path || SuffixMatch(aliases[i].name.Buffer(), path)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; const char8 *tname = TypeDescriptor::GetTypeNameFromTypeDescriptor(s->type); json.Printf("\"Name\": \"%s\", \"Class\": \"Signal\", \"Type\": " "\"%s\", \"ID\": %d", s->name.Buffer(), tname ? tname : "Unknown", s->internalID); enrichAlias = aliases[i].name; found = true; break; } } if (!found) { fprintf(stderr, "[InfoNode][%p] signal '%s' NOT found in %u aliases:\n", (void*)this, path, (uint32)aliases.Size()); for (uint32 i = 0; i < aliases.Size(); i++) { fprintf(stderr, " alias[%u] = '%s'\n", i, aliases[i].name.Buffer()); } } mutex.FastUnLock(); if (found) EnrichWithConfig(enrichAlias.Buffer(), json); else json += "\"Error\": \"Object not found\""; } json += "}\nOK INFO\n"; uint32 s = json.Size(); (void)client->Write(json.Buffer(), s); } uint32 DebugService::ExportTree(ReferenceContainer *container, StreamString &json, const char8 *pathPrefix) { if (container == NULL_PTR(ReferenceContainer *)) return 0; uint32 size = container->Size(); uint32 validCount = 0; for (uint32 i = 0u; i < size; i++) { Reference child = container->Get(i); if (child.IsValid()) { if (validCount > 0u) json += ",\n"; StreamString nodeJson; const char8 *cname = child->GetName(); if (cname == NULL_PTR(const char8 *)) cname = "unnamed"; StreamString currentPath; if (pathPrefix != NULL_PTR(const char8 *)) { currentPath.Printf("%s.%s", pathPrefix, cname); } else { currentPath = cname; } nodeJson += "{\"Name\": \""; EscapeJson(cname, nodeJson); nodeJson += "\", \"Class\": \""; EscapeJson(child->GetClassProperties()->GetName(), nodeJson); nodeJson += "\""; 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 *))) { nodeJson += ", \"Children\": [\n"; uint32 subCount = 0u; if (inner != NULL_PTR(ReferenceContainer *)) subCount += ExportTree(inner, nodeJson, currentPath.Buffer()); if (ds != NULL_PTR(DataSourceI *)) { uint32 nSignals = ds->GetNumberOfSignals(); for (uint32 j = 0u; j < nSignals; j++) { if (subCount > 0u) nodeJson += ",\n"; subCount++; StreamString sname; (void)ds->GetSignalName(j, sname); const char8 *stype = TypeDescriptor::GetTypeNameFromTypeDescriptor( ds->GetSignalType(j)); uint8 dims = 0u; (void)ds->GetSignalNumberOfDimensions(j, dims); uint32 elems = 0u; (void)ds->GetSignalNumberOfElements(j, elems); StreamString signalFullPath; signalFullPath.Printf("%s.%s", currentPath.Buffer(), sname.Buffer()); bool traceable = false; bool forcable = false; (void)IsInstrumented(signalFullPath.Buffer(), traceable, forcable); nodeJson += "{\"Name\": \""; EscapeJson(sname.Buffer(), nodeJson); nodeJson += "\", \"Class\": \"Signal\", \"Type\": \""; EscapeJson(stype ? stype : "Unknown", nodeJson); nodeJson.Printf("\", \"Dimensions\": %d, \"Elements\": %u", dims, elems); nodeJson.Printf(", \"IsTraceable\": %s, \"IsForcable\": %s}", traceable ? "true" : "false", forcable ? "true" : "false"); } } if (gam != NULL_PTR(GAM *)) { uint32 nIn = gam->GetNumberOfInputSignals(); for (uint32 j = 0u; j < nIn; j++) { if (subCount > 0u) nodeJson += ",\n"; subCount++; StreamString sname; (void)gam->GetSignalName(InputSignals, j, sname); const char8 *stype = TypeDescriptor::GetTypeNameFromTypeDescriptor( gam->GetSignalType(InputSignals, j)); uint32 dims = 0u; (void)gam->GetSignalNumberOfDimensions(InputSignals, j, dims); uint32 elems = 0u; (void)gam->GetSignalNumberOfElements(InputSignals, j, elems); StreamString signalFullPath; signalFullPath.Printf("%s.In.%s", currentPath.Buffer(), sname.Buffer()); bool traceable = false; bool forcable = false; (void)IsInstrumented(signalFullPath.Buffer(), traceable, forcable); nodeJson += "{\"Name\": \"In."; EscapeJson(sname.Buffer(), nodeJson); nodeJson += "\", \"Class\": \"InputSignal\", \"Type\": \""; EscapeJson(stype ? stype : "Unknown", nodeJson); nodeJson.Printf("\", \"Dimensions\": %u, \"Elements\": %u", dims, elems); nodeJson.Printf(", \"IsTraceable\": %s, \"IsForcable\": %s}", traceable ? "true" : "false", forcable ? "true" : "false"); } uint32 nOut = gam->GetNumberOfOutputSignals(); for (uint32 j = 0u; j < nOut; j++) { if (subCount > 0u) nodeJson += ",\n"; subCount++; StreamString sname; (void)gam->GetSignalName(OutputSignals, j, sname); const char8 *stype = TypeDescriptor::GetTypeNameFromTypeDescriptor( gam->GetSignalType(OutputSignals, j)); uint32 dims = 0u; (void)gam->GetSignalNumberOfDimensions(OutputSignals, j, dims); uint32 elems = 0u; (void)gam->GetSignalNumberOfElements(OutputSignals, j, elems); StreamString signalFullPath; signalFullPath.Printf("%s.Out.%s", currentPath.Buffer(), sname.Buffer()); bool traceable = false; bool forcable = false; (void)IsInstrumented(signalFullPath.Buffer(), traceable, forcable); nodeJson += "{\"Name\": \"Out."; EscapeJson(sname.Buffer(), nodeJson); nodeJson += "\", \"Class\": \"OutputSignal\", \"Type\": \""; EscapeJson(stype ? stype : "Unknown", nodeJson); nodeJson.Printf("\", \"Dimensions\": %u, \"Elements\": %u", dims, elems); nodeJson.Printf(", \"IsTraceable\": %s, \"IsForcable\": %s}", traceable ? "true" : "false", forcable ? "true" : "false"); } } nodeJson += "\n]"; } nodeJson += "}"; json += nodeJson; validCount++; } } return validCount; } uint32 DebugService::ForceSignal(const char8 *name, const char8 *valueStr) { mutex.FastLock(); uint32 count = 0; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == name || 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); (void)TypeConvert(dest, source); count++; } } UpdateBrokersActiveStatus(); mutex.FastUnLock(); return count; } uint32 DebugService::UnforceSignal(const char8 *name) { mutex.FastLock(); uint32 count = 0; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { signals[aliases[i].signalIndex]->isForcing = false; count++; } } UpdateBrokersActiveStatus(); mutex.FastUnLock(); return count; } uint32 DebugService::TraceSignal(const char8 *name, bool enable, uint32 decimation) { mutex.FastLock(); uint32 count = 0; for (uint32 i = 0; i < aliases.Size(); i++) { printf("%s\n", aliases[i].name.Buffer()); if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; s->isTracing = enable; s->decimationFactor = decimation; s->decimationCounter = 0; count++; } } if (count == 0) { printf(" signal %s not found\n", name); } UpdateBrokersActiveStatus(); mutex.FastUnLock(); return count; } void DebugService::ConsumeStepIfNeeded(const char8 *gamName, const char8 *threadName) { if (stepRemaining == 0u) return; mutex.FastLock(); // If a thread filter is set, only the matching OS thread consumes step credits. 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(); } void DebugService::Step(uint32 n, const char8 *threadName) { mutex.FastLock(); stepRemaining = n; isPaused = false; stepThreadFilter = (threadName != NULL_PTR(const char8 *)) ? threadName : ""; mutex.FastUnLock(); } void DebugService::GetStepStatus(BasicTCPSocket *client) { if (client == NULL_PTR(BasicTCPSocket *)) return; mutex.FastLock(); bool paused = isPaused; uint32 remaining = stepRemaining; StreamString gam = pausedAtGam; StreamString threadFilter = stepThreadFilter; mutex.FastUnLock(); StreamString resp; resp.Printf("{\"Paused\": %s, \"PausedAtGam\": \"%s\", \"StepRemaining\": %u, \"StepThread\": \"%s\"}\nOK STEP_STATUS\n", paused ? "true" : "false", gam.Buffer(), remaining, threadFilter.Buffer()); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); } void DebugService::GetSignalValue(const char8 *name, BasicTCPSocket *client) { if (client == NULL_PTR(BasicTCPSocket *)) return; mutex.FastLock(); DebugSignalInfo *sig = NULL_PTR(DebugSignalInfo *); for (uint32 i = 0u; i < aliases.Size(); i++) { if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { sig = signals[aliases[i].signalIndex]; break; } } if (sig == NULL_PTR(DebugSignalInfo *)) { mutex.FastUnLock(); StreamString resp; resp.Printf("{\"Error\": \"Signal not found: %s\"}\nOK VALUE\n", name); uint32 s = resp.Size(); (void)client->Write(resp.Buffer(), s); return; } 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]; memset(localBuf, 0, sizeof(localBuf)); if (sig->memoryAddress != NULL_PTR(void *)) { memcpy(localBuf, sig->memoryAddress, totalBytes); } mutex.FastUnLock(); // Build the value string via CharString TypeConvert — the same path used by // EnrichWithConfig/JsonifyDatabase, which is known to work for all types. StreamString valueStr; if (nElem > 1u) { // Array or matrix: produce comma-separated text for (uint32 i = 0u; i < nElem; i++) { char8 elemBuf[128] = {'\0'}; AnyType srcElem(td, 0u, (void *)(localBuf + i * byteSize)); AnyType dstStr(CharString, 0u, elemBuf); dstStr.SetNumberOfElements(0u, 128u); (void)TypeConvert(dstStr, srcElem); if (i > 0u) valueStr += ", "; valueStr += elemBuf; } } else { char8 elemBuf[256] = {'\0'}; AnyType srcElem(td, 0u, (void *)localBuf); AnyType dstStr(CharString, 0u, elemBuf); dstStr.SetNumberOfElements(0u, 256u); (void)TypeConvert(dstStr, srcElem); valueStr = elemBuf; } StreamString resp; resp += "{\"Name\": \""; resp += name; resp += "\", \"Value\": \""; // Escape the value text (quotes inside a string value) const char8 *vp = valueStr.Buffer(); while (vp != NULL_PTR(const char8 *) && *vp != '\0') { if (*vp == '"') resp += "\\\""; else if (*vp == '\\') resp += "\\\\"; else { char8 tmp[2] = { *vp, '\0' }; resp += tmp; } vp++; } resp += "\", \"Elements\": "; // 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); } uint32 DebugService::SetBreak(const char8 *name, uint8 op, float64 threshold) { mutex.FastLock(); uint32 count = 0; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { DebugSignalInfo *s = signals[aliases[i].signalIndex]; s->breakThreshold = threshold; s->breakOp = op; count++; } } if (count > 0) UpdateBrokersBreakStatus(); mutex.FastUnLock(); return count; } uint32 DebugService::ClearBreak(const char8 *name) { mutex.FastLock(); uint32 count = 0; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == name || SuffixMatch(aliases[i].name.Buffer(), name)) { signals[aliases[i].signalIndex]->breakOp = BREAK_OFF; count++; } } if (count > 0) UpdateBrokersBreakStatus(); mutex.FastUnLock(); return count; } bool DebugService::IsInstrumented(const char8 *fullPath, bool &traceable, bool &forcable) { mutex.FastLock(); bool found = false; for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == fullPath || SuffixMatch(aliases[i].name.Buffer(), fullPath)) { found = true; break; } } mutex.FastUnLock(); traceable = found; forcable = found; return found; } uint32 DebugService::RegisterMonitorSignal(const char8 *path, uint32 periodMs) { mutex.FastLock(); uint32 count = 0; // Check if already monitored for (uint32 j = 0; j < monitoredSignals.Size(); j++) { if (monitoredSignals[j].path == path) { monitoredSignals[j].periodMs = periodMs; mutex.FastUnLock(); return 1; } } // Path resolution: find the DataSource object StreamString fullPath = path; fullPath.Seek(0); char8 term; Vec parts; StreamString token; while (fullPath.GetToken(token, ".", term)) { parts.Push(token); token = ""; } if (parts.Size() >= 2) { StreamString signalName = parts[parts.Size() - 1u]; StreamString dsPath; for (uint32 i = 0; 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 = 0; if (ds->GetSignalIndex(idx, signalName.Buffer())) { MonitoredSignal m; m.dataSource = ds; m.signalIdx = idx; m.path = path; m.periodMs = periodMs; m.lastPollTime = 0; m.size = 0; (void)ds->GetSignalByteSize(idx, m.size); if (m.size == 0) m.size = 4; // Use high-bit for polled signals to avoid conflict with brokered ones m.internalID = 0x80000000 | monitoredSignals.Size(); // Re-use existing ID if signal is also instrumented via broker for (uint32 i = 0; i < aliases.Size(); i++) { if (aliases[i].name == path || SuffixMatch(aliases[i].name.Buffer(), path)) { m.internalID = signals[aliases[i].signalIndex]->internalID; break; } } monitoredSignals.Push(m); count = 1; } } } mutex.FastUnLock(); return count; } uint32 DebugService::UnmonitorSignal(const char8 *path) { mutex.FastLock(); uint32 count = 0; for (uint32 i = 0; i < monitoredSignals.Size(); i++) { if (monitoredSignals[i].path == path || SuffixMatch(monitoredSignals[i].path.Buffer(), path)) { (void)monitoredSignals.Remove(i); i--; count++; } } mutex.FastUnLock(); return count; } void DebugService::Discover(BasicTCPSocket *client) { if (client) { StreamString header = "{\n \"Signals\": [\n"; uint32 s = header.Size(); (void)client->Write(header.Buffer(), s); mutex.FastLock(); uint32 total = 0; for (uint32 i = 0; i < aliases.Size(); i++) { if (total > 0) { uint32 commaSize = 2; (void)client->Write(",\n", commaSize); } StreamString line; DebugSignalInfo *sig = signals[aliases[i].signalIndex]; const char8 *typeName = TypeDescriptor::GetTypeNameFromTypeDescriptor(sig->type); line.Printf(" {\"name\": \"%s\", \"id\": %d, \"type\": \"%s\", \"dimensions\": %u, \"elements\": %u", aliases[i].name.Buffer(), sig->internalID, typeName ? typeName : "Unknown", sig->numberOfDimensions, sig->numberOfElements); EnrichWithConfig(aliases[i].name.Buffer(), line); line += "}"; s = line.Size(); (void)client->Write(line.Buffer(), s); total++; } // Export monitored signals not already in aliases for (uint32 i = 0; i < monitoredSignals.Size(); i++) { bool found = false; for (uint32 j = 0; j < aliases.Size(); j++) { if (aliases[j].name == monitoredSignals[i].path) { found = true; break; } } if (!found) { if (total > 0) { uint32 commaSize = 2; (void)client->Write(",\n", commaSize); } StreamString line; const char8 *typeName = TypeDescriptor::GetTypeNameFromTypeDescriptor( monitoredSignals[i].dataSource->GetSignalType( monitoredSignals[i].signalIdx)); uint8 dims = 0; uint32 elems = 1; (void)monitoredSignals[i].dataSource->GetSignalNumberOfDimensions(monitoredSignals[i].signalIdx, dims); (void)monitoredSignals[i].dataSource->GetSignalNumberOfElements(monitoredSignals[i].signalIdx, elems); line.Printf(" {\"name\": \"%s\", \"id\": %u, \"type\": \"%s\", \"dimensions\": %u, \"elements\": %u", monitoredSignals[i].path.Buffer(), monitoredSignals[i].internalID, typeName ? typeName : "Unknown", dims, elems); EnrichWithConfig(monitoredSignals[i].path.Buffer(), line); line += "}"; s = line.Size(); (void)client->Write(line.Buffer(), s); total++; } } mutex.FastUnLock(); StreamString footer = " ]\n}\nOK DISCOVER\n"; s = footer.Size(); (void)client->Write(footer.Buffer(), s); } } void DebugService::ListNodes(const char8 *path, BasicTCPSocket *client) { if (!client) return; Reference ref = (path == NULL_PTR(const char8 *) || StringHelper::Length(path) == 0 || StringHelper::Compare(path, "/") == 0) ? ObjectRegistryDatabase::Instance() : ObjectRegistryDatabase::Instance()->Find(path); if (ref.IsValid()) { StreamString out; out.Printf("Nodes under %s:\n", path ? path : "/"); ReferenceContainer *container = dynamic_cast(ref.operator->()); if (container) { for (uint32 i = 0; i < container->Size(); i++) { Reference child = container->Get(i); if (child.IsValid()) out.Printf(" %s [%s]\n", child->GetName(), child->GetClassProperties()->GetName()); } } const char *okMsg = "OK LS\n"; out += okMsg; uint32 s = out.Size(); (void)client->Write(out.Buffer(), s); } else { const char *msg = "ERROR: Path not found\n"; uint32 s = StringHelper::Length(msg); (void)client->Write(msg, s); } } } // namespace MARTe